Skip to content

Commit

Permalink
Merge pull request #825 from common-workflow-language/dlang-support-e…
Browse files Browse the repository at this point in the history
…xtension-objects

[dlang] Support extention objects in arrays and the `default` field
  • Loading branch information
mergify[bot] authored Aug 28, 2024
2 parents 9a92ffc + d23f263 commit 19ac14b
Showing 1 changed file with 136 additions and 37 deletions.
173 changes: 136 additions & 37 deletions schema_salad/dlang_codegen.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""D code generator for a given schema salad definition."""

import datetime
import functools
import json
import textwrap
from typing import IO, Any, Dict, List, Optional, Tuple, Union, cast
from typing import IO, Any, Dict, List, Optional, Set, Tuple, Union, cast

from . import _logger, schema
from .codegen_base import CodeGenBase, TypeDef
from .cpp_codegen import isArray, isEnumSchema, isRecordSchema, pred
from .cpp_codegen import isArray, isEnumSchema, isMapSchema, isRecordSchema, isUnionSchema, pred
from .exceptions import SchemaException
from .schema import shortname

Expand Down Expand Up @@ -61,11 +63,10 @@ def prologue(self) -> None:
self.target.write(
f"""module {self.package};
import salad.meta.dumper : genDumper;
import salad.meta.impl : genCtor_, genIdentifier, genOpEq;
import salad.meta.impl : genBody_;
import salad.meta.parser : import_ = importFromURI;
import salad.meta.uda : documentRoot, id, idMap, link, LinkResolver, secondaryFilesDSL, typeDSL;
import salad.primitives : SchemaBase;
import salad.meta.uda : defaultValue, documentRoot, id, idMap, link, LinkResolver, secondaryFilesDSL, typeDSL;
import salad.primitives : EnumSchemaBase, MapSchemaBase, RecordSchemaBase, UnionSchemaBase;
import salad.type : None, Union;
"""
Expand All @@ -81,9 +82,9 @@ def prologue(self) -> None:
f"""
enum saladVersion = "{self.salad_version}";
mixin template genCtor()
mixin template genBody()
{{
mixin genCtor_!saladVersion;
mixin genBody_!saladVersion;
}}
""" # noqa: B907
)
Expand All @@ -107,7 +108,11 @@ def epilogue(self, root_loader: TypeDef) -> None:
@("Test for generated parser")
unittest
{{
import std : dirEntries, SpanMode;
import std : dirEntries, SpanMode, stdThreadLocalLog, NullLogger;
auto currentLogger = stdThreadLocalLog;
stdThreadLocalLog = new NullLogger;
scope(exit) stdThreadLocalLog = currentLogger;
auto resourceDir = "{self.examples}";
foreach (file; dirEntries(resourceDir, SpanMode.depth))
Expand Down Expand Up @@ -158,10 +163,15 @@ def to_doc_comment(self, doc: Union[None, str, List[str]]) -> str:
"""

