sera-2 1.26.2__py3-none-any.whl → 1.26.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,811 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Callable
5
+
6
+ from codegen.models import (
7
+ AST,
8
+ DeferredVar,
9
+ ImportHelper,
10
+ PredefinedFn,
11
+ Program,
12
+ expr,
13
+ stmt,
14
+ )
15
+ from codegen.models.var import DeferredVar
16
+
17
+ from sera.make.ts_frontend.misc import TS_GLOBAL_IDENTS, get_normalizer
18
+ from sera.misc import assert_not_null, identity, to_camel_case, to_pascal_case
19
+ from sera.models import (
20
+ Class,
21
+ DataProperty,
22
+ ObjectProperty,
23
+ Package,
24
+ Schema,
25
+ TsTypeWithDep,
26
+ )
27
+ from sera.typing import is_set
28
+
29
+
30
+ def make_draft(
31
+ schema: Schema, cls: Class, pkg: Package, idprop_aliases: dict[str, TsTypeWithDep]
32
+ ):
33
+ if not cls.is_public:
34
+ # skip classes that are not public
35
+ return
36
+
37
+ idprop = cls.get_id_property()
38
+
39
+ draft_clsname = "Draft" + cls.name
40
+ draft_validators = f"draft{cls.name}Validators"
41
+
42
+ program = Program()
43
+ program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
44
+ program.import_("mobx.makeObservable", True)
45
+ program.import_("mobx.observable", True)
46
+ program.import_("mobx.action", True)
47
+ program.import_("sera-db.validators", True)
48
+
49
+ import_helper = ImportHelper(program, TS_GLOBAL_IDENTS)
50
+
51
+ program.root(
52
+ stmt.LineBreak(),
53
+ stmt.TypescriptStatement(
54
+ "const {getValidator, memoizeOneValidators} = validators;"
55
+ ),
56
+ stmt.LineBreak(),
57
+ )
58
+
59
+ # make sure that the property stale is not in existing properties
60
+ if "stale" in cls.properties:
61
+ raise ValueError(f"Class {cls.name} already has property stale")
62
+
63
+ # information about class primary key
64
+ cls_pk = None
65
+ observable_args: list[tuple[expr.Expr, expr.ExprIdent]] = []
66
+ prop_defs = []
67
+ prop_validators: list[tuple[expr.ExprIdent, expr.Expr]] = []
68
+ prop_constructor_assigns = []
69
+ # attrs needed for the cls.create function
70
+ create_args = []
71
+ update_args = []
72
+ ser_args = []
73
+ to_record_args = []
74
+ update_field_funcs: list[Callable[[AST], Any]] = []
75
+
76
+ prop2tsname = {}
77
+
78
+ for prop in cls.properties.values():
79
+ # if prop.data.is_private:
80
+ # # skip private fields as this is for APIs exchange
81
+ # continue
82
+
83
+ propname = to_camel_case(prop.name)
84
+ if isinstance(prop, ObjectProperty) and prop.target.db is not None:
85
+ propname = propname + "Id"
86
+ prop2tsname[prop.name] = propname
87
+
88
+ def _update_field_func(
89
+ prop: DataProperty | ObjectProperty,
90
+ propname: str,
91
+ tstype: TsTypeWithDep,
92
+ draft_clsname: str,
93
+ ):
94
+ return lambda ast: ast(
95
+ stmt.LineBreak(),
96
+ lambda ast01: ast01.func(
97
+ f"update{to_pascal_case(prop.name)}",
98
+ [
99
+ DeferredVar.simple(
100
+ "value",
101
+ expr.ExprIdent(tstype.type),
102
+ ),
103
+ ],
104
+ expr.ExprIdent(draft_clsname),
105
+ comment=f"Update the `{prop.name}` field",
106
+ )(
107
+ stmt.AssignStatement(
108
+ PredefinedFn.attr_getter(
109
+ expr.ExprIdent("this"), expr.ExprIdent(propname)
110
+ ),
111
+ expr.ExprIdent("value"),
112
+ ),
113
+ stmt.AssignStatement(
114
+ PredefinedFn.attr_getter(
115
+ expr.ExprIdent("this"), expr.ExprIdent("stale")
116
+ ),
117
+ expr.ExprConstant(True),
118
+ ),
119
+ stmt.ReturnStatement(expr.ExprIdent("this")),
120
+ ),
121
+ )
122
+
123
+ if isinstance(prop, DataProperty):
124
+ tstype = prop.get_data_model_datatype().get_typescript_type()
125
+ original_tstype = tstype
126
+
127
+ if idprop is not None and prop.name == idprop.name:
128
+ # use id type alias
129
+ tstype = TsTypeWithDep(
130
+ type=f"{cls.name}Id",
131
+ spectype=tstype.spectype,
132
+ deps=[f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}Id"],
133
+ )
134
+ elif tstype.type not in schema.enums:
135
+ # for none id & none enum properties, we need to include a type for "invalid" value
136
+ tstype = _inject_type_for_invalid_value(tstype)
137
+
138
+ if prop.is_optional:
139
+ # convert type to optional
140
+ tstype = tstype.as_optional_type()
141
+ original_tstype = original_tstype.as_optional_type()
142
+
143
+ for dep in tstype.deps:
144
+ program.import_(dep, True)
145
+
146
+ # however, if this is a primary key and auto-increment, we set a different default value
147
+ # to be -1 to avoid start from 0
148
+ if (
149
+ prop.db is not None
150
+ and prop.db.is_primary_key
151
+ and prop.db.is_auto_increment
152
+ ):
153
+ create_propvalue = expr.ExprConstant(-1)
154
+ elif is_set(prop.data.default_value):
155
+ create_propvalue = expr.ExprConstant(prop.data.default_value)
156
+ else:
157
+ if tstype.type in idprop_aliases:
158
+ create_propvalue = idprop_aliases[tstype.type].get_default()
159
+ elif tstype.type in schema.enums:
160
+ enum_value_name = next(
161
+ iter(schema.enums[tstype.type].values.values())
162
+ ).name
163
+ assert isinstance(enum_value_name, str), enum_value_name
164
+ create_propvalue = expr.ExprIdent(
165
+ tstype.type + "." + enum_value_name
166
+ )
167
+ else:
168
+ create_propvalue = tstype.get_default()
169
+
170
+ prop_validators.append(
171
+ (
172
+ expr.ExprIdent(propname),
173
+ expr.ExprFuncCall(
174
+ expr.ExprIdent("getValidator"),
175
+ [
176
+ PredefinedFn.list(
177
+ [
178
+ expr.ExprConstant(
179
+ constraint.get_typescript_constraint()
180
+ )
181
+ for constraint in prop.data.constraints
182
+ ]
183
+ ),
184
+ expr.ExprConstant(prop.is_optional),
185
+ ],
186
+ ),
187
+ )
188
+ )
189
+
190
+ if prop.db is not None and prop.db.is_primary_key:
191
+ # for checking if the primary key is from the database or default (create_propvalue)
192
+ cls_pk = (expr.ExprIdent(propname), create_propvalue)
193
+
194
+ # if this field is private, we cannot get it from the normal record
195
+ # we have to create a default value for it.
196
+ if prop.data.is_private:
197
+ update_propvalue = create_propvalue
198
+ else:
199
+ update_propvalue = PredefinedFn.attr_getter(
200
+ expr.ExprIdent("record"), expr.ExprIdent(propname)
201
+ )
202
+
203
+ if original_tstype.type != tstype.type and tstype.type != f"{cls.name}Id":
204
+ norm_func = get_norm_func(original_tstype, import_helper)
205
+ else:
206
+ norm_func = identity
207
+
208
+ ser_args.append(
209
+ (
210
+ expr.ExprIdent(prop.name),
211
+ (
212
+ original_tstype.get_json_ser_func(
213
+ norm_func(
214
+ PredefinedFn.attr_getter(
215
+ expr.ExprIdent("this"), expr.ExprIdent(propname)
216
+ )
217
+ )
218
+ )
219
+ ),
220
+ )
221
+ )
222
+
223
+ if not prop.data.is_private:
224
+ # private property does not include in the public record
225
+ to_record_args.append(
226
+ (
227
+ expr.ExprIdent(propname),
228
+ (
229
+ norm_func(
230
+ PredefinedFn.attr_getter(
231
+ expr.ExprIdent("this"), expr.ExprIdent(propname)
232
+ )
233
+ )
234
+ ),
235
+ )
236
+ )
237
+ if not (prop.db is not None and prop.db.is_primary_key):
238
+ # skip observable for primary key as it is not needed
239
+ observable_args.append(
240
+ (
241
+ expr.ExprIdent(propname),
242
+ expr.ExprIdent("observable"),
243
+ )
244
+ )
245
+ observable_args.append(
246
+ (
247
+ expr.ExprIdent(f"update{to_pascal_case(prop.name)}"),
248
+ expr.ExprIdent("action"),
249
+ )
250
+ )
251
+ else:
252
+ assert isinstance(prop, ObjectProperty)
253
+ if prop.target.db is not None:
254
+ # this class is stored in the database, we store the id instead
255
+ tstype = TsTypeWithDep(
256
+ type=f"{prop.target.name}Id",
257
+ spectype=assert_not_null(prop.target.get_id_property())
258
+ .get_data_model_datatype()
259
+ .get_typescript_type()
260
+ .spectype,
261
+ deps=[
262
+ f"@.models.{prop.target.get_tsmodule_name()}.{prop.target.name}.{prop.target.name}Id"
263
+ ],
264
+ )
265
+ if prop.cardinality.is_star_to_many():
266
+ tstype = tstype.as_list_type()
267
+ create_propvalue = expr.ExprConstant([])
268
+ else:
269
+ if prop.is_optional:
270
+ # convert type to optional - for list type, we don't need to do this
271
+ # as we will use empty list as no value
272
+ tstype = tstype.as_optional_type()
273
+ # if target class has an auto-increment primary key, we set a different default value
274
+ # to be -1 to avoid start from 0
275
+ target_idprop = prop.target.get_id_property()
276
+ if (
277
+ target_idprop is not None
278
+ and target_idprop.db is not None
279
+ and target_idprop.db.is_primary_key
280
+ and target_idprop.db.is_auto_increment
281
+ ):
282
+ create_propvalue = expr.ExprConstant(-1)
283
+ else:
284
+ assert tstype.type in idprop_aliases
285
+ create_propvalue = idprop_aliases[tstype.type].get_default()
286
+
287
+ update_propvalue = PredefinedFn.attr_getter(
288
+ expr.ExprIdent("record"), expr.ExprIdent(propname)
289
+ )
290
+ ser_args.append(
291
+ (
292
+ expr.ExprIdent(prop.name + "_id"),
293
+ PredefinedFn.attr_getter(
294
+ expr.ExprIdent("this"), expr.ExprIdent(propname)
295
+ ),
296
+ )
297
+ )
298
+
299
+ if not prop.data.is_private:
300
+ # private property does not include in the public record
301
+ to_record_args.append(
302
+ (
303
+ expr.ExprIdent(propname),
304
+ PredefinedFn.attr_getter(
305
+ expr.ExprIdent("this"), expr.ExprIdent(propname)
306
+ ),
307
+ )
308
+ )
309
+ else:
310
+ # we are going to store the whole object
311
+ tstype = TsTypeWithDep(
312
+ type=f"Draft{prop.target.name}",
313
+ spectype=f"Draft{prop.target.name}",
314
+ deps=[
315
+ f"@.models.{prop.target.get_tsmodule_name()}.Draft{prop.target.name}.Draft{prop.target.name}"
316
+ ],
317
+ )
318
+ if prop.cardinality.is_star_to_many():
319
+ create_propvalue = expr.ExprConstant([])
320
+ update_propvalue = PredefinedFn.map_list(
321
+ PredefinedFn.attr_getter(
322
+ expr.ExprIdent("record"), expr.ExprIdent(propname)
323
+ ),
324
+ lambda item: expr.ExprMethodCall(
325
+ expr.ExprIdent(tstype.type),
326
+ "update",
327
+ [item],
328
+ ),
329
+ )
330
+ ser_args.append(
331
+ (
332
+ expr.ExprIdent(prop.name),
333
+ PredefinedFn.map_list(
334
+ PredefinedFn.attr_getter(
335
+ expr.ExprIdent("this"), expr.ExprIdent(propname)
336
+ ),
337
+ lambda item: expr.ExprMethodCall(item, "ser", []),
338
+ (
339
+ (
340
+ lambda item: PredefinedFn.attr_getter(
341
+ expr.ExprFuncCall(
342
+ PredefinedFn.attr_getter(
343
+ expr.ExprIdent(draft_validators),
344
+ expr.ExprIdent(propname),
345
+ ),
346
+ [item],
347
+ ),
348
+ expr.ExprIdent("isValid"),
349
+ )
350
+ )
351
+ if prop.is_optional
352
+ else None
353
+ ),
354
+ ),
355
+ )
356
+ )
357
+
358
+ if not prop.data.is_private:
359
+ # private property does not include in the public record
360
+ to_record_args.append(
361
+ (
362
+ expr.ExprIdent(propname),
363
+ PredefinedFn.map_list(
364
+ PredefinedFn.attr_getter(
365
+ expr.ExprIdent("this"),
366
+ expr.ExprIdent(propname),
367
+ ),
368
+ lambda item: expr.ExprMethodCall(
369
+ item, "toRecord", []
370
+ ),
371
+ # TODO: if a property is optional and all of its target properties are also optional.
372
+ # then, we will consider a record is empty and skip it.
373
+ (
374
+ (
375
+ lambda item: expr.ExprFuncCall(
376
+ PredefinedFn.attr_getter(
377
+ item,
378
+ expr.ExprIdent("isEmpty"),
379
+ ),
380
+ [],
381
+ )
382
+ )
383
+ if prop.is_optional
384
+ else None
385
+ ),
386
+ ),
387
+ )
388
+ )
389
+
390
+ tstype = tstype.as_list_type()
391
+ else:
392
+ create_propvalue = expr.ExprMethodCall(
393
+ expr.ExprIdent(tstype.type),
394
+ "create",
395
+ [],
396
+ )
397
+ update_propvalue = expr.ExprMethodCall(
398
+ expr.ExprIdent(tstype.type),
399
+ "update",
400
+ [
401
+ PredefinedFn.attr_getter(
402
+ expr.ExprIdent("record"), expr.ExprIdent(propname)
403
+ ),
404
+ ],
405
+ )
406
+
407
+ if prop.is_optional:
408
+ ser_args.append(
409
+ (
410
+ expr.ExprIdent(prop.name),
411
+ expr.ExprTernary(
412
+ PredefinedFn.attr_getter(
413
+ expr.ExprFuncCall(
414
+ PredefinedFn.attr_getter(
415
+ expr.ExprIdent(draft_validators),
416
+ expr.ExprIdent(propname),
417
+ ),
418
+ [
419
+ PredefinedFn.attr_getter(
420
+ expr.ExprIdent("this"),
421
+ expr.ExprIdent(propname),
422
+ )
423
+ ],
424
+ ),
425
+ expr.ExprIdent("isValid"),
426
+ ),
427
+ expr.ExprMethodCall(
428
+ PredefinedFn.attr_getter(
429
+ expr.ExprIdent("this"),
430
+ expr.ExprIdent(propname),
431
+ ),
432
+ "ser",
433
+ [],
434
+ ),
435
+ expr.ExprIdent("undefined"),
436
+ ),
437
+ )
438
+ )
439
+ if not prop.data.is_private:
440
+ # private property does not include in the public record
441
+ to_record_args.append(
442
+ (
443
+ expr.ExprIdent(propname),
444
+ expr.ExprMethodCall(
445
+ PredefinedFn.attr_getter(
446
+ expr.ExprIdent("this"),
447
+ expr.ExprIdent(propname),
448
+ ),
449
+ "toRecord",
450
+ [],
451
+ ),
452
+ )
453
+ )
454
+ else:
455
+ ser_args.append(
456
+ (
457
+ expr.ExprIdent(prop.name),
458
+ expr.ExprMethodCall(
459
+ PredefinedFn.attr_getter(
460
+ expr.ExprIdent("this"),
461
+ expr.ExprIdent(propname),
462
+ ),
463
+ "ser",
464
+ [],
465
+ ),
466
+ )
467
+ )
468
+ if not prop.data.is_private:
469
+ # private property does not include in the public record
470
+ to_record_args.append(
471
+ (
472
+ expr.ExprIdent(propname),
473
+ expr.ExprMethodCall(
474
+ PredefinedFn.attr_getter(
475
+ expr.ExprIdent("this"),
476
+ expr.ExprIdent(propname),
477
+ ),
478
+ "toRecord",
479
+ [],
480
+ ),
481
+ )
482
+ )
483
+
484
+ if prop.is_optional:
485
+ # convert type to optional - for list type, we don't need to do this
486
+ # as we will use empty list as no value
487
+ tstype = tstype.as_optional_type()
488
+
489
+ for dep in tstype.deps:
490
+ program.import_(
491
+ dep,
492
+ True,
493
+ )
494
+
495
+ observable_args.append(
496
+ (
497
+ expr.ExprIdent(propname),
498
+ expr.ExprIdent("observable"),
499
+ )
500
+ )
501
+ observable_args.append(
502
+ (
503
+ expr.ExprIdent(f"update{to_pascal_case(prop.name)}"),
504
+ expr.ExprIdent("action"),
505
+ )
506
+ )
507
+
508
+ # TODO: fix me! fix me what?? next time give more context.
509
+ prop_validators.append(
510
+ (
511
+ expr.ExprIdent(propname),
512
+ expr.ExprFuncCall(
513
+ expr.ExprIdent("getValidator"),
514
+ [
515
+ PredefinedFn.list(
516
+ [
517
+ expr.ExprConstant(
518
+ constraint.get_typescript_constraint()
519
+ )
520
+ for constraint in prop.data.constraints
521
+ ]
522
+ ),
523
+ expr.ExprConstant(prop.is_optional),
524
+ ],
525
+ ),
526
+ )
527
+ )
528
+
529
+ prop_defs.append(stmt.DefClassVarStatement(propname, tstype.type))
530
+ prop_constructor_assigns.append(
531
+ stmt.AssignStatement(
532
+ PredefinedFn.attr_getter(
533
+ expr.ExprIdent("this"),
534
+ expr.ExprIdent(propname),
535
+ ),
536
+ expr.ExprIdent("args." + propname),
537
+ )
538
+ )
539
+ create_args.append((expr.ExprIdent(propname), create_propvalue))
540
+ update_args.append(
541
+ (
542
+ expr.ExprIdent(propname),
543
+ # if this is mutable property, we need to copy to make it immutable.
544
+ clone_prop(prop, update_propvalue),
545
+ )
546
+ )
547
+ update_field_funcs.append(
548
+ _update_field_func(prop, propname, tstype, draft_clsname)
549
+ )
550
+
551
+ prop_defs.append(stmt.DefClassVarStatement("stale", "boolean"))
552
+ prop_constructor_assigns.append(
553
+ stmt.AssignStatement(
554
+ PredefinedFn.attr_getter(expr.ExprIdent("this"), expr.ExprIdent("stale")),
555
+ expr.ExprIdent("args.stale"),
556
+ )
557
+ )
558
+ observable_args.append(
559
+ (
560
+ expr.ExprIdent("stale"),
561
+ expr.ExprIdent("observable"),
562
+ )
563
+ )
564
+ create_args.append(
565
+ (
566
+ expr.ExprIdent("stale"),
567
+ expr.ExprConstant(True),
568
+ ),
569
+ )
570
+ update_args.append(
571
+ (
572
+ expr.ExprIdent("stale"),
573
+ expr.ExprConstant(False),
574
+ ),
575
+ )
576
+ observable_args.sort(key=lambda x: {"observable": 0, "action": 1}[x[1].ident])
577
+
578
+ validators = expr.ExprFuncCall(
579
+ expr.ExprIdent("memoizeOneValidators"), [PredefinedFn.dict(prop_validators)]
580
+ )
581
+
582
+ # if all properties are optional, we generate a helper function that checks if all
583
+ # properties are empty,
584
+ is_empty_func = []
585
+ if all(prop.is_optional for prop in cls.properties.values()):
586
+ is_empty_func.append(stmt.LineBreak())
587
+ is_empty_func.append(
588
+ lambda ast14: ast14.func(
589
+ "isEmpty",
590
+ [],
591
+ expr.ExprIdent("boolean"),
592
+ comment="Check if this draft is empty",
593
+ )(
594
+ stmt.ReturnStatement(
595
+ expr.ExprRawTypescript(
596
+ " && ".join(
597
+ f"validators.isEmpty(this.{prop2tsname[prop.name]})"
598
+ for prop in cls.properties.values()
599
+ )
600
+ )
601
+ )
602
+ )
603
+ )
604
+
605
+ program.root(
606
+ lambda ast00: ast00.class_like(
607
+ "interface",
608
+ draft_clsname + "ConstructorArgs",
609
+ )(*prop_defs),
610
+ stmt.LineBreak(),
611
+ lambda ast10: ast10.class_(draft_clsname)(
612
+ *prop_defs,
613
+ stmt.LineBreak(),
614
+ lambda ast10: ast10.func(
615
+ "constructor",
616
+ [
617
+ DeferredVar.simple(
618
+ "args",
619
+ expr.ExprIdent(draft_clsname + "ConstructorArgs"),
620
+ ),
621
+ ],
622
+ )(
623
+ *prop_constructor_assigns,
624
+ stmt.LineBreak(),
625
+ stmt.SingleExprStatement(
626
+ expr.ExprFuncCall(
627
+ expr.ExprIdent("makeObservable"),
628
+ [
629
+ expr.ExprIdent("this"),
630
+ PredefinedFn.dict(observable_args),
631
+ ],
632
+ )
633
+ ),
634
+ ),
635
+ stmt.LineBreak(),
636
+ lambda ast11: (
637
+ ast11.func(
638
+ "isNewRecord",
639
+ [],
640
+ expr.ExprIdent("boolean"),
641
+ comment="Check if this draft is for creating a new record",
642
+ )(
643
+ stmt.ReturnStatement(
644
+ expr.ExprEqual(
645
+ PredefinedFn.attr_getter(expr.ExprIdent("this"), cls_pk[0]),
646
+ cls_pk[1],
647
+ )
648
+ )
649
+ )
650
+ if cls_pk is not None
651
+ else None
652
+ ),
653
+ stmt.LineBreak(),
654
+ lambda ast12: ast12.func(
655
+ "create",
656
+ [],
657
+ expr.ExprIdent(draft_clsname),
658
+ is_static=True,
659
+ comment="Make a new draft for creating a new record",
660
+ )(
661
+ stmt.ReturnStatement(
662
+ expr.ExprNewInstance(
663
+ expr.ExprIdent(draft_clsname),
664
+ [PredefinedFn.dict(create_args)],
665
+ )
666
+ ),
667
+ ),
668
+ stmt.LineBreak(),
669
+ lambda ast13: ast13.func(
670
+ "update",
671
+ [DeferredVar.simple("record", expr.ExprIdent(cls.name))],
672
+ expr.ExprIdent(draft_clsname),
673
+ is_static=True,
674
+ comment="Make a new draft for updating an existing record",
675
+ )(
676
+ stmt.ReturnStatement(
677
+ expr.ExprNewInstance(
678
+ expr.ExprIdent(draft_clsname),
679
+ [PredefinedFn.dict(update_args)],
680
+ )
681
+ ),
682
+ ),
683
+ *update_field_funcs,
684
+ stmt.LineBreak(),
685
+ lambda ast14: ast14.func(
686
+ "isValid",
687
+ [],
688
+ expr.ExprIdent("boolean"),
689
+ comment="Check if the draft is valid",
690
+ )(
691
+ stmt.ReturnStatement(
692
+ expr.ExprRawTypescript(
693
+ " && ".join(
694
+ f"{draft_validators}.{prop2tsname[prop.name]}(this.{prop2tsname[prop.name]}).isValid"
695
+ for prop in cls.properties.values()
696
+ )
697
+ )
698
+ )
699
+ ),
700
+ *is_empty_func,
701
+ stmt.LineBreak(),
702
+ lambda ast15: ast15.func(
703
+ "ser",
704
+ [],
705
+ expr.ExprIdent("any"),
706
+ comment="Serialize the draft to communicate with the server. `isValid` must be called first to ensure all data is valid",
707
+ )(
708
+ stmt.ReturnStatement(
709
+ PredefinedFn.dict(ser_args),
710
+ ),
711
+ ),
712
+ stmt.LineBreak(),
713
+ lambda ast16: ast16.func(
714
+ "toRecord",
715
+ [],
716
+ expr.ExprIdent(cls.name),
717
+ comment="Convert the draft to a normal record. `isValid` must be called first to ensure all data is valid",
718
+ )(
719
+ stmt.ReturnStatement(
720
+ expr.ExprNewInstance(
721
+ expr.ExprIdent(cls.name),
722
+ [PredefinedFn.dict(to_record_args)],
723
+ ),
724
+ )
725
+ ),
726
+ ),
727
+ stmt.LineBreak(),
728
+ stmt.TypescriptStatement(
729
+ f"export const {draft_validators} = " + validators.to_typescript() + ";"
730
+ ),
731
+ )
732
+
733
+ pkg.module("Draft" + cls.name).write(program)
734
+
735
+
736
+ def clone_prop(prop: DataProperty | ObjectProperty, value: expr.Expr):
737
+ # detect all complex types is hard, we can assume that any update to this does not mutate
738
+ # the original object, then it's okay.
739
+ return value
740
+
741
+
742
+ def _inject_type_for_invalid_value(tstype: TsTypeWithDep) -> TsTypeWithDep:
743
+ """
744
+ Inject a type for "invalid" values into the given TypeScript type. For context, see the discussion in Data Modeling Problems:
745
+ What would be an appropriate type for an invalid value? Since it's user input, it will be a string type.
746
+
747
+ However, there are some exceptions such as boolean type, which will always be valid and do not need injection.
748
+
749
+ If the type already includes `string` type, no changes are needed. Otherwise, we add `string` to the type. For example:
750
+ - (number | undefined) -> (number | undefined | string)
751
+ - number | undefined -> number | undefined | string
752
+ - number[] -> (number | string)[]
753
+ - (number | undefined)[] -> (number | undefined | string)[]
754
+ """
755
+ if tstype.type == "boolean":
756
+ return tstype
757
+
758
+ # TODO: fix me and make it more robust!
759
+ m = re.match(r"(\(?[a-zA-Z \|]+\)?)(\[\])", tstype.type)
760
+ if m is not None:
761
+ # This is an array type, add string to the inner type
762
+ inner_type = m.group(1)
763
+ inner_spectype = assert_not_null(
764
+ re.match(r"(\(?[a-zA-Z \|]+\)?)(\[\])", tstype.spectype)
765
+ ).group(1)
766
+ if "string" not in inner_type:
767
+ if inner_type.startswith("(") and inner_type.endswith(")"):
768
+ # Already has parentheses
769
+ inner_type = f"{inner_type[:-1]} | string)"
770
+ inner_spectype = f"{inner_spectype[:-1]} | string)"
771
+ else:
772
+ # Need to add parentheses
773
+ inner_type = f"({inner_type} | string)"
774
+ inner_spectype = f"({inner_spectype} | string)"
775
+ return TsTypeWithDep(inner_type + "[]", inner_spectype + "[]", tstype.deps)
776
+
777
+ m = re.match(r"^\(?[a-zA-Z \|]+\)?$", tstype.type)
778
+ if m is not None:
779
+ if "string" not in tstype.type:
780
+ if tstype.type.startswith("(") and tstype.type.endswith(")"):
781
+ # Already has parentheses
782
+ new_type = f"{tstype.type[:-1]} | string)"
783
+ new_spectype = f"{tstype.spectype[:-1]} | string)"
784
+ else:
785
+ # Needs parentheses for clarity
786
+ new_type = f"({tstype.type} | string)"
787
+ new_spectype = f"({tstype.spectype} | string)"
788
+ return TsTypeWithDep(new_type, new_spectype, tstype.deps)
789
+ return tstype
790
+
791
+ raise NotImplementedError(tstype.type)
792
+
793
+
794
+ def get_norm_func(
795
+ tstype: TsTypeWithDep, import_helper: ImportHelper
796
+ ) -> Callable[[expr.Expr], expr.Expr]:
797
+ """
798
+ Get the normalizer function for the given TypeScript type.
799
+ If no normalizer is available, return None.
800
+ """
801
+ norm_func = get_normalizer(tstype, import_helper)
802
+ if norm_func is not None:
803
+
804
+ def modify_expr(value: expr.Expr) -> expr.Expr:
805
+ return expr.ExprFuncCall(
806
+ norm_func,
807
+ [value],
808
+ )
809
+
810
+ return modify_expr
811
+ return identity # Return the value as is if no normalizer is available