Skip to content

Commit 1f3fa02

Browse files
authored
Add dlang codegen (#623)
1 parent 7662c7f commit 1f3fa02

File tree

5 files changed

+369
-6
lines changed

5 files changed

+369
-6
lines changed

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ as an example.
119119
+-------------+---------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
120120
| C++ | https://github.com/common-workflow-lab/cwl-cpp-auto | `cwl_output_example.cpp <https://github.com/common-workflow-lab/cwl-cpp-auto/blob/main/cwl_output_example.cpp>`_ | (Not yet implemented) |
121121
+-------------+---------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
122+
| D | https://github.com/common-workflow-lab/cwl-d-auto | `How to use <https://github.com/common-workflow-lab/cwl-d-auto#how-to-use>`_ | `How to use <https://github.com/common-workflow-lab/cwl-d-auto#how-to-use>`_ |
123+
+-------------+---------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
122124

123125
Quick Start
124126
-----------

schema_salad/codegen.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from . import schema
1717
from .codegen_base import CodeGenBase
1818
from .cpp_codegen import CppCodeGen
19+
from .dlang_codegen import DlangCodeGen
1920
from .dotnet_codegen import DotNetCodeGen
2021
from .exceptions import SchemaSaladException
2122
from .java_codegen import JavaCodeGen
@@ -59,7 +60,7 @@ def codegen(
5960
)
6061
)
6162
info = parser_info or pkg
62-
if lang == "python" or lang == "cpp":
63+
if lang == "python" or lang == "cpp" or lang == "dlang":
6364
if target:
6465
dest: Union[TextIOWrapper, TextIO] = open(
6566
target, mode="w", encoding="utf-8"
@@ -76,6 +77,17 @@ def codegen(
7677
)
7778
gen.parse(j)
7879
return
80+
elif lang == "dlang":
81+
gen = DlangCodeGen(
82+
base,
83+
dest,
84+
examples,
85+
pkg,
86+
copyright,
87+
info,
88+
)
89+
gen.parse(j)
90+
return
7991
else:
8092
gen = PythonCodeGen(dest, copyright=copyright, parser_info=info)
8193

schema_salad/dlang_codegen.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
"""D code generator for a given schema salad definition."""
2+
3+
import datetime
4+
import textwrap
5+
from typing import IO, Any, Dict, List, Optional, Tuple, Union, cast
6+
7+
from . import _logger, schema
8+
from .codegen_base import CodeGenBase, TypeDef
9+
from .cpp_codegen import isArray, isEnumSchema, isRecordSchema, pred
10+
from .exceptions import SchemaException
11+
from .schema import shortname
12+
13+
14+
class DlangCodeGen(CodeGenBase):
15+
"""Generation of D code for a given Schema Salad definition."""
16+
17+
def __init__(
18+
self,
19+
base: str,
20+
target: IO[str],
21+
examples: Optional[str],
22+
package: str,
23+
copyright_: Optional[str],
24+
parser_info: Optional[str],
25+
) -> None:
26+
"""Initialize the D codegen."""
27+
super().__init__()
28+
self.base_uri = base
29+
self.examples = examples
30+
self.target = target
31+
self.package = package
32+
self.copyright = copyright_
33+
self.parser_info = parser_info
34+
self.doc_root_types: List[str] = []
35+
36+
def prologue(self) -> None:
37+
"""Trigger to generate the prolouge code."""
38+
self.target.write(
39+
f"""/**
40+
* Generated by schema-salad code generator
41+
*
42+
* Date: {datetime.date.today().isoformat()}
43+
"""
44+
)
45+
if self.copyright:
46+
self.target.write(f" * Copyright: {self.copyright}\n")
47+
self.target.write(
48+
f""" */
49+
module {self.package};
50+
51+
import salad.meta.dumper : genDumper;
52+
import salad.meta.impl : genCtor, genIdentifier, genOpEq;
53+
import salad.meta.parser : import_ = importFromURI;
54+
import salad.meta.uda : documentRoot, id, idMap, link, secondaryFilesDSL, typeDSL;
55+
import salad.primitives : SchemaBase;
56+
import salad.type : None, Either;
57+
58+
"""
59+
)
60+
if self.parser_info:
61+
self.target.write(
62+
f"""/// parser information
63+
enum parserInfo = "{self.parser_info}";
64+
"""
65+
)
66+
67+
def epilogue(self, root_loader: TypeDef) -> None:
68+
"""Trigger to generate the epilouge code."""
69+
doc_root_type_str = ", ".join(self.doc_root_types)
70+
doc_root_type = f"Either!({doc_root_type_str})"
71+
self.target.write(
72+
f"""
73+
///
74+
alias DocumentRootType = {doc_root_type};
75+
76+
///
77+
alias importFromURI = import_!DocumentRootType;
78+
"""
79+
)
80+
if self.examples:
81+
self.target.write(
82+
f"""
83+
@("Test for generated parser")
84+
unittest
85+
{{
86+
import std : dirEntries, SpanMode;
87+
88+
auto resourceDir = "{self.examples}";
89+
foreach (file; dirEntries(resourceDir, SpanMode.depth))
90+
{{
91+
import std : assertNotThrown, baseName, format, startsWith;
92+
import salad.resolver : absoluteURI;
93+
94+
if (!file.baseName.startsWith("valid"))
95+
{{
96+
continue;
97+
}}
98+
importFromURI(file.absoluteURI).assertNotThrown(format!"Failed to load %s"(file));
99+
}}
100+
}}
101+
"""
102+
)
103+
104+
@staticmethod
105+
def safe_name(name: str) -> str:
106+
"""Generate a safe version of the given name."""
107+
avn = schema.avro_field_name(name)
108+
if avn in ("class", "abstract", "default", "package"):
109+
# reserved words
110+
avn = avn + "_"
111+
if avn and avn.startswith("anon."):
112+
avn = avn[5:]
113+
return avn
114+
115+
def to_doc_comment(self, doc: Union[None, str, List[str]]) -> str:
116+
"""Return an embedded documentation comments for a given string."""
117+
if doc is None:
118+
return "///\n"
119+
if isinstance(doc, str):
120+
lines = doc.split("\n")
121+
else:
122+
lines = sum((d.split("\n") for d in doc), [])
123+
124+
doc_lines = "\n".join((f" * {line}" for line in lines if line))
125+
126+
return f"""/**
127+
{doc_lines}
128+
*/
129+
"""
130+
131+
def parse_record_field_type(
132+
self, type_: Any, jsonld_pred: Union[None, str, Dict[str, Any]]
133+
) -> Tuple[str, str]:
134+
"""Return an annotation string and a type string."""
135+
annotations: List[str] = []
136+
if isinstance(jsonld_pred, str):
137+
if jsonld_pred == "@id":
138+
annotations.append("@id")
139+
elif isinstance(jsonld_pred, dict):
140+
if jsonld_pred.get("typeDSL", False):
141+
annotations.append("@typeDSL")
142+
if jsonld_pred.get("secondaryFilesDSL", False):
143+
annotations.append("@secondaryFilesDSL")
144+
if "mapSubject" in jsonld_pred:
145+
subject = jsonld_pred["mapSubject"]
146+
if "mapPredicate" in jsonld_pred:
147+
predicate = jsonld_pred["mapPredicate"]
148+
annotations.append(f'@idMap("{subject}", "{predicate}")')
149+
else:
150+
annotations.append(f'@idMap("{subject}")')
151+
if jsonld_pred.get("_type", "") == "@id":
152+
annotations.append("@link")
153+
if annotations:
154+
annotate_str = " ".join(annotations) + " "
155+
else:
156+
annotate_str = ""
157+
158+
if isinstance(type_, str):
159+
stype = shortname(type_)
160+
if stype == "boolean":
161+
type_str = "bool"
162+
elif stype == "null":
163+
type_str = "None"
164+
else:
165+
type_str = stype
166+
elif isinstance(type_, list):
167+
t_str = [self.parse_record_field_type(t, None)[1] for t in type_]
168+
union_types = ", ".join(t_str)
169+
type_str = f"Either!({union_types})"
170+
elif shortname(type_["type"]) == "array":
171+
item_type = self.parse_record_field_type(type_["items"], None)[1]
172+
type_str = f"{item_type}[]"
173+
elif shortname(type_["type"]) == "record":
174+
return annotate_str, shortname(type_.get("name", "record"))
175+
elif shortname(type_["type"]) == "enum":
176+
return annotate_str, "'not yet implemented'"
177+
return annotate_str, type_str
178+
179+
def parse_record_field(
180+
self, field: Dict[str, Any], parent_name: Optional[str] = None
181+
) -> str:
182+
"""Return a declaration string for a given record field."""
183+
fname = shortname(field["name"]) + "_"
184+
jsonld_pred = field.get("jsonldPredicate", None)
185+
doc_comment = self.to_doc_comment(field.get("doc", None))
186+
type_ = field["type"]
187+
if (
188+
(
189+
(isinstance(type_, dict) and shortname(type_.get("type", "")) == "enum")
190+
or (isinstance(type_, str) and shortname(type_) == "string")
191+
)
192+
and isinstance(jsonld_pred, dict)
193+
and (
194+
shortname(jsonld_pred.get("_id", "")) == "type"
195+
or shortname(jsonld_pred.get("_id", "")) == "@type"
196+
)
197+
and jsonld_pred.get("_type", "") == "@vocab"
198+
):
199+
# special case
200+
if isinstance(type_, dict):
201+
# assert len(type["symbols"]) == 1
202+
value = shortname(type_["symbols"][0])
203+
else:
204+
value = cast(str, parent_name)
205+
return f'{doc_comment}static immutable {fname} = "{value}";'
206+
207+
annotate_str, type_str = self.parse_record_field_type(type_, jsonld_pred)
208+
return f"{doc_comment}{annotate_str}{type_str} {fname};"
209+
210+
def parse_record_schema(self, stype: Dict[str, Any]) -> str:
211+
"""Return a declaration string for a given record schema."""
212+
name = cast(str, stype["name"])
213+
classname = self.safe_name(name)
214+
215+
field_decls = []
216+
if "fields" in stype:
217+
for field in stype["fields"]:
218+
field_decls.append(self.parse_record_field(field, classname))
219+
decl_str = "\n".join((textwrap.indent(f"{d}", " " * 4) for d in field_decls))
220+
221+
if stype.get("documentRoot", False):
222+
doc_root_annotation = "@documentRoot "
223+
self.doc_root_types.append(classname)
224+
else:
225+
doc_root_annotation = ""
226+
227+
doc_comment = self.to_doc_comment(stype.get("doc", None))
228+
229+
return f"""
230+
{doc_comment}{doc_root_annotation}class {classname} : SchemaBase
231+
{{
232+
{decl_str}
233+
234+
mixin genCtor;
235+
mixin genIdentifier;
236+
mixin genDumper;
237+
}}"""
238+
239+
def parse_enum(self, stype: Dict[str, Any]) -> str:
240+
"""Return a declaration string for a given enum schema."""
241+
name = cast(str, stype["name"])
242+
if shortname(name) == "Any":
243+
return "\n///\npublic import salad.primitives : Any;"
244+
if shortname(name) == "Expression":
245+
return "\n///\npublic import salad.primitives : Expression;"
246+
247+
classname = self.safe_name(name)
248+
syms = [
249+
f' s{i} = "{shortname(sym)}"'
250+
for i, sym in enumerate(stype["symbols"])
251+
]
252+
syms_def = ",\n".join(syms)
253+
254+
if stype.get("documentRoot", False):
255+
doc_root_annotation = "@documentRoot "
256+
self.doc_root_types.append(classname)
257+
else:
258+
doc_root_annotation = ""
259+
260+
if "doc" in stype:
261+
doc_comment = self.to_doc_comment(stype["doc"])
262+
else:
263+
doc_comment = ""
264+
265+
return f"""
266+
{doc_comment}{doc_root_annotation}class {classname} : SchemaBase
267+
{{
268+
enum Symbol
269+
{{
270+
{syms_def}
271+
}}
272+
273+
Symbol value;
274+
275+
mixin genCtor;
276+
mixin genOpEq;
277+
mixin genDumper;
278+
}}"""
279+
280+
def parse(self, items: List[Dict[str, Any]]) -> None:
281+
"""Generate D code from items and write it to target."""
282+
dlang_defs = []
283+
284+
self.prologue()
285+
286+
for stype in items:
287+
if "type" in stype and stype["type"] == "documentation":
288+
continue
289+
290+
if not (pred(stype) or isArray(stype)):
291+
raise SchemaException("not a valid SaladRecordField")
292+
293+
# parsing a record
294+
if isRecordSchema(stype):
295+
if stype.get("abstract", False):
296+
continue
297+
dlang_defs.append(self.parse_record_schema(stype))
298+
elif isEnumSchema(stype):
299+
dlang_defs.append(self.parse_enum(stype))
300+
else:
301+
_logger.error("not parsed %s", stype)
302+
303+
self.target.write("\n".join(dlang_defs))
304+
self.target.write("\n")
305+
306+
self.epilogue(TypeDef("dummy", "data"))
307+
308+
self.target.close()

schema_salad/main.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,23 @@ def arg_parser() -> argparse.ArgumentParser:
9595
type=str,
9696
metavar="language",
9797
help="Generate classes in target language, currently supported: "
98-
"python, java, typescript, dotnet, cpp",
98+
"python, java, typescript, dotnet, cpp, dlang",
9999
)
100100

