|
| 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() |
0 commit comments