def parse_record_field_type(
self, type_: Any, jsonld_pred: Union[None, str, Dict[str, Any]]
self,
type_: Any,
jsonld_pred: Union[None, str, Dict[str, Any]],
parent_has_idmap: bool = False,
has_default: bool = False,
) -> Tuple[str, str]:
"""Return an annotation string and a type string."""
annotations: List[str] = []
has_idmap = False or parent_has_idmap
if isinstance(jsonld_pred, str):
if jsonld_pred == "@id":
annotations.append("@id")
Expand All @@ -172,6 +182,7 @@ def parse_record_field_type(
annotations.append("@secondaryFilesDSL")
if "mapSubject" in jsonld_pred:
subject = jsonld_pred["mapSubject"]
has_idmap = True
if "mapPredicate" in jsonld_pred:
predicate = jsonld_pred["mapPredicate"]
annotations.append(f'@idMap("{subject}", "{predicate}")') # noqa: B907
Expand All @@ -196,16 +207,32 @@ def parse_record_field_type(
else:
type_str = stype
elif isinstance(type_, list):
t_str = [self.parse_record_field_type(t, None)[1] for t in type_]
union_types = ", ".join(t_str)
type_str = f"Union!({union_types})"
t_str = [
self.parse_record_field_type(t, None, parent_has_idmap=has_idmap)[1] for t in type_
]
if has_default:
t_str = [t for t in t_str if t != "None"]
if len(t_str) == 1:
type_str = t_str[0]
else:
if are_dispatchable(type_, has_idmap):
t_str += ["Any"]
union_types = ", ".join(t_str)
type_str = f"Union!({union_types})"
elif shortname(type_["type"]) == "array":
item_type = self.parse_record_field_type(type_["items"], None)[1]
item_type = self.parse_record_field_type(
type_["items"], None, parent_has_idmap=has_idmap
)[1]
type_str = f"{item_type}[]"
elif shortname(type_["type"]) == "record":
return annotate_str, shortname(type_.get("name", "record"))
elif shortname(type_["type"]) == "enum":
return annotate_str, "'not yet implemented'"
elif shortname(type_["type"]) == "map":
value_type = self.parse_record_field_type(
type_["values"], None, parent_has_idmap=has_idmap, has_default=True
)[1]
type_str = f"{value_type}[string]"
return annotate_str, type_str

def parse_record_field(self, field: Dict[str, Any], parent_name: Optional[str] = None) -> str:
Expand All @@ -214,18 +241,7 @@ def parse_record_field(self, field: Dict[str, Any], parent_name: Optional[str] =
jsonld_pred = field.get("jsonldPredicate", None)
doc_comment = self.to_doc_comment(field.get("doc", None))
type_ = field["type"]
if (
(
(isinstance(type_, dict) and shortname(type_.get("type", "")) == "enum")
or (isinstance(type_, str) and shortname(type_) == "string")
)
and isinstance(jsonld_pred, dict)
and (
shortname(jsonld_pred.get("_id", "")) == "type"
or shortname(jsonld_pred.get("_id", "")) == "@type"
)
and jsonld_pred.get("_type", "") == "@vocab"
):
if is_constant_field(field):
# special case
if isinstance(type_, dict):
# assert len(type["symbols"]) == 1
Expand All @@ -234,8 +250,16 @@ def parse_record_field(self, field: Dict[str, Any], parent_name: Optional[str] =
value = cast(str, parent_name)
return f'{doc_comment}static immutable {fname} = "{value}";' # noqa: B907

annotate_str, type_str = self.parse_record_field_type(type_, jsonld_pred)
return f"{doc_comment}{annotate_str}{type_str} {fname};"
if field.get("default", None) is not None:
default_value = json.dumps(field["default"])
default_str = f'@defaultValue(q"<{default_value}>") '
else:
default_str = ""

annotate_str, type_str = self.parse_record_field_type(
type_, jsonld_pred, has_default="default" in field
)
return f"{doc_comment}{default_str}{annotate_str}{type_str} {fname};"

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

return f"""
{doc_comment}{doc_root_annotation}class {classname} : SchemaBase
{doc_comment}{doc_root_annotation}class {classname} : RecordSchemaBase
{{
{decl_str}
mixin genCtor;
mixin genIdentifier;
mixin genDumper;
mixin genBody;
}}"""

def parse_enum(self, stype: Dict[str, Any]) -> str:
Expand Down Expand Up @@ -294,7 +316,7 @@ def parse_enum(self, stype: Dict[str, Any]) -> str:
doc_comment = ""

return f"""
{doc_comment}{doc_root_annotation}class {classname} : SchemaBase
{doc_comment}{doc_root_annotation}class {classname} : EnumSchemaBase
{{
///
enum Symbol
Expand All @@ -304,9 +326,47 @@ def parse_enum(self, stype: Dict[str, Any]) -> str:
Symbol value;
mixin genCtor;
mixin genOpEq;
mixin genDumper;
mixin genBody;
}}"""

def parse_union(self, stype: Dict[str, Any]) -> str:
"""Return a declaration string for a given union schema."""
name = cast(str, stype["name"])
classname = self.safe_name(name)

types = self.parse_record_field_type(stype["names"], None)[1]

if "doc" in stype:
doc_comment = self.to_doc_comment(stype["doc"])
else:
doc_comment = ""

return f"""
{doc_comment}class {classname} : UnionSchemaBase
{{
{types} payload;
mixin genBody;
}}"""

def parse_map(self, stype: Dict[str, Any]) -> str:
"""Return a declaration string for a given map schema."""
name = cast(str, stype["name"])
classname = self.safe_name(name)

values = self.parse_record_field_type(stype["values"], None, has_default=True)[1]

if "doc" in stype:
doc_comment = self.to_doc_comment(stype["doc"])
else:
doc_comment = ""

return f"""
{doc_comment}class {classname} : MapSchemaBase
{{
{values}[string] payload;
mixin genBody;
}}"""

def parse(self, items: List[Dict[str, Any]]) -> None:
Expand All @@ -329,12 +389,51 @@ def parse(self, items: List[Dict[str, Any]]) -> None:
dlang_defs.append(self.parse_record_schema(stype))
elif isEnumSchema(stype):
dlang_defs.append(self.parse_enum(stype))
elif isUnionSchema(stype):
dlang_defs.append(self.parse_union(stype))
elif isMapSchema(stype):
dlang_defs.append(self.parse_map(stype))
else:
_logger.error("not parsed %s", stype)
_logger.error("not parsed %s", json.dumps(stype))

self.target.write("\n".join(dlang_defs))
self.target.write("\n")

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

self.target.close()


def is_constant_field(field: Dict[str, Any]) -> bool:
"""Return True if a given field only takes the specified string."""
jsonld_pred = field.get("jsonldPredicate", None)
type_ = field["type"]
if (
(
(isinstance(type_, dict) and shortname(type_.get("type", "")) == "enum")
or (isinstance(type_, str) and shortname(type_) == "string")
)
and isinstance(jsonld_pred, dict)
and (
shortname(jsonld_pred.get("_id", "")) == "type"
or shortname(jsonld_pred.get("_id", "")) == "@type"
)
and jsonld_pred.get("_type", "") == "@vocab"
):
return True
return False


def constant_fields_of(type_: Any) -> Set[str]:
"""Return a list of constant fields name from a given record schema."""
if isinstance(type_, dict):
return set(shortname(f["name"]) for f in type_.get("fields", []) if is_constant_field(f))
return set()


def are_dispatchable(types: List[Any], parent_has_idmap: bool) -> bool:
"""Return True if a given list of types are dispatchable."""
if any(t for t in types if not isinstance(t, dict)):
return False
constants = (constant_fields_of(t) for t in types)
return len(functools.reduce(lambda lhs, rhs: lhs & rhs, constants)) > 0 and parent_has_idmap

0 comments on commit 19ac14b

Please sign in to comment.