101101
parser.add_argument(
102102
"--codegen-target",
103103
type=str,
104104
default=None,
105-
help="Defaults to sys.stdout for Python/C++ and ./ for " "Java/TypeScript/.Net",
105+
help="Defaults to sys.stdout for Python/C++/Dlang and ./ for "
106+
"Java/TypeScript/.Net",
106107
)
107108

108109
parser.add_argument(
109110
"--codegen-examples",
110111
type=str,
111112
metavar="directory",
112113
default=None,
113-
help="Directory of example documents for test case generation (Java/TypeScript/.Net only).",
114+
help="Directory of example documents for test case generation (Java/TypeScript/.Net/Dlang only).",
114115
)
115116

116117
parser.add_argument(
@@ -119,7 +120,7 @@ def arg_parser() -> argparse.ArgumentParser:
119120
metavar="dotted.package",
120121
default=None,
121122
help="Optional override of the package name which is other derived "
122-
"from the base URL (Java/TypeScript/.Net only).",
123+
"from the base URL (Java/TypeScript/.Net/Dlang only).",
123124
),
124125

125126
parser.add_argument(
@@ -135,7 +136,7 @@ def arg_parser() -> argparse.ArgumentParser:
135136
metavar="parser_info",
136137
type=str,
137138
default=None,
138-
help="Optional parser name which is accessible via resulted parser API (Python only)",
139+
help="Optional parser name which is accessible via resulted parser API (Python and Dlang only)",
139140
)
140141

141142
exgroup.add_argument(

0 commit comments

Comments
 (0)