diff --git a/CHANGES.md b/CHANGES.md index 6f62ecd6..4d341c1e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,13 @@ 2.12.0 (xxxx-xx-xx) -* atdgen: Annotate generated code with types to disambiguate OCaml classic variants (#331) +* atdgen: Annotate generated code with types to disambiguate OCaml + classic variants (#331) +* atdpy: Support the option type more correctly so that it follows + ATD's convention for JSON encoding. This allows compatibility with + JSON produced by other tools of the ATD suite. The Python type, + however, is still a nullable (`Optional`) to make things simpler for + Python programmers. This prevents distinguishing `["Some", "None"]` + from `"None"` which both translate to `None` in Python. (#332) 2.11.0 (2023-02-08) ------------------- diff --git a/atdpy/src/lib/Codegen.ml b/atdpy/src/lib/Codegen.ml index bd226531..915166f6 100644 --- a/atdpy/src/lib/Codegen.ml +++ b/atdpy/src/lib/Codegen.ml @@ -332,6 +332,19 @@ def _atd_read_nullable(read_elt: Callable[[Any], Any]) \ return read_nullable +def _atd_read_option(read_elt: Callable[[Any], Any]) \ + -> Callable[[Optional[Any]], Optional[Any]]: + def read_option(x: Any) -> Any: + if x == 'None': + return None + elif isinstance(x, List) and len(x) == 2 and x[0] == 'Some': + return read_elt(x[1]) + else: + _atd_bad_json('option', x) + raise AssertionError('impossible') # keep mypy happy + return read_option + + def _atd_write_unit(x: Any) -> None: if x is None: return x @@ -427,6 +440,16 @@ def _atd_write_nullable(write_elt: Callable[[Any], Any]) \ return write_nullable +def _atd_write_option(write_elt: Callable[[Any], Any]) \ + -> Callable[[Optional[Any]], Optional[Any]]: + def write_option(x: Any) -> Any: + if x is None: + return 'None' + else: + return ['Some', write_elt(x)] + return write_option + + ############################################################################ # Public classes ############################################################################|} @@ -609,7 +632,8 @@ let rec json_writer env e = sprintf "_atd_write_assoc_list_to_object(%s)" (json_writer env value) ) - | Option (loc, e, an) + | Option (loc, e, an) -> + sprintf "_atd_write_option(%s)" (json_writer env e) | Nullable (loc, e, an) -> sprintf "_atd_write_nullable(%s)" (json_writer env e) | Shared (loc, e, an) -> not_implemented loc "shared" @@ -690,7 +714,8 @@ let rec json_reader env (e : type_expr) = sprintf "_atd_read_assoc_object_into_list(%s)" (json_reader env value) ) - | Option (loc, e, an) + | Option (loc, e, an) -> + sprintf "_atd_read_option(%s)" (json_reader env e) | Nullable (loc, e, an) -> sprintf "_atd_read_nullable(%s)" (json_reader env e) | Shared (loc, e, an) -> not_implemented loc "shared" diff --git a/atdpy/test/python-expected/everything.py b/atdpy/test/python-expected/everything.py index 15e4b83b..b1c27e37 100644 --- a/atdpy/test/python-expected/everything.py +++ b/atdpy/test/python-expected/everything.py @@ -137,6 +137,19 @@ def read_nullable(x: Any) -> Any: return read_nullable +def _atd_read_option(read_elt: Callable[[Any], Any]) \ + -> Callable[[Optional[Any]], Optional[Any]]: + def read_option(x: Any) -> Any: + if x == 'None': + return None + elif isinstance(x, List) and len(x) == 2 and x[0] == 'Some': + return read_elt(x[1]) + else: + _atd_bad_json('option', x) + raise AssertionError('impossible') # keep mypy happy + return read_option + + def _atd_write_unit(x: Any) -> None: if x is None: return x @@ -232,6 +245,16 @@ def write_nullable(x: Any) -> Any: return write_nullable +def _atd_write_option(write_elt: Callable[[Any], Any]) \ + -> Callable[[Optional[Any]], Optional[Any]]: + def write_option(x: Any) -> Any: + if x is None: + return 'None' + else: + return ['Some', write_elt(x)] + return write_option + + ############################################################################ # Public classes ############################################################################ @@ -499,7 +522,7 @@ def from_json(cls, x: Any) -> 'Root': assoc3=_atd_read_assoc_array_into_dict(_atd_read_float, _atd_read_int)(x['assoc3']) if 'assoc3' in x else _atd_missing_json_field('Root', 'assoc3'), assoc4=_atd_read_assoc_object_into_dict(_atd_read_int)(x['assoc4']) if 'assoc4' in x else _atd_missing_json_field('Root', 'assoc4'), nullables=_atd_read_list(_atd_read_nullable(_atd_read_int))(x['nullables']) if 'nullables' in x else _atd_missing_json_field('Root', 'nullables'), - options=_atd_read_list(_atd_read_nullable(_atd_read_int))(x['options']) if 'options' in x else _atd_missing_json_field('Root', 'options'), + options=_atd_read_list(_atd_read_option(_atd_read_int))(x['options']) if 'options' in x else _atd_missing_json_field('Root', 'options'), untyped_things=_atd_read_list((lambda x: x))(x['untyped_things']) if 'untyped_things' in x else _atd_missing_json_field('Root', 'untyped_things'), parametrized_record=IntFloatParametrizedRecord.from_json(x['parametrized_record']) if 'parametrized_record' in x else _atd_missing_json_field('Root', 'parametrized_record'), parametrized_tuple=KindParametrizedTuple.from_json(x['parametrized_tuple']) if 'parametrized_tuple' in x else _atd_missing_json_field('Root', 'parametrized_tuple'), @@ -524,7 +547,7 @@ def to_json(self) -> Any: res['assoc3'] = _atd_write_assoc_dict_to_array(_atd_write_float, _atd_write_int)(self.assoc3) res['assoc4'] = _atd_write_assoc_dict_to_object(_atd_write_int)(self.assoc4) res['nullables'] = _atd_write_list(_atd_write_nullable(_atd_write_int))(self.nullables) - res['options'] = _atd_write_list(_atd_write_nullable(_atd_write_int))(self.options) + res['options'] = _atd_write_list(_atd_write_option(_atd_write_int))(self.options) res['untyped_things'] = _atd_write_list((lambda x: x))(self.untyped_things) res['parametrized_record'] = (lambda x: x.to_json())(self.parametrized_record) res['parametrized_tuple'] = (lambda x: x.to_json())(self.parametrized_tuple) diff --git a/atdpy/test/python-tests/test_atdpy.py b/atdpy/test/python-tests/test_atdpy.py index f1a8de8d..f746b603 100644 --- a/atdpy/test/python-tests/test_atdpy.py +++ b/atdpy/test/python-tests/test_atdpy.py @@ -168,9 +168,15 @@ def test_everything_to_json() -> None: 34 ], "options": [ - 56, - null, - 78 + [ + "Some", + 56 + ], + "None", + [ + "Some", + 78 + ] ], "untyped_things": [ [ diff --git a/doc/atdpy.rst b/doc/atdpy.rst index 68c8d918..bbad7c14 100644 --- a/doc/atdpy.rst +++ b/doc/atdpy.rst @@ -234,33 +234,35 @@ Reference Type mapping ------------ -+--------------------+----------------------+-------------------------+ -| ATD type | Python type | JSON example | -+====================+======================+=========================+ -| ``unit`` | ``None`` | ``null`` | -+--------------------+----------------------+-------------------------+ -| ``bool`` | ``bool`` | ``True`` | -+--------------------+----------------------+-------------------------+ -| ``int`` | ``int`` | ``42`` | -+--------------------+----------------------+-------------------------+ -| ``float`` | ``float`` | ``6.28`` | -+--------------------+----------------------+-------------------------+ -| ``string`` | ``str`` | ``"Hello"`` | -+--------------------+----------------------+-------------------------+ -| ``int list`` | ``List[int]`` | ``[1, 2, 3]`` | -+--------------------+----------------------+-------------------------+ -| ``(int * int)`` | ``Tuple[int, int]`` | ``[-1, 1]`` | -+--------------------+----------------------+-------------------------+ -| ``int nullable`` | ``Union[int, None]`` | ``42`` or ``null`` | -+--------------------+----------------------+-------------------------+ -| ``abstract`` | ``Any`` | anything | -+--------------------+----------------------+-------------------------+ -| record type | class | ``{"id": 17}`` | -+--------------------+----------------------+-------------------------+ -| ``[A | B of int]`` | ``Union[A, B]`` | ``"A"`` or ``["B", 5]`` | -+--------------------+----------------------+-------------------------+ -| ``foo_bar`` | ``FooBar`` | | -+--------------------+----------------------+-------------------------+ ++--------------------+----------------------+--------------------------------+ +| ATD type | Python type | JSON example | ++====================+======================+================================+ +| ``unit`` | ``None`` | ``null`` | ++--------------------+----------------------+--------------------------------+ +| ``bool`` | ``bool`` | ``True`` | ++--------------------+----------------------+--------------------------------+ +| ``int`` | ``int`` | ``42`` | ++--------------------+----------------------+--------------------------------+ +| ``float`` | ``float`` | ``6.28`` | ++--------------------+----------------------+--------------------------------+ +| ``string`` | ``str`` | ``"Hello"`` | ++--------------------+----------------------+--------------------------------+ +| ``int list`` | ``List[int]`` | ``[1, 2, 3]`` | ++--------------------+----------------------+--------------------------------+ +| ``(int * int)`` | ``Tuple[int, int]`` | ``[-1, 1]`` | ++--------------------+----------------------+--------------------------------+ +| ``int nullable`` | ``Optional[int]`` | ``42`` or ``null`` | ++--------------------+----------------------+--------------------------------+ +| ``int option`` | ``Optional[int]`` | ``["Some", 42]`` or ``"None"`` | ++--------------------+----------------------+--------------------------------+ +| ``abstract`` | ``Any`` | anything | ++--------------------+----------------------+--------------------------------+ +| record type | class | ``{"id": 17}`` | ++--------------------+----------------------+--------------------------------+ +| ``[A | B of int]`` | ``Union[A, B]`` | ``"A"`` or ``["B", 5]`` | ++--------------------+----------------------+--------------------------------+ +| ``foo_bar`` | ``FooBar`` | | ++--------------------+----------------------+--------------------------------+ Supported ATD annotations -------------------------