Skip to content

Commit 811d2b5

Browse files
committed
Check explicitly for model type in TypeScript
Please see #510 for more details. This is a fix particularly for TypeScript generation.
1 parent 56482d6 commit 811d2b5

File tree

2 files changed

+888
-597
lines changed

2 files changed

+888
-597
lines changed

aas_core_codegen/typescript/jsonization/_generate.py

+113-24
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def _generate_dispatch_from_jsonable(interface: intermediate.Interface) -> Strip
357357
{I}const modelType = jsonable["modelType"];
358358
{I}if (modelType === undefined) {{
359359
{II}return newDeserializationError<AasTypes.{interface_name}>(
360-
{III}"Expected the property modelType, but got none"
360+
{III}"The required property modelType is missing"
361361
{II});
362362
{I}}}
363363
@@ -432,8 +432,10 @@ def _parse_function_for_atomic_value(
432432

433433
else:
434434
assert_never(our_type)
435+
raise AssertionError("Unexpected code path")
435436
else:
436437
assert_never(type_annotation)
438+
raise AssertionError("Unexpected code path")
437439

438440
return Stripped(function_name)
439441

@@ -456,30 +458,30 @@ def _generate_setter(cls: intermediate.ConcreteClass) -> Stripped:
456458

457459
blocks.append(Stripped(f"{prop_name}: {prop_type} = null;"))
458460

459-
blocks.append(
460-
Stripped(
461-
f"""\
462-
/**
463-
* Ignore `jsonable` and do not set anything.
464-
*
465-
* @param jsonable - to be ignored instead of set
466-
* @returns error, if any
467-
*/
468-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
469-
ignore(jsonable: JsonValue): DeserializationError | null {{
470-
{I}// Intentionally empty.
471-
{I}return null;
472-
}}"""
461+
if cls.serialization.with_model_type:
462+
# NOTE (mristin):
463+
# If the serialization requires a model type, we consequently parse and set it
464+
# in the setter. The model type thus obtained is *not* used for any dispatch. We
465+
# only use this value for verification to make sure that the model type
466+
# of the instances is consistent with the expected value for its concrete
467+
# class. This will be performed even though the code might have had to parse
468+
# model type before for the dispatch. We decided to double-check to cover the
469+
# case where a dispatch is *unnecessary* (*e.g.*, the caller knows the expected
470+
# runtime type), but the model type might still be invalid in the input. Hence,
471+
# when the dispatch is *necessary*, the model type JSON property will be parsed
472+
# twice, which is a cost we currently find acceptable.
473+
prop_name = typescript_naming.property_name(Identifier("model_type"))
474+
blocks.append(
475+
Stripped(
476+
f"""\
477+
// Used only for verification, not for dispatch!
478+
{prop_name}: string | null = null;"""
479+
)
473480
)
474-
)
475481

476482
for i, prop in enumerate(cls.properties):
477483
prop_name = typescript_naming.property_name(prop.name)
478484

479-
method_name = typescript_naming.method_name(
480-
Identifier(f"set_{prop.name}_from_jsonable")
481-
)
482-
483485
type_anno = intermediate.beneath_optional(prop.type_annotation)
484486
if isinstance(
485487
type_anno,
@@ -565,6 +567,10 @@ def _generate_setter(cls: intermediate.ConcreteClass) -> Stripped:
565567
assert_never(type_anno)
566568
raise AssertionError("Unexpected execution path")
567569

570+
method_name = typescript_naming.method_name(
571+
Identifier(f"set_{prop.name}_from_jsonable")
572+
)
573+
568574
method_writer = io.StringIO()
569575
method_writer.write(
570576
f"""\
@@ -583,6 +589,39 @@ def _generate_setter(cls: intermediate.ConcreteClass) -> Stripped:
583589

584590
blocks.append(Stripped(method_writer.getvalue()))
585591

592+
if cls.serialization.with_model_type:
593+
method_name = typescript_naming.method_name(
594+
Identifier("set_model_type_from_jsonable")
595+
)
596+
prop_name = typescript_naming.property_name(Identifier("model_type"))
597+
598+
blocks.append(
599+
Stripped(
600+
f"""\
601+
/**
602+
* Parse `jsonable` as the model type of the concrete instance.
603+
*
604+
* This is intended only for verification, and no dispatch is performed.
605+
*
606+
* @param jsonable - to be parsed
607+
* @returns error, if any
608+
*/
609+
{method_name}(
610+
{I}jsonable: JsonValue
611+
): DeserializationError | null {{
612+
{I}const parsedOrError = stringFromJsonable(
613+
{II}jsonable
614+
{I});
615+
{I}if (parsedOrError.error !== null) {{
616+
{II}return parsedOrError.error;
617+
{I}}} else {{
618+
{II}this.{prop_name} = parsedOrError.mustValue();
619+
{II}return null;
620+
{I}}}
621+
}}"""
622+
)
623+
)
624+
586625
cls_name = typescript_naming.class_name(cls.name)
587626
setter_cls_name = typescript_naming.class_name(Identifier(f"Setter_for_{cls.name}"))
588627

@@ -655,22 +694,47 @@ def _generate_setter_map(cls: intermediate.ConcreteClass) -> Stripped:
655694
{II}[
656695
"""
657696
)
697+
658698
for identifier, expression in identifiers_expressions:
659699
writer.write(
660700
f"""\
661701
{III}[
662702
{IIII}{typescript_common.string_literal(identifier)},
663703
{IIII}{indent_but_first_line(expression, IIII)}
664704
{III}],
705+
"""
706+
)
707+
708+
if cls.serialization.with_model_type:
709+
# NOTE (mristin):
710+
# If the serialization requires a model type, we consequently parse and set it
711+
# in the setter. The model type thus obtained is *not* used for any dispatch. We
712+
# only use this value for verification to make sure that the model type
713+
# of the instances is consistent with the expected value for its concrete
714+
# class. This will be performed even though the code might have had to parse
715+
# model type before for the dispatch. We decided to double-check to cover the
716+
# case where a dispatch is *unnecessary* (*e.g.*, the caller knows the expected
717+
# runtime type), but the model type might still be invalid in the input. Hence,
718+
# when the dispatch is *necessary*, the model type JSON property will be parsed
719+
# twice, which is a cost we currently find acceptable.
720+
json_identifier = naming.json_property(Identifier("model_type"))
721+
method_name = typescript_naming.method_name(
722+
Identifier("set_model_type_from_jsonable")
723+
)
724+
expression = Stripped(f"{setter_cls_name}.prototype.{method_name}")
725+
726+
writer.write(
727+
f"""\
728+
{III}[
729+
{IIII}// The model type here is used only for verification, not for dispatch.
730+
{IIII}{typescript_common.string_literal(json_identifier)},
731+
{IIII}{indent_but_first_line(expression, IIII)}
732+
{III}],
665733
"""
666734
)
667735

668736
writer.write(
669737
f"""\
670-
{III}[
671-
{IIII}"modelType",
672-
{IIII}{setter_cls_name}.prototype.ignore
673-
{III}]
674738
{II}]
675739
{I});"""
676740
)
@@ -797,6 +861,30 @@ def _generate_concrete_class_from_jsonable(
797861

798862
# endregion
799863

864+
if cls.serialization.with_model_type:
865+
model_type = naming.json_model_type(cls.name)
866+
prop_name = typescript_naming.property_name(Identifier("model_type"))
867+
868+
blocks.append(
869+
Stripped(
870+
f"""\
871+
if (setter.{prop_name} === null) {{
872+
{I}return newDeserializationError<
873+
{II}AasTypes.{cls_name}
874+
{I}>(
875+
{II}"The required property 'modelType' is missing"
876+
{I});
877+
}} else if (setter.{prop_name} != "{model_type}") {{
878+
{I}return newDeserializationError<
879+
{II}AasTypes.{cls_name}
880+
{I}>(
881+
{II}"Expected model type '{model_type}', " +
882+
{II}`but got: ${{setter.{prop_name}}}`
883+
{I});
884+
}}"""
885+
)
886+
)
887+
800888
# region Pass in arguments to the constructor
801889

802890
cls_name = typescript_naming.class_name(cls.name)
@@ -1041,6 +1129,7 @@ def _generate_transform(cls: intermediate.ConcreteClass) -> Stripped:
10411129

10421130
else:
10431131
assert_never(type_anno)
1132+
raise AssertionError("Unexpected code path")
10441133

10451134
if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation):
10461135
block = Stripped(

0 commit comments

Comments
 (0)