1
1
"""D code generator for a given schema salad definition."""
2
2
3
3
import datetime
4
+ import functools
5
+ import json
4
6
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
6
8
7
9
from . import _logger , schema
8
10
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
10
12
from .exceptions import SchemaException
11
13
from .schema import shortname
12
14
@@ -61,11 +63,10 @@ def prologue(self) -> None:
61
63
self .target .write (
62
64
f"""module { self .package } ;
63
65
64
- import salad.meta.dumper : genDumper;
65
- import salad.meta.impl : genCtor_, genIdentifier, genOpEq;
66
+ import salad.meta.impl : genBody_;
66
67
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 ;
69
70
import salad.type : None, Union;
70
71
71
72
"""
@@ -81,9 +82,9 @@ def prologue(self) -> None:
81
82
f"""
82
83
enum saladVersion = "{ self .salad_version } ";
83
84
84
- mixin template genCtor ()
85
+ mixin template genBody ()
85
86
{{
86
- mixin genCtor_ !saladVersion;
87
+ mixin genBody_ !saladVersion;
87
88
}}
88
89
""" # noqa: B907
89
90
)
@@ -107,7 +108,11 @@ def epilogue(self, root_loader: TypeDef) -> None:
107
108
@("Test for generated parser")
108
109
unittest
109
110
{{
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;
111
116
112
117
auto resourceDir = "{ self .examples } ";
113
118
foreach (file; dirEntries(resourceDir, SpanMode.depth))
@@ -158,10 +163,15 @@ def to_doc_comment(self, doc: Union[None, str, List[str]]) -> str:
158
163
"""
159
164
160
165
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 ,
162
171
) -> Tuple [str , str ]:
163
172
"""Return an annotation string and a type string."""
164
173
annotations : List [str ] = []
174
+ has_idmap = False or parent_has_idmap
165
175
if isinstance (jsonld_pred , str ):
166
176
if jsonld_pred == "@id" :
167
177
annotations .append ("@id" )
@@ -172,6 +182,7 @@ def parse_record_field_type(
172
182
annotations .append ("@secondaryFilesDSL" )
173
183
if "mapSubject" in jsonld_pred :
174
184
subject = jsonld_pred ["mapSubject" ]
185
+ has_idmap = True
175
186
if "mapPredicate" in jsonld_pred :
176
187
predicate = jsonld_pred ["mapPredicate" ]
177
188
annotations .append (f'@idMap("{ subject } ", "{ predicate } ")' ) # noqa: B907
@@ -196,16 +207,32 @@ def parse_record_field_type(
196
207
else :
197
208
type_str = stype
198
209
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 } )"
202
222
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 ]
204
226
type_str = f"{ item_type } []"
205
227
elif shortname (type_ ["type" ]) == "record" :
206
228
return annotate_str , shortname (type_ .get ("name" , "record" ))
207
229
elif shortname (type_ ["type" ]) == "enum" :
208
230
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]"
209
236
return annotate_str , type_str
210
237
211
238
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] =
214
241
jsonld_pred = field .get ("jsonldPredicate" , None )
215
242
doc_comment = self .to_doc_comment (field .get ("doc" , None ))
216
243
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 ):
229
245
# special case
230
246
if isinstance (type_ , dict ):
231
247
# assert len(type["symbols"]) == 1
@@ -234,8 +250,16 @@ def parse_record_field(self, field: Dict[str, Any], parent_name: Optional[str] =
234
250
value = cast (str , parent_name )
235
251
return f'{ doc_comment } static immutable { fname } = "{ value } ";' # noqa: B907
236
252
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 } ;"
239
263
240
264
def parse_record_schema (self , stype : Dict [str , Any ]) -> str :
241
265
"""Return a declaration string for a given record schema."""
@@ -257,13 +281,11 @@ def parse_record_schema(self, stype: Dict[str, Any]) -> str:
257
281
doc_comment = self .to_doc_comment (stype .get ("doc" , None ))
258
282
259
283
return f"""
260
- { doc_comment } { doc_root_annotation } class { classname } : SchemaBase
284
+ { doc_comment } { doc_root_annotation } class { classname } : RecordSchemaBase
261
285
{{
262
286
{ decl_str }
263
287
264
- mixin genCtor;
265
- mixin genIdentifier;
266
- mixin genDumper;
288
+ mixin genBody;
267
289
}}"""
268
290
269
291
def parse_enum (self , stype : Dict [str , Any ]) -> str :
@@ -294,7 +316,7 @@ def parse_enum(self, stype: Dict[str, Any]) -> str:
294
316
doc_comment = ""
295
317
296
318
return f"""
297
- { doc_comment } { doc_root_annotation } class { classname } : SchemaBase
319
+ { doc_comment } { doc_root_annotation } class { classname } : EnumSchemaBase
298
320
{{
299
321
///
300
322
enum Symbol
@@ -304,9 +326,47 @@ def parse_enum(self, stype: Dict[str, Any]) -> str:
304
326
305
327
Symbol value;
306
328
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;
310
370
}}"""
311
371
312
372
def parse (self , items : List [Dict [str , Any ]]) -> None :
@@ -329,12 +389,51 @@ def parse(self, items: List[Dict[str, Any]]) -> None:
329
389
dlang_defs .append (self .parse_record_schema (stype ))
330
390
elif isEnumSchema (stype ):
331
391
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 ))
332
396
else :
333
- _logger .error ("not parsed %s" , stype )
397
+ _logger .error ("not parsed %s" , json . dumps ( stype ) )
334
398
335
399
self .target .write ("\n " .join (dlang_defs ))
336
400
self .target .write ("\n " )
337
401
338
402
self .epilogue (TypeDef ("dummy" , "data" ))
339
403
340
404
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