Skip to content

Commit 19ac14b

Browse files
authored
Merge pull request #825 from common-workflow-language/dlang-support-extension-objects
[dlang] Support extention objects in arrays and the `default` field
2 parents 9a92ffc + d23f263 commit 19ac14b

File tree

1 file changed

+136
-37
lines changed

1 file changed

+136
-37
lines changed

schema_salad/dlang_codegen.py

Lines changed: 136 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""D code generator for a given schema salad definition."""
22

33
import datetime
4+
import functools
5+
import json
46
import textwrap
5-
from typing import IO, Any, Dict, List, Optional, Tuple, Union, cast
7+
from typing import IO, Any, Dict, List, Optional, Set, Tuple, Union, cast
68

79
from . import _logger, schema
810
from .codegen_base import CodeGenBase, TypeDef
9-
from .cpp_codegen import isArray, isEnumSchema, isRecordSchema, pred
11+
from .cpp_codegen import isArray, isEnumSchema, isMapSchema, isRecordSchema, isUnionSchema, pred
1012
from .exceptions import SchemaException
1113
from .schema import shortname
1214

@@ -61,11 +63,10 @@ def prologue(self) -> None:
6163
self.target.write(
6264
f"""module {self.package};
6365
64-
import salad.meta.dumper : genDumper;
65-
import salad.meta.impl : genCtor_, genIdentifier, genOpEq;
66+
import salad.meta.impl : genBody_;
6667
import salad.meta.parser : import_ = importFromURI;
67-
import salad.meta.uda : documentRoot, id, idMap, link, LinkResolver, secondaryFilesDSL, typeDSL;
68-
import salad.primitives : SchemaBase;
68+
import salad.meta.uda : defaultValue, documentRoot, id, idMap, link, LinkResolver, secondaryFilesDSL, typeDSL;
69+
import salad.primitives : EnumSchemaBase, MapSchemaBase, RecordSchemaBase, UnionSchemaBase;
6970
import salad.type : None, Union;
7071
7172
"""
@@ -81,9 +82,9 @@ def prologue(self) -> None:
8182
f"""
8283
enum saladVersion = "{self.salad_version}";
8384
84-
mixin template genCtor()
85+
mixin template genBody()
8586
{{
86-
mixin genCtor_!saladVersion;
87+
mixin genBody_!saladVersion;
8788
}}
8889
""" # noqa: B907
8990
)
@@ -107,7 +108,11 @@ def epilogue(self, root_loader: TypeDef) -> None:
107108
@("Test for generated parser")
108109
unittest
109110
{{
110-
import std : dirEntries, SpanMode;
111+
import std : dirEntries, SpanMode, stdThreadLocalLog, NullLogger;
112+
113+
auto currentLogger = stdThreadLocalLog;
114+
stdThreadLocalLog = new NullLogger;
115+
scope(exit) stdThreadLocalLog = currentLogger;
111116
112117
auto resourceDir = "{self.examples}";
113118
foreach (file; dirEntries(resourceDir, SpanMode.depth))
@@ -158,10 +163,15 @@ def to_doc_comment(self, doc: Union[None, str, List[str]]) -> str:
158163
"""
159164

160165
def parse_record_field_type(
161-
self, type_: Any, jsonld_pred: Union[None, str, Dict[str, Any]]
166+
self,
167+
type_: Any,
168+
jsonld_pred: Union[None, str, Dict[str, Any]],
169+
parent_has_idmap: bool = False,
170+
has_default: bool = False,
162171
) -> Tuple[str, str]:
163172
"""Return an annotation string and a type string."""
164173
annotations: List[str] = []
174+
has_idmap = False or parent_has_idmap
165175
if isinstance(jsonld_pred, str):
166176
if jsonld_pred == "@id":
167177
annotations.append("@id")
@@ -172,6 +182,7 @@ def parse_record_field_type(
172182
annotations.append("@secondaryFilesDSL")
173183
if "mapSubject" in jsonld_pred:
174184
subject = jsonld_pred["mapSubject"]
185+
has_idmap = True
175186
if "mapPredicate" in jsonld_pred:
176187
predicate = jsonld_pred["mapPredicate"]
177188
annotations.append(f'@idMap("{subject}", "{predicate}")') # noqa: B907
@@ -196,16 +207,32 @@ def parse_record_field_type(
196207
else:
197208
type_str = stype
198209
elif isinstance(type_, list):
199-
t_str = [self.parse_record_field_type(t, None)[1] for t in type_]
200-
union_types = ", ".join(t_str)
201-
type_str = f"Union!({union_types})"
210+
t_str = [
211+
self.parse_record_field_type(t, None, parent_has_idmap=has_idmap)[1] for t in type_
212+
]
213+
if has_default:
214+
t_str = [t for t in t_str if t != "None"]
215+
if len(t_str) == 1:
216+
type_str = t_str[0]
217+
else:
218+
if are_dispatchable(type_, has_idmap):
219+
t_str += ["Any"]
220+
union_types = ", ".join(t_str)
221+
type_str = f"Union!({union_types})"
202222
elif shortname(type_["type"]) == "array":
203-
item_type = self.parse_record_field_type(type_["items"], None)[1]
223+
item_type = self.parse_record_field_type(
224+
type_["items"], None, parent_has_idmap=has_idmap
225+
)[1]
204226
type_str = f"{item_type}[]"
205227
elif shortname(type_["type"]) == "record":
206228
return annotate_str, shortname(type_.get("name", "record"))
207229
elif shortname(type_["type"]) == "enum":
208230
return annotate_str, "'not yet implemented'"
231+
elif shortname(type_["type"]) == "map":
232+
value_type = self.parse_record_field_type(
233+
type_["values"], None, parent_has_idmap=has_idmap, has_default=True
234+
)[1]
235+
type_str = f"{value_type}[string]"
209236
return annotate_str, type_str
210237

211238
def parse_record_field(self, field: Dict[str, Any], parent_name: Optional[str] = None) -> str:
@@ -214,18 +241,7 @@ def parse_record_field(self, field: Dict[str, Any], parent_name: Optional[str] =
214241
jsonld_pred = field.get("jsonldPredicate", None)
215242
doc_comment = self.to_doc_comment(field.get("doc", None))
216243
type_ = field["type"]
217-
if (
218-
(
219-
(isinstance(type_, dict) and shortname(type_.get("type", "")) == "enum")
220-
or (isinstance(type_, str) and shortname(type_) == "string")
221-
)
222-
and isinstance(jsonld_pred, dict)
223-
and (
224-
shortname(jsonld_pred.get("_id", "")) == "type"
225-
or shortname(jsonld_pred.get("_id", "")) == "@type"
226-
)
227-
and jsonld_pred.get("_type", "") == "@vocab"
228-
):
244+
if is_constant_field(field):
229245
# special case
230246
if isinstance(type_, dict):
231247
# assert len(type["symbols"]) == 1
@@ -234,8 +250,16 @@ def parse_record_field(self, field: Dict[str, Any], parent_name: Optional[str] =
234250
value = cast(str, parent_name)
235251
return f'{doc_comment}static immutable {fname} = "{value}";' # noqa: B907
236252

237-
annotate_str, type_str = self.parse_record_field_type(type_, jsonld_pred)
238-
return f"{doc_comment}{annotate_str}{type_str} {fname};"
253+
if field.get("default", None) is not None:
254+
default_value = json.dumps(field["default"])
255+
default_str = f'@defaultValue(q"<{default_value}>") '
256+
else:
257+
default_str = ""
258+
259+
annotate_str, type_str = self.parse_record_field_type(
260+
type_, jsonld_pred, has_default="default" in field
261+
)
262+
return f"{doc_comment}{default_str}{annotate_str}{type_str} {fname};"
239263

240264
def parse_record_schema(self, stype: Dict[str, Any]) -> str:
241265
"""Return a declaration string for a given record schema."""
@@ -257,13 +281,11 @@ def parse_record_schema(self, stype: Dict[str, Any]) -> str:
257281
doc_comment = self.to_doc_comment(stype.get("doc", None))
258282

259283
return f"""
260-
{doc_comment}{doc_root_annotation}class {classname} : SchemaBase
284+
{doc_comment}{doc_root_annotation}class {classname} : RecordSchemaBase
261285
{{
262286
{decl_str}
263287
264-
mixin genCtor;
265-
mixin genIdentifier;
266-
mixin genDumper;
288+
mixin genBody;
267289
}}"""
268290

269291
def parse_enum(self, stype: Dict[str, Any]) -> str:
@@ -294,7 +316,7 @@ def parse_enum(self, stype: Dict[str, Any]) -> str:
294316
doc_comment = ""
295317

296318
return f"""
297-
{doc_comment}{doc_root_annotation}class {classname} : SchemaBase
319+
{doc_comment}{doc_root_annotation}class {classname} : EnumSchemaBase
298320
{{
299321
///
300322
enum Symbol
@@ -304,9 +326,47 @@ def parse_enum(self, stype: Dict[str, Any]) -> str:
304326
305327
Symbol value;
306328
307-
mixin genCtor;
308-
mixin genOpEq;
309-
mixin genDumper;
329+
mixin genBody;
330+
}}"""
331+
332+
def parse_union(self, stype: Dict[str, Any]) -> str:
333+
"""Return a declaration string for a given union schema."""
334+
name = cast(str, stype["name"])
335+
classname = self.safe_name(name)
336+
337+
types = self.parse_record_field_type(stype["names"], None)[1]
338+
339+
if "doc" in stype:
340+
doc_comment = self.to_doc_comment(stype["doc"])
341+
else:
342+
doc_comment = ""
343+
344+
return f"""
345+
{doc_comment}class {classname} : UnionSchemaBase
346+
{{
347+
{types} payload;
348+
349+
mixin genBody;
350+
}}"""
351+
352+
def parse_map(self, stype: Dict[str, Any]) -> str:
353+
"""Return a declaration string for a given map schema."""
354+
name = cast(str, stype["name"])
355+
classname = self.safe_name(name)
356+
357+
values = self.parse_record_field_type(stype["values"], None, has_default=True)[1]
358+
359+
if "doc" in stype:
360+
doc_comment = self.to_doc_comment(stype["doc"])
361+
else:
362+
doc_comment = ""
363+
364+
return f"""
365+
{doc_comment}class {classname} : MapSchemaBase
366+
{{
367+
{values}[string] payload;
368+
369+
mixin genBody;
310370
}}"""
311371

312372
def parse(self, items: List[Dict[str, Any]]) -> None:
@@ -329,12 +389,51 @@ def parse(self, items: List[Dict[str, Any]]) -> None:
329389
dlang_defs.append(self.parse_record_schema(stype))
330390
elif isEnumSchema(stype):
331391
dlang_defs.append(self.parse_enum(stype))
392+
elif isUnionSchema(stype):
393+
dlang_defs.append(self.parse_union(stype))
394+
elif isMapSchema(stype):
395+
dlang_defs.append(self.parse_map(stype))
332396
else:
333-
_logger.error("not parsed %s", stype)
397+
_logger.error("not parsed %s", json.dumps(stype))
334398

335399
self.target.write("\n".join(dlang_defs))
336400
self.target.write("\n")
337401

338402
self.epilogue(TypeDef("dummy", "data"))
339403

340404
self.target.close()
405+
406+
407+
def is_constant_field(field: Dict[str, Any]) -> bool:
408+
"""Return True if a given field only takes the specified string."""
409+
jsonld_pred = field.get("jsonldPredicate", None)
410+
type_ = field["type"]
411+
if (
412+
(
413+
(isinstance(type_, dict) and shortname(type_.get("type", "")) == "enum")
414+
or (isinstance(type_, str) and shortname(type_) == "string")
415+
)
416+
and isinstance(jsonld_pred, dict)
417+
and (
418+
shortname(jsonld_pred.get("_id", "")) == "type"
419+
or shortname(jsonld_pred.get("_id", "")) == "@type"
420+
)
421+
and jsonld_pred.get("_type", "") == "@vocab"
422+
):
423+
return True
424+
return False
425+
426+
427+
def constant_fields_of(type_: Any) -> Set[str]:
428+
"""Return a list of constant fields name from a given record schema."""
429+
if isinstance(type_, dict):
430+
return set(shortname(f["name"]) for f in type_.get("fields", []) if is_constant_field(f))
431+
return set()
432+
433+
434+
def are_dispatchable(types: List[Any], parent_has_idmap: bool) -> bool:
435+
"""Return True if a given list of types are dispatchable."""
436+
if any(t for t in types if not isinstance(t, dict)):
437+
return False
438+
constants = (constant_fields_of(t) for t in types)
439+
return len(functools.reduce(lambda lhs, rhs: lhs & rhs, constants)) > 0 and parent_has_idmap

0 commit comments

Comments
 (0)