diff --git a/aas_core_codegen/cpp/lib/_generate_json_value_verification.py b/aas_core_codegen/cpp/lib/_generate_json_value_verification.py index c231c42a6..12f142be9 100644 --- a/aas_core_codegen/cpp/lib/_generate_json_value_verification.py +++ b/aas_core_codegen/cpp/lib/_generate_json_value_verification.py @@ -50,8 +50,8 @@ def _generate_json_value_verificator_declaration() -> List[Stripped]: * \\brief Exhaustively verify that a ``nlohmann::json`` value is JSON-able. * * A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys -- in particular, ``null``, a binary + * a ``null``, a boolean, a number, a string, an array of JSON-able values or + * an object of JSON-able values with string keys -- in particular, a binary * value and a discarded value are rejected, at any depth. * * We deliberately walk the value with an explicit stack instead of plain @@ -188,10 +188,10 @@ def _generate_json_value_verificator_implementation() -> List[Stripped]: common::optional CheckShallowly( {I}const nlohmann::json& value ) {{ -{I}if (value.is_null() || value.is_binary() || value.is_discarded()) {{ +{I}if (value.is_binary() || value.is_discarded()) {{ {II}return common::make_optional( {III}common::Concat( -{IIII}L"Expected a JSON-able value (a boolean, a number, a string, " +{IIII}L"Expected a JSON-able value (a null, a boolean, a number, a string, " {IIII}L"an array or an object), but got a value of type: ", {IIII}common::Utf8ToWstring(value.type_name()) {III}) diff --git a/aas_core_codegen/cpp/lib/_generate_jsonization.py b/aas_core_codegen/cpp/lib/_generate_jsonization.py index d448d26b9..2eddc0fc1 100644 --- a/aas_core_codegen/cpp/lib/_generate_jsonization.py +++ b/aas_core_codegen/cpp/lib/_generate_jsonization.py @@ -1010,8 +1010,8 @@ def _generate_deserialize_json_value() -> Stripped: """ Generate the function to recursively de-serialize a JSON-able value. - This rejects ``null``, a binary value and a discarded value -- none of - which are representable as a ``JSONValue`` -- at any depth, but does not + This rejects a binary value and a discarded value -- neither of which is + representable as a ``JSONValue`` -- at any depth, but does not otherwise restrict the top-level shape (unlike :py:func:`_generate_deserialize_json_array`/ :py:func:`_generate_deserialize_json_object`). @@ -1024,7 +1024,7 @@ def _generate_deserialize_json_value() -> Stripped: > DeserializeJsonValue( {I}const nlohmann::json& json ) {{ -{I}if (json.is_null() || json.is_binary() || json.is_discarded()) {{ +{I}if (json.is_binary() || json.is_discarded()) {{ {II}return std::make_pair< {III}common::optional, {III}common::optional @@ -1032,7 +1032,7 @@ def _generate_deserialize_json_value() -> Stripped: {III}common::nullopt, {III}common::make_optional( {IIII}common::Concat( -{IIIII}L"Expected a JSON-able value (a boolean, a number, a string, " +{IIIII}L"Expected a JSON-able value (a null, a boolean, a number, a string, " {IIIII}L"an array or an object), but got a value of type: ", {IIIII}common::Utf8ToWstring(json.type_name()) {IIII}) @@ -1186,7 +1186,7 @@ def _generate_serialize_json_value() -> Stripped: This re-validates the value even though it is already a ``nlohmann::json`` in memory, since ``nlohmann::json`` can represent - shapes (``null``, a binary value, a discarded value) which are not + shapes (a binary value, a discarded value) which are not representable as a ``JSONValue`` -- the constructor of a class instance does not, by itself, guard against these. """ @@ -1198,12 +1198,12 @@ def _generate_serialize_json_value() -> Stripped: > SerializeJsonValue( {I}const nlohmann::json& value ) {{ -{I}if (value.is_null() || value.is_binary() || value.is_discarded()) {{ +{I}if (value.is_binary() || value.is_discarded()) {{ {II}return std::make_pair( {III}common::nullopt, {III}common::make_optional( {IIII}common::Concat( -{IIIII}L"Expected a JSON-able value (a boolean, a number, a string, " +{IIIII}L"Expected a JSON-able value (a null, a boolean, a number, a string, " {IIIII}L"an array or an object), but got a value of type: ", {IIIII}common::Utf8ToWstring(value.type_name()) {IIII}) @@ -2952,6 +2952,29 @@ def _generate_parse_properties_of_cls_implementation( parse_expr = _json_parse_expr(prop) + # NOTE (mristin): + # An optional JSON-able value which is a JSON null is treated as absent, + # as most of the SDKs can not tell the two apart. + if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation) and ( + isinstance(prop.type_annotation.value, intermediate.JsonValueTypeAnnotation) + ): + case_blocks.append( + Stripped( + f"""\ +case properties::{enum_name}::{literal_name}: {{ +{I}common::optional parse_error = ParseInto( +{II}{var_name}, +{II}{indent_but_first_line(parse_expr, II)} +{I}); +{I}if (!parse_error.has_value() && {var_name}->is_null()) {{ +{II}{var_name} = common::nullopt; +{I}}} +{I}return parse_error; +}}""" + ) + ) + continue + case_blocks.append( Stripped( f"""\ @@ -5015,6 +5038,15 @@ def _generate_serialize_property( types_namespace=cpp_common.TYPES_NAMESPACE, ) + # NOTE (mristin): + # An optional JSON-able value which is a JSON null is treated as absent, + # as most of the SDKs can not tell the two apart. + condition = ( + f"{maybe_var}.has_value() && !{maybe_var}->is_null()" + if isinstance(type_anno, intermediate.JsonValueTypeAnnotation) + else f"{maybe_var}.has_value()" + ) + # NOTE (mristin): # We test the binding rather than the getter. The getter is virtual, so # calling it a second time here would double its cost for every optional @@ -5024,7 +5056,7 @@ def _generate_serialize_property( {maybe_var_type} {maybe_var}( {I}that.{getter}() ); -if ({maybe_var}.has_value()) {{ +if ({condition}) {{ {I}{indent_but_first_line(code, I)} }}""" ) diff --git a/aas_core_codegen/cpp/lib/_generate_xml_rpc.py b/aas_core_codegen/cpp/lib/_generate_xml_rpc.py index 59b078f9a..244a3b8bf 100644 --- a/aas_core_codegen/cpp/lib/_generate_xml_rpc.py +++ b/aas_core_codegen/cpp/lib/_generate_xml_rpc.py @@ -5,10 +5,11 @@ # the public headers in ``include/``), just like ``xml_common`` on which it # depends. It implements a restricted subset of XML-RPC's own element # vocabulary -- ````, ````, ````, ````, -# ````, ````, ````, ```` and ```` -- to -# de/serialize a ``nlohmann::json`` value which is JSON-able (*i.e.*, -# recursively a boolean, a number, a string, an array of JSON-able values or -# an object of JSON-able values with string keys; never ``null``). +# ````, ````, ````, ```` and ````, together +# with the widespread ```` extension -- to de/serialize +# a ``nlohmann::json`` value which is JSON-able (*i.e.*, recursively a null, +# a boolean, a number, a string, an array of JSON-able values or an object of +# JSON-able values with string keys). # # It is meant to be used in two different ways: # * Stand-alone, over a whole ```` document (see @@ -124,8 +125,8 @@ def _generate_deserialize_definitions() -> List[Stripped]: * document which is already being read through \\p reader. * * \\p reader is expected to be positioned at the start element of - * the discriminator (\\c \\, \\c \\, \\c \\, - * \\c \\ or \\c \\). On success, \\p reader is left + * the discriminator (\\c \\, \\c \\, \\c \\, + * \\c \\, \\c \\ or \\c \\). On success, \\p reader is left * positioned at the node right after the discriminator's own stop element, * mirroring how the primitive ``Deserialize*`` functions in ``xmlization`` * behave. @@ -221,8 +222,8 @@ def _generate_serialize_definitions() -> List[Stripped]: * * \\param os the UTF-8-encoded output stream where the XML will be written * \\param value to be serialized; it must be JSON-able (recursively - * a boolean, a number, a string, an array or an object with string keys), - * and never \\c null, or a \\ref SerializationError is returned + * a null, a boolean, a number, a string, an array or an object with string + * keys), or a \\ref SerializationError is returned * \\return An error, if any. * * The XML-RPC elements reside in no namespace at all, as the XML-RPC @@ -240,8 +241,9 @@ def _generate_serialize_definitions() -> List[Stripped]: * \\brief Serialize \\p value, embedded within a larger XML document which is * already being written through \\p writer. * - * This writes exactly one discriminator element (\\c \\, - * \\c \\, \\c \\, \\c \\ or \\c \\). + * This writes exactly one discriminator element (\\c \\, + * \\c \\, \\c \\, \\c \\, \\c \\ or + * \\c \\). * * \\p undeclare_namespace tells whether that element is the outermost one * which resides in no namespace. That one undeclares the default namespace of @@ -529,7 +531,7 @@ def _generate_deserialize_scalar_bodies_implementation() -> List[Stripped]: (or, for a ````, possibly already at the stop element if the string is empty). None of them consume the discriminator's own stop element -- that is uniformly handled by ``DeserializeValueFrom`` for all - five discriminators alike. + six discriminators alike. """ return [ Stripped( @@ -757,7 +759,7 @@ def _generate_deserialize_value_from_implementation() -> Stripped: {III}common::nullopt, {III}common::make_optional( {IIII}common::Concat( -{IIIII}L"Expected one of , , , " +{IIIII}L"Expected one of , , , , " {IIIII}L"or , but got ", {IIIII}xml_common::NodeToHumanReadableWstring(reader.node()) {IIII}) @@ -782,7 +784,16 @@ def _generate_deserialize_value_from_implementation() -> Stripped: {I}common::optional value; {I}common::optional error; -{I}if (discriminator == "boolean") {{ +{I}if (discriminator == "nil") {{ +{II}// NOTE (mristin): +{II}// XML-RPC itself knows no null, so we follow the widespread extension, +{II}// and read a JSON null from an empty . Any content other than +{II}// whitespace is refused by the check of the stop element below. +{II}error = SkipWhitespace(reader); +{II}if (!error.has_value()) {{ +{III}value = nlohmann::json(nullptr); +{II}}} +{I}}} else if (discriminator == "boolean") {{ {II}std::tie(value, error) = DeserializeBooleanBodyFrom(reader); {I}}} else if (discriminator == "double") {{ {II}std::tie(value, error) = DeserializeDoubleBodyFrom(reader); @@ -797,7 +808,7 @@ def _generate_deserialize_value_from_implementation() -> Stripped: {III}common::nullopt, {III}common::make_optional( {IIII}common::Concat( -{IIIII}L"Expected one of , , , " +{IIIII}L"Expected one of , , , , " {IIIII}L"or , but got a start element <", {IIIII}common::Utf8ToWstring(discriminator), {IIIII}L">" @@ -1312,7 +1323,13 @@ def _generate_serialize_value_body_to_implementation() -> Stripped: {I}const nlohmann::json& value, {I}bool undeclare_namespace ) {{ -{I}if (value.is_boolean()) {{ +{I}if (value.is_null()) {{ +{II}// NOTE (mristin): +{II}// XML-RPC itself knows no null, so we follow the widespread extension, +{II}// and write a JSON null as an empty . +{II}writer.StartElementInNoNamespace("nil", undeclare_namespace); +{II}writer.StopElement("nil"); +{I}}} else if (value.is_boolean()) {{ {II}writer.StartElementInNoNamespace( {III}"boolean", undeclare_namespace {II}); @@ -1383,11 +1400,11 @@ def _generate_serialize_value_body_to_implementation() -> Stripped: {II}writer.StopElement("struct"); {I}}} else {{ {II}// NOTE (mristin): -{II}// This covers ``null``, a binary value and a discarded value -- none of -{II}// which are representable as a ``JSONValue``. +{II}// This covers a binary value and a discarded value -- neither of which +{II}// is representable as a ``JSONValue``. {II}return common::make_optional( {III}common::Concat( -{IIII}L"Expected a JSON-able value (a boolean, a number, a string, " +{IIII}L"Expected a JSON-able value (a null, a boolean, a number, a string, " {IIII}L"an array or an object), but got a value of type: ", {IIII}common::Utf8ToWstring(value.type_name()) {III}) diff --git a/aas_core_codegen/cpp/lib/_generate_xmlization.py b/aas_core_codegen/cpp/lib/_generate_xmlization.py index a1d6b41ed..8200823aa 100644 --- a/aas_core_codegen/cpp/lib/_generate_xmlization.py +++ b/aas_core_codegen/cpp/lib/_generate_xmlization.py @@ -3532,6 +3532,29 @@ def _generate_from_sequence( var_name = cpp_naming.variable_name(Identifier(f"the_{prop.name}")) + # NOTE (mristin): + # An optional JSON-able value which is a JSON null is treated as absent, + # as most of the SDKs can not tell the two apart. + if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation) and ( + isinstance(prop.type_annotation.value, intermediate.JsonValueTypeAnnotation) + ): + case_blocks.append( + Stripped( + f"""\ +case properties::{prop_enum_name}::{prop_literal}: {{ +{I}common::optional read_error = ReadInto( +{II}{var_name}, +{II}{indent_but_first_line(read_expr, II)} +{I}); +{I}if (!read_error.has_value() && {var_name}->is_null()) {{ +{II}{var_name} = common::nullopt; +{I}}} +{I}return read_error; +}}""" + ) + ) + continue + case_blocks.append( Stripped( f"""\ @@ -4960,6 +4983,33 @@ def _generate_write_property_statements(prop: intermediate.Property) -> Stripped writer_exprs_joined = ",\n".join(writer_exprs) + # NOTE (mristin): + # An optional JSON-able value which is a JSON null is treated as absent, + # as most of the SDKs can not tell the two apart. + if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation) and ( + isinstance(type_anno, intermediate.JsonValueTypeAnnotation) + ): + maybe_var = cpp_naming.variable_name(Identifier(f"maybe_{prop.name}")) + + return Stripped( + f"""\ +const common::optional& {maybe_var}( +{I}that.{getter_name}() +); +if ({maybe_var}.has_value() && !{maybe_var}->is_null()) {{ +{I}error = {function_name}( +{II}{xml_name_literal}, +{II}*{maybe_var}, +{II}writer, +{II}iteration::Property::{prop_literal}, +{II}{indent_but_first_line(writer_exprs_joined, II)} +{I}); +{I}if (error.has_value()) {{ +{II}return error; +{I}}} +}}""" + ) + return Stripped( f"""\ error = {function_name}( diff --git a/aas_core_codegen/cpp/optionaling.py b/aas_core_codegen/cpp/optionaling.py index 97290135b..de98447eb 100644 --- a/aas_core_codegen/cpp/optionaling.py +++ b/aas_core_codegen/cpp/optionaling.py @@ -185,9 +185,9 @@ def transform_index(self, node: parse_tree.Index) -> Optional[Error]: ): # NOTE (mristin): # An item of a JSON-able array, and a value of a JSON-able object, - # is itself a JSON-able value. A JSON-able value is never optional - # -- JSON knows a ``null``, but a ``JSONValue`` deliberately does - # not. + # is itself a JSON-able value, which is a plain ``nlohmann::json`` + # and never a ``common::optional``. It might be a JSON null, but + # that is a value of ``nlohmann::json`` like any other. self.is_optional_map[node] = False return None diff --git a/aas_core_codegen/cpp/tests/_generate_test_json_value_verification.py b/aas_core_codegen/cpp/tests/_generate_test_json_value_verification.py index a06baa1f0..a0f5e01d1 100644 --- a/aas_core_codegen/cpp/tests/_generate_test_json_value_verification.py +++ b/aas_core_codegen/cpp/tests/_generate_test_json_value_verification.py @@ -118,8 +118,29 @@ def generate_implementation(library_namespace: Stripped) -> str: ), Stripped( f"""\ -TEST_CASE("Reject a null value at the top level") {{ -{I}const nlohmann::json value(nullptr); +TEST_CASE("Accept a null value at every depth") {{ +{I}for ( +{II}const char* text : {{ +{III}"null", +{III}"[null]", +{III}R"({{"a": [null, {{"b": null}}]}})" +{II}}} +{I}) {{ +{II}const nlohmann::json value = nlohmann::json::parse(text); + +{II}verification::JsonValueVerificator verificator( +{III}value, verification::JsonValueShape::kAny +{II}); + +{II}INFO(std::string("The value is: ") + text); +{II}REQUIRE(CollectErrors(verificator).empty()); +{I}}} +}}""" + ), + Stripped( + f"""\ +TEST_CASE("Reject a discarded value at the top level") {{ +{I}const nlohmann::json value(nlohmann::json::value_t::discarded); {I}verification::JsonValueVerificator verificator( {II}value, verification::JsonValueShape::kAny @@ -181,8 +202,10 @@ def generate_implementation(library_namespace: Stripped) -> str: {I}"Report every rejected value at every depth, not just the first" ) {{ {I}nlohmann::json value = nlohmann::json::parse( -{II}R"({{"a": [1.0, null, {{"b": null}}]}})" +{II}R"({{"a": [1.0, 0.0, {{"b": 0.0}}]}})" {I}); +{I}value["a"][1] = std::numeric_limits::quiet_NaN(); +{I}value["a"][2]["b"] = std::numeric_limits::infinity(); {I}verification::JsonValueVerificator verificator( {II}value, verification::JsonValueShape::kAny @@ -191,8 +214,8 @@ def generate_implementation(library_namespace: Stripped) -> str: {I}const std::vector errors(CollectErrors(verificator)); {I}INFO( -{II}"Expected exactly two violations (one for each nested null), " -{II}"but got: " +{II}"Expected exactly two violations (one for each nested non-finite " +{II}"number), but got: " {II}+ std::to_string(errors.size()) {I}); {I}REQUIRE(errors.size() == 2); @@ -210,8 +233,10 @@ def generate_implementation(library_namespace: Stripped) -> str: {I}// what distinguishes the depth-first walk from a breadth-first one -- {I}// the latter would report ``z`` first, as it sits one level higher. {I}nlohmann::json value = nlohmann::json::parse( -{II}R"({{"a": {{"deep": null}}, "z": null}})" +{II}R"({{"a": {{"deep": 0.0}}, "z": 0.0}})" {I}); +{I}value["a"]["deep"] = std::numeric_limits::quiet_NaN(); +{I}value["z"] = std::numeric_limits::quiet_NaN(); {I}verification::JsonValueVerificator verificator( {II}value, verification::JsonValueShape::kObject @@ -234,8 +259,9 @@ def generate_implementation(library_namespace: Stripped) -> str: {I}// ``nlohmann::json`` value, so a key is escaped exactly as a JSON {I}// string is. {I}nlohmann::json value = nlohmann::json::parse( -{II}R"({{"a\\"b\\\\c\\nd": null}})" +{II}R"({{"a\\"b\\\\c\\nd": 0.0}})" {I}); +{I}value.begin().value() = std::numeric_limits::quiet_NaN(); {I}verification::JsonValueVerificator verificator( {II}value, verification::JsonValueShape::kObject diff --git a/aas_core_codegen/cpp/tests/_generate_test_xml_rpc.py b/aas_core_codegen/cpp/tests/_generate_test_xml_rpc.py index d2af329c0..de6b7920b 100644 --- a/aas_core_codegen/cpp/tests/_generate_test_xml_rpc.py +++ b/aas_core_codegen/cpp/tests/_generate_test_xml_rpc.py @@ -227,12 +227,37 @@ def generate_implementation(library_namespace: Stripped) -> str: ), Stripped( f"""\ -TEST_CASE("Serialize rejects null") {{ +TEST_CASE("Round-trip a null at every depth") {{ +{I}RequireRoundTrip(nlohmann::json(nullptr)); + +{I}RequireRoundTrip( +{II}nlohmann::json::parse( +{III}R"({{"a": null, "b": [null, {{"c": null}}]}})" +{II}) +{I}); +}}""" + ), + Stripped( + f"""\ +TEST_CASE("Serialize a null as an empty nil") {{ {I}std::ostringstream oss; {I}common::optional error( {II}xml_rpc::SerializeValue(oss, nlohmann::json(nullptr)) {I}); -{I}REQUIRE(error.has_value()); +{I}REQUIRE(!error.has_value()); +{I}REQUIRE(oss.str().find("") != std::string::npos); +}}""" + ), + Stripped( + f"""\ +TEST_CASE("Deserialize rejects a nil with text") {{ +{I}std::istringstream iss( +{II}"x" +{I}); +{I}common::expected result( +{II}xml_rpc::DeserializeValue(iss) +{I}); +{I}REQUIRE(!result.has_value()); }}""" ), Stripped( @@ -240,7 +265,7 @@ def generate_implementation(library_namespace: Stripped) -> str: TEST_CASE("Serialize reports the path to a nested invalid value") {{ {I}nlohmann::json value = nlohmann::json::array(); {I}value.push_back(1.0); -{I}value.push_back(nlohmann::json(nullptr)); +{I}value.push_back(std::numeric_limits::quiet_NaN()); {I}std::ostringstream oss; {I}common::optional error( @@ -255,7 +280,7 @@ def generate_implementation(library_namespace: Stripped) -> str: TEST_CASE("Serialize reports the key of an invalid member") {{ {I}nlohmann::json value = nlohmann::json::object(); {I}value["a"] = 1.0; -{I}value["b"] = nlohmann::json(nullptr); +{I}value["b"] = std::numeric_limits::quiet_NaN(); {I}std::ostringstream oss; {I}common::optional error( @@ -281,7 +306,7 @@ def generate_implementation(library_namespace: Stripped) -> str: {II}"" {II}"nested" {II}"1" -{II}"" +{II}"1" {II}"" {II}"" {I}); @@ -310,7 +335,7 @@ def generate_implementation(library_namespace: Stripped) -> str: {I}// a property. {I}std::istringstream iss( {II}"" -{II}"" +{II}"1" {II}"" {I}); {I}xml_common::ReaderMergingText reader( @@ -334,7 +359,7 @@ def generate_implementation(library_namespace: Stripped) -> str: {I}// definition an object holds the elements directly. {I}std::istringstream iss( {II}"" -{II}"k" +{II}"k1" {II}"" {I}); {I}xml_common::ReaderMergingText reader( @@ -358,7 +383,7 @@ def generate_implementation(library_namespace: Stripped) -> str: {I}// break out of the XPath predicate which matches on it. {I}std::istringstream iss( {II}"" -{II}"a&b"c" +{II}"a&b"c1" {II}"" {I}); {I}xml_common::ReaderMergingText reader( @@ -381,7 +406,7 @@ def generate_implementation(library_namespace: Stripped) -> str: f"""\ TEST_CASE("Deserialize rejects an unexpected element") {{ {I}std::istringstream iss( -{II}"" +{II}"1" {I}); {I}common::expected result( {II}xml_rpc::DeserializeValue(iss) diff --git a/aas_core_codegen/cpp/transpilation.py b/aas_core_codegen/cpp/transpilation.py index beb6cba19..b6d9c9b5a 100644 --- a/aas_core_codegen/cpp/transpilation.py +++ b/aas_core_codegen/cpp/transpilation.py @@ -2047,12 +2047,40 @@ def transform_constant( raise AssertionError("Should not have gotten here") + def _json_nullness_check( + self, node: parse_tree.Expression, value: Stripped + ) -> Optional[Stripped]: + """ + Generate the check that the JSON-able ``value`` is neither absent nor null. + + Return ``None`` if the ``node`` is not a JSON-able value. + + A JSON-able value is never absent if it is not optional, *e.g.*, a guarded + index, but might still be a JSON null. An optional JSON-able value which is + a JSON null is treated as absent, as most of the SDKs can not tell the two + apart. + """ + node_type = self.type_map[node] + + if isinstance(node_type, intermediate_type_inference.JsonValueTypeAnnotation): + return Stripped(f"!{value}.is_null()") + + if isinstance( + node_type, intermediate_type_inference.OptionalTypeAnnotation + ) and isinstance( + node_type.value, intermediate_type_inference.JsonValueTypeAnnotation + ): + return Stripped(f"({value}.has_value() && !{value}->is_null())") + + return None + def transform_is_none( self, node: parse_tree.IsNone ) -> Tuple[Optional[Stripped], Optional[Error]]: value, error = self.transform(node.value) if error is not None: return None, error + assert value is not None no_parentheses_types = ( parse_tree.Member, @@ -2066,10 +2094,24 @@ def transform_is_none( parse_tree.All, parse_tree.Any, ) - if isinstance(node.value, no_parentheses_types): - return Stripped(f"!({value}.has_value())"), None - else: - return Stripped(f"!(({value}).has_value())"), None + + if not isinstance(node.value, no_parentheses_types): + value = Stripped(f"({value})") + + # NOTE (mristin): + # A JSON-able value might be a JSON null, which ``nlohmann::json`` + # represents as a null value, see :py:meth:`_json_nullness_check`. + if isinstance( + self.type_map[node.value], + intermediate_type_inference.JsonValueTypeAnnotation, + ): + return Stripped(f"{value}.is_null()"), None + + json_nullness_check = self._json_nullness_check(node=node.value, value=value) + if json_nullness_check is not None: + return Stripped(f"!{json_nullness_check}"), None + + return Stripped(f"!({value}.has_value())"), None def transform_is_not_none( self, node: parse_tree.IsNotNone @@ -2077,6 +2119,7 @@ def transform_is_not_none( value, error = self.transform(node.value) if error is not None: return None, error + assert value is not None no_parentheses_types_in_this_context = ( parse_tree.Member, @@ -2090,10 +2133,15 @@ def transform_is_not_none( parse_tree.All, parse_tree.Any, ) - if isinstance(node.value, no_parentheses_types_in_this_context): - return Stripped(f"{value}.has_value()"), None - else: - return Stripped(f"({value}).has_value()"), None + + if not isinstance(node.value, no_parentheses_types_in_this_context): + value = Stripped(f"({value})") + + json_nullness_check = self._json_nullness_check(node=node.value, value=value) + if json_nullness_check is not None: + return json_nullness_check, None + + return Stripped(f"{value}.has_value()"), None @abc.abstractmethod def transform_name( diff --git a/aas_core_codegen/csharp/common.py b/aas_core_codegen/csharp/common.py index 0ec673c74..10a22fa29 100644 --- a/aas_core_codegen/csharp/common.py +++ b/aas_core_codegen/csharp/common.py @@ -198,10 +198,18 @@ def generate_type( value = generate_type( type_annotation=type_annotation.value, our_type_qualifier=our_type_qualifier ) + + # NOTE (mristin): + # A JSON-able value is already nullable, see below. + if isinstance(type_annotation.value, intermediate.JsonValueTypeAnnotation): + return value + return Stripped(f"{value}?") elif isinstance(type_annotation, intermediate.JsonValueTypeAnnotation): - return Stripped("Nodes.JsonNode") + # NOTE (mristin): + # A JSON null is a C# ``null`` in ``System.Text.Json.Nodes``. + return Stripped("Nodes.JsonNode?") elif isinstance(type_annotation, intermediate.JsonArrayTypeAnnotation): return Stripped("Nodes.JsonArray") diff --git a/aas_core_codegen/csharp/lib/_generate_copying.py b/aas_core_codegen/csharp/lib/_generate_copying.py index 3a72897f3..2b68b4a84 100644 --- a/aas_core_codegen/csharp/lib/_generate_copying.py +++ b/aas_core_codegen/csharp/lib/_generate_copying.py @@ -177,10 +177,22 @@ def _json_deep_copy_expr( and the generated code has to build against .NET 6, where ``SerializeToNode`` is the spelling of the same thing. Its return type is nullable only because it serializes an arbitrary value, and a null one - gives a null node; ``source_expr`` is known not to be null here, so - the ``throw`` can not be reached. + gives a null node. An array and an object are known not to be null here, so + the ``throw`` can not be reached. A JSON-able value might be a JSON null, + which is copied as such. """ item_type = csharp_common.generate_type(type_anno) + + # NOTE (mristin): + # A JSON-able value might be a JSON null, which ``SerializeToNode`` copies + # as a null node, so there is nothing to throw. + if isinstance(type_anno, intermediate.JsonValueTypeAnnotation): + return Stripped( + f"""\ +System.Text.Json.JsonSerializer.SerializeToNode( +{I}{source_expr})""" + ) + return Stripped( f"""\ ({item_type})( diff --git a/aas_core_codegen/csharp/lib/_generate_json_value_verification.py b/aas_core_codegen/csharp/lib/_generate_json_value_verification.py index cfe51bd6d..313c8a0b6 100644 --- a/aas_core_codegen/csharp/lib/_generate_json_value_verification.py +++ b/aas_core_codegen/csharp/lib/_generate_json_value_verification.py @@ -31,10 +31,10 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: Generate code to verify JSON-able values. This checks that a ``System.Text.Json.Nodes.JsonNode`` is JSON-able - (*i.e.*, recursively a boolean, a number, a string, an array of - JSON-able values or an object of JSON-able values with string keys -- - never a ``null``, at any depth), and, optionally, that it matches - a specific top-level shape (an array or an object). + (*i.e.*, recursively a null, a boolean, a number, a string, an array of + JSON-able values or an object of JSON-able values with string keys), + and, optionally, that it matches a specific top-level shape (an array or + an object). This class does not depend on the meta-model at all, and is invoked from the ordinary per-class ``Verification.Verify`` dispatch exactly like @@ -55,7 +55,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: public enum ExpectedShape {{ {I}/// -{I}/// The value may be any JSON-able shape (a boolean, a number, +{I}/// The value may be any JSON-able shape (a null, a boolean, a number, {I}/// a string, an array or an object). {I}/// {I}Any, @@ -74,14 +74,14 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: Stripped( f"""\ /// -/// Verify that holds neither a null nor -/// a non-finite number anywhere, at any depth. +/// Verify that holds no non-finite number anywhere, +/// at any depth. /// /// -/// A JSON null has no representation as a JSON-able value, and JSON -/// knows neither an infinity nor a not-a-number -- yet +/// JSON knows neither an infinity nor a not-a-number -- yet /// holds either happily if it was put -/// together programmatically. +/// together programmatically. A null is a JSON null, and hence +/// a JSON-able value. /// private static IEnumerable VerifyRecursively( {I}Nodes.JsonNode that) @@ -93,15 +93,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {IIII}int index = 0; {IIII}foreach (Nodes.JsonNode? item in jsonArray) {IIII}{{ -{IIIII}if (item == null) -{IIIII}{{ -{IIIIII}var error = new Reporting.Error( -{IIIIIII}"Expected a JSON-able value, but got a null"); -{IIIIII}error.PrependSegment( -{IIIIIII}new Reporting.IndexSegment(index)); -{IIIIII}yield return error; -{IIIII}}} -{IIIII}else +{IIIII}if (item != null) {IIIII}{{ {IIIIII}foreach (var error in VerifyRecursively(item)) {IIIIII}{{ @@ -121,15 +113,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {IIIII}KeyValuePair member {IIIIII}in jsonObject) {IIII}{{ -{IIIII}if (member.Value == null) -{IIIII}{{ -{IIIIII}var error = new Reporting.Error( -{IIIIIII}"Expected a JSON-able value, but got a null"); -{IIIIII}error.PrependSegment( -{IIIIIII}new Reporting.KeySegment(member.Key)); -{IIIIII}yield return error; -{IIIII}}} -{IIIII}else +{IIIII}if (member.Value != null) {IIIII}{{ {IIIIII}foreach (var error in VerifyRecursively(member.Value)) {IIIIII}{{ @@ -167,9 +151,8 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: f"""\ /// /// Verify that is JSON-able (recursively -/// a boolean, a finite number, a string, an array or an object of -/// JSON-able values -- never a null and never a non-finite number, -/// at any depth) and, if +/// a null, a boolean, a finite number, a string, an array or an object of +/// JSON-able values -- never a non-finite number, at any depth) and, if /// is not /// , that its top-level shape matches. /// @@ -179,8 +162,13 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {{ {I}if (that == null) {I}{{ -{II}yield return new Reporting.Error( -{III}"Expected a JSON-able value, but got a null"); +{II}// NOTE (mristin): +{II}// A JSON null is a JSON-able value, but neither an array nor an object. +{II}if (expectedShape != ExpectedShape.Any) +{II}{{ +{III}yield return new Reporting.Error( +{IIII}$"Expected a Json{{expectedShape}}, but got a null"); +{II}}} {II}yield break; {I}}} diff --git a/aas_core_codegen/csharp/lib/_generate_jsonization.py b/aas_core_codegen/csharp/lib/_generate_jsonization.py index ab5db1a40..30c14ba0c 100644 --- a/aas_core_codegen/csharp/lib/_generate_jsonization.py +++ b/aas_core_codegen/csharp/lib/_generate_jsonization.py @@ -25,8 +25,6 @@ INDENT4 as IIII, INDENT5 as IIIII, INDENT6 as IIIIII, - INDENT7 as IIIIIII, - INDENT8 as IIIIIIII, ) from aas_core_codegen.intermediate import uses as intermediate_uses @@ -1120,10 +1118,19 @@ def _generate_from_method_for_class( json_name = cls.properties_by_name[arg.name].json_name assert not csharp_common.needs_escaping(json_name) + # NOTE (mristin): + # A JSON-able value might be a JSON null, which is a C# ``null``, so we + # have to check whether the property was there at all. + condition = ( + f'!obj.ContainsKey("{json_name}")' + if isinstance(arg.type_annotation, intermediate.JsonValueTypeAnnotation) + else f"{arg_var} == null" + ) + required_checks.append( Stripped( f"""\ -if ({arg_var} == null) +if ({condition}) {{ {I}error = new Reporting.Error( {II}"Required property \\"{json_name}\\" is missing"); @@ -1210,7 +1217,11 @@ def _generate_from_method_for_class( init_writer.write(f"{I}{arg_var}") if not isinstance( - prop.type_annotation, intermediate.OptionalTypeAnnotation + prop.type_annotation, + ( + intermediate.OptionalTypeAnnotation, + intermediate.JsonValueTypeAnnotation, + ), ): init_writer.write("\n") @@ -1280,14 +1291,13 @@ def _generate_deserialize_json_helpers() -> List[Stripped]: /// /// /// A JSON-able value is, recursively, exactly as JSON itself is defined: -/// a boolean, a finite number, a string, an array of JSON-able values or -/// an object of JSON-able values with string keys -- never null and +/// a null (JSON null), a boolean, a finite number, a string, an array +/// of JSON-able values or an object of JSON-able values with string keys -- /// never an infinity or a not-a-number, at any depth. /// /// JSON node to be checked /// Error, if any, describing why it is not JSON-able private static bool CheckIsJsonAble( -{I}[CodeAnalysis.NotNullWhen(true)] {I}Nodes.JsonNode? node, {I}out Reporting.Error? error) {{ @@ -1295,9 +1305,7 @@ def _generate_deserialize_json_helpers() -> List[Stripped]: {I}if (node == null) {I}{{ -{II}error = new Reporting.Error( -{III}"Expected a JSON-able value, but got a null"); -{II}return false; +{II}return true; {I}}} {I}switch (node) @@ -1367,7 +1375,7 @@ def _generate_deserialize_json_helpers() -> List[Stripped]: /// /// JSON node to be parsed /// Error, if any, during the deserialization -internal static Nodes.JsonNode JsonValueFrom( +internal static Nodes.JsonNode? JsonValueFrom( {I}Nodes.JsonNode? node, {I}out Reporting.Error? error) {{ @@ -1376,6 +1384,11 @@ def _generate_deserialize_json_helpers() -> List[Stripped]: {II}return default!; {I}}} +{I}if (node == null) +{I}{{ +{II}return null; +{I}}} + {I}// NOTE (mristin): {I}// A System.Text.Json node remembers its parent, and ``node`` is still {I}// attached to the document which the caller parsed. Handing it out @@ -1414,7 +1427,7 @@ def _generate_deserialize_json_helpers() -> List[Stripped]: {II}return default!; {I}}} -{I}Nodes.JsonNode result = JsonValueFrom(node, out error); +{I}Nodes.JsonNode? result = JsonValueFrom(node, out error); {I}if (error != null) {I}{{ {II}return default!; @@ -1443,7 +1456,7 @@ def _generate_deserialize_json_helpers() -> List[Stripped]: {II}return default!; {I}}} -{I}Nodes.JsonNode result = JsonValueFrom(node, out error); +{I}Nodes.JsonNode? result = JsonValueFrom(node, out error); {I}if (error != null) {I}{{ {II}return default!; @@ -2537,13 +2550,14 @@ def _generate_serialize_json_helpers() -> List[Stripped]: /// Check that is a JSON-able value, at any depth. /// /// -/// Neither a null nor a non-finite number has a representation as -/// a JSON-able value, and rules out neither -/// of the two statically. +/// A non-finite number has no representation as a JSON-able value, and +/// does not rule it out statically. A null +/// is a JSON null, and hence a JSON-able value. /// /// -/// Thrown if either is found anywhere within , -/// carrying the path to the culprit within . +/// Thrown if a non-finite number is found anywhere within +/// , carrying the path to the culprit within +/// . /// private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) {{ @@ -2555,13 +2569,10 @@ def _generate_serialize_json_helpers() -> List[Stripped]: {III}{{ {IIII}try {IIII}{{ -{IIIII}if (item == null) +{IIIII}if (item != null) {IIIII}{{ -{IIIIII}throw new SerializationFailure( -{IIIIIII}new Reporting.Error( -{IIIIIIII}"Expected a JSON-able value, but got a null")); +{IIIIII}CheckIsJsonAbleForSerialization(item); {IIIII}}} -{IIIII}CheckIsJsonAbleForSerialization(item); {IIII}}} {IIII}catch (SerializationFailure failure) {IIII}{{ @@ -2580,13 +2591,10 @@ def _generate_serialize_json_helpers() -> List[Stripped]: {III}{{ {IIII}try {IIII}{{ -{IIIII}if (member.Value == null) +{IIIII}if (member.Value != null) {IIIII}{{ -{IIIIII}throw new SerializationFailure( -{IIIIIII}new Reporting.Error( -{IIIIIIII}"Expected a JSON-able value, but got a null")); +{IIIIII}CheckIsJsonAbleForSerialization(member.Value); {IIIII}}} -{IIIII}CheckIsJsonAbleForSerialization(member.Value); {IIII}}} {IIII}catch (SerializationFailure failure) {IIII}{{ @@ -2629,11 +2637,16 @@ def _generate_serialize_json_helpers() -> List[Stripped]: /// , never itself. /// /// -/// Thrown if contains a null or a non-finite -/// number anywhere, at any depth. +/// Thrown if contains a non-finite number anywhere, +/// at any depth. /// -private static Nodes.JsonNode SerializeJsonValue(Nodes.JsonNode that) +private static Nodes.JsonNode? SerializeJsonValue(Nodes.JsonNode? that) {{ +{I}if (that == null) +{I}{{ +{II}return null; +{I}}} + {I}// NOTE (mristin): {I}// A System.Text.Json node remembers its parent, and attaching a node {I}// which already has one throws @@ -2676,7 +2689,7 @@ def _generate_serialize_json_helpers() -> List[Stripped]: /// private static Nodes.JsonArray SerializeJsonArray(Nodes.JsonArray that) {{ -{I}return (Nodes.JsonArray)SerializeJsonValue(that); +{I}return (Nodes.JsonArray)SerializeJsonValue(that)!; }}""" ), Stripped( @@ -2686,7 +2699,7 @@ def _generate_serialize_json_helpers() -> List[Stripped]: /// private static Nodes.JsonObject SerializeJsonObject(Nodes.JsonObject that) {{ -{I}return (Nodes.JsonObject)SerializeJsonValue(that); +{I}return (Nodes.JsonObject)SerializeJsonValue(that)!; }}""" ), ] diff --git a/aas_core_codegen/csharp/lib/_generate_xml_rpc.py b/aas_core_codegen/csharp/lib/_generate_xml_rpc.py index 368816ded..5e733eced 100644 --- a/aas_core_codegen/csharp/lib/_generate_xml_rpc.py +++ b/aas_core_codegen/csharp/lib/_generate_xml_rpc.py @@ -31,10 +31,11 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: This implements a restricted subset of XML-RPC's own element vocabulary -- ````, ````, ````, ````, ````, - ````, ````, ```` and ```` -- to de/serialize + ````, ````, ```` and ````, together with + the widespread ```` extension -- to de/serialize a ``System.Text.Json.Nodes.JsonNode`` which is JSON-able (*i.e.*, - recursively a boolean, a number, a string, an array of JSON-able values or - an object of JSON-able values with string keys; never JSON ``null``). + recursively a JSON null, a boolean, a number, a string, an array of + JSON-able values or an object of JSON-able values with string keys). This class does not depend on the meta-model at all, and can be used in two different ways: @@ -228,7 +229,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: /// /// /// is expected to be positioned at the start -/// element of the discriminator (<boolean>, +/// element of the discriminator (<nil>, <boolean>, /// <double>, <string>, <array> or /// <struct>). On success, is left /// positioned at the node right after the discriminator's own end @@ -245,7 +246,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {I}if (reader.EOF || reader.NodeType != Xml.XmlNodeType.Element) {I}{{ {II}error = new Reporting.Error( -{III}"Expected one of the elements , , , " + +{III}"Expected one of the elements , , , , " + {III}" or , but got " + {III}( {IIII}reader.EOF @@ -274,6 +275,13 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {I}Nodes.JsonNode? result = null; {I}switch (discriminator) {I}{{ +{II}case "nil": +{III}// NOTE (mristin): +{III}// A has no content, which consuming its end element below +{III}// checks. +{III}result = null; +{III}break; + {II}case "boolean": {III}if (isEmpty) {III}{{ @@ -365,7 +373,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {III}// It is this very element which does not belong here, so naming it {III}// in the path as well as in the message would say nothing more. {III}error = new Reporting.Error( -{IIII}"Expected one of the elements , , , " + +{IIII}"Expected one of the elements , , , , " + {IIII}$" or , but got: <{{discriminator}}>"); {III}return null; {I}}} @@ -406,7 +414,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {I}if (isEmpty) {I}{{ {II}error = new Reporting.Error( -{III}"Expected one of the elements , , , " + +{III}"Expected one of the elements , , , , " + {III}" or as the content of a element, " + {III}"but got no content at all"); {II}return null; @@ -572,7 +580,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {I}if (isEmptyValue) {I}{{ {II}error = new Reporting.Error( -{III}"Expected one of the elements , , , " + +{III}"Expected one of the elements , , , , " + {III}" or as the content of a element, " + {III}"but got no content at all"); {II}error.PrependSegment( @@ -702,7 +710,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {I}if (isEmpty) {I}{{ {II}error = new Reporting.Error( -{III}"Expected one of the elements , , , " + +{III}"Expected one of the elements , , , , " + {III}" or as the content of a element, " + {III}"but got no content at all"); {II}return null; @@ -733,11 +741,11 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: /// . /// /// -/// This writes exactly one discriminator element (<boolean>, -/// <double>, <string>, <array> or -/// <struct>). must be JSON-able -/// (recursively a boolean, a number, a string, an array or an object with -/// string keys), and never null, or a +/// This writes exactly one discriminator element (<nil>, +/// <boolean>, <double>, <string>, +/// <array> or <struct>). +/// must be JSON-able (recursively a null, a boolean, a number, a string, +/// an array or an object with string keys), or a /// is thrown, carrying /// the path to the culprit within . The enclosing /// serializer converts it -- does, @@ -752,10 +760,12 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {I}switch (that) {I}{{ {II}case null: -{III}throw new SerializationFailure( -{IIII}new Reporting.Error( -{IIIII}"Expected a JSON-able value (a boolean, a number, " + -{IIIII}"a string, an array or an object), but got null")); +{III}// NOTE (mristin): +{III}// XML-RPC itself knows no null, so we follow the widespread extension, +{III}// and write a JSON null as an empty . +{III}writer.WriteStartElement("nil", ""); +{III}writer.WriteEndElement(); +{III}break; {II}case Nodes.JsonArray jsonArray: {III}writer.WriteStartElement("array", ""); @@ -812,7 +822,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {III}{{ {IIII}throw new SerializationFailure( {IIIII}new Reporting.Error( -{IIIIII}"Expected a JSON-able value (a boolean, a number, " + +{IIIIII}"Expected a JSON-able value (a null, a boolean, a number, " + {IIIIII}"a string, an array or an object), but got a value of " + {IIIIII}$"an unexpected underlying type: {{jsonValue}}")); {III}}} @@ -821,7 +831,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {II}default: {III}throw new SerializationFailure( {IIII}new Reporting.Error( -{IIIII}"Expected a JSON-able value (a boolean, a number, " + +{IIIII}"Expected a JSON-able value (a null, a boolean, a number, " + {IIIII}"a string, an array or an object), but got: " + {IIIII}$"{{that.GetType()}}")); {I}}} diff --git a/aas_core_codegen/csharp/lib/_generate_xmlization.py b/aas_core_codegen/csharp/lib/_generate_xmlization.py index 2340df8cc..d2dfd6b6c 100644 --- a/aas_core_codegen/csharp/lib/_generate_xmlization.py +++ b/aas_core_codegen/csharp/lib/_generate_xmlization.py @@ -1249,6 +1249,26 @@ def _generate_deserialize_property( target_var = csharp_naming.variable_name(Identifier(f"the_{prop.name}")) reader_name = _content_reader_name(type_anno) + # NOTE (mristin): + # A JSON-able value might be a JSON null, which is a C# ``null``, so we mark + # with a flag that we have read it. + if isinstance(type_anno, intermediate.JsonValueTypeAnnotation): + seen_var = csharp_naming.variable_name(Identifier(f"seen_{prop.name}")) + return ( + Stripped( + f"""\ +if ({seen_var}) +{{ +{I}error = DuplicatePropertyError(elementName); +{I}break; +}} +{seen_var} = true; +{target_var} = {reader_name}( +{I}reader, isEmptyProperty, out error);""" + ), + None, + ) + # NOTE (mristin): # A variable which is not null can only have been set by an earlier turn of # the property loop, so it tells us that the property comes a second time. @@ -1328,6 +1348,14 @@ def _generate_deserialize_impl_cls_from_sequence( target_type = Stripped(f"{target_type}?") init_target_var_stmts.append(Stripped(f"{target_type} {target_var} = null;")) + + if isinstance( + intermediate.beneath_optional(prop.type_annotation), + intermediate.JsonValueTypeAnnotation, + ): + seen_var = csharp_naming.variable_name(Identifier(f"seen_{prop.name}")) + init_target_var_stmts.append(Stripped(f"bool {seen_var} = false;")) + blocks.append(Stripped("\n".join(init_target_var_stmts))) # noinspection PyListCreation @@ -1452,10 +1480,21 @@ def _generate_deserialize_impl_cls_from_sequence( target_var = csharp_naming.variable_name(Identifier(f"the_{prop.name}")) if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation): + # NOTE (mristin): + # A JSON-able value might be a JSON null, see the note in + # :py:func:`_generate_deserialize_property`. + condition = ( + "!" + csharp_naming.variable_name(Identifier(f"seen_{prop.name}")) + if isinstance( + prop.type_annotation, intermediate.JsonValueTypeAnnotation + ) + else f"{target_var} == null" + ) + blocks.append( Stripped( f"""\ -if ({target_var} == null) +if ({condition}) {{ {I}error = new Reporting.Error( {II}"The required property {prop_csharp} has not been given " + @@ -1522,7 +1561,13 @@ def _generate_deserialize_impl_cls_from_sequence( arg_var = csharp_naming.variable_name(Identifier(f"the_{arg.name}")) init_writer.write(f"{I}{arg_var}") - if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation): + if not isinstance( + prop.type_annotation, + ( + intermediate.OptionalTypeAnnotation, + intermediate.JsonValueTypeAnnotation, + ), + ): init_writer.write("\n") # Dedention could not work here due to prefix indention at the very @@ -1763,7 +1808,7 @@ def _generate_read_json_content_functions() -> List[Stripped]: Stripped( f"""\ error = new Reporting.Error( -{I}"Expected one of the elements , , , " + +{I}"Expected one of the elements , , , , " + {I}" or as the content of the element, " + {I}"but the element was self-closing"); return default!;""" @@ -1796,13 +1841,31 @@ def _generate_read_json_content_functions() -> List[Stripped]: ), ), ): + # NOTE (mristin): + # A JSON-able value might be a JSON null, which is a C# ``null``, while + # an array and an object are never null. + is_json_value = function_name == "ReadJsonValueContent" + + return_type = f"{csharp_type}?" if is_json_value else csharp_type + + return_result = ( + Stripped("return result;") + if is_json_value + else Stripped( + f"""\ +return result +{I}?? throw new System.InvalidOperationException( +{II}"Unexpected result null when error is null");""" + ) + ) + result.append( Stripped( f"""\ /// /// Read the content of an element typed as {csharp_type}. /// -private static {csharp_type} {function_name}( +private static {return_type} {function_name}( {I}Xml.XmlReader reader, {I}bool isEmpty, {I}out Reporting.Error? error) @@ -1819,9 +1882,7 @@ def _generate_read_json_content_functions() -> List[Stripped]: {II}return default!; {I}}} -{I}return result -{II}?? throw new System.InvalidOperationException( -{III}"Unexpected result null when error is null"); +{I}{indent_but_first_line(return_result, I)} }}""" ) ) diff --git a/aas_core_codegen/csharp/tests/_generate_common_json.py b/aas_core_codegen/csharp/tests/_generate_common_json.py index c8650b4a5..90aa91503 100644 --- a/aas_core_codegen/csharp/tests/_generate_common_json.py +++ b/aas_core_codegen/csharp/tests/_generate_common_json.py @@ -136,13 +136,27 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: Stripped( f"""\ public static void CheckJsonNodesEqual( -{I}Nodes.JsonNode that, -{I}Nodes.JsonNode other, +{I}Nodes.JsonNode? that, +{I}Nodes.JsonNode? other, {I}out Reporting.Error? error ) {{ {I}error = null; +{I}// NOTE (mristin): +{I}// A JSON null is a C# null. +{I}if (that == null || other == null) +{I}{{ +{II}if (that != other) +{II}{{ +{III}error = new Reporting.Error( +{IIII}$"Mismatch in nulls: {{that?.ToJsonString() ?? "null"}} != " + +{IIII}$"{{other?.ToJsonString() ?? "null"}}" +{III}); +{II}}} +{II}return; +{I}}} + {I}var thatNodeKind = GetNodeKind(that); {I}var otherNodeKind = GetNodeKind(other); @@ -169,7 +183,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {IIII}for (int i = 0; i < thatArray.Count; i++) {IIII}{{ -{IIIII}CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); +{IIIII}CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); {IIIII}if (error != null) {IIIII}{{ {IIIII} error.PrependSegment(new Reporting.IndexSegment(i)); @@ -181,8 +195,8 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {III}}} {II}case Nodes.JsonObject thatObject: {III}{{ -{IIII}var thatDictionary = thatObject as IDictionary; -{IIII}var otherDictionary = (other as IDictionary)!; +{IIII}var thatDictionary = thatObject as IDictionary; +{IIII}var otherDictionary = (other as IDictionary)!; {IIII}var thatKeys = thatDictionary.Keys.ToList(); {IIII}thatKeys.Sort(); diff --git a/aas_core_codegen/csharp/tests/_generate_test_copying.py b/aas_core_codegen/csharp/tests/_generate_test_copying.py index ff5d42619..c36cb4661 100644 --- a/aas_core_codegen/csharp/tests/_generate_test_copying.py +++ b/aas_core_codegen/csharp/tests/_generate_test_copying.py @@ -369,9 +369,17 @@ def _generate_deep_copy_checker( Stripped( f"""\ private static bool JsonNodesEqualButDistinct( -{I}Nodes.JsonNode that, -{I}Nodes.JsonNode other) +{I}Nodes.JsonNode? that, +{I}Nodes.JsonNode? other) {{ +{I}// NOTE (mristin): +{I}// A JSON null is a C# null, and there is nothing to be distinct about +{I}// two of them. +{I}if (that == null || other == null) +{I}{{ +{II}return that == other; +{I}}} + {I}// NOTE (mristin): {I}// A JsonNode compares only by reference, so we compare the canonical {I}// JSON text. diff --git a/aas_core_codegen/csharp/tests/_generate_test_xml_rpc.py b/aas_core_codegen/csharp/tests/_generate_test_xml_rpc.py index 313d6c4a1..41067b233 100644 --- a/aas_core_codegen/csharp/tests/_generate_test_xml_rpc.py +++ b/aas_core_codegen/csharp/tests/_generate_test_xml_rpc.py @@ -235,19 +235,41 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: Stripped( f"""\ [Test] -public void Test_serialize_rejects_null() +public void Test_round_trip_null() {{ -{I}Our.SerializationException? caught = null; -{I}try -{I}{{ -{II}SerializeToString(null); -{I}}} -{I}catch (Our.SerializationException exception) -{I}{{ -{II}caught = exception; -{I}}} +{I}string text = SerializeToString(null); -{I}Assert.IsNotNull(caught); +{I}Assert.IsTrue(text.Contains(" str: {I}Nodes.JsonNode original = new Nodes.JsonArray( {II}Nodes.JsonValue.Create(1.0), {II}Nodes.JsonValue.Create(2.0), -{II}null); +{II}Nodes.JsonValue.Create(double.NaN)); {I}Our.SerializationException? caught = null; {I}try @@ -283,7 +305,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {II}"" {III}+ "" {IIII}+ "1" -{IIII}+ "" +{IIII}+ "1" {III}+ "" {II}+ ""); @@ -307,7 +329,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: [Test] public void Test_deserialize_rejects_unexpected_element() {{ -{I}string text = ""; +{I}string text = "1"; {I}Nodes.JsonNode? result = DeserializeFromString( {II}text, out Our.Reporting.Error? error); @@ -315,7 +337,7 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: {I}Assert.IsNull(result); {I}Assert.IsNotNull(error); {I}Assert.IsTrue( -{II}error!.Cause.Contains("nil"), +{II}error!.Cause.Contains("i4"), {II}$"Unexpected cause: {{error.Cause}}"); }}""" ), diff --git a/aas_core_codegen/csharp/transpilation.py b/aas_core_codegen/csharp/transpilation.py index 934c003fe..26ac04877 100644 --- a/aas_core_codegen/csharp/transpilation.py +++ b/aas_core_codegen/csharp/transpilation.py @@ -455,36 +455,11 @@ def transform_index( if not isinstance(node.collection, no_parentheses_types): collection = Stripped(f"({collection})") - collection_type = intermediate_type_inference.beneath_optional( - self.type_map[node.collection] - ) - - if isinstance( - collection_type, - ( - intermediate_type_inference.JsonArrayTypeAnnotation, - intermediate_type_inference.JsonObjectTypeAnnotation, - ), - ): - # NOTE (mristin): - # The indexer of a ``Nodes.JsonObject`` and of a ``Nodes.JsonArray`` - # gives a nullable node, as the key or the position might be - # missing, and so does the type inference. Once the meta-model - # guarded the index with a membership or a length check, the type - # inference strips the optionality, and we have to tell that to - # the C# compiler, which tracks the nullability neither through - # an indexer nor through such a guard. - # - # If the meta-model did not guard the index, we deliberately do - # *not* forgive the nullability, so that the C# compiler reports - # the un-guarded index just as it reports an un-guarded optional - # property. - if not isinstance( - self.type_map[node], - intermediate_type_inference.OptionalTypeAnnotation, - ): - return Stripped(f"{collection}[{index}]!"), None - + # NOTE (mristin): + # The indexer of a ``Nodes.JsonObject`` and of a ``Nodes.JsonArray`` + # gives a nullable node. This is exactly our ``JSONValue``, which might + # be a JSON null even if the meta-model guarded the index with + # a membership or a length check, so there is nothing to forgive. return Stripped(f"{collection}[{index}]"), None @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None)) diff --git a/aas_core_codegen/golang/lib/_generate_jsonization.py b/aas_core_codegen/golang/lib/_generate_jsonization.py index 0a46593bd..c5c3c7a3b 100644 --- a/aas_core_codegen/golang/lib/_generate_jsonization.py +++ b/aas_core_codegen/golang/lib/_generate_jsonization.py @@ -1890,10 +1890,10 @@ def _generate_json_able_helpers( // Parse `jsonable` as a JSON-able value. // // A JSON-able value is, recursively, exactly as JSON itself is defined: -// a bool, a float64, a string, an array of JSON-able values or an object of -// JSON-able values with string keys. A nil is rejected at any depth, as -// the JSON null has no representation as a JSON-able value, and so are -// an infinity and a not-a-number, which JSON can not represent at all. +// a nil (JSON null), a bool, a float64, a string, an array of JSON-able +// values or an object of JSON-able values with string keys. An infinity and +// a not-a-number are rejected at any depth, as JSON can not represent them +// at all. // // The result is a new structure, and never `jsonable` itself, so that // the parsed instance does not alias the document it came from. @@ -1904,9 +1904,6 @@ def _generate_json_able_helpers( {I}jsonable interface{{}}, ) (result ourtypes.JsonValue, err error) {{ {I}if jsonable == nil {{ -{II}err = newDeserializationError( -{III}"Expected a JSON-able value, but got a nil", -{II}) {II}return {I}}} @@ -2028,8 +2025,9 @@ def _generate_json_able_helpers( // Serialize `value` as a JSON-able structure. // // A JSON-able value is almost JSON-able as it comes, but not quite: its type -// is an `any`, which rules out neither a nil nor a non-finite number, neither -// of which JSON can represent. Each is refused here, at any depth. +// is an `any`, which does not rule out a non-finite number, which JSON can not +// represent. It is refused here, at any depth. A nil is written as +// a JSON null. // // The result is a new structure, and never `value` itself, so that // the serialized document does not alias the instance it came from. @@ -2037,9 +2035,6 @@ def _generate_json_able_helpers( {I}value ourtypes.JsonValue, ) (result interface{{}}, err error) {{ {I}if value == nil {{ -{II}err = newSerializationError( -{III}"Expected a JSON-able value, but got a nil", -{II}) {II}return {I}}} diff --git a/aas_core_codegen/golang/lib/_generate_types.py b/aas_core_codegen/golang/lib/_generate_types.py index eb0a62654..2b984cd21 100644 --- a/aas_core_codegen/golang/lib/_generate_types.py +++ b/aas_core_codegen/golang/lib/_generate_types.py @@ -1888,11 +1888,16 @@ def _generate_json_aliases( """\ // Represent a value which JSON can carry. // -// This is, recursively, exactly as JSON itself is defined: a bool, a float64, -// a string, a [JsonArray] of JSON-able values or a [JsonObject] of JSON-able -// values with string keys -- never a nil, and never an infinity or +// This is, recursively, exactly as JSON itself is defined: a nil (JSON null), +// a bool, a float64, a string, a [JsonArray] of JSON-able values or +// a [JsonObject] of JSON-able values with string keys -- never an infinity or // a not-a-number, which JSON can not represent at all. // +// A nil might appear at any depth, including the value itself. Since +// an absent optional property is also nil, an optional JSON-able property +// which is a JSON null is indistinguishable from an absent one, and is treated +// as absent. +// // Go has no sum type, so this is an `any`, and what it actually holds is // checked at run-time -- by the verification, and again by the de/serialization. type JsonValue = interface{}""" diff --git a/aas_core_codegen/golang/lib/_generate_verification.py b/aas_core_codegen/golang/lib/_generate_verification.py index 73fb05e0a..b2ccc3381 100644 --- a/aas_core_codegen/golang/lib/_generate_verification.py +++ b/aas_core_codegen/golang/lib/_generate_verification.py @@ -1161,9 +1161,10 @@ def _generate_verify_json_value( Generate the functions verifying that a value is JSON-able. A JSON-able value is, recursively, exactly as JSON itself is defined, but - ``JsonValue`` is an ``any``, so it rules out none of the three ways of not - being one: a ``nil``, a non-finite number and a value of some other type - altogether. Each is reported here, at any depth. + ``JsonValue`` is an ``any``, so it rules out none of the two ways of not + being one: a non-finite number and a value of some other type altogether. + Each is reported here, at any depth. A ``nil`` is a JSON null, and hence + a JSON-able value. The path *within* a JSON-able value is a path of the error like any other: an index segment points at an item of a JSON-able array, and a key segment @@ -1198,11 +1199,7 @@ def _generate_verify_json_value( {I}onError func(*VerificationError) bool, ) (abort bool) {{ {I}if value == nil {{ -{II}return onError( -{III}newVerificationError( -{IIII}"Expected a JSON-able value, but got a nil", -{III}), -{II}) +{II}return false {I}}} {I}switch casted := value.(type) {{ diff --git a/aas_core_codegen/golang/lib/_generate_xml_rpc.py b/aas_core_codegen/golang/lib/_generate_xml_rpc.py index 5578ff6b9..3d23eca6c 100644 --- a/aas_core_codegen/golang/lib/_generate_xml_rpc.py +++ b/aas_core_codegen/golang/lib/_generate_xml_rpc.py @@ -57,8 +57,9 @@ def _generate_readers() -> List[Stripped]: f"""\ // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. @@ -80,6 +81,8 @@ def _generate_readers() -> List[Stripped]: {I}local string, ) (value ourtypes.JsonValue, next xml.Token, err error) {{ {I}switch local {{ +{II}case "nil": +{III}value, next, err = readNil(decoder, current) {II}case "boolean": {III}value, next, err = readBoolean(decoder, current) {II}case "double": @@ -105,7 +108,7 @@ def _generate_readers() -> List[Stripped]: {III}// this very element which does not belong here. {III}err = xmlcommon.NewDeserializationError( {IIII}fmt.Sprintf( -{IIIII}"Expected a discriminator element (one of boolean, double, "+ +{IIIII}"Expected a discriminator element (one of nil, boolean, double, "+ {IIIII}"string, array or struct), but got: %s", {IIIII}local, {IIII}), @@ -117,6 +120,34 @@ def _generate_readers() -> List[Stripped]: {II}xmlcommon.MustDeserializationError(err).PrependName(local) {I}}} {I}return +}}""" + ), + Stripped( + f"""\ +// Read the content of a `` element as a JSON null. +// +// XML-RPC itself knows no null, so we follow the widespread extension, and +// read a JSON null from an empty ``. +func readNil( +{I}decoder *xml.Decoder, +{I}current xml.Token, +) (value ourtypes.JsonValue, next xml.Token, err error) {{ +{I}var text string +{I}text, next, err = xmlcommon.ReadText(decoder, current) +{I}if err != nil {{ +{II}return +{I}}} + +{I}text = xmlcommon.CollapseWhitespace(text) +{I}if text != "" {{ +{II}err = xmlcommon.NewDeserializationError( +{III}fmt.Sprintf( +{IIII}"Expected no text in a nil element, but got: %s", +{IIII}text, +{III}), +{II}) +{I}}} +{I}return }}""" ), Stripped( @@ -501,9 +532,16 @@ def _generate_writers() -> List[Stripped]: {I}undeclareNamespace bool, ) (err error) {{ {I}if value == nil {{ -{II}err = xmlcommon.NewSerializationError( -{III}"Expected a JSON-able value, but got a nil", +{II}// NOTE (mristin): +{II}// XML-RPC itself knows no null, so we follow the widespread extension, +{II}// and write a JSON null as an empty ``. +{II}err = xmlcommon.WriteStartElementInNoNamespace( +{III}encoder, "nil", undeclareNamespace, {II}) +{II}if err != nil {{ +{III}return +{II}}} +{II}err = xmlcommon.WriteEndElement(encoder, "nil", false) {II}return {I}}} @@ -740,8 +778,9 @@ def generate(repo_url: Stripped) -> str: // // JSON prescribes no XML representation of its own, so a JSON-able value is // de/serialized over the subset of XML-RPC which covers exactly what such -// a value can be: ``, ``, ``, `` and -// ``. +// a value can be: ``, ``, ``, ``, `` and +// ``. XML-RPC itself knows no null, so we follow the widespread +// extension, and write a JSON null as an empty ``. // // Every function reads from or writes to the decoder or the encoder which // the caller passes in, and consumes or produces exactly the content of one diff --git a/aas_core_codegen/golang/lib/_generate_xmlization.py b/aas_core_codegen/golang/lib/_generate_xmlization.py index c1583428c..a94bde2ad 100644 --- a/aas_core_codegen/golang/lib/_generate_xmlization.py +++ b/aas_core_codegen/golang/lib/_generate_xmlization.py @@ -878,8 +878,9 @@ def _generate_json_value_readers( f"""\ // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. diff --git a/aas_core_codegen/intermediate/_types.py b/aas_core_codegen/intermediate/_types.py index f91a86dd7..9a6609ba8 100644 --- a/aas_core_codegen/intermediate/_types.py +++ b/aas_core_codegen/intermediate/_types.py @@ -374,10 +374,13 @@ class JsonValueTypeAnnotation(TypeAnnotation): Represent a type annotation for an arbitrary, open JSON-able value. A JSON-able value is, recursively, exactly as JSON itself is defined: - a boolean, a number or a string, an open JSON-able array of such values - (see :class:`JsonArrayTypeAnnotation`), or an open, JSON-object-shaped - value with string-like keys and JSON-able values (see + a null, a boolean, a number or a string, an open JSON-able array of such + values (see :class:`JsonArrayTypeAnnotation`), or an open, + JSON-object-shaped value with string-like keys and JSON-able values (see :class:`JsonObjectTypeAnnotation`). + + The value might be a JSON null at any depth, including the value itself. + An optional JSON-able value which is a JSON null is treated as absent. """ def __str__(self) -> str: diff --git a/aas_core_codegen/intermediate/type_inference.py b/aas_core_codegen/intermediate/type_inference.py index 0863696a7..a9812238a 100644 --- a/aas_core_codegen/intermediate/type_inference.py +++ b/aas_core_codegen/intermediate/type_inference.py @@ -623,7 +623,8 @@ class JsonValueTypeAnnotation(AtomicTypeAnnotation): Since neither its shape nor any further operation on it can be determined statically, we treat it analogous to ``Unknown`` -- no member access, - indexing or other operation is allowed on a value of this type. + indexing or other operation is allowed on a value of this type. The only + exception is a nullness check, which asks whether the value is a JSON null. """ def __str__(self) -> str: @@ -5006,15 +5007,23 @@ def _nullness_check_over_json_index_is_refused( ): return False + # NOTE (mristin): + # A guarded index is a ``JSONValue``, and asking whether it is null + # asks whether it is a JSON ``null``. This is unambiguous in all + # the targets, so we allow it. + if not isinstance(self.type_map[value], OptionalTypeAnnotation): + return False + self.errors.append( Error( value.original_node, - f"A JSON-able value is never null, so {check} over an index into " - f"a JSON-able object or array really asks whether the key or " - f"the position is there. Please ask that directly, as " - f"`` in `` and as ``len() > ``, " - f"which strips the optionality of the index in the guarded " - f"expression.", + f"An index into a JSON-able object or array might be missing, " + f"or hold a JSON null, so {check} over an unguarded index " + f"conflates the two. Please first ask whether the key or " + f"the position is there, as `` in `` and as " + f"``len() > ``, which strips the optionality " + f"of the index in the guarded expression. Then {check} over " + f"the guarded index asks whether it is a JSON null.", ) ) return True @@ -5032,12 +5041,17 @@ def transform_is_none( if self._nullness_check_over_json_index_is_refused(node.value, "``is None``"): return None - if not isinstance(value_type, OptionalTypeAnnotation): + # NOTE (mristin): + # A ``JSONValue`` might be a JSON null, so it can be checked for + # nullness even though it is not optional. + if not isinstance( + value_type, (OptionalTypeAnnotation, JsonValueTypeAnnotation) + ): self.errors.append( Error( node.value.original_node, - f"Expected the value to be of an optional type for " - f"a nullness check (``is None``), " + f"Expected the value to be of an optional type, " + f"or a JSONValue, for a nullness check (``is None``), " f"but got {value_type}", ) ) @@ -5062,12 +5076,16 @@ def transform_is_not_none( ): return None - if not isinstance(value_type, OptionalTypeAnnotation): + # NOTE (mristin): + # See the note in :py:meth:`transform_is_none` on ``JSONValue``. + if not isinstance( + value_type, (OptionalTypeAnnotation, JsonValueTypeAnnotation) + ): self.errors.append( Error( node.value.original_node, - f"Expected the value to be of an optional type " - f"for a non-nullness check (``is not None``), " + f"Expected the value to be of an optional type, " + f"or a JSONValue, for a non-nullness check (``is not None``), " f"but got {value_type}", ) ) diff --git a/aas_core_codegen/java/lib/_generate_jsonization.py b/aas_core_codegen/java/lib/_generate_jsonization.py index 40a3c82f9..9ccb2e243 100644 --- a/aas_core_codegen/java/lib/_generate_jsonization.py +++ b/aas_core_codegen/java/lib/_generate_jsonization.py @@ -1348,10 +1348,10 @@ def _generate_json_able_helpers( * Parse {{@code node}} as a JSON-able value. * *

A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys. A null is rejected at any depth, as - * the JSON null has no representation as a JSON-able value, and so are - * an infinity and a not-a-number, which JSON can not represent at all. + * a null node (JSON null), a boolean, a number, a string, an array of + * JSON-able values or an object of JSON-able values with string keys. + * An infinity and a not-a-number are rejected at any depth, as JSON can not + * represent them at all. * *

The result is a deep copy, and never {{@code node}} itself, so that * the parsed instance does not share a tree with the document it came from. @@ -1362,11 +1362,15 @@ def _generate_json_able_helpers( */ private static Reporting.Result tryJsonValueFromAt( {II}JsonNode node, String path) {{ -{I}if (node == null || node.isNull() || node.isMissingNode()) {{ +{I}if (node == null || node.isMissingNode()) {{ {II}return Reporting.Result.failure(new Reporting.Error( {III}"Expected a JSON-able value at " + path + ", but got: " + node)); {I}}} +{I}if (node.isNull()) {{ +{II}return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); +{I}}} + {I}if (node.isNumber()) {{ {II}// NOTE (mristin): {II}// JSON knows neither an infinity nor a not-a-number. Jackson's own @@ -1416,7 +1420,7 @@ def _generate_json_able_helpers( {I}}} {I}return Reporting.Result.failure(new Reporting.Error( -{II}"Expected a JSON-able value (a boolean, a number, a string, " + +{II}"Expected a JSON-able value (a null, a boolean, a number, a string, " + {II}"an array or an object) at " + path + ", but got: " + {II}node.getNodeType())); }}""" @@ -1490,8 +1494,9 @@ def _generate_json_able_serializer( * Serialize {{@code that}} as a JSON node. * *

A JSON-able value is almost a JSON node as it comes, but not quite: its - * type rules out neither a null nor a non-finite number, neither of which - * JSON can represent. Each is refused here, at any depth. + * type rules out neither a missing node nor a non-finite number, neither of + * which JSON can represent. Each is refused here, at any depth. A null node + * is written as a JSON null. * *

The result is a deep copy, and never {{@code that}} itself, so that * the serialized document does not share a tree with the instance it came @@ -1501,12 +1506,16 @@ def _generate_json_able_serializer( * @return {{@code that}}, as a node of a JSON document */ private static JsonNode jsonValueToJsonNode(JsonNode that) {{ -{I}if (that == null || that.isNull() || that.isMissingNode()) {{ +{I}if (that == null || that.isMissingNode()) {{ {II}throw new _SerializeFailure( {III}new Reporting.Error( {IIII}"Expected a JSON-able value, but got: " + that)); {I}}} +{I}if (that.isNull()) {{ +{II}return JsonNodeFactory.instance.nullNode(); +{I}}} + {I}if (that.isNumber()) {{ {II}final double value = that.doubleValue(); {II}if (Double.isInfinite(value) || Double.isNaN(value)) {{ @@ -1556,7 +1565,7 @@ def _generate_json_able_serializer( {I}throw new _SerializeFailure( {II}new Reporting.Error( -{III}"Expected a JSON-able value (a boolean, a number, a string, " + +{III}"Expected a JSON-able value (a null, a boolean, a number, a string, " + {III}"an array or an object), but got: " + that.getNodeType())); }}""" ), diff --git a/aas_core_codegen/java/lib/_generate_types.py b/aas_core_codegen/java/lib/_generate_types.py index 7ee43e4a4..5b7c99812 100644 --- a/aas_core_codegen/java/lib/_generate_types.py +++ b/aas_core_codegen/java/lib/_generate_types.py @@ -1584,12 +1584,27 @@ def _generate_class( get_set_blocks: List[Stripped] = [] if isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation): + # NOTE (mristin): + # An optional JSON-able property which is a JSON null is treated as + # absent in all the SDKs, as most of them can not tell the two apart. + # We normalize it here, once, so that the serialization, the copying + # and the verification all see it as absent. + getter_body = ( + f"""\ +return Optional.ofNullable({prop_name}) +{I}.filter(value -> !value.isNull());""" + if isinstance( + prop.type_annotation.value, intermediate.JsonValueTypeAnnotation + ) + else f"return Optional.ofNullable({prop_name});" + ) + get_set_blocks.append( Stripped( f"""\ @Override public {prop_type} {getter_name}() {{ -{I}return Optional.ofNullable({prop_name}); +{I}{indent_but_first_line(getter_body, I)} }}""" ) ) diff --git a/aas_core_codegen/java/lib/_generate_verification.py b/aas_core_codegen/java/lib/_generate_verification.py index d9d6f1c25..21312cd35 100644 --- a/aas_core_codegen/java/lib/_generate_verification.py +++ b/aas_core_codegen/java/lib/_generate_verification.py @@ -1217,8 +1217,9 @@ def _generate_verify_json_value( Generate the methods verifying that a value is JSON-able. A JSON-able value is, recursively, exactly as JSON itself is defined, but - a Jackson node rules out neither of the two ways of not being one: a null - node and a non-finite number. Both are reported here, at any depth. + a Jackson node rules out neither of the two ways of not being one: + a missing node and a non-finite number. Both are reported here, at any + depth. A null node is a JSON null, and hence a JSON-able value. The path *within* a JSON-able value goes into the message rather than into the structured path of the error: a name segment points at a property of @@ -1253,13 +1254,17 @@ def _generate_verify_json_value( */ private static Stream verifyJsonValueAt( {II}JsonNode node, String path) {{ -{I}if (node == null || node.isNull() || node.isMissingNode()) {{ +{I}if (node == null || node.isMissingNode()) {{ {II}return Stream.of( {III}new Reporting.Error( {IIII}"Expected a JSON-able value at " + path {IIIII}+ ", but got: " + node)); {I}}} +{I}if (node.isNull()) {{ +{II}return Stream.empty(); +{I}}} + {I}if (node.isNumber()) {{ {II}// NOTE (mristin): {II}// JSON knows neither an infinity nor a not-a-number, so neither is @@ -1296,7 +1301,7 @@ def _generate_verify_json_value( {I}return Stream.of( {II}new Reporting.Error( -{III}"Expected a JSON-able value (a boolean, a number, a string, " +{III}"Expected a JSON-able value (a null, a boolean, a number, a string, " {IIII}+ "an array or an object) at " + path {IIII}+ ", but got: " + node.getNodeType())); }}""" diff --git a/aas_core_codegen/java/lib/_generate_xml_rpc.py b/aas_core_codegen/java/lib/_generate_xml_rpc.py index 7a259e348..3990c5b9e 100644 --- a/aas_core_codegen/java/lib/_generate_xml_rpc.py +++ b/aas_core_codegen/java/lib/_generate_xml_rpc.py @@ -41,16 +41,16 @@ def _generate_readers() -> List[Stripped]: /** * Read the content of an element holding a JSON-able value. * - *

The content is a single discriminator element -- {{@code }}, - * {{@code }}, {{@code }}, {{@code }} or - * {{@code }} -- which says what the value is. + *

The content is a single discriminator element -- {{@code }}, + * {{@code }}, {{@code }}, {{@code }}, + * {{@code }} or {{@code }} -- which says what the value is. */ public static Reporting.Result readValueContent( {I}XMLEventReader reader, boolean isEmpty) {{ {I}if (isEmpty) {{ {II}return Reporting.Result.failure(new Reporting.Error( -{III}"Expected one of the elements boolean, double, string, array or " + -{III}"struct as the content of the element, but the element was empty")); +{III}"Expected one of the elements nil, boolean, double, string, array " + +{III}"or struct as the content of the element, but the element was empty")); {I}}} {I}final Reporting.Result tryElementName = XmlCommon.peekElementNameInNoNamespace(reader); @@ -91,6 +91,7 @@ def _generate_readers() -> List[Stripped]: */ private static boolean isUnexpectedDiscriminator(String local) {{ {I}switch (local) {{ +{II}case "nil": {II}case "boolean": {II}case "double": {II}case "string": @@ -110,6 +111,8 @@ def _generate_readers() -> List[Stripped]: private static Reporting.Result readXmlRpcDiscriminator( {I}XMLEventReader reader, String local, boolean isEmpty) {{ {I}switch (local) {{ +{II}case "nil": +{III}return readXmlRpcNil(reader, isEmpty); {II}case "boolean": {III}return readXmlRpcBoolean(reader, isEmpty); {II}case "double": @@ -132,7 +135,7 @@ def _generate_readers() -> List[Stripped]: {II}}} {II}default: {III}return Reporting.Result.failure(new Reporting.Error( -{IIII}"Expected a discriminator element (one of boolean, double, " + +{IIII}"Expected a discriminator element (one of nil, boolean, double, " + {IIII}"string, array or struct), but got: " + local)); {I}}} }}""" @@ -246,6 +249,27 @@ def _generate_readers() -> List[Stripped]: {II}return Reporting.Result.failure(new Reporting.Error( {III}exception.getMessage())); {I}}} +}}""" + ), + Stripped( + f"""\ +/** + * Read the content of a {{@code }} element as a JSON null. + * + *

XML-RPC itself knows no null, so we follow the widespread extension, + * and read a JSON null from an empty {{@code }}. + */ +private static Reporting.Result readXmlRpcNil( +{I}XMLEventReader reader, boolean isEmpty) {{ +{I}if (!isEmpty) {{ +{II}final String text = readXmlRpcCollapsedText(reader); +{II}if (!text.isEmpty()) {{ +{III}return Reporting.Result.failure(new Reporting.Error( +{IIII}"Expected no text in a nil element, but got: " + text)); +{II}}} +{I}}} + +{I}return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); }}""" ), Stripped( @@ -458,11 +482,21 @@ def _generate_writers() -> List[Stripped]: private static void writeValueContent( {I}JsonNode that, XMLStreamWriter writer, boolean undeclareNamespace) {{ -{I}if (that == null || that.isNull() || that.isMissingNode()) {{ +{I}if (that == null || that.isMissingNode()) {{ {II}throw new XmlCommon.SerializeFailure(new Reporting.Error( {III}"Expected a JSON-able value, but got: " + that)); {I}}} +{I}if (that.isNull()) {{ +{II}// NOTE (mristin): +{II}// XML-RPC itself knows no null, so we follow the widespread extension, +{II}// and write a JSON null as an empty . +{II}XmlCommon.writeElementInNoNamespace( +{III}"nil", that, writer, undeclareNamespace, +{III}(node, aWriter) -> {{ }}); +{II}return; +{I}}} + {I}if (that.isBoolean()) {{ {II}// NOTE (mristin): {II}// Real XML-RPC tooling writes and expects a strict 1/0, and not @@ -514,7 +548,7 @@ def _generate_writers() -> List[Stripped]: {I}}} {I}throw new XmlCommon.SerializeFailure(new Reporting.Error( -{II}"Expected a JSON-able value (a boolean, a number, a string, " + +{II}"Expected a JSON-able value (a null, a boolean, a number, a string, " + {II}"an array or an object), but got: " + that.getNodeType())); }}""" ), @@ -658,8 +692,10 @@ def generate(package: java_common.PackageIdentifier) -> List[java_common.JavaFil * *

JSON prescribes no XML representation of its own, so a JSON-able value * is de/serialized over the subset of XML-RPC which covers exactly what such - * a value can be: {{@code }}, {{@code }}, {{@code }}, - * {{@code }} and {{@code }}. + * a value can be: {{@code }}, {{@code }}, {{@code }}, + * {{@code }}, {{@code }} and {{@code }}. XML-RPC itself + * knows no null, so we follow the widespread extension, and write a JSON null + * as an empty {{@code }}. * *

Every method reads from or writes to the reader or the writer which * the caller passes in, and consumes or produces exactly the content of one diff --git a/aas_core_codegen/java/transpilation.py b/aas_core_codegen/java/transpilation.py index ad42eaead..bb601718f 100644 --- a/aas_core_codegen/java/transpilation.py +++ b/aas_core_codegen/java/transpilation.py @@ -1432,6 +1432,18 @@ def transform_is_none( parse_tree.Slice, ) + # NOTE (mristin): + # A non-optional JSON-able value is never a Java ``null``, but might be + # a JSON null, which Jackson represents as a null node. + if isinstance( + self.type_map[node.value], + intermediate_type_inference.JsonValueTypeAnnotation, + ): + if isinstance(node.value, no_parentheses_types): + return Stripped(f"{value}.isNull()"), None + else: + return Stripped(f"({value}).isNull()"), None + if isinstance(node.value, no_parentheses_types): if self._optional_map[node.value]: return Stripped(f"!{value}.isPresent()"), None @@ -1465,6 +1477,17 @@ def transform_is_not_none( parse_tree.Slice, ) + # NOTE (mristin): + # See the note in :py:meth:`transform_is_none` on the JSON null. + if isinstance( + self.type_map[node.value], + intermediate_type_inference.JsonValueTypeAnnotation, + ): + if isinstance(node.value, no_parentheses_types_in_this_context): + return Stripped(f"!{value}.isNull()"), None + else: + return Stripped(f"!({value}).isNull()"), None + if isinstance(node.value, no_parentheses_types_in_this_context): if self._optional_map[node.value]: return Stripped(f"{value}.isPresent()"), None diff --git a/aas_core_codegen/jsonschema/main.py b/aas_core_codegen/jsonschema/main.py index c1470694c..a9985b160 100644 --- a/aas_core_codegen/jsonschema/main.py +++ b/aas_core_codegen/jsonschema/main.py @@ -873,9 +873,9 @@ def _define_json_type_definitions() -> MutableMapping[str, Any]: (see :func:`_define_type`), the same way a ``List`` and a ``Tuple`` already are. - The five shapes are spelled out as an ``anyOf`` whitelist, and not as - a permissive ``{}`` or a ``{"not": {"type": "null"}}``, so that a JSON - ``null`` is excluded at every depth rather than only at the top. + The six shapes, including a JSON ``null``, are spelled out as an ``anyOf`` + whitelist, and not as a permissive ``{}``, so that the definition states + exactly what a JSON-able value is. The definitions are to be *extended* with the resulting mapping. """ @@ -888,6 +888,7 @@ def _define_json_type_definitions() -> MutableMapping[str, Any]: ( "anyOf", [ + {"type": "null"}, {"type": "boolean"}, {"type": "number"}, {"type": "string"}, diff --git a/aas_core_codegen/parse/_types.py b/aas_core_codegen/parse/_types.py index 36aff9360..974bdb2e4 100644 --- a/aas_core_codegen/parse/_types.py +++ b/aas_core_codegen/parse/_types.py @@ -53,7 +53,7 @@ } #: Name of the special atomic type annotation denoting an arbitrary, open -#: JSON-able value -- a boolean, a number or a string, an open JSON-able +#: JSON-able value -- a null, a boolean, a number or a string, an open JSON-able #: array of such values, or an open, JSON-able object with string-like #: keys and values which are themselves JSON-able, all recursively, exactly #: as JSON itself is defined diff --git a/aas_core_codegen/python/lib/_generate_json_value_verification.py b/aas_core_codegen/python/lib/_generate_json_value_verification.py index dfe4c6e66..21bee75b4 100644 --- a/aas_core_codegen/python/lib/_generate_json_value_verification.py +++ b/aas_core_codegen/python/lib/_generate_json_value_verification.py @@ -26,8 +26,8 @@ def _generate_module_docstring( Verify that a value is JSON-able. A JSON-able value is, recursively, exactly as JSON itself is defined: -a boolean, a finite number, a string, an array of JSON-able values or -an object of JSON-able values with string keys. Python offers no type which +a ``None`` (JSON null), a boolean, a finite number, a string, an array of +JSON-able values or an object of JSON-able values with string keys. Python offers no type which says exactly that, so a value has to be walked to make sure of it. This module depends on no class of the meta-model. It is invoked from @@ -89,6 +89,9 @@ def verify_json_value( {I}:param value: to be verified {I}:yield: errors, if any {I}""" +{I}if value is None: +{II}return + {I}# NOTE (mristin): {I}# A ``bool`` is an ``int`` in Python, so it has to be checked first -- {I}# otherwise every boolean would be taken for a number. @@ -154,8 +157,8 @@ def verify_json_value( {II}return {I}yield Error( -{II}f"Expected a JSON-able value (a boolean, a number, a string, an array " -{II}f"or an object), but got: {{type(value)}}" +{II}f"Expected a JSON-able value (a null, a boolean, a number, a string, " +{II}f"an array or an object), but got: {{type(value)}}" {I})''' ), Stripped( diff --git a/aas_core_codegen/python/lib/_generate_jsonization.py b/aas_core_codegen/python/lib/_generate_jsonization.py index 4eb3e54db..1ceaf0387 100644 --- a/aas_core_codegen/python/lib/_generate_jsonization.py +++ b/aas_core_codegen/python/lib/_generate_jsonization.py @@ -681,11 +681,10 @@ def _json_value_from_jsonable( {I}Parse :paramref:`jsonable` as a JSON-able value. {I}A JSON-able value is, recursively, exactly as JSON itself is defined: -{I}a boolean, a number, a string, an array of JSON-able values or an object -{I}of JSON-able values with string keys. A ``None`` is rejected at any -{I}depth, as the JSON ``null`` has no representation as a JSON-able value, -{I}and so are an infinity and a not-a-number, which JSON can not represent -{I}at all. +{I}a ``None`` (JSON null), a boolean, a number, a string, an array of +{I}JSON-able values or an object of JSON-able values with string keys. +{I}An infinity and a not-a-number are rejected at any depth, as JSON can not +{I}represent them at all. {I}The result is a new structure, and never :paramref:`jsonable` itself, so {I}that the parsed instance does not alias the document it came from. @@ -694,6 +693,9 @@ def _json_value_from_jsonable( {I}:return: parsed JSON-able value {I}:raise: :py:class:`DeserializationException` if unexpected :paramref:`jsonable` {I}""" +{I}if jsonable is None: +{II}return None + {I}# NOTE (mristin): {I}# A ``bool`` is an ``int`` in Python, so it has to be checked first -- {I}# otherwise every boolean would come out as a number. @@ -761,8 +763,8 @@ def _json_value_from_jsonable( {II}return items {I}raise DeserializationException( -{II}f"Expected a JSON-able value (a boolean, a number, a string, an array " -{II}f"or an object), but got: {{type(jsonable)}}" +{II}f"Expected a JSON-able value (a null, a boolean, a number, a string, " +{II}f"an array or an object), but got: {{type(jsonable)}}" {I})''' ), "_json_array_from_jsonable": Stripped( @@ -1519,10 +1521,19 @@ def _generate_concrete_class_from_jsonable( f"The required property {prop.json_name!r} is missing" ) + # NOTE (mristin): + # A JSON-able value might be a JSON null, which we parse as ``None``, + # so we have to check whether the property was there at all. + condition = ( + f"{python_common.string_literal(prop.json_name)} not in mapping" + if isinstance(prop.type_annotation, intermediate.JsonValueTypeAnnotation) + else f"{variable_by_prop_name[prop.name]} is None" + ) + blocks.append( Stripped( f"""\ -if {variable_by_prop_name[prop.name]} is None: +if {condition}: {I}raise DeserializationException( {II}{cause_literal} {I})""" @@ -1706,10 +1717,9 @@ def _json_value_to_jsonable( {I}Serialize :paramref:`value` as a JSON-able structure. {I}A JSON-able value is almost JSON-able as it comes, but not quite: its type -{I}rules out neither a ``None``, which the JSON ``null`` gives no -{I}representation for, nor an infinity and a not-a-number, which JSON can not +{I}rules out neither an infinity and a not-a-number, which JSON can not {I}represent at all, nor a non-string key in an object. Each is refused here, -{I}at any depth. +{I}at any depth. A ``None`` is written out as a JSON null. {I}The result is a new structure, and never :paramref:`value` itself, so that {I}the serialized document does not alias the instance it came from. @@ -1718,6 +1728,9 @@ def _json_value_to_jsonable( {I}:return: :paramref:`value`, as a JSON-able structure {I}:raise: :py:class:`SerializationException` if :paramref:`value` is not JSON-able {I}""" +{I}if value is None: +{II}return None + {I}# NOTE (mristin): {I}# A ``bool`` is an ``int`` in Python, so it has to be checked first -- {I}# otherwise every boolean would come out as a number. @@ -1781,8 +1794,8 @@ def _json_value_to_jsonable( {II}return items {I}raise SerializationException( -{II}f"Expected a JSON-able value (a boolean, a number, a string, an array " -{II}f"or an object), but got: {{type(value)}}" +{II}f"Expected a JSON-able value (a null, a boolean, a number, a string, " +{II}f"an array or an object), but got: {{type(value)}}" {I})''' ) @@ -2800,6 +2813,7 @@ class DeserializationException(Exception): # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ +{I}None, {I}bool, {I}int, {I}float, @@ -2811,6 +2825,7 @@ class DeserializationException(Exception): Stripped( f"""\ MutableJsonable = Union[ +{I}None, {I}bool, {I}int, {I}float, diff --git a/aas_core_codegen/python/lib/_generate_types.py b/aas_core_codegen/python/lib/_generate_types.py index 561521b0b..cd3c44c75 100644 --- a/aas_core_codegen/python/lib/_generate_types.py +++ b/aas_core_codegen/python/lib/_generate_types.py @@ -1829,10 +1829,15 @@ def _generate_json_aliases(uses_json_types: bool) -> List[Stripped]: f"""\ # NOTE (mristin): # A JSON-able value is, recursively, exactly as JSON itself is defined: -# a boolean, a number, a string, an array of JSON-able values or an object -# of JSON-able values with string keys -- never a ``None``, and never +# a ``None`` (JSON null), a boolean, a number, a string, an array of JSON-able +# values or an object of JSON-able values with string keys -- never # an infinity or a not-a-number, neither of which JSON can represent. # +# A ``None`` might appear at any depth, including the value itself. Since +# an absent optional property is also ``None``, an optional JSON-able property +# which is a JSON null is indistinguishable from an absent one, and is treated +# as absent. +# # JSON knows a single numeric type. Python tells an ``int`` from a ``float``, # and a caller will naturally write either, so both are admitted here -- but # an ``int`` only so long as a ``float`` can hold it exactly. The @@ -1844,6 +1849,7 @@ def _generate_json_aliases(uses_json_types: bool) -> List[Stripped]: # (see https://github.com/python/mypy/issues/731), so the item and the value # types have to be spelled ``Any`` instead of ``JsonValue``. JsonValue = Union[ +{I}None, {I}bool, {I}int, {I}float, diff --git a/aas_core_codegen/python/lib/_generate_xml_common.py b/aas_core_codegen/python/lib/_generate_xml_common.py index b867c48d6..40349ae27 100644 --- a/aas_core_codegen/python/lib/_generate_xml_common.py +++ b/aas_core_codegen/python/lib/_generate_xml_common.py @@ -185,6 +185,23 @@ def parse_unqualified_element_tag( {II}\"\"\" {II}self._open_in_no_namespace -= 1 {II}self.stream.write(f'') + +{I}def write_empty_element_in_no_namespace( +{III}self, +{III}name: str +{I}) -> None: +{II}\"\"\" +{II}Write the empty element with the tag name :paramref:`name`, which resides +{II}in no namespace at all. + +{II}If it is the outermost such element, it undeclares the default namespace. + +{II}:param name: of the element tag. Expected to contain no XML special characters. +{II}\"\"\" +{II}if self._open_in_no_namespace == 0: +{III}self.stream.write(f'<{{name}} xmlns=""/>') +{II}else: +{III}self.stream.write(f'<{{name}}/>') """ if uses_json_types else "" diff --git a/aas_core_codegen/python/lib/_generate_xml_rpc.py b/aas_core_codegen/python/lib/_generate_xml_rpc.py index ec6f3ebf2..94edd0c01 100644 --- a/aas_core_codegen/python/lib/_generate_xml_rpc.py +++ b/aas_core_codegen/python/lib/_generate_xml_rpc.py @@ -27,8 +27,10 @@ def _generate_module_docstring( JSON prescribes no XML representation of its own, so we borrow the one of XML-RPC for it: a value is written as a single discriminator element -- -````, ````, ````, ```` or ```` -- -which says what the value is. +````, ````, ````, ````, ```` or +```` -- which says what the value is. XML-RPC itself knows no null, +so we follow the widespread extension, and write a JSON null as an empty +````. This module knows nothing of the meta-model beyond the JSON-able types, and it drives no document of its own. The caller hands over the very stream of @@ -57,9 +59,9 @@ def read_value_content( {I}""" {I}Read the content of :paramref:`element` as a JSON-able value. -{I}The content is a single discriminator element -- ````, -{I}````, ````, ```` or ```` -- which says what -{I}the value is. +{I}The content is a single discriminator element -- ````, +{I}````, ````, ````, ```` or ```` -- +{I}which says what the value is. {I}The end element corresponding to :paramref:`element` will be read as well. @@ -77,7 +79,7 @@ def read_value_content( {I}discriminator = read_next_start_element( {II}iterator, {II}'a discriminator ' -{II}'(one of , , , or )' +{II}'(one of , , , , or )' {I}) {I}result = _read_discriminator(discriminator, iterator) @@ -109,7 +111,7 @@ def _read_discriminator( {I}iterator: Iterator[Tuple[str, Element]] ) -> our_types.JsonValue: {I}""" -{I}Read :paramref:`element` as one of the five XML-RPC discriminators. +{I}Read :paramref:`element` as one of the six XML-RPC discriminators. {I}The end element corresponding to :paramref:`element` will be read as well. @@ -130,6 +132,19 @@ def _read_discriminator( {I}# very element which does not belong here, so naming it in the path as well {I}# as in the message would say nothing more. {I}try: +{II}if tag_wo_ns == 'nil': +{III}read_end_element(element, iterator) + +{III}# NOTE (mristin): +{III}# The text of an element is only complete at its end, so we can check +{III}# it only now. +{III}if element.text is not None and len(element.text.strip()) != 0: +{IIII}raise DeserializationException( +{IIIII}f"Expected no text in a element, but got: {{element.text!r}}" +{IIII}) + +{III}return None + {II}if tag_wo_ns == 'boolean': {III}# NOTE (mristin): {III}# Real XML-RPC tooling writes and expects a strict ``1``/``0``, and not @@ -196,7 +211,7 @@ def _read_discriminator( {I}raise DeserializationException( {II}f"Expected a discriminator element " -{II}f"(one of , , , or ), " +{II}f"(one of , , , , or ), " {II}f"but got: {{tag_wo_ns!r}}" {I})''' ), @@ -439,12 +454,16 @@ def write_discriminator( {I}writer: Writer ) -> None: {I}""" -{I}Write :paramref:`value` as one of the five XML-RPC discriminators. +{I}Write :paramref:`value` as one of the six XML-RPC discriminators. {I}:param value: to be serialized {I}:param writer: to write to {I}:raise: :py:class:`SerializationException` if the value could not be written {I}""" +{I}if value is None: +{II}writer.write_empty_element_in_no_namespace('nil') +{II}return + {I}# NOTE (mristin): {I}# A ``bool`` is an ``int`` in Python, so it has to be checked first -- {I}# otherwise every boolean would be written as a number. @@ -521,8 +540,8 @@ def write_discriminator( {II}return {I}raise SerializationException( -{II}f"Expected a JSON-able value (a boolean, a number, a string, an array " -{II}f"or an object), but got: {{type(value)}}" +{II}f"Expected a JSON-able value (a null, a boolean, a number, a string, " +{II}f"an array or an object), but got: {{type(value)}}" {I})''' ), Stripped( diff --git a/aas_core_codegen/python/lib/_generate_xmlization.py b/aas_core_codegen/python/lib/_generate_xmlization.py index d3eac6eea..dc90e5d7f 100644 --- a/aas_core_codegen/python/lib/_generate_xmlization.py +++ b/aas_core_codegen/python/lib/_generate_xmlization.py @@ -1609,10 +1609,21 @@ def _generate_read_as_sequence(cls: intermediate.ConcreteClass) -> Stripped: f"The required property {prop.xml_name!r} is missing" ) + # NOTE (mristin): + # A JSON-able value might be a JSON null, which we read as ``None``, + # so we have to check whether the property was there at all. + condition = ( + f"{python_common.string_literal(prop.xml_name)} not in values" + if isinstance( + prop.type_annotation, intermediate.JsonValueTypeAnnotation + ) + else f"{variable_by_prop_name[prop.name]} is None" + ) + blocks.append( Stripped( f"""\ -if {variable_by_prop_name[prop.name]} is None: +if {condition}: {I}raise DeserializationException( {II}{cause_literal} {I})""" diff --git a/aas_core_codegen/python/tests/_generate_common_jsonization.py b/aas_core_codegen/python/tests/_generate_common_jsonization.py index 797f572b8..3a27350d1 100644 --- a/aas_core_codegen/python/tests/_generate_common_jsonization.py +++ b/aas_core_codegen/python/tests/_generate_common_jsonization.py @@ -139,7 +139,7 @@ def check_equal( {I}:param got: obtained JSON-able value {I}:yield: differences {I}""" -{I}if isinstance(expected, (bool, int, float, str, bytes)): +{I}if expected is None or isinstance(expected, (bool, int, float, str, bytes)): {II}if type(expected) is not type(got): {III}yield Difference(f"Expected {{type(expected)}}, but got {{type(got)}}") diff --git a/aas_core_codegen/python/tests/_generate_test_json_value_verification.py b/aas_core_codegen/python/tests/_generate_test_json_value_verification.py index 67f6516ae..988458d46 100644 --- a/aas_core_codegen/python/tests/_generate_test_json_value_verification.py +++ b/aas_core_codegen/python/tests/_generate_test_json_value_verification.py @@ -174,6 +174,16 @@ class TestJsonValue(unittest.TestCase): {IIII}paths_and_causes[0][1] {III}) +{I}def test_a_null_is_accepted_at_any_depth(self) -> None: +{II}for value in (None, [None], {{"a": [None, {{"b": None}}]}}): +{III}self.assertEqual( +{IIII}[], +{IIII}_paths_and_causes( +{IIIII}our_json_value_verification.verify_json_value(value) +{IIII}), +{IIII}f"for the value {{value!r}}" +{III}) + {I}def test_a_value_of_an_unexpected_type_is_refused(self) -> None: {II}paths_and_causes = _paths_and_causes( {III}our_json_value_verification.verify_json_value({{"a": object()}}) diff --git a/aas_core_codegen/python/tests/_generate_test_xml_rpc.py b/aas_core_codegen/python/tests/_generate_test_xml_rpc.py index 080aa5902..04c9bf10d 100644 --- a/aas_core_codegen/python/tests/_generate_test_xml_rpc.py +++ b/aas_core_codegen/python/tests/_generate_test_xml_rpc.py @@ -88,17 +88,30 @@ class TestReading(unittest.TestCase): {III}f'n1' {III}f'f1.5' {III}f'sx' +{III}f'z' {III}f'a' {III}f'0' +{III}f'' {III}f'' {III}f'' {II}) {II}self.assertEqual( -{III}{{"b": True, "n": 1.0, "f": 1.5, "s": "x", "a": [0.0]}}, +{III}{{"b": True, "n": 1.0, "f": 1.5, "s": "x", "z": None, "a": [0.0, None]}}, {III}our_xmlrpc.read_value_content(element, iterator) {II}) +{I}def test_a_nil_with_text_is_refused(self) -> None: +{II}element, iterator = _enter( +{III}f'x' +{II}) + +{II}with self.assertRaises(our_xmlcommon.DeserializationException) as context: +{III}our_xmlrpc.read_value_content(element, iterator) + +{II}self.assertEqual('nil', str(context.exception.path)) +{II}self.assertIn("Expected no text in a element", context.exception.cause) + {I}def test_a_discriminator_in_the_document_namespace_is_refused(self) -> None: {II}# NOTE (mristin): {II}# The XML-RPC elements reside in no namespace at all, so a discriminator @@ -288,6 +301,10 @@ class TestWriting(unittest.TestCase): {IIII}'0.0' {III}), {III}( +{IIII}[None], +{IIII}'' +{III}), +{III}( {IIII}{{'k': 0.0}}, {IIII}( {IIIII}'k' @@ -311,6 +328,14 @@ class TestWriting(unittest.TestCase): {IIII}f"for the value {{value!r}}" {III}) +{I}def test_a_null_is_written_as_an_empty_nil(self) -> None: +{II}stream = io.StringIO() +{II}writer = our_xmlcommon.Writer(stream) + +{II}our_xmlrpc.write_discriminator(None, writer) + +{II}self.assertEqual('', stream.getvalue()) + {I}def test_the_path_of_a_failure_points_into_the_value(self) -> None: {II}stream = io.StringIO() {II}writer = our_xmlcommon.Writer(stream) diff --git a/aas_core_codegen/typescript/lib/_generate_common.py b/aas_core_codegen/typescript/lib/_generate_common.py index 79bca8504..f68fa7823 100644 --- a/aas_core_codegen/typescript/lib/_generate_common.py +++ b/aas_core_codegen/typescript/lib/_generate_common.py @@ -401,6 +401,9 @@ def generate( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -416,22 +419,18 @@ def generate( {I}readonly error: ErrorT | null; {I}/** -{I} * Assert that value is set and return it. +{I} * Assert that there is no error and return the value. {I} * -{I} * @returns {{@link value}}, or throw if `null` +{I} * @returns {{@link value}}, or throw if there is an error {I} */ {I}mustValue(): ValueT {{ -{II}if (this.value === null) {{ -{III}throw new Error("Expected value to be set, but it was null"); +{II}if (this.error !== null) {{ +{III}throw new Error("Expected no error, but got one"); {II}}} -{II}return this.value; +{II}return this.value as ValueT; {I}}} {I}constructor(value: ValueT | null, error: ErrorT | null) {{ -{II}if (value === null && error === null) {{ -{III}throw new Error("Unexpected both value and error null in an Either"); -{II}}} - {II}if (value !== null && error !== null) {{ {III}throw new Error("Unexpected both value and error non-null in an Either"); {II}}} diff --git a/aas_core_codegen/typescript/lib/_generate_jsonization.py b/aas_core_codegen/typescript/lib/_generate_jsonization.py index 705de48cc..35c8aa158 100644 --- a/aas_core_codegen/typescript/lib/_generate_jsonization.py +++ b/aas_core_codegen/typescript/lib/_generate_jsonization.py @@ -1281,11 +1281,20 @@ def _generate_parse_properties_of_class(cls: intermediate.ConcreteClass) -> Stri prop.type_annotation, types_module=Identifier("OurTypes") ) + # NOTE (mristin): + # A JSON-able value might be a JSON null, so we mark a required JSON-able + # property which we have not read yet with ``undefined`` instead. + unset = ( + "undefined" + if isinstance(prop.type_annotation, intermediate.JsonValueTypeAnnotation) + else "null" + ) + # NOTE (mristin): # We make all the variables optional since the properties can come in any # order, so we can not tell a missing one from one which we have not read yet. if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation): - var_type = Stripped(f"{var_type} | null") + var_type = Stripped(f"{var_type} | {unset}") message_literal = typescript_common.string_literal( f"The required property {prop.json_name!r} is missing" @@ -1293,7 +1302,7 @@ def _generate_parse_properties_of_class(cls: intermediate.ConcreteClass) -> Stri required_checks.append( Stripped( f"""\ -if ({var_name} === null) {{ +if ({var_name} === {unset}) {{ {I}return newDeserializationError< {II}OurTypes.{cls_name} {I}>( @@ -1303,7 +1312,7 @@ def _generate_parse_properties_of_class(cls: intermediate.ConcreteClass) -> Stri ) ) - var_declarations.append(Stripped(f"let {var_name}: {var_type} = null;")) + var_declarations.append(Stripped(f"let {var_name}: {var_type} = {unset};")) parse_cases.append( _generate_parse_case( @@ -1716,7 +1725,13 @@ def _generate_json_able_helpers( function jsonValueFromJsonable( {I}jsonable: JsonValue ): OurCommon.Either {{ -{I}if (jsonable === null || jsonable === undefined) {{ +{I}if (jsonable === null) {{ +{II}return new OurCommon.Either( +{III}null, null +{II}); +{I}}} + +{I}if (jsonable === undefined) {{ {II}return newDeserializationError( {III}`Expected a JSON-able value, but got: ${{jsonable}}` {II}); @@ -1749,7 +1764,7 @@ def _generate_json_able_helpers( {II}default: {III}return newDeserializationError( -{IIII}`Expected a JSON-able value (a boolean, a number, a string, ` + +{IIII}`Expected a JSON-able value (a null, a boolean, a number, a string, ` + {IIIII}`an array or an object), but got: ${{typeof jsonable}}` {III}); {I}}} @@ -1869,7 +1884,11 @@ def _generate_json_able_helpers( function jsonValueToJsonable(that: OurTypes.JsonObject): JsonObject; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue {{ -{I}if (that === null || that === undefined) {{ +{I}if (that === null) {{ +{II}return null; +{I}}} + +{I}if (that === undefined) {{ {II}throw new SerializationError( {III}`Expected a JSON-able value, but got: ${{that}}` {II}); @@ -1897,7 +1916,7 @@ def _generate_json_able_helpers( {II}default: {III}throw new SerializationError( -{IIII}`Expected a JSON-able value (a boolean, a number, a string, ` + +{IIII}`Expected a JSON-able value (a null, a boolean, a number, a string, ` + {IIIII}`an array or an object), but got: ${{typeof that}}` {III}); {I}}} @@ -2754,7 +2773,13 @@ def generate( ), Stripped( """\ -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue };""" diff --git a/aas_core_codegen/typescript/lib/_generate_types.py b/aas_core_codegen/typescript/lib/_generate_types.py index af38643e3..c4eb617fb 100644 --- a/aas_core_codegen/typescript/lib/_generate_types.py +++ b/aas_core_codegen/typescript/lib/_generate_types.py @@ -2514,12 +2514,18 @@ def _generate_json_aliases(symbol_table: intermediate.SymbolTable) -> List[Strip /** * Represent a value which JSON can carry. * - * This is, recursively, exactly as JSON itself is defined: a boolean, - * a number, a string, an array of JSON-able values or an object of JSON-able - * values with string keys -- never a `null`, and never a non-finite number, - * which JSON can not represent at all. + * This is, recursively, exactly as JSON itself is defined: a `null`, + * a boolean, a number, a string, an array of JSON-able values or an object of + * JSON-able values with string keys -- never a non-finite number, which JSON + * can not represent at all. + * + * A `null` might appear at any depth, including the value itself. Since + * an absent optional property is also `null`, an optional JSON-able property + * which is a JSON null is indistinguishable from an absent one, and is treated + * as absent. */ export type JsonValue = +{I}| null {I}| boolean {I}| number {I}| string diff --git a/aas_core_codegen/typescript/lib/_generate_verification.py b/aas_core_codegen/typescript/lib/_generate_verification.py index 5a55541db..f6fca060b 100644 --- a/aas_core_codegen/typescript/lib/_generate_verification.py +++ b/aas_core_codegen/typescript/lib/_generate_verification.py @@ -1236,9 +1236,10 @@ def _generate_verify_json_value( A JSON-able value is, recursively, exactly as JSON itself is defined, but its TypeScript type rules out neither of the two ways of not being one: - a non-finite number and a ``null``, which `strictNullChecks` catches at - the boundary but not inside a value handed over as `any` by a JavaScript - caller. Both are reported here, at any depth. + a non-finite number and an ``undefined``, which `strictNullChecks` catches + at the boundary but not inside a value handed over as `any` by + a JavaScript caller. Both are reported here, at any depth. A ``null`` is + a JSON null, and hence a JSON-able value. The path of an error leads all the way down to the culprit: an item of an open JSON array contributes an `IndexSegment` and a member of an open @@ -1270,7 +1271,11 @@ def _generate_verify_json_value( export function *verifyJsonValue( {I}value: OurTypes.JsonValue ): IterableIterator {{ -{I}if (value === null || value === undefined) {{ +{I}if (value === null) {{ +{II}return; +{I}}} + +{I}if (value === undefined) {{ {II}yield new VerificationError( {III}`Expected a JSON-able value, but got: ${{value}}` {II}); @@ -1299,7 +1304,7 @@ def _generate_verify_json_value( {II}default: {III}yield new VerificationError( -{IIII}`Expected a JSON-able value (a boolean, a number, a string, ` + +{IIII}`Expected a JSON-able value (a null, a boolean, a number, a string, ` + {IIIII}`an array or an object), but got: ${{typeof value}}` {III}); {III}return; diff --git a/aas_core_codegen/typescript/lib/_generate_xml_rpc.py b/aas_core_codegen/typescript/lib/_generate_xml_rpc.py index 4b70d4f1c..b891f3ab4 100644 --- a/aas_core_codegen/typescript/lib/_generate_xml_rpc.py +++ b/aas_core_codegen/typescript/lib/_generate_xml_rpc.py @@ -46,8 +46,9 @@ def _generate_parsers() -> List[Stripped]: * Parse the content of an element holding a JSON-able value. * * @remarks - * The content is a single discriminator element -- ``, ``, - * ``, `` or `` -- which says what the value is. + * The content is a single discriminator element -- ``, ``, + * ``, ``, `` or `` -- which says what + * the value is. * * @param cursor - to read from * @returns parsed JSON-able value, or an error @@ -71,8 +72,8 @@ def _generate_parsers() -> List[Stripped]: {II}// it is that very element which does not belong here, so naming it in {II}// the path as well as in the message would say nothing more. {II}return newDeserializationError( -{III}`Expected a discriminator element (one of 'boolean', 'double', ` + -{IIII}`'string', 'array' or 'struct'), but got: '${{localName}}'` +{III}`Expected a discriminator element (one of 'nil', 'boolean', ` + +{IIII}`'double', 'string', 'array' or 'struct'), but got: '${{localName}}'` {II}); {I}}} @@ -102,6 +103,8 @@ def _generate_parsers() -> List[Stripped]: {I}localName: string ): ContentParser | null {{ {I}switch (localName) {{ +{II}case "nil": +{III}return parseNilContent; {II}case "boolean": {III}return parseBooleanContent; {II}case "double": @@ -115,6 +118,33 @@ def _generate_parsers() -> List[Stripped]: {II}default: {III}return null; {I}}} +}}""" + ), + Stripped( + f"""\ +/** + * Parse the content of a `` element. + * + * @remarks + * XML-RPC itself knows no null, so we follow the widespread extension, and + * read a JSON null from an empty ``. + * + * @param cursor - to read from + * @returns JSON null, or an error + */ +function parseNilContent( +{I}cursor: XmlCursor +): OurCommon.Either {{ +{I}const text = parseTextContent(cursor); +{I}if (text !== "") {{ +{II}return newDeserializationError( +{III}`Expected no text in a 'nil' element, but got: '${{text}}'` +{II}); +{I}}} + +{I}return new OurCommon.Either( +{II}null, null +{I}); }}""" ), Stripped( @@ -453,7 +483,12 @@ def _generate_writers() -> List[Stripped]: ): void {{ {I}const xmlns = undeclareNamespace ? ' xmlns=""' : ""; -{I}if (value === null || value === undefined) {{ +{I}if (value === null) {{ +{II}parts.push(``); +{II}return; +{I}}} + +{I}if (value === undefined) {{ {II}throw new SerializationError( {III}`Expected a JSON-able value, but got: ${{value}}` {II}); @@ -495,7 +530,7 @@ def _generate_writers() -> List[Stripped]: {II}default: {III}throw new SerializationError( -{IIII}`Expected a JSON-able value (a boolean, a number, a string, ` + +{IIII}`Expected a JSON-able value (a null, a boolean, a number, a string, ` + {IIIII}`an array or an object), but got: ${{typeof value}}` {III}); {I}}} @@ -623,7 +658,9 @@ def generate() -> str: * * JSON prescribes no XML representation of its own, so a JSON-able value is * written as the subset of XML-RPC which covers exactly what such a value can - * be: ``, ``, ``, `` and ``. + * be: ``, ``, ``, ``, `` and ``. + * XML-RPC itself knows no null, so we follow the widespread extension, and + * write a JSON null as an empty ``. * * These elements reside in no namespace at all, as the XML-RPC specification * prescribes, and not in the namespace of the enclosing document. The diff --git a/aas_core_codegen/typescript/lib/_generate_xmlization.py b/aas_core_codegen/typescript/lib/_generate_xmlization.py index a49a591a9..d415b37f4 100644 --- a/aas_core_codegen/typescript/lib/_generate_xmlization.py +++ b/aas_core_codegen/typescript/lib/_generate_xmlization.py @@ -668,6 +668,24 @@ def _generate_parse_tuple_function(arity: int) -> Stripped: ) +def _unset_literal(type_annotation: intermediate.TypeAnnotationUnion) -> str: + """ + Give the literal which marks a property that we have not read yet. + + A JSON-able value might be a JSON null, so we mark it with ``undefined`` + instead of ``null``. Otherwise, we could tell neither a missing required + property nor a duplicate one. + """ + return ( + "undefined" + if isinstance( + intermediate.beneath_optional(type_annotation), + intermediate.JsonValueTypeAnnotation, + ) + else "null" + ) + + @require(lambda cls, prop: intermediate.runtime_id(prop) in cls.property_id_set) def _generate_parse_case_for_property( cls: intermediate.ConcreteClass, @@ -683,6 +701,8 @@ def _generate_parse_case_for_property( xml_name_literal = typescript_common.string_literal(prop.xml_name) + unset = _unset_literal(prop.type_annotation) + # NOTE (mristin): # A case sits two levels below the ``switch``, which the class's parser indents by # three, so the body of a case lands on the fourth level. @@ -712,7 +732,7 @@ def _generate_parse_case_for_property( return Stripped( f"""\ case {xml_name_literal}: {{ -{I}if ({var_name} !== null) {{ +{I}if ({var_name} !== {unset}) {{ {II}propertyError = duplicatePropertyError(propertyLocalName); {II}break; {I}}} @@ -744,10 +764,16 @@ def _generate_parse_concrete_class(cls: intermediate.ConcreteClass) -> Stripped: prop.type_annotation, types_module=Identifier("OurTypes"), ) - if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation): - var_type = Stripped(f"{var_type} | null") - var_declarations.append(Stripped(f"let {var_name}: {var_type} = null;")) + unset = _unset_literal(prop.type_annotation) + + if ( + not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation) + or unset == "undefined" + ): + var_type = Stripped(f"{var_type} | {unset}") + + var_declarations.append(Stripped(f"let {var_name}: {var_type} = {unset};")) if not isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation): message_literal = typescript_common.string_literal( @@ -756,7 +782,7 @@ def _generate_parse_concrete_class(cls: intermediate.ConcreteClass) -> Stripped: required_checks.append( Stripped( f"""\ -if ({var_name} === null) {{ +if ({var_name} === {unset}) {{ {I}return newDeserializationError( {II}{message_literal} {I}); @@ -796,7 +822,17 @@ def _generate_parse_concrete_class(cls: intermediate.ConcreteClass) -> Stripped: writer.write(f"const instance = new OurTypes.{cls_name}(\n") for i, arg in enumerate(cls.constructor.arguments): var_name = var_name_by_property[arg.name] - writer.write(f"{I}{var_name}") + + # NOTE (mristin): + # An optional JSON-able property which we did not read is + # ``undefined``, see :py:func:`_unset_literal`. + if ( + isinstance(arg.type_annotation, intermediate.OptionalTypeAnnotation) + and _unset_literal(arg.type_annotation) == "undefined" + ): + writer.write(f"{I}{var_name} ?? null") + else: + writer.write(f"{I}{var_name}") if i < len(cls.constructor.arguments) - 1: writer.write(",\n") else: diff --git a/aas_core_codegen/typescript/tests/_generate_verification_json_value_spec.py b/aas_core_codegen/typescript/tests/_generate_verification_json_value_spec.py index 72498b3ae..aab61c247 100644 --- a/aas_core_codegen/typescript/tests/_generate_verification_json_value_spec.py +++ b/aas_core_codegen/typescript/tests/_generate_verification_json_value_spec.py @@ -156,8 +156,24 @@ def generate() -> str: ), Stripped( f"""\ -test("a null is refused at any depth", () => {{ -{I}const value = {{ a: [null] }} as unknown as OurTypes.JsonValue; +test("a null is accepted at any depth", () => {{ +{I}const values: Array = [ +{II}null, +{II}[null], +{II}{{ a: [null, {{ b: null }}] }} +{I}]; + +{I}for (const value of values) {{ +{II}expect( +{III}pathsAndMessages(OurVerification.verifyJsonValue(value)) +{II}).toStrictEqual([]); +{I}}} +}});""" + ), + Stripped( + f"""\ +test("an undefined is refused at any depth", () => {{ +{I}const value = {{ a: [undefined] }} as unknown as OurTypes.JsonValue; {I}const pathsAndCauses = pathsAndMessages( {II}OurVerification.verifyJsonValue(value) @@ -165,7 +181,7 @@ def generate() -> str: {I}expect(pathsAndCauses.length).toStrictEqual(1); {I}expect(pathsAndCauses[0][0]).toStrictEqual('["a"][0]'); -{I}expect(pathsAndCauses[0][1]).toContain("but got: null"); +{I}expect(pathsAndCauses[0][1]).toContain("but got: undefined"); }});""" ), Stripped( diff --git a/aas_core_codegen/typescript/tests/_generate_xml_rpc_spec.py b/aas_core_codegen/typescript/tests/_generate_xml_rpc_spec.py index d6be72b04..2c122dcb2 100644 --- a/aas_core_codegen/typescript/tests/_generate_xml_rpc_spec.py +++ b/aas_core_codegen/typescript/tests/_generate_xml_rpc_spec.py @@ -96,8 +96,10 @@ def generate(symbol_table: intermediate.SymbolTable) -> str: {III}"b1" + {III}"f1.5" + {III}"sx" + +{III}"z" + {III}"a" + {IIII}"0" + +{IIII}"" + {III}"" + {III}"" {I}); @@ -109,8 +111,21 @@ def generate(symbol_table: intermediate.SymbolTable) -> str: {II}b: true, {II}f: 1.5, {II}s: "x", -{II}a: [0] +{II}z: null, +{II}a: [0, null] {I}}}); +}});""" + ), + Stripped( + f"""\ +test("a nil with text is refused", () => {{ +{I}const cursor = enter(`x`); + +{I}const parsed = OurXmlRpc.parseValueContent(cursor); + +{I}const error = mustError(parsed); +{I}expect(error.path.toString()).toStrictEqual("nil"); +{I}expect(error.message).toContain("Expected no text in a 'nil' element"); }});""" ), Stripped( @@ -260,6 +275,8 @@ def generate(symbol_table: intermediate.SymbolTable) -> str: {II}[1, '1'], {II}[1.5, '1.5'], {II}["aa<b&c'], +{II}[null, ''], +{II}[[null], ''], {II}[ {III}[0], {III}'0' diff --git a/aas_core_codegen/xsd/main.py b/aas_core_codegen/xsd/main.py index baea96e23..890d17063 100644 --- a/aas_core_codegen/xsd/main.py +++ b/aas_core_codegen/xsd/main.py @@ -322,13 +322,13 @@ def _translate_to_simple_type( # NOTE (mristin): # We represent ``JSONValue``/``JSONArray``/``JSONObject`` with a restricted subset # of XML-RPC's own element vocabulary: ````, ````, ````, -# ````, ````, ````, ````, ```` and ````. +# ````, ````, ````, ````, ```` and ````, +# together with the widespread ```` extension for a JSON null. # We deliberately keep XML-RPC's wrapper elements (````, ````, # ````, ````) instead of a flatter, wrapper-free alternative, for # real interoperability with existing XML-RPC tooling. We leave out the parts of # XML-RPC that a ``JSONValue`` has no equivalent for -- ````/````, -# ````, ```` and ```` (there is no ``null`` -# variant of a JSON-able value). +# ```` and ````. # # These elements reside in no namespace at all, as the XML-RPC specification # prescribes, and not in the target namespace of this schema, so each of their @@ -391,6 +391,14 @@ def _generate_json_type_definitions() -> List[ET.Element]: The root element is to be *extended* with the resulting list. """ xs_value_choice = ET.Element("xs:choice") + + # NOTE (mristin): + # XML-RPC itself knows no null. We follow the widespread extension, and + # represent a JSON null as an empty ````. + xs_nil = _xml_rpc_element({"name": "nil"}) + ET.SubElement(xs_nil, "xs:complexType") + xs_value_choice.append(xs_nil) + xs_value_choice.append(_xml_rpc_element({"name": "boolean", "type": "xs:boolean"})) xs_value_choice.append(_xml_rpc_element({"name": "double", "type": "xs:double"})) xs_value_choice.append(_xml_rpc_element({"name": "string", "type": "xs:string"})) diff --git a/dev/test_data/live_tests/common_test_data/json_types/Json/Expected/Something/nulls.json b/dev/test_data/live_tests/common_test_data/json_types/Json/Expected/Something/nulls.json new file mode 100644 index 000000000..5a89d1b5f --- /dev/null +++ b/dev/test_data/live_tests/common_test_data/json_types/Json/Expected/Something/nulls.json @@ -0,0 +1,19 @@ +{ + "value": null, + "values": [ + 1.5, + null + ], + "mapping": { + "a": null, + "b": [ + null, + { + "c": null + } + ] + }, + "mappingWithConstrainedKey": { + "nonEmptyKey": null + } +} diff --git a/dev/test_data/live_tests/common_test_data/json_types/Json/Unexpected/Unserializable/TypeViolation/Something/value_as_null.json b/dev/test_data/live_tests/common_test_data/json_types/Json/Unexpected/Unserializable/TypeViolation/Something/value_as_null.json deleted file mode 100644 index ef8909793..000000000 --- a/dev/test_data/live_tests/common_test_data/json_types/Json/Unexpected/Unserializable/TypeViolation/Something/value_as_null.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "value": null, - "values": [ - 1.0, - "two", - false - ], - "mapping": { - "a": 1.0 - }, - "mappingWithConstrainedKey": { - "nonEmptyKey": "some value" - } -} diff --git a/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/mapping_as_array.json.error b/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/mapping_as_array.json.error deleted file mode 100644 index 426f35e4f..000000000 --- a/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/mapping_as_array.json.error +++ /dev/null @@ -1 +0,0 @@ -mapping: Expected a JSON object, but got a value of type: array \ No newline at end of file diff --git a/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/value_as_null.json.error b/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/value_as_null.json.error deleted file mode 100644 index 10100dbc7..000000000 --- a/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/value_as_null.json.error +++ /dev/null @@ -1 +0,0 @@ -value: Expected a JSON-able value (a boolean, a number, a string, an array or an object), but got a value of type: null \ No newline at end of file diff --git a/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/values_as_object.json.error b/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/values_as_object.json.error deleted file mode 100644 index b080aaed2..000000000 --- a/dev/test_data/live_tests/common_test_data/json_types/JsonizationError/Unexpected/Unserializable/TypeViolation/Something/values_as_object.json.error +++ /dev/null @@ -1 +0,0 @@ -values: Expected a JSON array, but got a value of type: object \ No newline at end of file diff --git a/dev/test_data/live_tests/common_test_data/json_types/VerificationError/Unexpected/Invalid/ConstrainViolated/something/empty_key_in_mapping_with_constrained_key.xml.errors b/dev/test_data/live_tests/common_test_data/json_types/VerificationError/Unexpected/Invalid/ConstrainViolated/something/empty_key_in_mapping_with_constrained_key.xml.errors deleted file mode 100644 index 89c9d6ea5..000000000 --- a/dev/test_data/live_tests/common_test_data/json_types/VerificationError/Unexpected/Invalid/ConstrainViolated/something/empty_key_in_mapping_with_constrained_key.xml.errors +++ /dev/null @@ -1 +0,0 @@ -.mapping_with_constrained_key[""]: At least one character \ No newline at end of file diff --git a/dev/test_data/live_tests/common_test_data/json_types/Xml/Expected/something/nulls.xml b/dev/test_data/live_tests/common_test_data/json_types/Xml/Expected/something/nulls.xml new file mode 100644 index 000000000..33e9bd791 --- /dev/null +++ b/dev/test_data/live_tests/common_test_data/json_types/Xml/Expected/something/nulls.xml @@ -0,0 +1 @@ +1.5abcnonEmptyKey \ No newline at end of file diff --git a/dev/test_data/live_tests/common_test_data/json_types/Xml/Unexpected/Unserializable/TypeViolation/something/nil_with_content.xml b/dev/test_data/live_tests/common_test_data/json_types/Xml/Unexpected/Unserializable/TypeViolation/something/nil_with_content.xml new file mode 100644 index 000000000..1d2d104a0 --- /dev/null +++ b/dev/test_data/live_tests/common_test_data/json_types/Xml/Unexpected/Unserializable/TypeViolation/something/nil_with_content.xml @@ -0,0 +1 @@ +unexpected1.5a1.5nonEmptyKeysome value \ No newline at end of file diff --git a/dev/test_data/live_tests/common_test_data/json_types/XmlizationError/Unexpected/Unserializable/TypeViolation/something/unexpected_element_in_value.xml.error b/dev/test_data/live_tests/common_test_data/json_types/XmlizationError/Unexpected/Unserializable/TypeViolation/something/unexpected_element_in_value.xml.error deleted file mode 100644 index 2dcf990ab..000000000 --- a/dev/test_data/live_tests/common_test_data/json_types/XmlizationError/Unexpected/Unserializable/TypeViolation/something/unexpected_element_in_value.xml.error +++ /dev/null @@ -1 +0,0 @@ -something/value: Expected one of , , , or , but got a start element \ No newline at end of file diff --git a/dev/test_data/live_tests/common_test_data/json_types/XmlizationError/Unexpected/Unserializable/TypeViolation/something/values_as_struct.xml.error b/dev/test_data/live_tests/common_test_data/json_types/XmlizationError/Unexpected/Unserializable/TypeViolation/something/values_as_struct.xml.error deleted file mode 100644 index 44639d5f7..000000000 --- a/dev/test_data/live_tests/common_test_data/json_types/XmlizationError/Unexpected/Unserializable/TypeViolation/something/values_as_struct.xml.error +++ /dev/null @@ -1 +0,0 @@ -something/values: Expected a start element , but got a start node \ No newline at end of file diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.cpp b/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.cpp index 1f9abe379..893e09045 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.cpp @@ -24,10 +24,10 @@ namespace { common::optional CheckShallowly( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.hpp b/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.hpp index b2e6d473e..42c9230f8 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.hpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/src/json_value_verification.hpp @@ -32,8 +32,8 @@ enum class JsonValueShape { * \brief Exhaustively verify that a ``nlohmann::json`` value is JSON-able. * * A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys -- in particular, ``null``, a binary + * a ``null``, a boolean, a number, a string, an array of JSON-able values or + * an object of JSON-able values with string keys -- in particular, a binary * value and a discarded value are rejected, at any depth. * * We deliberately walk the value with an explicit stack instead of plain diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/json_types/expected_output/src/jsonization.cpp index 694fa5c5a..77e398aac 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/src/jsonization.cpp @@ -702,7 +702,7 @@ std::pair< > DeserializeJsonValue( const nlohmann::json& json ) { - if (json.is_null() || json.is_binary() || json.is_discarded()) { + if (json.is_binary() || json.is_discarded()) { return std::make_pair< common::optional, common::optional @@ -710,7 +710,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(json.type_name()) ) @@ -1155,11 +1155,16 @@ std::pair< the_mapping_with_constrained_key, DeserializeJsonObject(value) ); - case properties::OfSomething::kOptionalValue: - return ParseInto( + case properties::OfSomething::kOptionalValue: { + common::optional parse_error = ParseInto( the_optional_value, DeserializeJsonValue(value) ); + if (!parse_error.has_value() && the_optional_value->is_null()) { + the_optional_value = common::nullopt; + } + return parse_error; + } case properties::OfSomething::kOptionalValues: return ParseInto( the_optional_values, @@ -1708,12 +1713,12 @@ std::pair< > SerializeJsonValue( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return std::make_pair( common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) @@ -1937,7 +1942,7 @@ std::pair< const common::optional& maybe_optional_value( that.optional_value() ); - if (maybe_optional_value.has_value()) { + if (maybe_optional_value.has_value() && !maybe_optional_value->is_null()) { error = SerializeInto( result, "optionalValue", diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.cpp b/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.cpp index de61be596..abf1d37d3 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.cpp @@ -349,7 +349,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got ", xml_common::NodeToHumanReadableWstring(reader.node()) ) @@ -374,7 +374,16 @@ std::pair< common::optional value; common::optional error; - if (discriminator == "boolean") { + if (discriminator == "nil") { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and read a JSON null from an empty . Any content other than + // whitespace is refused by the check of the stop element below. + error = SkipWhitespace(reader); + if (!error.has_value()) { + value = nlohmann::json(nullptr); + } + } else if (discriminator == "boolean") { std::tie(value, error) = DeserializeBooleanBodyFrom(reader); } else if (discriminator == "double") { std::tie(value, error) = DeserializeDoubleBodyFrom(reader); @@ -389,7 +398,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got a start element <", common::Utf8ToWstring(discriminator), L">" @@ -874,7 +883,13 @@ common::optional SerializeValueBodyTo( const nlohmann::json& value, bool undeclare_namespace ) { - if (value.is_boolean()) { + if (value.is_null()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.StartElementInNoNamespace("nil", undeclare_namespace); + writer.StopElement("nil"); + } else if (value.is_boolean()) { writer.StartElementInNoNamespace( "boolean", undeclare_namespace ); @@ -945,11 +960,11 @@ common::optional SerializeValueBodyTo( writer.StopElement("struct"); } else { // NOTE (mristin): - // This covers ``null``, a binary value and a discarded value -- none of - // which are representable as a ``JSONValue``. + // This covers a binary value and a discarded value -- neither of which + // is representable as a ``JSONValue``. return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.hpp b/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.hpp index e17a87864..25488cf4c 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.hpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/src/xml_rpc.hpp @@ -88,8 +88,8 @@ common::expected< * document which is already being read through \p reader. * * \p reader is expected to be positioned at the start element of - * the discriminator (\c \, \c \, \c \, - * \c \ or \c \). On success, \p reader is left + * the discriminator (\c \, \c \, \c \, + * \c \, \c \ or \c \). On success, \p reader is left * positioned at the node right after the discriminator's own stop element, * mirroring how the primitive ``Deserialize*`` functions in ``xmlization`` * behave. @@ -164,8 +164,8 @@ struct SerializationError { * * \param os the UTF-8-encoded output stream where the XML will be written * \param value to be serialized; it must be JSON-able (recursively - * a boolean, a number, a string, an array or an object with string keys), - * and never \c null, or a \ref SerializationError is returned + * a null, a boolean, a number, a string, an array or an object with string + * keys), or a \ref SerializationError is returned * \return An error, if any. * * The XML-RPC elements reside in no namespace at all, as the XML-RPC @@ -181,8 +181,9 @@ common::optional SerializeValue( * \brief Serialize \p value, embedded within a larger XML document which is * already being written through \p writer. * - * This writes exactly one discriminator element (\c \, - * \c \, \c \, \c \ or \c \). + * This writes exactly one discriminator element (\c \, + * \c \, \c \, \c \, \c \ or + * \c \). * * \p undeclare_namespace tells whether that element is the outermost one * which resides in no namespace. That one undeclares the default namespace of diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/src/xmlization.cpp b/dev/test_data/main/cpp/expected/json_types/expected_output/src/xmlization.cpp index 4e553dd9a..e67bfabdb 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/src/xmlization.cpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/src/xmlization.cpp @@ -1492,11 +1492,16 @@ std::pair< the_mapping_with_constrained_key, DeserializeJsonObjectFromXmlRpc(reader) ); - case properties::OfSomething::kOptionalValue: - return ReadInto( + case properties::OfSomething::kOptionalValue: { + common::optional read_error = ReadInto( the_optional_value, DeserializeJsonValueFromXmlRpc(reader) ); + if (!read_error.has_value() && the_optional_value->is_null()) { + the_optional_value = common::nullopt; + } + return read_error; + } case properties::OfSomething::kOptionalValues: return ReadInto( the_optional_values, @@ -2044,15 +2049,20 @@ common::optional SerializeSomethingAsSequence( return error; } - error = WriteProperty( - "optionalValue", - that.optional_value(), - writer, - iteration::Property::kOptionalValue, - WriteJsonValueToXmlRpc + const common::optional& maybe_optional_value( + that.optional_value() ); - if (error.has_value()) { - return error; + if (maybe_optional_value.has_value() && !maybe_optional_value->is_null()) { + error = WriteProperty( + "optionalValue", + *maybe_optional_value, + writer, + iteration::Property::kOptionalValue, + WriteJsonValueToXmlRpc + ); + if (error.has_value()) { + return error; + } } error = WriteProperty( diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_json_value_verification.cpp b/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_json_value_verification.cpp index a382299be..64626bf8a 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_json_value_verification.cpp @@ -81,8 +81,27 @@ TEST_CASE("Report no violation for a deeply nested, valid value") { REQUIRE(CollectErrors(verificator).empty()); } -TEST_CASE("Reject a null value at the top level") { - const nlohmann::json value(nullptr); +TEST_CASE("Accept a null value at every depth") { + for ( + const char* text : { + "null", + "[null]", + R"({"a": [null, {"b": null}]})" + } + ) { + const nlohmann::json value = nlohmann::json::parse(text); + + verification::JsonValueVerificator verificator( + value, verification::JsonValueShape::kAny + ); + + INFO(std::string("The value is: ") + text); + REQUIRE(CollectErrors(verificator).empty()); + } +} + +TEST_CASE("Reject a discarded value at the top level") { + const nlohmann::json value(nlohmann::json::value_t::discarded); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -138,8 +157,10 @@ TEST_CASE( "Report every rejected value at every depth, not just the first" ) { nlohmann::json value = nlohmann::json::parse( - R"({"a": [1.0, null, {"b": null}]})" + R"({"a": [1.0, 0.0, {"b": 0.0}]})" ); + value["a"][1] = std::numeric_limits::quiet_NaN(); + value["a"][2]["b"] = std::numeric_limits::infinity(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -148,8 +169,8 @@ TEST_CASE( const std::vector errors(CollectErrors(verificator)); INFO( - "Expected exactly two violations (one for each nested null), " - "but got: " + "Expected exactly two violations (one for each nested non-finite " + "number), but got: " + std::to_string(errors.size()) ); REQUIRE(errors.size() == 2); @@ -165,8 +186,10 @@ TEST_CASE("Report the violations in the order of a depth-first walk") { // what distinguishes the depth-first walk from a breadth-first one -- // the latter would report ``z`` first, as it sits one level higher. nlohmann::json value = nlohmann::json::parse( - R"({"a": {"deep": null}, "z": null})" + R"({"a": {"deep": 0.0}, "z": 0.0})" ); + value["a"]["deep"] = std::numeric_limits::quiet_NaN(); + value["z"] = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject @@ -187,8 +210,9 @@ TEST_CASE("Escape a key of a JSON object in the path") { // ``nlohmann::json`` value, so a key is escaped exactly as a JSON // string is. nlohmann::json value = nlohmann::json::parse( - R"({"a\"b\\c\nd": null})" + R"({"a\"b\\c\nd": 0.0})" ); + value.begin().value() = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_xml_rpc.cpp b/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_xml_rpc.cpp index 4578e4a5a..89bda2904 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/test/test_xml_rpc.cpp @@ -176,18 +176,39 @@ TEST_CASE("Round-trip an object") { ); } -TEST_CASE("Serialize rejects null") { +TEST_CASE("Round-trip a null at every depth") { + RequireRoundTrip(nlohmann::json(nullptr)); + + RequireRoundTrip( + nlohmann::json::parse( + R"({"a": null, "b": [null, {"c": null}]})" + ) + ); +} + +TEST_CASE("Serialize a null as an empty nil") { std::ostringstream oss; common::optional error( xml_rpc::SerializeValue(oss, nlohmann::json(nullptr)) ); - REQUIRE(error.has_value()); + REQUIRE(!error.has_value()); + REQUIRE(oss.str().find("") != std::string::npos); +} + +TEST_CASE("Deserialize rejects a nil with text") { + std::istringstream iss( + "x" + ); + common::expected result( + xml_rpc::DeserializeValue(iss) + ); + REQUIRE(!result.has_value()); } TEST_CASE("Serialize reports the path to a nested invalid value") { nlohmann::json value = nlohmann::json::array(); value.push_back(1.0); - value.push_back(nlohmann::json(nullptr)); + value.push_back(std::numeric_limits::quiet_NaN()); std::ostringstream oss; common::optional error( @@ -200,7 +221,7 @@ TEST_CASE("Serialize reports the path to a nested invalid value") { TEST_CASE("Serialize reports the key of an invalid member") { nlohmann::json value = nlohmann::json::object(); value["a"] = 1.0; - value["b"] = nlohmann::json(nullptr); + value["b"] = std::numeric_limits::quiet_NaN(); std::ostringstream oss; common::optional error( @@ -224,7 +245,7 @@ TEST_CASE("Deserialize reports the XPath to a nested invalid value") { "" "nested" "1" - "" + "1" "" "" ); @@ -251,7 +272,7 @@ TEST_CASE("Deserialize reports no wrapper for an array body") { // a property. std::istringstream iss( "" - "" + "1" "" ); xml_common::ReaderMergingText reader( @@ -273,7 +294,7 @@ TEST_CASE("Deserialize reports no wrapper for a struct body") { // definition an object holds the elements directly. std::istringstream iss( "" - "k" + "k1" "" ); xml_common::ReaderMergingText reader( @@ -295,7 +316,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { // break out of the XPath predicate which matches on it. std::istringstream iss( "" - "a&b"c" + "a&b"c1" "" ); xml_common::ReaderMergingText reader( @@ -316,7 +337,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { TEST_CASE("Deserialize rejects an unexpected element") { std::istringstream iss( - "" + "1" ); common::expected result( xml_rpc::DeserializeValue(iss) diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.cpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.cpp index 1f9abe379..893e09045 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.cpp @@ -24,10 +24,10 @@ namespace { common::optional CheckShallowly( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.hpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.hpp index b2e6d473e..42c9230f8 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/json_value_verification.hpp @@ -32,8 +32,8 @@ enum class JsonValueShape { * \brief Exhaustively verify that a ``nlohmann::json`` value is JSON-able. * * A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys -- in particular, ``null``, a binary + * a ``null``, a boolean, a number, a string, an array of JSON-able values or + * an object of JSON-able values with string keys -- in particular, a binary * value and a discarded value are rejected, at any depth. * * We deliberately walk the value with an explicit stack instead of plain diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/jsonization.cpp index 3f7c456c0..8cccedbc2 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/jsonization.cpp @@ -702,7 +702,7 @@ std::pair< > DeserializeJsonValue( const nlohmann::json& json ) { - if (json.is_null() || json.is_binary() || json.is_discarded()) { + if (json.is_binary() || json.is_discarded()) { return std::make_pair< common::optional, common::optional @@ -710,7 +710,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(json.type_name()) ) @@ -2013,12 +2013,12 @@ std::pair< > SerializeJsonValue( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return std::make_pair( common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.cpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.cpp index de61be596..abf1d37d3 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.cpp @@ -349,7 +349,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got ", xml_common::NodeToHumanReadableWstring(reader.node()) ) @@ -374,7 +374,16 @@ std::pair< common::optional value; common::optional error; - if (discriminator == "boolean") { + if (discriminator == "nil") { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and read a JSON null from an empty . Any content other than + // whitespace is refused by the check of the stop element below. + error = SkipWhitespace(reader); + if (!error.has_value()) { + value = nlohmann::json(nullptr); + } + } else if (discriminator == "boolean") { std::tie(value, error) = DeserializeBooleanBodyFrom(reader); } else if (discriminator == "double") { std::tie(value, error) = DeserializeDoubleBodyFrom(reader); @@ -389,7 +398,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got a start element <", common::Utf8ToWstring(discriminator), L">" @@ -874,7 +883,13 @@ common::optional SerializeValueBodyTo( const nlohmann::json& value, bool undeclare_namespace ) { - if (value.is_boolean()) { + if (value.is_null()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.StartElementInNoNamespace("nil", undeclare_namespace); + writer.StopElement("nil"); + } else if (value.is_boolean()) { writer.StartElementInNoNamespace( "boolean", undeclare_namespace ); @@ -945,11 +960,11 @@ common::optional SerializeValueBodyTo( writer.StopElement("struct"); } else { // NOTE (mristin): - // This covers ``null``, a binary value and a discarded value -- none of - // which are representable as a ``JSONValue``. + // This covers a binary value and a discarded value -- neither of which + // is representable as a ``JSONValue``. return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.hpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.hpp index e17a87864..25488cf4c 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/src/xml_rpc.hpp @@ -88,8 +88,8 @@ common::expected< * document which is already being read through \p reader. * * \p reader is expected to be positioned at the start element of - * the discriminator (\c \, \c \, \c \, - * \c \ or \c \). On success, \p reader is left + * the discriminator (\c \, \c \, \c \, + * \c \, \c \ or \c \). On success, \p reader is left * positioned at the node right after the discriminator's own stop element, * mirroring how the primitive ``Deserialize*`` functions in ``xmlization`` * behave. @@ -164,8 +164,8 @@ struct SerializationError { * * \param os the UTF-8-encoded output stream where the XML will be written * \param value to be serialized; it must be JSON-able (recursively - * a boolean, a number, a string, an array or an object with string keys), - * and never \c null, or a \ref SerializationError is returned + * a null, a boolean, a number, a string, an array or an object with string + * keys), or a \ref SerializationError is returned * \return An error, if any. * * The XML-RPC elements reside in no namespace at all, as the XML-RPC @@ -181,8 +181,9 @@ common::optional SerializeValue( * \brief Serialize \p value, embedded within a larger XML document which is * already being written through \p writer. * - * This writes exactly one discriminator element (\c \, - * \c \, \c \, \c \ or \c \). + * This writes exactly one discriminator element (\c \, + * \c \, \c \, \c \, \c \ or + * \c \). * * \p undeclare_namespace tells whether that element is the outermost one * which resides in no namespace. That one undeclares the default namespace of diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_json_value_verification.cpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_json_value_verification.cpp index a382299be..64626bf8a 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_json_value_verification.cpp @@ -81,8 +81,27 @@ TEST_CASE("Report no violation for a deeply nested, valid value") { REQUIRE(CollectErrors(verificator).empty()); } -TEST_CASE("Reject a null value at the top level") { - const nlohmann::json value(nullptr); +TEST_CASE("Accept a null value at every depth") { + for ( + const char* text : { + "null", + "[null]", + R"({"a": [null, {"b": null}]})" + } + ) { + const nlohmann::json value = nlohmann::json::parse(text); + + verification::JsonValueVerificator verificator( + value, verification::JsonValueShape::kAny + ); + + INFO(std::string("The value is: ") + text); + REQUIRE(CollectErrors(verificator).empty()); + } +} + +TEST_CASE("Reject a discarded value at the top level") { + const nlohmann::json value(nlohmann::json::value_t::discarded); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -138,8 +157,10 @@ TEST_CASE( "Report every rejected value at every depth, not just the first" ) { nlohmann::json value = nlohmann::json::parse( - R"({"a": [1.0, null, {"b": null}]})" + R"({"a": [1.0, 0.0, {"b": 0.0}]})" ); + value["a"][1] = std::numeric_limits::quiet_NaN(); + value["a"][2]["b"] = std::numeric_limits::infinity(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -148,8 +169,8 @@ TEST_CASE( const std::vector errors(CollectErrors(verificator)); INFO( - "Expected exactly two violations (one for each nested null), " - "but got: " + "Expected exactly two violations (one for each nested non-finite " + "number), but got: " + std::to_string(errors.size()) ); REQUIRE(errors.size() == 2); @@ -165,8 +186,10 @@ TEST_CASE("Report the violations in the order of a depth-first walk") { // what distinguishes the depth-first walk from a breadth-first one -- // the latter would report ``z`` first, as it sits one level higher. nlohmann::json value = nlohmann::json::parse( - R"({"a": {"deep": null}, "z": null})" + R"({"a": {"deep": 0.0}, "z": 0.0})" ); + value["a"]["deep"] = std::numeric_limits::quiet_NaN(); + value["z"] = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject @@ -187,8 +210,9 @@ TEST_CASE("Escape a key of a JSON object in the path") { // ``nlohmann::json`` value, so a key is escaped exactly as a JSON // string is. nlohmann::json value = nlohmann::json::parse( - R"({"a\"b\\c\nd": null})" + R"({"a\"b\\c\nd": 0.0})" ); + value.begin().value() = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_xml_rpc.cpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_xml_rpc.cpp index 4578e4a5a..89bda2904 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/test/test_xml_rpc.cpp @@ -176,18 +176,39 @@ TEST_CASE("Round-trip an object") { ); } -TEST_CASE("Serialize rejects null") { +TEST_CASE("Round-trip a null at every depth") { + RequireRoundTrip(nlohmann::json(nullptr)); + + RequireRoundTrip( + nlohmann::json::parse( + R"({"a": null, "b": [null, {"c": null}]})" + ) + ); +} + +TEST_CASE("Serialize a null as an empty nil") { std::ostringstream oss; common::optional error( xml_rpc::SerializeValue(oss, nlohmann::json(nullptr)) ); - REQUIRE(error.has_value()); + REQUIRE(!error.has_value()); + REQUIRE(oss.str().find("") != std::string::npos); +} + +TEST_CASE("Deserialize rejects a nil with text") { + std::istringstream iss( + "x" + ); + common::expected result( + xml_rpc::DeserializeValue(iss) + ); + REQUIRE(!result.has_value()); } TEST_CASE("Serialize reports the path to a nested invalid value") { nlohmann::json value = nlohmann::json::array(); value.push_back(1.0); - value.push_back(nlohmann::json(nullptr)); + value.push_back(std::numeric_limits::quiet_NaN()); std::ostringstream oss; common::optional error( @@ -200,7 +221,7 @@ TEST_CASE("Serialize reports the path to a nested invalid value") { TEST_CASE("Serialize reports the key of an invalid member") { nlohmann::json value = nlohmann::json::object(); value["a"] = 1.0; - value["b"] = nlohmann::json(nullptr); + value["b"] = std::numeric_limits::quiet_NaN(); std::ostringstream oss; common::optional error( @@ -224,7 +245,7 @@ TEST_CASE("Deserialize reports the XPath to a nested invalid value") { "" "nested" "1" - "" + "1" "" "" ); @@ -251,7 +272,7 @@ TEST_CASE("Deserialize reports no wrapper for an array body") { // a property. std::istringstream iss( "" - "" + "1" "" ); xml_common::ReaderMergingText reader( @@ -273,7 +294,7 @@ TEST_CASE("Deserialize reports no wrapper for a struct body") { // definition an object holds the elements directly. std::istringstream iss( "" - "k" + "k1" "" ); xml_common::ReaderMergingText reader( @@ -295,7 +316,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { // break out of the XPath predicate which matches on it. std::istringstream iss( "" - "a&b"c" + "a&b"c1" "" ); xml_common::ReaderMergingText reader( @@ -316,7 +337,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { TEST_CASE("Deserialize rejects an unexpected element") { std::istringstream iss( - "" + "1" ); common::expected result( xml_rpc::DeserializeValue(iss) diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/enhancing.hpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/enhancing.hpp index 698861ac7..c7fabd877 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/enhancing.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/enhancing.hpp @@ -107,6 +107,20 @@ class EnhancedSomething instance_->set_optional_mapping(value); } + const common::optional& optional_value() const override { + return instance_->optional_value(); + } + + common::optional& mutable_optional_value() override { + return instance_->mutable_optional_value(); + } + + void set_optional_value( + common::optional value + ) override { + instance_->set_optional_value(value); + } + const std::shared_ptr& enhancement() const { return enhancement_; } diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/iteration.hpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/iteration.hpp index 6040364b8..a53fc2bc1 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/iteration.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/iteration.hpp @@ -29,7 +29,8 @@ namespace iteration { enum class Property : std::uint32_t { kMapping = 0, kOptionalMapping = 1, - kValues = 2 + kOptionalValue = 2, + kValues = 3 }; std::wstring PropertyToWstring( diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/types.hpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/types.hpp index 4a313fe49..f32cd2955 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/types.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/types.hpp @@ -84,6 +84,14 @@ class ISomething common::optional value ) = 0; + virtual const common::optional& optional_value() const = 0; + + virtual common::optional& mutable_optional_value() = 0; + + virtual void set_optional_value( + common::optional value + ) = 0; + virtual ~ISomething() = default; }; @@ -97,7 +105,8 @@ class Something Something( nlohmann::json mapping, nlohmann::json values, - common::optional optional_mapping = common::nullopt + common::optional optional_mapping = common::nullopt, + common::optional optional_value = common::nullopt ); ModelType model_type() const override; @@ -138,6 +147,18 @@ class Something // endregion + // region Get and set optional_value_ + + const common::optional& optional_value() const override; + + common::optional& mutable_optional_value() override; + + void set_optional_value( + common::optional value + ) override; + + // endregion + ~Something() override = default; private: @@ -146,6 +167,8 @@ class Something nlohmann::json values_; common::optional optional_mapping_; + + common::optional optional_value_; }; // endregion diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/iteration.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/iteration.cpp index 7bdf1736b..06cb45cd7 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/iteration.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/iteration.cpp @@ -24,6 +24,8 @@ std::wstring PropertyToWstring( return L"mapping"; case Property::kOptionalMapping: return L"optional_mapping"; + case Property::kOptionalValue: + return L"optional_value"; case Property::kValues: return L"values"; default: diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.cpp index 1f9abe379..893e09045 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.cpp @@ -24,10 +24,10 @@ namespace { common::optional CheckShallowly( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.hpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.hpp index b2e6d473e..42c9230f8 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/json_value_verification.hpp @@ -32,8 +32,8 @@ enum class JsonValueShape { * \brief Exhaustively verify that a ``nlohmann::json`` value is JSON-able. * * A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys -- in particular, ``null``, a binary + * a ``null``, a boolean, a number, a string, an array of JSON-able values or + * an object of JSON-able values with string keys -- in particular, a binary * value and a discarded value are rejected, at any depth. * * We deliberately walk the value with an explicit stack instead of plain diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/jsonization.cpp index bf1b477fc..e5c8d4f76 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/jsonization.cpp @@ -702,7 +702,7 @@ std::pair< > DeserializeJsonValue( const nlohmann::json& json ) { - if (json.is_null() || json.is_binary() || json.is_discarded()) { + if (json.is_binary() || json.is_discarded()) { return std::make_pair< common::optional, common::optional @@ -710,7 +710,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(json.type_name()) ) @@ -849,7 +849,8 @@ namespace properties { enum class OfSomething : std::uint32_t { kMapping, kValues, - kOptionalMapping + kOptionalMapping, + kOptionalValue }; // enum class OfSomething const std::unordered_map< @@ -867,6 +868,10 @@ const std::unordered_map< { "optionalMapping", OfSomething::kOptionalMapping + }, + { + "optionalValue", + OfSomething::kOptionalValue } }; @@ -1097,6 +1102,8 @@ std::pair< common::optional the_optional_mapping; + common::optional the_optional_value; + common::optional error( ParseProperties( json, @@ -1122,6 +1129,16 @@ std::pair< the_optional_mapping, DeserializeJsonObject(value) ); + case properties::OfSomething::kOptionalValue: { + common::optional parse_error = ParseInto( + the_optional_value, + DeserializeJsonValue(value) + ); + if (!parse_error.has_value() && the_optional_value->is_null()) { + the_optional_value = common::nullopt; + } + return parse_error; + } default: throw UnexpectedPropertyLiteralError( "properties::OfSomething", @@ -1166,7 +1183,8 @@ std::pair< new types::Something( std::move(*the_mapping), std::move(*the_values), - std::move(the_optional_mapping) + std::move(the_optional_mapping), + std::move(the_optional_value) ) ), common::nullopt @@ -1640,12 +1658,12 @@ std::pair< > SerializeJsonValue( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return std::make_pair( common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) @@ -1857,6 +1875,25 @@ std::pair< } } + const common::optional& maybe_optional_value( + that.optional_value() + ); + if (maybe_optional_value.has_value() && !maybe_optional_value->is_null()) { + error = SerializeInto( + result, + "optionalValue", + iteration::Property::kOptionalValue, + SerializeJsonValue( + *maybe_optional_value + ) + ); + if (error.has_value()) { + return NoJsonAndSerializationError( + std::move(*error) + ); + } + } + return std::make_pair< common::optional, common::optional diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/types.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/types.cpp index 8c77ab737..77e12fc06 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/types.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/types.cpp @@ -12,13 +12,16 @@ namespace types { Something::Something( nlohmann::json mapping, nlohmann::json values, - common::optional optional_mapping + common::optional optional_mapping, + common::optional optional_value ) { mapping_ = std::move(mapping); values_ = std::move(values); optional_mapping_ = std::move(optional_mapping); + + optional_value_ = std::move(optional_value); } ModelType Something::model_type() const { @@ -67,6 +70,20 @@ void Something::set_optional_mapping( optional_mapping_ = value; } +const common::optional& Something::optional_value() const { + return optional_value_; +} + +common::optional& Something::mutable_optional_value() { + return optional_value_; +} + +void Something::set_optional_value( + common::optional value +) { + optional_value_ = value; +} + // endregion Something // region Is-a functions diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/verification.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/verification.cpp index ace04a972..4e676c021 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/verification.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/verification.cpp @@ -96,6 +96,49 @@ struct Check { bool Something_0( const void* value +) { + const types::ISomething* that = ( + static_cast(value) + ); + return !((that->optional_value().has_value() && !that->optional_value()->is_null())) + || verification::IsAcceptable( + (*(that->optional_value())) + ); +} + +bool Something_1( + const void* value +) { + const types::ISomething* that = ( + static_cast(value) + ); + return !(( + that->values().size() > 0 + && (that->values().at(0).is_null()) + )); +} + +bool Something_2( + const void* value +) { + const types::ISomething* that = ( + static_cast(value) + ); + return !(that->optional_mapping().has_value()) + || (!(*(that->optional_mapping())).contains( + common::WstringToUtf8( + L"type" + ) + ) + || (!(*(that->optional_mapping())).at( + common::WstringToUtf8( + L"type" + ) + ).is_null())); +} + +bool Something_3( + const void* value ) { const types::ISomething* that = ( static_cast(value) @@ -106,7 +149,7 @@ bool Something_0( ); } -bool Something_1( +bool Something_4( const void* value ) { const types::ISomething* that = ( @@ -118,7 +161,7 @@ bool Something_1( ); } -bool Something_2( +bool Something_5( const void* value ) { const types::ISomething* that = ( @@ -127,7 +170,7 @@ bool Something_2( return that->values().size() > 0; } -bool Something_3( +bool Something_6( const void* value ) { const types::ISomething* that = ( @@ -136,7 +179,7 @@ bool Something_3( return that->mapping().size() > 1; } -bool Something_4( +bool Something_7( const void* value ) { const types::ISomething* that = ( @@ -156,7 +199,7 @@ bool Something_4( ); } -bool Something_5( +bool Something_8( const void* value ) { const types::ISomething* that = ( @@ -167,7 +210,7 @@ bool Something_5( ); } -bool Something_6( +bool Something_9( const void* value ) { const types::ISomething* that = ( @@ -181,7 +224,7 @@ bool Something_6( ); } -bool Something_7( +bool Something_10( const void* value ) { const types::ISomething* that = ( @@ -205,35 +248,47 @@ const std::vector& ChecksOf(Shape shape) { static const std::vector checks = { { &Something_0, - L"The last value must be acceptable" + L"The optional value must be acceptable" }, { &Something_1, - L"The first value must be acceptable" + L"The first value must not be a JSON null" }, { &Something_2, - L"There must be at least one value" + L"The type of the optional mapping must not be a JSON null" }, { &Something_3, - L"The mapping must specify something besides the type" + L"The last value must be acceptable" }, { &Something_4, - L"The type of the mapping must be acceptable" + L"The first value must be acceptable" }, { &Something_5, + L"There must be at least one value" + }, + { + &Something_6, + L"The mapping must specify something besides the type" + }, + { + &Something_7, + L"The type of the mapping must be acceptable" + }, + { + &Something_8, L"The mapping must specify the type, checked in " L"a verification function" }, { - &Something_6, + &Something_9, L"The optional mapping must specify the type" }, { - &Something_7, + &Something_10, L"The mapping must specify the type" } }; @@ -632,6 +687,12 @@ std::unique_ptr Over_Something( that.optional_mapping().has_value() ? One(&(*that.optional_mapping()), Shape::kJSONObject) : Empty() + ), + InProperty( + iteration::Property::kOptionalValue, + that.optional_value().has_value() + ? One(&(*that.optional_value()), Shape::kJSONValue) + : Empty() ) ); } diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.cpp index de61be596..abf1d37d3 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.cpp @@ -349,7 +349,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got ", xml_common::NodeToHumanReadableWstring(reader.node()) ) @@ -374,7 +374,16 @@ std::pair< common::optional value; common::optional error; - if (discriminator == "boolean") { + if (discriminator == "nil") { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and read a JSON null from an empty . Any content other than + // whitespace is refused by the check of the stop element below. + error = SkipWhitespace(reader); + if (!error.has_value()) { + value = nlohmann::json(nullptr); + } + } else if (discriminator == "boolean") { std::tie(value, error) = DeserializeBooleanBodyFrom(reader); } else if (discriminator == "double") { std::tie(value, error) = DeserializeDoubleBodyFrom(reader); @@ -389,7 +398,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got a start element <", common::Utf8ToWstring(discriminator), L">" @@ -874,7 +883,13 @@ common::optional SerializeValueBodyTo( const nlohmann::json& value, bool undeclare_namespace ) { - if (value.is_boolean()) { + if (value.is_null()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.StartElementInNoNamespace("nil", undeclare_namespace); + writer.StopElement("nil"); + } else if (value.is_boolean()) { writer.StartElementInNoNamespace( "boolean", undeclare_namespace ); @@ -945,11 +960,11 @@ common::optional SerializeValueBodyTo( writer.StopElement("struct"); } else { // NOTE (mristin): - // This covers ``null``, a binary value and a discarded value -- none of - // which are representable as a ``JSONValue``. + // This covers a binary value and a discarded value -- neither of which + // is representable as a ``JSONValue``. return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.hpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.hpp index e17a87864..25488cf4c 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xml_rpc.hpp @@ -88,8 +88,8 @@ common::expected< * document which is already being read through \p reader. * * \p reader is expected to be positioned at the start element of - * the discriminator (\c \, \c \, \c \, - * \c \ or \c \). On success, \p reader is left + * the discriminator (\c \, \c \, \c \, + * \c \, \c \ or \c \). On success, \p reader is left * positioned at the node right after the discriminator's own stop element, * mirroring how the primitive ``Deserialize*`` functions in ``xmlization`` * behave. @@ -164,8 +164,8 @@ struct SerializationError { * * \param os the UTF-8-encoded output stream where the XML will be written * \param value to be serialized; it must be JSON-able (recursively - * a boolean, a number, a string, an array or an object with string keys), - * and never \c null, or a \ref SerializationError is returned + * a null, a boolean, a number, a string, an array or an object with string + * keys), or a \ref SerializationError is returned * \return An error, if any. * * The XML-RPC elements reside in no namespace at all, as the XML-RPC @@ -181,8 +181,9 @@ common::optional SerializeValue( * \brief Serialize \p value, embedded within a larger XML document which is * already being written through \p writer. * - * This writes exactly one discriminator element (\c \, - * \c \, \c \, \c \ or \c \). + * This writes exactly one discriminator element (\c \, + * \c \, \c \, \c \, \c \ or + * \c \). * * \p undeclare_namespace tells whether that element is the outermost one * which resides in no namespace. That one undeclares the default namespace of diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xmlization.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xmlization.cpp index d14075397..bc98a34c0 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xmlization.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/src/xmlization.cpp @@ -1386,10 +1386,11 @@ namespace properties { enum class OfSomething : std::uint32_t { kMapping = 0, kValues = 1, - kOptionalMapping = 2 + kOptionalMapping = 2, + kOptionalValue = 3 }; // enum class OfSomething -const std::size_t kPropertyCountOfSomething = 3; +const std::size_t kPropertyCountOfSomething = 4; const std::unordered_map< std::string, @@ -1406,6 +1407,10 @@ const std::unordered_map< { "optionalMapping", OfSomething::kOptionalMapping + }, + { + "optionalValue", + OfSomething::kOptionalValue } }; @@ -1431,6 +1436,8 @@ std::pair< common::optional the_optional_mapping; + common::optional the_optional_value; + // endregion Initialization common::optional error( @@ -1459,6 +1466,16 @@ std::pair< the_optional_mapping, DeserializeJsonObjectFromXmlRpc(reader) ); + case properties::OfSomething::kOptionalValue: { + common::optional read_error = ReadInto( + the_optional_value, + DeserializeJsonValueFromXmlRpc(reader) + ); + if (!read_error.has_value() && the_optional_value->is_null()) { + the_optional_value = common::nullopt; + } + return read_error; + } default: throw UnexpectedPropertyLiteralError( "properties::OfSomething", @@ -1507,7 +1524,8 @@ std::pair< new types::Something( std::move(*the_mapping), std::move(*the_values), - std::move(the_optional_mapping) + std::move(the_optional_mapping), + std::move(the_optional_value) ) ), common::nullopt @@ -1965,6 +1983,22 @@ common::optional SerializeSomethingAsSequence( return error; } + const common::optional& maybe_optional_value( + that.optional_value() + ); + if (maybe_optional_value.has_value() && !maybe_optional_value->is_null()) { + error = WriteProperty( + "optionalValue", + *maybe_optional_value, + writer, + iteration::Property::kOptionalValue, + WriteJsonValueToXmlRpc + ); + if (error.has_value()) { + return error; + } + } + return common::nullopt; } diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_json_value_verification.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_json_value_verification.cpp index a382299be..64626bf8a 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_json_value_verification.cpp @@ -81,8 +81,27 @@ TEST_CASE("Report no violation for a deeply nested, valid value") { REQUIRE(CollectErrors(verificator).empty()); } -TEST_CASE("Reject a null value at the top level") { - const nlohmann::json value(nullptr); +TEST_CASE("Accept a null value at every depth") { + for ( + const char* text : { + "null", + "[null]", + R"({"a": [null, {"b": null}]})" + } + ) { + const nlohmann::json value = nlohmann::json::parse(text); + + verification::JsonValueVerificator verificator( + value, verification::JsonValueShape::kAny + ); + + INFO(std::string("The value is: ") + text); + REQUIRE(CollectErrors(verificator).empty()); + } +} + +TEST_CASE("Reject a discarded value at the top level") { + const nlohmann::json value(nlohmann::json::value_t::discarded); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -138,8 +157,10 @@ TEST_CASE( "Report every rejected value at every depth, not just the first" ) { nlohmann::json value = nlohmann::json::parse( - R"({"a": [1.0, null, {"b": null}]})" + R"({"a": [1.0, 0.0, {"b": 0.0}]})" ); + value["a"][1] = std::numeric_limits::quiet_NaN(); + value["a"][2]["b"] = std::numeric_limits::infinity(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -148,8 +169,8 @@ TEST_CASE( const std::vector errors(CollectErrors(verificator)); INFO( - "Expected exactly two violations (one for each nested null), " - "but got: " + "Expected exactly two violations (one for each nested non-finite " + "number), but got: " + std::to_string(errors.size()) ); REQUIRE(errors.size() == 2); @@ -165,8 +186,10 @@ TEST_CASE("Report the violations in the order of a depth-first walk") { // what distinguishes the depth-first walk from a breadth-first one -- // the latter would report ``z`` first, as it sits one level higher. nlohmann::json value = nlohmann::json::parse( - R"({"a": {"deep": null}, "z": null})" + R"({"a": {"deep": 0.0}, "z": 0.0})" ); + value["a"]["deep"] = std::numeric_limits::quiet_NaN(); + value["z"] = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject @@ -187,8 +210,9 @@ TEST_CASE("Escape a key of a JSON object in the path") { // ``nlohmann::json`` value, so a key is escaped exactly as a JSON // string is. nlohmann::json value = nlohmann::json::parse( - R"({"a\"b\\c\nd": null})" + R"({"a\"b\\c\nd": 0.0})" ); + value.begin().value() = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_xml_rpc.cpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_xml_rpc.cpp index 4578e4a5a..89bda2904 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/test/test_xml_rpc.cpp @@ -176,18 +176,39 @@ TEST_CASE("Round-trip an object") { ); } -TEST_CASE("Serialize rejects null") { +TEST_CASE("Round-trip a null at every depth") { + RequireRoundTrip(nlohmann::json(nullptr)); + + RequireRoundTrip( + nlohmann::json::parse( + R"({"a": null, "b": [null, {"c": null}]})" + ) + ); +} + +TEST_CASE("Serialize a null as an empty nil") { std::ostringstream oss; common::optional error( xml_rpc::SerializeValue(oss, nlohmann::json(nullptr)) ); - REQUIRE(error.has_value()); + REQUIRE(!error.has_value()); + REQUIRE(oss.str().find("") != std::string::npos); +} + +TEST_CASE("Deserialize rejects a nil with text") { + std::istringstream iss( + "x" + ); + common::expected result( + xml_rpc::DeserializeValue(iss) + ); + REQUIRE(!result.has_value()); } TEST_CASE("Serialize reports the path to a nested invalid value") { nlohmann::json value = nlohmann::json::array(); value.push_back(1.0); - value.push_back(nlohmann::json(nullptr)); + value.push_back(std::numeric_limits::quiet_NaN()); std::ostringstream oss; common::optional error( @@ -200,7 +221,7 @@ TEST_CASE("Serialize reports the path to a nested invalid value") { TEST_CASE("Serialize reports the key of an invalid member") { nlohmann::json value = nlohmann::json::object(); value["a"] = 1.0; - value["b"] = nlohmann::json(nullptr); + value["b"] = std::numeric_limits::quiet_NaN(); std::ostringstream oss; common::optional error( @@ -224,7 +245,7 @@ TEST_CASE("Deserialize reports the XPath to a nested invalid value") { "" "nested" "1" - "" + "1" "" "" ); @@ -251,7 +272,7 @@ TEST_CASE("Deserialize reports no wrapper for an array body") { // a property. std::istringstream iss( "" - "" + "1" "" ); xml_common::ReaderMergingText reader( @@ -273,7 +294,7 @@ TEST_CASE("Deserialize reports no wrapper for a struct body") { // definition an object holds the elements directly. std::istringstream iss( "" - "k" + "k1" "" ); xml_common::ReaderMergingText reader( @@ -295,7 +316,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { // break out of the XPath predicate which matches on it. std::istringstream iss( "" - "a&b"c" + "a&b"c1" "" ); xml_common::ReaderMergingText reader( @@ -316,7 +337,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { TEST_CASE("Deserialize rejects an unexpected element") { std::istringstream iss( - "" + "1" ); common::expected result( xml_rpc::DeserializeValue(iss) diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/meta_model.py b/dev/test_data/main/cpp/expected/json_types_in_invariants/meta_model.py index b3b507d2d..a45501a44 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/meta_model.py +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/meta_model.py @@ -63,6 +63,29 @@ def is_acceptable(value: JSONValue) -> bool: or is_acceptable(self.values[-1]), "The last value must be acceptable", ) +@invariant( + lambda self: + not (self.optional_mapping is not None) + or ( + not ("type" in self.optional_mapping) + or self.optional_mapping["type"] is not None + ), + "The type of the optional mapping must not be a JSON null", +) +@invariant( + lambda self: + not ( + len(self.values) > 0 + and self.values[0] is None + ), + "The first value must not be a JSON null", +) +@invariant( + lambda self: + not (self.optional_value is not None) + or is_acceptable(self.optional_value), + "The optional value must be acceptable", +) class Something(DBC): #: Test the length of, the membership in and the indexing into a JSON-able #: object @@ -74,15 +97,21 @@ class Something(DBC): #: Test the membership in an optional JSON-able object optional_mapping: Optional[JSONObject[str]] + #: Test the nullness check over an optional JSON-able value, which is + #: treated as absent if it is a JSON null + optional_value: Optional[JSONValue] + def __init__( self, mapping: JSONObject[str], values: JSONArray, optional_mapping: Optional[JSONObject[str]] = None, + optional_value: Optional[JSONValue] = None, ) -> None: self.mapping = mapping self.values = values self.optional_mapping = optional_mapping + self.optional_value = optional_value __version__ = "dummy" diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.cpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.cpp index 1f9abe379..893e09045 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.cpp @@ -24,10 +24,10 @@ namespace { common::optional CheckShallowly( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.hpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.hpp index b2e6d473e..42c9230f8 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.hpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/json_value_verification.hpp @@ -32,8 +32,8 @@ enum class JsonValueShape { * \brief Exhaustively verify that a ``nlohmann::json`` value is JSON-able. * * A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys -- in particular, ``null``, a binary + * a ``null``, a boolean, a number, a string, an array of JSON-able values or + * an object of JSON-able values with string keys -- in particular, a binary * value and a discarded value are rejected, at any depth. * * We deliberately walk the value with an explicit stack instead of plain diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/jsonization.cpp index d90d2e092..c231c2f75 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/jsonization.cpp @@ -702,7 +702,7 @@ std::pair< > DeserializeJsonValue( const nlohmann::json& json ) { - if (json.is_null() || json.is_binary() || json.is_discarded()) { + if (json.is_binary() || json.is_discarded()) { return std::make_pair< common::optional, common::optional @@ -710,7 +710,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(json.type_name()) ) @@ -2748,12 +2748,12 @@ std::pair< > SerializeJsonValue( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return std::make_pair( common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.cpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.cpp index de61be596..abf1d37d3 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.cpp @@ -349,7 +349,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got ", xml_common::NodeToHumanReadableWstring(reader.node()) ) @@ -374,7 +374,16 @@ std::pair< common::optional value; common::optional error; - if (discriminator == "boolean") { + if (discriminator == "nil") { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and read a JSON null from an empty . Any content other than + // whitespace is refused by the check of the stop element below. + error = SkipWhitespace(reader); + if (!error.has_value()) { + value = nlohmann::json(nullptr); + } + } else if (discriminator == "boolean") { std::tie(value, error) = DeserializeBooleanBodyFrom(reader); } else if (discriminator == "double") { std::tie(value, error) = DeserializeDoubleBodyFrom(reader); @@ -389,7 +398,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got a start element <", common::Utf8ToWstring(discriminator), L">" @@ -874,7 +883,13 @@ common::optional SerializeValueBodyTo( const nlohmann::json& value, bool undeclare_namespace ) { - if (value.is_boolean()) { + if (value.is_null()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.StartElementInNoNamespace("nil", undeclare_namespace); + writer.StopElement("nil"); + } else if (value.is_boolean()) { writer.StartElementInNoNamespace( "boolean", undeclare_namespace ); @@ -945,11 +960,11 @@ common::optional SerializeValueBodyTo( writer.StopElement("struct"); } else { // NOTE (mristin): - // This covers ``null``, a binary value and a discarded value -- none of - // which are representable as a ``JSONValue``. + // This covers a binary value and a discarded value -- neither of which + // is representable as a ``JSONValue``. return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.hpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.hpp index e17a87864..25488cf4c 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.hpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/src/xml_rpc.hpp @@ -88,8 +88,8 @@ common::expected< * document which is already being read through \p reader. * * \p reader is expected to be positioned at the start element of - * the discriminator (\c \, \c \, \c \, - * \c \ or \c \). On success, \p reader is left + * the discriminator (\c \, \c \, \c \, + * \c \, \c \ or \c \). On success, \p reader is left * positioned at the node right after the discriminator's own stop element, * mirroring how the primitive ``Deserialize*`` functions in ``xmlization`` * behave. @@ -164,8 +164,8 @@ struct SerializationError { * * \param os the UTF-8-encoded output stream where the XML will be written * \param value to be serialized; it must be JSON-able (recursively - * a boolean, a number, a string, an array or an object with string keys), - * and never \c null, or a \ref SerializationError is returned + * a null, a boolean, a number, a string, an array or an object with string + * keys), or a \ref SerializationError is returned * \return An error, if any. * * The XML-RPC elements reside in no namespace at all, as the XML-RPC @@ -181,8 +181,9 @@ common::optional SerializeValue( * \brief Serialize \p value, embedded within a larger XML document which is * already being written through \p writer. * - * This writes exactly one discriminator element (\c \, - * \c \, \c \, \c \ or \c \). + * This writes exactly one discriminator element (\c \, + * \c \, \c \, \c \, \c \ or + * \c \). * * \p undeclare_namespace tells whether that element is the outermost one * which resides in no namespace. That one undeclares the default namespace of diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_json_value_verification.cpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_json_value_verification.cpp index a382299be..64626bf8a 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_json_value_verification.cpp @@ -81,8 +81,27 @@ TEST_CASE("Report no violation for a deeply nested, valid value") { REQUIRE(CollectErrors(verificator).empty()); } -TEST_CASE("Reject a null value at the top level") { - const nlohmann::json value(nullptr); +TEST_CASE("Accept a null value at every depth") { + for ( + const char* text : { + "null", + "[null]", + R"({"a": [null, {"b": null}]})" + } + ) { + const nlohmann::json value = nlohmann::json::parse(text); + + verification::JsonValueVerificator verificator( + value, verification::JsonValueShape::kAny + ); + + INFO(std::string("The value is: ") + text); + REQUIRE(CollectErrors(verificator).empty()); + } +} + +TEST_CASE("Reject a discarded value at the top level") { + const nlohmann::json value(nlohmann::json::value_t::discarded); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -138,8 +157,10 @@ TEST_CASE( "Report every rejected value at every depth, not just the first" ) { nlohmann::json value = nlohmann::json::parse( - R"({"a": [1.0, null, {"b": null}]})" + R"({"a": [1.0, 0.0, {"b": 0.0}]})" ); + value["a"][1] = std::numeric_limits::quiet_NaN(); + value["a"][2]["b"] = std::numeric_limits::infinity(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -148,8 +169,8 @@ TEST_CASE( const std::vector errors(CollectErrors(verificator)); INFO( - "Expected exactly two violations (one for each nested null), " - "but got: " + "Expected exactly two violations (one for each nested non-finite " + "number), but got: " + std::to_string(errors.size()) ); REQUIRE(errors.size() == 2); @@ -165,8 +186,10 @@ TEST_CASE("Report the violations in the order of a depth-first walk") { // what distinguishes the depth-first walk from a breadth-first one -- // the latter would report ``z`` first, as it sits one level higher. nlohmann::json value = nlohmann::json::parse( - R"({"a": {"deep": null}, "z": null})" + R"({"a": {"deep": 0.0}, "z": 0.0})" ); + value["a"]["deep"] = std::numeric_limits::quiet_NaN(); + value["z"] = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject @@ -187,8 +210,9 @@ TEST_CASE("Escape a key of a JSON object in the path") { // ``nlohmann::json`` value, so a key is escaped exactly as a JSON // string is. nlohmann::json value = nlohmann::json::parse( - R"({"a\"b\\c\nd": null})" + R"({"a\"b\\c\nd": 0.0})" ); + value.begin().value() = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_xml_rpc.cpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_xml_rpc.cpp index 4578e4a5a..89bda2904 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/test/test_xml_rpc.cpp @@ -176,18 +176,39 @@ TEST_CASE("Round-trip an object") { ); } -TEST_CASE("Serialize rejects null") { +TEST_CASE("Round-trip a null at every depth") { + RequireRoundTrip(nlohmann::json(nullptr)); + + RequireRoundTrip( + nlohmann::json::parse( + R"({"a": null, "b": [null, {"c": null}]})" + ) + ); +} + +TEST_CASE("Serialize a null as an empty nil") { std::ostringstream oss; common::optional error( xml_rpc::SerializeValue(oss, nlohmann::json(nullptr)) ); - REQUIRE(error.has_value()); + REQUIRE(!error.has_value()); + REQUIRE(oss.str().find("") != std::string::npos); +} + +TEST_CASE("Deserialize rejects a nil with text") { + std::istringstream iss( + "x" + ); + common::expected result( + xml_rpc::DeserializeValue(iss) + ); + REQUIRE(!result.has_value()); } TEST_CASE("Serialize reports the path to a nested invalid value") { nlohmann::json value = nlohmann::json::array(); value.push_back(1.0); - value.push_back(nlohmann::json(nullptr)); + value.push_back(std::numeric_limits::quiet_NaN()); std::ostringstream oss; common::optional error( @@ -200,7 +221,7 @@ TEST_CASE("Serialize reports the path to a nested invalid value") { TEST_CASE("Serialize reports the key of an invalid member") { nlohmann::json value = nlohmann::json::object(); value["a"] = 1.0; - value["b"] = nlohmann::json(nullptr); + value["b"] = std::numeric_limits::quiet_NaN(); std::ostringstream oss; common::optional error( @@ -224,7 +245,7 @@ TEST_CASE("Deserialize reports the XPath to a nested invalid value") { "" "nested" "1" - "" + "1" "" "" ); @@ -251,7 +272,7 @@ TEST_CASE("Deserialize reports no wrapper for an array body") { // a property. std::istringstream iss( "" - "" + "1" "" ); xml_common::ReaderMergingText reader( @@ -273,7 +294,7 @@ TEST_CASE("Deserialize reports no wrapper for a struct body") { // definition an object holds the elements directly. std::istringstream iss( "" - "k" + "k1" "" ); xml_common::ReaderMergingText reader( @@ -295,7 +316,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { // break out of the XPath predicate which matches on it. std::istringstream iss( "" - "a&b"c" + "a&b"c1" "" ); xml_common::ReaderMergingText reader( @@ -316,7 +337,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { TEST_CASE("Deserialize rejects an unexpected element") { std::istringstream iss( - "" + "1" ); common::expected result( xml_rpc::DeserializeValue(iss) diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.cpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.cpp index 1f9abe379..893e09045 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.cpp @@ -24,10 +24,10 @@ namespace { common::optional CheckShallowly( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.hpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.hpp index b2e6d473e..42c9230f8 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.hpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/json_value_verification.hpp @@ -32,8 +32,8 @@ enum class JsonValueShape { * \brief Exhaustively verify that a ``nlohmann::json`` value is JSON-able. * * A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys -- in particular, ``null``, a binary + * a ``null``, a boolean, a number, a string, an array of JSON-able values or + * an object of JSON-able values with string keys -- in particular, a binary * value and a discarded value are rejected, at any depth. * * We deliberately walk the value with an explicit stack instead of plain diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/jsonization.cpp index f057568c5..c97d7549d 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/jsonization.cpp @@ -702,7 +702,7 @@ std::pair< > DeserializeJsonValue( const nlohmann::json& json ) { - if (json.is_null() || json.is_binary() || json.is_discarded()) { + if (json.is_binary() || json.is_discarded()) { return std::make_pair< common::optional, common::optional @@ -710,7 +710,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(json.type_name()) ) @@ -1999,12 +1999,12 @@ std::pair< > SerializeJsonValue( const nlohmann::json& value ) { - if (value.is_null() || value.is_binary() || value.is_discarded()) { + if (value.is_binary() || value.is_discarded()) { return std::make_pair( common::nullopt, common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.cpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.cpp index de61be596..abf1d37d3 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.cpp @@ -349,7 +349,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got ", xml_common::NodeToHumanReadableWstring(reader.node()) ) @@ -374,7 +374,16 @@ std::pair< common::optional value; common::optional error; - if (discriminator == "boolean") { + if (discriminator == "nil") { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and read a JSON null from an empty . Any content other than + // whitespace is refused by the check of the stop element below. + error = SkipWhitespace(reader); + if (!error.has_value()) { + value = nlohmann::json(nullptr); + } + } else if (discriminator == "boolean") { std::tie(value, error) = DeserializeBooleanBodyFrom(reader); } else if (discriminator == "double") { std::tie(value, error) = DeserializeDoubleBodyFrom(reader); @@ -389,7 +398,7 @@ std::pair< common::nullopt, common::make_optional( common::Concat( - L"Expected one of , , , " + L"Expected one of , , , , " L"or , but got a start element <", common::Utf8ToWstring(discriminator), L">" @@ -874,7 +883,13 @@ common::optional SerializeValueBodyTo( const nlohmann::json& value, bool undeclare_namespace ) { - if (value.is_boolean()) { + if (value.is_null()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.StartElementInNoNamespace("nil", undeclare_namespace); + writer.StopElement("nil"); + } else if (value.is_boolean()) { writer.StartElementInNoNamespace( "boolean", undeclare_namespace ); @@ -945,11 +960,11 @@ common::optional SerializeValueBodyTo( writer.StopElement("struct"); } else { // NOTE (mristin): - // This covers ``null``, a binary value and a discarded value -- none of - // which are representable as a ``JSONValue``. + // This covers a binary value and a discarded value -- neither of which + // is representable as a ``JSONValue``. return common::make_optional( common::Concat( - L"Expected a JSON-able value (a boolean, a number, a string, " + L"Expected a JSON-able value (a null, a boolean, a number, a string, " L"an array or an object), but got a value of type: ", common::Utf8ToWstring(value.type_name()) ) diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.hpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.hpp index e17a87864..25488cf4c 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.hpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/src/xml_rpc.hpp @@ -88,8 +88,8 @@ common::expected< * document which is already being read through \p reader. * * \p reader is expected to be positioned at the start element of - * the discriminator (\c \, \c \, \c \, - * \c \ or \c \). On success, \p reader is left + * the discriminator (\c \, \c \, \c \, + * \c \, \c \ or \c \). On success, \p reader is left * positioned at the node right after the discriminator's own stop element, * mirroring how the primitive ``Deserialize*`` functions in ``xmlization`` * behave. @@ -164,8 +164,8 @@ struct SerializationError { * * \param os the UTF-8-encoded output stream where the XML will be written * \param value to be serialized; it must be JSON-able (recursively - * a boolean, a number, a string, an array or an object with string keys), - * and never \c null, or a \ref SerializationError is returned + * a null, a boolean, a number, a string, an array or an object with string + * keys), or a \ref SerializationError is returned * \return An error, if any. * * The XML-RPC elements reside in no namespace at all, as the XML-RPC @@ -181,8 +181,9 @@ common::optional SerializeValue( * \brief Serialize \p value, embedded within a larger XML document which is * already being written through \p writer. * - * This writes exactly one discriminator element (\c \, - * \c \, \c \, \c \ or \c \). + * This writes exactly one discriminator element (\c \, + * \c \, \c \, \c \, \c \ or + * \c \). * * \p undeclare_namespace tells whether that element is the outermost one * which resides in no namespace. That one undeclares the default namespace of diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_json_value_verification.cpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_json_value_verification.cpp index a382299be..64626bf8a 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_json_value_verification.cpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_json_value_verification.cpp @@ -81,8 +81,27 @@ TEST_CASE("Report no violation for a deeply nested, valid value") { REQUIRE(CollectErrors(verificator).empty()); } -TEST_CASE("Reject a null value at the top level") { - const nlohmann::json value(nullptr); +TEST_CASE("Accept a null value at every depth") { + for ( + const char* text : { + "null", + "[null]", + R"({"a": [null, {"b": null}]})" + } + ) { + const nlohmann::json value = nlohmann::json::parse(text); + + verification::JsonValueVerificator verificator( + value, verification::JsonValueShape::kAny + ); + + INFO(std::string("The value is: ") + text); + REQUIRE(CollectErrors(verificator).empty()); + } +} + +TEST_CASE("Reject a discarded value at the top level") { + const nlohmann::json value(nlohmann::json::value_t::discarded); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -138,8 +157,10 @@ TEST_CASE( "Report every rejected value at every depth, not just the first" ) { nlohmann::json value = nlohmann::json::parse( - R"({"a": [1.0, null, {"b": null}]})" + R"({"a": [1.0, 0.0, {"b": 0.0}]})" ); + value["a"][1] = std::numeric_limits::quiet_NaN(); + value["a"][2]["b"] = std::numeric_limits::infinity(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kAny @@ -148,8 +169,8 @@ TEST_CASE( const std::vector errors(CollectErrors(verificator)); INFO( - "Expected exactly two violations (one for each nested null), " - "but got: " + "Expected exactly two violations (one for each nested non-finite " + "number), but got: " + std::to_string(errors.size()) ); REQUIRE(errors.size() == 2); @@ -165,8 +186,10 @@ TEST_CASE("Report the violations in the order of a depth-first walk") { // what distinguishes the depth-first walk from a breadth-first one -- // the latter would report ``z`` first, as it sits one level higher. nlohmann::json value = nlohmann::json::parse( - R"({"a": {"deep": null}, "z": null})" + R"({"a": {"deep": 0.0}, "z": 0.0})" ); + value["a"]["deep"] = std::numeric_limits::quiet_NaN(); + value["z"] = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject @@ -187,8 +210,9 @@ TEST_CASE("Escape a key of a JSON object in the path") { // ``nlohmann::json`` value, so a key is escaped exactly as a JSON // string is. nlohmann::json value = nlohmann::json::parse( - R"({"a\"b\\c\nd": null})" + R"({"a\"b\\c\nd": 0.0})" ); + value.begin().value() = std::numeric_limits::quiet_NaN(); verification::JsonValueVerificator verificator( value, verification::JsonValueShape::kObject diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_xml_rpc.cpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_xml_rpc.cpp index 4578e4a5a..89bda2904 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_xml_rpc.cpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/test/test_xml_rpc.cpp @@ -176,18 +176,39 @@ TEST_CASE("Round-trip an object") { ); } -TEST_CASE("Serialize rejects null") { +TEST_CASE("Round-trip a null at every depth") { + RequireRoundTrip(nlohmann::json(nullptr)); + + RequireRoundTrip( + nlohmann::json::parse( + R"({"a": null, "b": [null, {"c": null}]})" + ) + ); +} + +TEST_CASE("Serialize a null as an empty nil") { std::ostringstream oss; common::optional error( xml_rpc::SerializeValue(oss, nlohmann::json(nullptr)) ); - REQUIRE(error.has_value()); + REQUIRE(!error.has_value()); + REQUIRE(oss.str().find("") != std::string::npos); +} + +TEST_CASE("Deserialize rejects a nil with text") { + std::istringstream iss( + "x" + ); + common::expected result( + xml_rpc::DeserializeValue(iss) + ); + REQUIRE(!result.has_value()); } TEST_CASE("Serialize reports the path to a nested invalid value") { nlohmann::json value = nlohmann::json::array(); value.push_back(1.0); - value.push_back(nlohmann::json(nullptr)); + value.push_back(std::numeric_limits::quiet_NaN()); std::ostringstream oss; common::optional error( @@ -200,7 +221,7 @@ TEST_CASE("Serialize reports the path to a nested invalid value") { TEST_CASE("Serialize reports the key of an invalid member") { nlohmann::json value = nlohmann::json::object(); value["a"] = 1.0; - value["b"] = nlohmann::json(nullptr); + value["b"] = std::numeric_limits::quiet_NaN(); std::ostringstream oss; common::optional error( @@ -224,7 +245,7 @@ TEST_CASE("Deserialize reports the XPath to a nested invalid value") { "" "nested" "1" - "" + "1" "" "" ); @@ -251,7 +272,7 @@ TEST_CASE("Deserialize reports no wrapper for an array body") { // a property. std::istringstream iss( "" - "" + "1" "" ); xml_common::ReaderMergingText reader( @@ -273,7 +294,7 @@ TEST_CASE("Deserialize reports no wrapper for a struct body") { // definition an object holds the elements directly. std::istringstream iss( "" - "k" + "k1" "" ); xml_common::ReaderMergingText reader( @@ -295,7 +316,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { // break out of the XPath predicate which matches on it. std::istringstream iss( "" - "a&b"c" + "a&b"c1" "" ); xml_common::ReaderMergingText reader( @@ -316,7 +337,7 @@ TEST_CASE("Deserialize escapes a member name in the XPath") { TEST_CASE("Deserialize rejects an unexpected element") { std::istringstream iss( - "" + "1" ); common::expected result( xml_rpc::DeserializeValue(iss) diff --git a/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0.Tests/CommonJson.cs index 320a91dab..26bfc6176 100644 --- a/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/comparisons/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/comparisons/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/comparisons/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/comparisons/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/enum/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/enum/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/enum/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/enum/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/float_only/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/float_only/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/float_only/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/float_only/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/int_parsing/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/int_parsing/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/int_parsing/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/int_parsing/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestCopying.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestCopying.cs index 766b0ec61..e949ceb48 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestCopying.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestCopying.cs @@ -40,9 +40,17 @@ private static bool BytesEqualButDistinct( } private static bool JsonNodesEqualButDistinct( - Nodes.JsonNode that, - Nodes.JsonNode other) + Nodes.JsonNode? that, + Nodes.JsonNode? other) { + // NOTE (mristin): + // A JSON null is a C# null, and there is nothing to be distinct about + // two of them. + if (that == null || other == null) + { + return that == other; + } + // NOTE (mristin): // A JsonNode compares only by reference, so we compare the canonical // JSON text. diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestXmlRpc.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestXmlRpc.cs index f368e3e64..3a028501a 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestXmlRpc.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy.Tests/TestXmlRpc.cs @@ -192,19 +192,39 @@ public void Test_round_trip_nested_object() } [Test] - public void Test_serialize_rejects_null() + public void Test_round_trip_null() { - Our.SerializationException? caught = null; - try - { - SerializeToString(null); - } - catch (Our.SerializationException exception) - { - caught = exception; - } + string text = SerializeToString(null); - Assert.IsNotNull(caught); + Assert.IsTrue(text.Contains("" + "" + "1" - + "" + + "1" + "" + ""); @@ -258,7 +278,7 @@ public void Test_deserialize_reports_path_on_nested_error() [Test] public void Test_deserialize_rejects_unexpected_element() { - string text = ""; + string text = "1"; Nodes.JsonNode? result = DeserializeFromString( text, out Our.Reporting.Error? error); @@ -266,7 +286,7 @@ public void Test_deserialize_rejects_unexpected_element() Assert.IsNull(result); Assert.IsNotNull(error); Assert.IsTrue( - error!.Cause.Contains("nil"), + error!.Cause.Contains("i4"), $"Unexpected cause: {error.Cause}"); } diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/copying.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/copying.cs index 5879f429f..b82c77103 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/copying.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/copying.cs @@ -70,12 +70,8 @@ Our.ISomething that ) { return new Our.Something( - (Nodes.JsonNode)( - System.Text.Json.JsonSerializer.SerializeToNode( - that.Value) - ?? throw new System.InvalidOperationException( - "Expected SerializeToNode to copy a non-null value, " - + "but it returned null")), + System.Text.Json.JsonSerializer.SerializeToNode( + that.Value), (Nodes.JsonArray)( System.Text.Json.JsonSerializer.SerializeToNode( that.Values) @@ -95,12 +91,8 @@ Our.ISomething that "Expected SerializeToNode to copy a non-null value, " + "but it returned null")), (that.OptionalValue != null) - ? (Nodes.JsonNode)( - System.Text.Json.JsonSerializer.SerializeToNode( - that.OptionalValue) - ?? throw new System.InvalidOperationException( - "Expected SerializeToNode to copy a non-null value, " - + "but it returned null")) + ? System.Text.Json.JsonSerializer.SerializeToNode( + that.OptionalValue) : null, (that.OptionalValues != null) ? (Nodes.JsonArray)( diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/enhancing.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/enhancing.cs index fe4632695..abda003a8 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/enhancing.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/enhancing.cs @@ -44,7 +44,7 @@ TEnhancement enhancement _instance = instance; } - public Nodes.JsonNode Value + public Nodes.JsonNode? Value { get => _instance.Value; set => _instance.Value = value; diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonization.cs index 698b48070..3cea62cb6 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonization.cs @@ -245,14 +245,13 @@ internal static byte[] BytesFrom( /// /// /// A JSON-able value is, recursively, exactly as JSON itself is defined: - /// a boolean, a finite number, a string, an array of JSON-able values or - /// an object of JSON-able values with string keys -- never null and + /// a null (JSON null), a boolean, a finite number, a string, an array + /// of JSON-able values or an object of JSON-able values with string keys -- /// never an infinity or a not-a-number, at any depth. /// /// JSON node to be checked /// Error, if any, describing why it is not JSON-able private static bool CheckIsJsonAble( - [CodeAnalysis.NotNullWhen(true)] Nodes.JsonNode? node, out Reporting.Error? error) { @@ -260,9 +259,7 @@ private static bool CheckIsJsonAble( if (node == null) { - error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - return false; + return true; } switch (node) @@ -330,7 +327,7 @@ private static bool CheckIsJsonAble( /// /// JSON node to be parsed /// Error, if any, during the deserialization - internal static Nodes.JsonNode JsonValueFrom( + internal static Nodes.JsonNode? JsonValueFrom( Nodes.JsonNode? node, out Reporting.Error? error) { @@ -339,6 +336,11 @@ internal static Nodes.JsonNode JsonValueFrom( return default!; } + if (node == null) + { + return null; + } + // NOTE (mristin): // A System.Text.Json node remembers its parent, and ``node`` is still // attached to the document which the caller parsed. Handing it out @@ -375,7 +377,7 @@ internal static Nodes.JsonArray JsonArrayFrom( return default!; } - Nodes.JsonNode result = JsonValueFrom(node, out error); + Nodes.JsonNode? result = JsonValueFrom(node, out error); if (error != null) { return default!; @@ -402,7 +404,7 @@ internal static Nodes.JsonObject JsonObjectFrom( return default!; } - Nodes.JsonNode result = JsonValueFrom(node, out error); + Nodes.JsonNode? result = JsonValueFrom(node, out error); if (error != null) { return default!; @@ -487,7 +489,7 @@ internal static Our.Something SomethingFrom( } } - if (theValue == null) + if (!obj.ContainsKey("value")) { error = new Reporting.Error( "Required property \"value\" is missing"); @@ -516,9 +518,7 @@ internal static Our.Something SomethingFrom( } return new Our.Something( - theValue - ?? throw new System.InvalidOperationException( - "Unexpected null, had to be handled before"), + theValue, theValues ?? throw new System.InvalidOperationException( "Unexpected null, had to be handled before"), @@ -715,13 +715,14 @@ private static Nodes.JsonValue ToJsonValue(double that) /// Check that is a JSON-able value, at any depth. /// /// - /// Neither a null nor a non-finite number has a representation as - /// a JSON-able value, and rules out neither - /// of the two statically. + /// A non-finite number has no representation as a JSON-able value, and + /// does not rule it out statically. A null + /// is a JSON null, and hence a JSON-able value. /// /// - /// Thrown if either is found anywhere within , - /// carrying the path to the culprit within . + /// Thrown if a non-finite number is found anywhere within + /// , carrying the path to the culprit within + /// . /// private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { @@ -733,13 +734,10 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { try { - if (item == null) + if (item != null) { - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value, but got a null")); + CheckIsJsonAbleForSerialization(item); } - CheckIsJsonAbleForSerialization(item); } catch (SerializationFailure failure) { @@ -758,13 +756,10 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { try { - if (member.Value == null) + if (member.Value != null) { - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value, but got a null")); + CheckIsJsonAbleForSerialization(member.Value); } - CheckIsJsonAbleForSerialization(member.Value); } catch (SerializationFailure failure) { @@ -805,11 +800,16 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) /// , never itself. /// /// - /// Thrown if contains a null or a non-finite - /// number anywhere, at any depth. + /// Thrown if contains a non-finite number anywhere, + /// at any depth. /// - private static Nodes.JsonNode SerializeJsonValue(Nodes.JsonNode that) + private static Nodes.JsonNode? SerializeJsonValue(Nodes.JsonNode? that) { + if (that == null) + { + return null; + } + // NOTE (mristin): // A System.Text.Json node remembers its parent, and attaching a node // which already has one throws @@ -850,7 +850,7 @@ private static Nodes.JsonNode SerializeJsonValue(Nodes.JsonNode that) /// private static Nodes.JsonArray SerializeJsonArray(Nodes.JsonArray that) { - return (Nodes.JsonArray)SerializeJsonValue(that); + return (Nodes.JsonArray)SerializeJsonValue(that)!; } ///

@@ -858,10 +858,10 @@ private static Nodes.JsonArray SerializeJsonArray(Nodes.JsonArray that) /// private static Nodes.JsonObject SerializeJsonObject(Nodes.JsonObject that) { - return (Nodes.JsonObject)SerializeJsonValue(that); + return (Nodes.JsonObject)SerializeJsonValue(that)!; } - private static readonly Serializer Serialize_JsonValue = + private static readonly Serializer Serialize_JsonValue = Transformer.SerializeJsonValue; private static readonly Serializer Serialize_JsonArray = diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonvalueverification.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonvalueverification.cs index f7d6b6d9b..6eb114d83 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonvalueverification.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/jsonvalueverification.cs @@ -21,7 +21,7 @@ public static class JsonValueVerification public enum ExpectedShape { /// - /// The value may be any JSON-able shape (a boolean, a number, + /// The value may be any JSON-able shape (a null, a boolean, a number, /// a string, an array or an object). /// Any, @@ -38,14 +38,14 @@ public enum ExpectedShape } /// - /// Verify that holds neither a null nor - /// a non-finite number anywhere, at any depth. + /// Verify that holds no non-finite number anywhere, + /// at any depth. /// /// - /// A JSON null has no representation as a JSON-able value, and JSON - /// knows neither an infinity nor a not-a-number -- yet + /// JSON knows neither an infinity nor a not-a-number -- yet /// holds either happily if it was put - /// together programmatically. + /// together programmatically. A null is a JSON null, and hence + /// a JSON-able value. /// private static IEnumerable VerifyRecursively( Nodes.JsonNode that) @@ -57,15 +57,7 @@ public enum ExpectedShape int index = 0; foreach (Nodes.JsonNode? item in jsonArray) { - if (item == null) - { - var error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - error.PrependSegment( - new Reporting.IndexSegment(index)); - yield return error; - } - else + if (item != null) { foreach (var error in VerifyRecursively(item)) { @@ -85,15 +77,7 @@ public enum ExpectedShape KeyValuePair member in jsonObject) { - if (member.Value == null) - { - var error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - error.PrependSegment( - new Reporting.KeySegment(member.Key)); - yield return error; - } - else + if (member.Value != null) { foreach (var error in VerifyRecursively(member.Value)) { @@ -129,9 +113,8 @@ public enum ExpectedShape /// /// Verify that is JSON-able (recursively - /// a boolean, a finite number, a string, an array or an object of - /// JSON-able values -- never a null and never a non-finite number, - /// at any depth) and, if + /// a null, a boolean, a finite number, a string, an array or an object of + /// JSON-able values -- never a non-finite number, at any depth) and, if /// is not /// , that its top-level shape matches. /// @@ -141,8 +124,13 @@ public enum ExpectedShape { if (that == null) { - yield return new Reporting.Error( - "Expected a JSON-able value, but got a null"); + // NOTE (mristin): + // A JSON null is a JSON-able value, but neither an array nor an object. + if (expectedShape != ExpectedShape.Any) + { + yield return new Reporting.Error( + $"Expected a Json{expectedShape}, but got a null"); + } yield break; } diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/types.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/types.cs index bdbc99af7..879097873 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/types.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/types.cs @@ -58,7 +58,7 @@ public T Transform( public interface ISomething : IClass { - public Nodes.JsonNode Value { get; set; } + public Nodes.JsonNode? Value { get; set; } public Nodes.JsonArray Values { get; set; } @@ -75,7 +75,7 @@ public interface ISomething : IClass public class Something : ISomething { - public Nodes.JsonNode Value { get; set; } + public Nodes.JsonNode? Value { get; set; } public Nodes.JsonArray Values { get; set; } @@ -149,7 +149,7 @@ public T Transform( } public Something( - Nodes.JsonNode value, + Nodes.JsonNode? value, Nodes.JsonArray values, Nodes.JsonObject mapping, Nodes.JsonObject mappingWithConstrainedKey, diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlization.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlization.cs index 576161227..d191bfd1d 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlization.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlization.cs @@ -177,7 +177,7 @@ private static Reporting.Error DuplicatePropertyError(string elementName) /// /// Read the content of an element typed as Nodes.JsonNode. /// - private static Nodes.JsonNode ReadJsonValueContent( + private static Nodes.JsonNode? ReadJsonValueContent( Xml.XmlReader reader, bool isEmpty, out Reporting.Error? error) @@ -185,7 +185,7 @@ private static Nodes.JsonNode ReadJsonValueContent( if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of the element, " + "but the element was self-closing"); return default!; @@ -198,9 +198,7 @@ private static Nodes.JsonNode ReadJsonValueContent( return default!; } - return result - ?? throw new System.InvalidOperationException( - "Unexpected result null when error is null"); + return result; } /// @@ -268,7 +266,7 @@ private static Nodes.JsonObject ReadJsonObjectContent( AtElement( SomethingFromSequence, "something")); - private static readonly ContentReader Read_jsonValue = ( + private static readonly ContentReader Read_jsonValue = ( ReadJsonValueContent); private static readonly ContentReader Read_jsonArray = ( @@ -293,10 +291,12 @@ internal static Our.Something SomethingFromSequence( error = null; Nodes.JsonNode? theValue = null; + bool seenValue = false; Nodes.JsonArray? theValues = null; Nodes.JsonObject? theMapping = null; Nodes.JsonObject? theMappingWithConstrainedKey = null; Nodes.JsonNode? theOptionalValue = null; + bool seenOptionalValue = false; Nodes.JsonArray? theOptionalValues = null; Nodes.JsonObject? theOptionalMapping = null; @@ -320,11 +320,12 @@ internal static Our.Something SomethingFromSequence( switch (elementName) { case "value": - if (theValue != null) + if (seenValue) { error = DuplicatePropertyError(elementName); break; } + seenValue = true; theValue = Read_jsonValue( reader, isEmptyProperty, out error); break; @@ -356,11 +357,12 @@ internal static Our.Something SomethingFromSequence( reader, isEmptyProperty, out error); break; case "optionalValue": - if (theOptionalValue != null) + if (seenOptionalValue) { error = DuplicatePropertyError(elementName); break; } + seenOptionalValue = true; theOptionalValue = Read_jsonValue( reader, isEmptyProperty, out error); break; @@ -420,7 +422,7 @@ internal static Our.Something SomethingFromSequence( } } - if (theValue == null) + if (!seenValue) { error = new Reporting.Error( "The required property Value has not been given " + @@ -453,9 +455,7 @@ internal static Our.Something SomethingFromSequence( } return new Our.Something( - theValue - ?? throw new System.InvalidOperationException( - "Unexpected null, had to be handled before"), + theValue, theValues ?? throw new System.InvalidOperationException( "Unexpected null, had to be handled before"), @@ -664,7 +664,7 @@ internal static void WriteIClass( that.Accept(_instance, writer); } - private static readonly ContentWriter Write_jsonValue = ( + private static readonly ContentWriter Write_jsonValue = ( XmlRpc.SerializeValueTo); private static readonly ContentWriter Write_jsonArray = ( diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlrpc.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlrpc.cs index bbf004467..90d6e95e9 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlrpc.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/xmlrpc.cs @@ -182,7 +182,7 @@ public static class XmlRpc /// /// /// is expected to be positioned at the start - /// element of the discriminator (<boolean>, + /// element of the discriminator (<nil>, <boolean>, /// <double>, <string>, <array> or /// <struct>). On success, is left /// positioned at the node right after the discriminator's own end @@ -199,7 +199,7 @@ public static class XmlRpc if (reader.EOF || reader.NodeType != Xml.XmlNodeType.Element) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or , but got " + ( reader.EOF @@ -228,6 +228,13 @@ public static class XmlRpc Nodes.JsonNode? result = null; switch (discriminator) { + case "nil": + // NOTE (mristin): + // A has no content, which consuming its end element below + // checks. + result = null; + break; + case "boolean": if (isEmpty) { @@ -319,7 +326,7 @@ public static class XmlRpc // It is this very element which does not belong here, so naming it // in the path as well as in the message would say nothing more. error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + $" or , but got: <{discriminator}>"); return null; } @@ -358,7 +365,7 @@ public static class XmlRpc if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); return null; @@ -520,7 +527,7 @@ public static class XmlRpc if (isEmptyValue) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); error.PrependSegment( @@ -646,7 +653,7 @@ public static class XmlRpc if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); return null; @@ -675,11 +682,11 @@ public static class XmlRpc /// . /// /// - /// This writes exactly one discriminator element (<boolean>, - /// <double>, <string>, <array> or - /// <struct>). must be JSON-able - /// (recursively a boolean, a number, a string, an array or an object with - /// string keys), and never null, or a + /// This writes exactly one discriminator element (<nil>, + /// <boolean>, <double>, <string>, + /// <array> or <struct>). + /// must be JSON-able (recursively a null, a boolean, a number, a string, + /// an array or an object with string keys), or a /// is thrown, carrying /// the path to the culprit within . The enclosing /// serializer converts it -- does, @@ -694,10 +701,12 @@ public static void SerializeValueTo( switch (that) { case null: - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + - "a string, an array or an object), but got null")); + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.WriteStartElement("nil", ""); + writer.WriteEndElement(); + break; case Nodes.JsonArray jsonArray: writer.WriteStartElement("array", ""); @@ -754,7 +763,7 @@ public static void SerializeValueTo( { throw new SerializationFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + + "Expected a JSON-able value (a null, a boolean, a number, " + "a string, an array or an object), but got a value of " + $"an unexpected underlying type: {jsonValue}")); } @@ -763,7 +772,7 @@ public static void SerializeValueTo( default: throw new SerializationFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + + "Expected a JSON-able value (a null, a boolean, a number, " + "a string, an array or an object), but got: " + $"{that.GetType()}")); } diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestCopying.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestCopying.cs index 695ecee73..27266882c 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestCopying.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestCopying.cs @@ -40,9 +40,17 @@ private static bool BytesEqualButDistinct( } private static bool JsonNodesEqualButDistinct( - Nodes.JsonNode that, - Nodes.JsonNode other) + Nodes.JsonNode? that, + Nodes.JsonNode? other) { + // NOTE (mristin): + // A JSON null is a C# null, and there is nothing to be distinct about + // two of them. + if (that == null || other == null) + { + return that == other; + } + // NOTE (mristin): // A JsonNode compares only by reference, so we compare the canonical // JSON text. @@ -80,8 +88,8 @@ public override bool TransformSomething( } private bool Check_ListOf_jsonValue( - List that, - List other) + List that, + List other) { if (ReferenceEquals(that, other) || that.Count != other.Count) { @@ -103,8 +111,8 @@ private bool Check_ListOf_jsonValue( } private bool Check_TupleOf4_string_jsonValue_jsonArray_jsonObject( - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) that, - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) other) + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) that, + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) other) { return ( that.Item1 == other.Item1 diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestXmlRpc.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestXmlRpc.cs index f368e3e64..3a028501a 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestXmlRpc.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy.Tests/TestXmlRpc.cs @@ -192,19 +192,39 @@ public void Test_round_trip_nested_object() } [Test] - public void Test_serialize_rejects_null() + public void Test_round_trip_null() { - Our.SerializationException? caught = null; - try - { - SerializeToString(null); - } - catch (Our.SerializationException exception) - { - caught = exception; - } + string text = SerializeToString(null); - Assert.IsNotNull(caught); + Assert.IsTrue(text.Contains("" + "" + "1" - + "" + + "1" + "" + ""); @@ -258,7 +278,7 @@ public void Test_deserialize_reports_path_on_nested_error() [Test] public void Test_deserialize_rejects_unexpected_element() { - string text = ""; + string text = "1"; Nodes.JsonNode? result = DeserializeFromString( text, out Our.Reporting.Error? error); @@ -266,7 +286,7 @@ public void Test_deserialize_rejects_unexpected_element() Assert.IsNull(result); Assert.IsNotNull(error); Assert.IsTrue( - error!.Cause.Contains("nil"), + error!.Cause.Contains("i4"), $"Unexpected cause: {error.Cause}"); } diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/copying.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/copying.cs index 570424ad5..f58e2caad 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/copying.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/copying.cs @@ -48,19 +48,15 @@ public static T Deep(T that) where T : Our.IClass /// /// Make a deep copy of . /// - private static List Deep_ListOf_jsonValue( - List that) + private static List Deep_ListOf_jsonValue( + List that) { - var result = new List(that.Count); + var result = new List(that.Count); foreach (var item in that) { result.Add( - (Nodes.JsonNode)( - System.Text.Json.JsonSerializer.SerializeToNode( - item) - ?? throw new System.InvalidOperationException( - "Expected SerializeToNode to copy a non-null value, " - + "but it returned null"))); + System.Text.Json.JsonSerializer.SerializeToNode( + item)); } return result; @@ -69,17 +65,13 @@ public static T Deep(T that) where T : Our.IClass /// /// Make a deep copy of . /// - private static (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) Deep_TupleOf4_string_jsonValue_jsonArray_jsonObject( - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) that) + private static (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) Deep_TupleOf4_string_jsonValue_jsonArray_jsonObject( + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) that) { return ( that.Item1, - (Nodes.JsonNode)( - System.Text.Json.JsonSerializer.SerializeToNode( - that.Item2) - ?? throw new System.InvalidOperationException( - "Expected SerializeToNode to copy a non-null value, " - + "but it returned null")), + System.Text.Json.JsonSerializer.SerializeToNode( + that.Item2), (Nodes.JsonArray)( System.Text.Json.JsonSerializer.SerializeToNode( that.Item3) diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/enhancing.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/enhancing.cs index 76bc17270..f7b714f64 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/enhancing.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/enhancing.cs @@ -44,13 +44,13 @@ TEnhancement enhancement _instance = instance; } - public List Values + public List Values { get => _instance.Values; set => _instance.Values = value; } - public (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) TupleWithJson + public (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) TupleWithJson { get => _instance.TupleWithJson; set => _instance.TupleWithJson = value; diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonization.cs index 4f06fa6cf..57aa1bb96 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonization.cs @@ -403,14 +403,14 @@ private static Deserializer> AsArrayOf( }; } - private static readonly Deserializer> Parse_ListOf_jsonValue = ( - AsArrayOf( + private static readonly Deserializer> Parse_ListOf_jsonValue = ( + AsArrayOf( JsonValueFrom)); private static readonly Deserializer< - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) > Parse_TupleOf4_string_jsonValue_jsonArray_jsonObject = ( - AsTuple4( + AsTuple4( StringFrom, JsonValueFrom, JsonArrayFrom, @@ -425,14 +425,13 @@ private static readonly Deserializer< /// /// /// A JSON-able value is, recursively, exactly as JSON itself is defined: - /// a boolean, a finite number, a string, an array of JSON-able values or - /// an object of JSON-able values with string keys -- never null and + /// a null (JSON null), a boolean, a finite number, a string, an array + /// of JSON-able values or an object of JSON-able values with string keys -- /// never an infinity or a not-a-number, at any depth. /// /// JSON node to be checked /// Error, if any, describing why it is not JSON-able private static bool CheckIsJsonAble( - [CodeAnalysis.NotNullWhen(true)] Nodes.JsonNode? node, out Reporting.Error? error) { @@ -440,9 +439,7 @@ private static bool CheckIsJsonAble( if (node == null) { - error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - return false; + return true; } switch (node) @@ -510,7 +507,7 @@ private static bool CheckIsJsonAble( /// /// JSON node to be parsed /// Error, if any, during the deserialization - internal static Nodes.JsonNode JsonValueFrom( + internal static Nodes.JsonNode? JsonValueFrom( Nodes.JsonNode? node, out Reporting.Error? error) { @@ -519,6 +516,11 @@ internal static Nodes.JsonNode JsonValueFrom( return default!; } + if (node == null) + { + return null; + } + // NOTE (mristin): // A System.Text.Json node remembers its parent, and ``node`` is still // attached to the document which the caller parsed. Handing it out @@ -555,7 +557,7 @@ internal static Nodes.JsonArray JsonArrayFrom( return default!; } - Nodes.JsonNode result = JsonValueFrom(node, out error); + Nodes.JsonNode? result = JsonValueFrom(node, out error); if (error != null) { return default!; @@ -582,7 +584,7 @@ internal static Nodes.JsonObject JsonObjectFrom( return default!; } - Nodes.JsonNode result = JsonValueFrom(node, out error); + Nodes.JsonNode? result = JsonValueFrom(node, out error); if (error != null) { return default!; @@ -612,8 +614,8 @@ internal static Our.Something SomethingFrom( return default!; } - List? theValues = null; - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject)? theTupleWithJson = null; + List? theValues = null; + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject)? theTupleWithJson = null; List? theOptionalObjects = null; Nodes.JsonObject? theOptionalObject = null; @@ -967,14 +969,14 @@ private static Serializer> SerializeList( }; } - private static readonly Serializer> Serialize_ListOf_jsonValue = ( - SerializeList( + private static readonly Serializer> Serialize_ListOf_jsonValue = ( + SerializeList( Transformer.SerializeJsonValue)); private static readonly Serializer< - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) > Serialize_TupleOf4_string_jsonValue_jsonArray_jsonObject = ( - SerializeTuple4( + SerializeTuple4( ToJsonValue, Transformer.SerializeJsonValue, Transformer.SerializeJsonArray, @@ -990,13 +992,14 @@ private static readonly Serializer< /// Check that is a JSON-able value, at any depth. /// /// - /// Neither a null nor a non-finite number has a representation as - /// a JSON-able value, and rules out neither - /// of the two statically. + /// A non-finite number has no representation as a JSON-able value, and + /// does not rule it out statically. A null + /// is a JSON null, and hence a JSON-able value. /// /// - /// Thrown if either is found anywhere within , - /// carrying the path to the culprit within . + /// Thrown if a non-finite number is found anywhere within + /// , carrying the path to the culprit within + /// . /// private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { @@ -1008,13 +1011,10 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { try { - if (item == null) + if (item != null) { - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value, but got a null")); + CheckIsJsonAbleForSerialization(item); } - CheckIsJsonAbleForSerialization(item); } catch (SerializationFailure failure) { @@ -1033,13 +1033,10 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { try { - if (member.Value == null) + if (member.Value != null) { - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value, but got a null")); + CheckIsJsonAbleForSerialization(member.Value); } - CheckIsJsonAbleForSerialization(member.Value); } catch (SerializationFailure failure) { @@ -1080,11 +1077,16 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) /// , never itself. /// /// - /// Thrown if contains a null or a non-finite - /// number anywhere, at any depth. + /// Thrown if contains a non-finite number anywhere, + /// at any depth. /// - private static Nodes.JsonNode SerializeJsonValue(Nodes.JsonNode that) + private static Nodes.JsonNode? SerializeJsonValue(Nodes.JsonNode? that) { + if (that == null) + { + return null; + } + // NOTE (mristin): // A System.Text.Json node remembers its parent, and attaching a node // which already has one throws @@ -1125,7 +1127,7 @@ private static Nodes.JsonNode SerializeJsonValue(Nodes.JsonNode that) /// private static Nodes.JsonArray SerializeJsonArray(Nodes.JsonArray that) { - return (Nodes.JsonArray)SerializeJsonValue(that); + return (Nodes.JsonArray)SerializeJsonValue(that)!; } /// @@ -1133,7 +1135,7 @@ private static Nodes.JsonArray SerializeJsonArray(Nodes.JsonArray that) /// private static Nodes.JsonObject SerializeJsonObject(Nodes.JsonObject that) { - return (Nodes.JsonObject)SerializeJsonValue(that); + return (Nodes.JsonObject)SerializeJsonValue(that)!; } private static readonly Serializer Serialize_JsonObject = diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.cs index f7d6b6d9b..6eb114d83 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.cs @@ -21,7 +21,7 @@ public static class JsonValueVerification public enum ExpectedShape { /// - /// The value may be any JSON-able shape (a boolean, a number, + /// The value may be any JSON-able shape (a null, a boolean, a number, /// a string, an array or an object). /// Any, @@ -38,14 +38,14 @@ public enum ExpectedShape } /// - /// Verify that holds neither a null nor - /// a non-finite number anywhere, at any depth. + /// Verify that holds no non-finite number anywhere, + /// at any depth. /// /// - /// A JSON null has no representation as a JSON-able value, and JSON - /// knows neither an infinity nor a not-a-number -- yet + /// JSON knows neither an infinity nor a not-a-number -- yet /// holds either happily if it was put - /// together programmatically. + /// together programmatically. A null is a JSON null, and hence + /// a JSON-able value. /// private static IEnumerable VerifyRecursively( Nodes.JsonNode that) @@ -57,15 +57,7 @@ public enum ExpectedShape int index = 0; foreach (Nodes.JsonNode? item in jsonArray) { - if (item == null) - { - var error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - error.PrependSegment( - new Reporting.IndexSegment(index)); - yield return error; - } - else + if (item != null) { foreach (var error in VerifyRecursively(item)) { @@ -85,15 +77,7 @@ public enum ExpectedShape KeyValuePair member in jsonObject) { - if (member.Value == null) - { - var error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - error.PrependSegment( - new Reporting.KeySegment(member.Key)); - yield return error; - } - else + if (member.Value != null) { foreach (var error in VerifyRecursively(member.Value)) { @@ -129,9 +113,8 @@ public enum ExpectedShape /// /// Verify that is JSON-able (recursively - /// a boolean, a finite number, a string, an array or an object of - /// JSON-able values -- never a null and never a non-finite number, - /// at any depth) and, if + /// a null, a boolean, a finite number, a string, an array or an object of + /// JSON-able values -- never a non-finite number, at any depth) and, if /// is not /// , that its top-level shape matches. /// @@ -141,8 +124,13 @@ public enum ExpectedShape { if (that == null) { - yield return new Reporting.Error( - "Expected a JSON-able value, but got a null"); + // NOTE (mristin): + // A JSON null is a JSON-able value, but neither an array nor an object. + if (expectedShape != ExpectedShape.Any) + { + yield return new Reporting.Error( + $"Expected a Json{expectedShape}, but got a null"); + } yield break; } diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/types.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/types.cs index 872b1ab80..d686c7ab3 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/types.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/types.cs @@ -59,9 +59,9 @@ public T Transform( public interface ISomething : IClass { - public List Values { get; set; } + public List Values { get; set; } - public (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) TupleWithJson { get; set; } + public (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) TupleWithJson { get; set; } public List? OptionalObjects { get; set; } @@ -80,9 +80,9 @@ public interface ISomething : IClass public class Something : ISomething { - public List Values { get; set; } + public List Values { get; set; } - public (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) TupleWithJson { get; set; } + public (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) TupleWithJson { get; set; } public List? OptionalObjects { get; set; } @@ -169,8 +169,8 @@ public T Transform( } public Something( - List values, - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) tupleWithJson, + List values, + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) tupleWithJson, List? optionalObjects = null, Nodes.JsonObject? optionalObject = null) { diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/verification.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/verification.cs index 2721bf332..348a266a1 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/verification.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/verification.cs @@ -114,7 +114,7 @@ var error in JsonValueVerification.Verify( /// Verify the items of recursively. /// private static IEnumerable Verify_ListOf_jsonValue( - List that) + List that) { int index = 0; foreach (var item in that) @@ -135,7 +135,7 @@ var error in JsonValueVerification.Verify( /// Verify the items of recursively. /// private static IEnumerable Verify_TupleOf4_string_jsonValue_jsonArray_jsonObject( - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) that) + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) that) { foreach ( var error in JsonValueVerification.Verify( diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlization.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlization.cs index 691356c27..7866b08af 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlization.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlization.cs @@ -480,7 +480,7 @@ out Reporting.Error? error /// /// Read the content of an element typed as Nodes.JsonNode. /// - private static Nodes.JsonNode ReadJsonValueContent( + private static Nodes.JsonNode? ReadJsonValueContent( Xml.XmlReader reader, bool isEmpty, out Reporting.Error? error) @@ -488,7 +488,7 @@ private static Nodes.JsonNode ReadJsonValueContent( if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of the element, " + "but the element was self-closing"); return default!; @@ -501,9 +501,7 @@ private static Nodes.JsonNode ReadJsonValueContent( return default!; } - return result - ?? throw new System.InvalidOperationException( - "Unexpected result null when error is null"); + return result; } /// @@ -571,11 +569,11 @@ private static Nodes.JsonObject ReadJsonObjectContent( AtElement( SomethingFromSequence, "something")); - private static readonly ContentReader Read_jsonValue = ( + private static readonly ContentReader Read_jsonValue = ( ReadJsonValueContent); - private static readonly ContentReader> Read_ListOf_jsonValue = ( - AsList( + private static readonly ContentReader> Read_ListOf_jsonValue = ( + AsList( AtElement( Read_jsonValue, "v"))); @@ -589,9 +587,9 @@ private static Nodes.JsonObject ReadJsonObjectContent( ReadJsonObjectContent); private static readonly ContentReader< - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) > Read_TupleOf4_string_jsonValue_jsonArray_jsonObject = ( - AsTuple4( + AsTuple4( AtElement( Read_string, "v1"), AtElement( @@ -621,8 +619,8 @@ internal static Our.Something SomethingFromSequence( { error = null; - List? theValues = null; - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject)? theTupleWithJson = null; + List? theValues = null; + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject)? theTupleWithJson = null; List? theOptionalObjects = null; Nodes.JsonObject? theOptionalObject = null; @@ -1062,11 +1060,11 @@ internal static void WriteIClass( that.Accept(_instance, writer); } - private static readonly ContentWriter Write_jsonValue = ( + private static readonly ContentWriter Write_jsonValue = ( XmlRpc.SerializeValueTo); - private static readonly ContentWriter> Write_ListOf_jsonValue = ( - WriteList( + private static readonly ContentWriter> Write_ListOf_jsonValue = ( + WriteList( WrapInElement( Write_jsonValue, "v"))); @@ -1080,9 +1078,9 @@ internal static void WriteIClass( XmlRpc.SerializeStructBodyTo); private static readonly ContentWriter< - (string, Nodes.JsonNode, Nodes.JsonArray, Nodes.JsonObject) + (string, Nodes.JsonNode?, Nodes.JsonArray, Nodes.JsonObject) > Write_TupleOf4_string_jsonValue_jsonArray_jsonObject = ( - WriteTuple4( + WriteTuple4( WrapInElement( Write_string, "v1"), WrapInElement( diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlrpc.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlrpc.cs index bbf004467..90d6e95e9 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlrpc.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/xmlrpc.cs @@ -182,7 +182,7 @@ public static class XmlRpc /// /// /// is expected to be positioned at the start - /// element of the discriminator (<boolean>, + /// element of the discriminator (<nil>, <boolean>, /// <double>, <string>, <array> or /// <struct>). On success, is left /// positioned at the node right after the discriminator's own end @@ -199,7 +199,7 @@ public static class XmlRpc if (reader.EOF || reader.NodeType != Xml.XmlNodeType.Element) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or , but got " + ( reader.EOF @@ -228,6 +228,13 @@ public static class XmlRpc Nodes.JsonNode? result = null; switch (discriminator) { + case "nil": + // NOTE (mristin): + // A has no content, which consuming its end element below + // checks. + result = null; + break; + case "boolean": if (isEmpty) { @@ -319,7 +326,7 @@ public static class XmlRpc // It is this very element which does not belong here, so naming it // in the path as well as in the message would say nothing more. error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + $" or , but got: <{discriminator}>"); return null; } @@ -358,7 +365,7 @@ public static class XmlRpc if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); return null; @@ -520,7 +527,7 @@ public static class XmlRpc if (isEmptyValue) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); error.PrependSegment( @@ -646,7 +653,7 @@ public static class XmlRpc if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); return null; @@ -675,11 +682,11 @@ public static class XmlRpc /// . /// /// - /// This writes exactly one discriminator element (<boolean>, - /// <double>, <string>, <array> or - /// <struct>). must be JSON-able - /// (recursively a boolean, a number, a string, an array or an object with - /// string keys), and never null, or a + /// This writes exactly one discriminator element (<nil>, + /// <boolean>, <double>, <string>, + /// <array> or <struct>). + /// must be JSON-able (recursively a null, a boolean, a number, a string, + /// an array or an object with string keys), or a /// is thrown, carrying /// the path to the culprit within . The enclosing /// serializer converts it -- does, @@ -694,10 +701,12 @@ public static void SerializeValueTo( switch (that) { case null: - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + - "a string, an array or an object), but got null")); + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.WriteStartElement("nil", ""); + writer.WriteEndElement(); + break; case Nodes.JsonArray jsonArray: writer.WriteStartElement("array", ""); @@ -754,7 +763,7 @@ public static void SerializeValueTo( { throw new SerializationFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + + "Expected a JSON-able value (a null, a boolean, a number, " + "a string, an array or an object), but got a value of " + $"an unexpected underlying type: {jsonValue}")); } @@ -763,7 +772,7 @@ public static void SerializeValueTo( default: throw new SerializationFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + + "Expected a JSON-able value (a null, a boolean, a number, " + "a string, an array or an object), but got: " + $"{that.GetType()}")); } diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestCopying.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestCopying.cs index 97d56a496..32043d543 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestCopying.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestCopying.cs @@ -40,9 +40,17 @@ private static bool BytesEqualButDistinct( } private static bool JsonNodesEqualButDistinct( - Nodes.JsonNode that, - Nodes.JsonNode other) + Nodes.JsonNode? that, + Nodes.JsonNode? other) { + // NOTE (mristin): + // A JSON null is a C# null, and there is nothing to be distinct about + // two of them. + if (that == null || other == null) + { + return that == other; + } + // NOTE (mristin): // A JsonNode compares only by reference, so we compare the canonical // JSON text. @@ -71,7 +79,12 @@ public override bool TransformSomething( ? JsonNodesEqualButDistinct( that.OptionalMapping, casted.OptionalMapping) - : that.OptionalMapping == null && casted.OptionalMapping == null)); + : that.OptionalMapping == null && casted.OptionalMapping == null) + && ((that.OptionalValue != null && casted.OptionalValue != null) + ? JsonNodesEqualButDistinct( + that.OptionalValue, + casted.OptionalValue) + : that.OptionalValue == null && casted.OptionalValue == null)); } } // internal class DeepCopyChecker @@ -85,7 +98,8 @@ private static bool SomethingShallowEquals( return ( that.Mapping == other.Mapping && that.Values == other.Values - && that.OptionalMapping == other.OptionalMapping); + && that.OptionalMapping == other.OptionalMapping + && that.OptionalValue == other.OptionalValue); } [Test] diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestXmlRpc.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestXmlRpc.cs index f368e3e64..3a028501a 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestXmlRpc.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy.Tests/TestXmlRpc.cs @@ -192,19 +192,39 @@ public void Test_round_trip_nested_object() } [Test] - public void Test_serialize_rejects_null() + public void Test_round_trip_null() { - Our.SerializationException? caught = null; - try - { - SerializeToString(null); - } - catch (Our.SerializationException exception) - { - caught = exception; - } + string text = SerializeToString(null); - Assert.IsNotNull(caught); + Assert.IsTrue(text.Contains("" + "" + "1" - + "" + + "1" + "" + ""); @@ -258,7 +278,7 @@ public void Test_deserialize_reports_path_on_nested_error() [Test] public void Test_deserialize_rejects_unexpected_element() { - string text = ""; + string text = "1"; Nodes.JsonNode? result = DeserializeFromString( text, out Our.Reporting.Error? error); @@ -266,7 +286,7 @@ public void Test_deserialize_rejects_unexpected_element() Assert.IsNull(result); Assert.IsNotNull(error); Assert.IsTrue( - error!.Cause.Contains("nil"), + error!.Cause.Contains("i4"), $"Unexpected cause: {error.Cause}"); } diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/copying.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/copying.cs index 10e4f3b03..6c6ad7c77 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/copying.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/copying.cs @@ -55,7 +55,8 @@ Our.ISomething that return new Our.Something( that.Mapping, that.Values, - that.OptionalMapping); + that.OptionalMapping, + that.OptionalValue); } } // internal class ShallowCopier @@ -85,6 +86,10 @@ Our.ISomething that ?? throw new System.InvalidOperationException( "Expected SerializeToNode to copy a non-null value, " + "but it returned null")) + : null, + (that.OptionalValue != null) + ? System.Text.Json.JsonSerializer.SerializeToNode( + that.OptionalValue) : null ); } diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/enhancing.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/enhancing.cs index e74381cb5..478f3ea50 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/enhancing.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/enhancing.cs @@ -62,6 +62,12 @@ public Nodes.JsonObject? OptionalMapping set => _instance.OptionalMapping = value; } + public Nodes.JsonNode? OptionalValue + { + get => _instance.OptionalValue; + set => _instance.OptionalValue = value; + } + public IEnumerable DescendOnce() { return _instance.DescendOnce(); diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonization.cs index 4877cf8e6..d21e7e00a 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonization.cs @@ -245,14 +245,13 @@ internal static byte[] BytesFrom( /// /// /// A JSON-able value is, recursively, exactly as JSON itself is defined: - /// a boolean, a finite number, a string, an array of JSON-able values or - /// an object of JSON-able values with string keys -- never null and + /// a null (JSON null), a boolean, a finite number, a string, an array + /// of JSON-able values or an object of JSON-able values with string keys -- /// never an infinity or a not-a-number, at any depth. /// /// JSON node to be checked /// Error, if any, describing why it is not JSON-able private static bool CheckIsJsonAble( - [CodeAnalysis.NotNullWhen(true)] Nodes.JsonNode? node, out Reporting.Error? error) { @@ -260,9 +259,7 @@ private static bool CheckIsJsonAble( if (node == null) { - error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - return false; + return true; } switch (node) @@ -330,7 +327,7 @@ private static bool CheckIsJsonAble( /// /// JSON node to be parsed /// Error, if any, during the deserialization - internal static Nodes.JsonNode JsonValueFrom( + internal static Nodes.JsonNode? JsonValueFrom( Nodes.JsonNode? node, out Reporting.Error? error) { @@ -339,6 +336,11 @@ internal static Nodes.JsonNode JsonValueFrom( return default!; } + if (node == null) + { + return null; + } + // NOTE (mristin): // A System.Text.Json node remembers its parent, and ``node`` is still // attached to the document which the caller parsed. Handing it out @@ -375,7 +377,7 @@ internal static Nodes.JsonArray JsonArrayFrom( return default!; } - Nodes.JsonNode result = JsonValueFrom(node, out error); + Nodes.JsonNode? result = JsonValueFrom(node, out error); if (error != null) { return default!; @@ -402,7 +404,7 @@ internal static Nodes.JsonObject JsonObjectFrom( return default!; } - Nodes.JsonNode result = JsonValueFrom(node, out error); + Nodes.JsonNode? result = JsonValueFrom(node, out error); if (error != null) { return default!; @@ -435,6 +437,7 @@ internal static Our.Something SomethingFrom( Nodes.JsonObject? theMapping = null; Nodes.JsonArray? theValues = null; Nodes.JsonObject? theOptionalMapping = null; + Nodes.JsonNode? theOptionalValue = null; foreach (var keyValue in obj) { @@ -452,6 +455,10 @@ internal static Our.Something SomethingFrom( theOptionalMapping = JsonObjectFrom( keyValue.Value, out error); break; + case "optionalValue": + theOptionalValue = JsonValueFrom( + keyValue.Value, out error); + break; default: error = new Reporting.Error( $"Unexpected property: {keyValue.Key}"); @@ -488,7 +495,8 @@ internal static Our.Something SomethingFrom( theValues ?? throw new System.InvalidOperationException( "Unexpected null, had to be handled before"), - theOptionalMapping); + theOptionalMapping, + theOptionalValue); } // internal static SomethingFrom } // public static class DeserializeImplementation @@ -673,13 +681,14 @@ private static Nodes.JsonValue ToJsonValue(double that) /// Check that is a JSON-able value, at any depth. /// /// - /// Neither a null nor a non-finite number has a representation as - /// a JSON-able value, and rules out neither - /// of the two statically. + /// A non-finite number has no representation as a JSON-able value, and + /// does not rule it out statically. A null + /// is a JSON null, and hence a JSON-able value. /// /// - /// Thrown if either is found anywhere within , - /// carrying the path to the culprit within . + /// Thrown if a non-finite number is found anywhere within + /// , carrying the path to the culprit within + /// . /// private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { @@ -691,13 +700,10 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { try { - if (item == null) + if (item != null) { - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value, but got a null")); + CheckIsJsonAbleForSerialization(item); } - CheckIsJsonAbleForSerialization(item); } catch (SerializationFailure failure) { @@ -716,13 +722,10 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) { try { - if (member.Value == null) + if (member.Value != null) { - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value, but got a null")); + CheckIsJsonAbleForSerialization(member.Value); } - CheckIsJsonAbleForSerialization(member.Value); } catch (SerializationFailure failure) { @@ -763,11 +766,16 @@ private static void CheckIsJsonAbleForSerialization(Nodes.JsonNode that) /// , never itself. /// /// - /// Thrown if contains a null or a non-finite - /// number anywhere, at any depth. + /// Thrown if contains a non-finite number anywhere, + /// at any depth. /// - private static Nodes.JsonNode SerializeJsonValue(Nodes.JsonNode that) + private static Nodes.JsonNode? SerializeJsonValue(Nodes.JsonNode? that) { + if (that == null) + { + return null; + } + // NOTE (mristin): // A System.Text.Json node remembers its parent, and attaching a node // which already has one throws @@ -808,7 +816,7 @@ private static Nodes.JsonNode SerializeJsonValue(Nodes.JsonNode that) /// private static Nodes.JsonArray SerializeJsonArray(Nodes.JsonArray that) { - return (Nodes.JsonArray)SerializeJsonValue(that); + return (Nodes.JsonArray)SerializeJsonValue(that)!; } /// @@ -816,7 +824,7 @@ private static Nodes.JsonArray SerializeJsonArray(Nodes.JsonArray that) /// private static Nodes.JsonObject SerializeJsonObject(Nodes.JsonObject that) { - return (Nodes.JsonObject)SerializeJsonValue(that); + return (Nodes.JsonObject)SerializeJsonValue(that)!; } private static readonly Serializer Serialize_JsonObject = @@ -825,6 +833,9 @@ private static Nodes.JsonObject SerializeJsonObject(Nodes.JsonObject that) private static readonly Serializer Serialize_JsonArray = Transformer.SerializeJsonArray; + private static readonly Serializer Serialize_JsonValue = + Transformer.SerializeJsonValue; + /// /// Set the property of /// to , serialized by @@ -876,6 +887,16 @@ Our.ISomething that Serialize_JsonObject); } + if (that.OptionalValue != null) + { + SetProperty( + result, + "optionalValue", + "OptionalValue", + that.OptionalValue, + Serialize_JsonValue); + } + return result; } } // internal class Transformer diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.cs index f7d6b6d9b..6eb114d83 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.cs @@ -21,7 +21,7 @@ public static class JsonValueVerification public enum ExpectedShape { /// - /// The value may be any JSON-able shape (a boolean, a number, + /// The value may be any JSON-able shape (a null, a boolean, a number, /// a string, an array or an object). /// Any, @@ -38,14 +38,14 @@ public enum ExpectedShape } /// - /// Verify that holds neither a null nor - /// a non-finite number anywhere, at any depth. + /// Verify that holds no non-finite number anywhere, + /// at any depth. /// /// - /// A JSON null has no representation as a JSON-able value, and JSON - /// knows neither an infinity nor a not-a-number -- yet + /// JSON knows neither an infinity nor a not-a-number -- yet /// holds either happily if it was put - /// together programmatically. + /// together programmatically. A null is a JSON null, and hence + /// a JSON-able value. /// private static IEnumerable VerifyRecursively( Nodes.JsonNode that) @@ -57,15 +57,7 @@ public enum ExpectedShape int index = 0; foreach (Nodes.JsonNode? item in jsonArray) { - if (item == null) - { - var error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - error.PrependSegment( - new Reporting.IndexSegment(index)); - yield return error; - } - else + if (item != null) { foreach (var error in VerifyRecursively(item)) { @@ -85,15 +77,7 @@ public enum ExpectedShape KeyValuePair member in jsonObject) { - if (member.Value == null) - { - var error = new Reporting.Error( - "Expected a JSON-able value, but got a null"); - error.PrependSegment( - new Reporting.KeySegment(member.Key)); - yield return error; - } - else + if (member.Value != null) { foreach (var error in VerifyRecursively(member.Value)) { @@ -129,9 +113,8 @@ public enum ExpectedShape /// /// Verify that is JSON-able (recursively - /// a boolean, a finite number, a string, an array or an object of - /// JSON-able values -- never a null and never a non-finite number, - /// at any depth) and, if + /// a null, a boolean, a finite number, a string, an array or an object of + /// JSON-able values -- never a non-finite number, at any depth) and, if /// is not /// , that its top-level shape matches. /// @@ -141,8 +124,13 @@ public enum ExpectedShape { if (that == null) { - yield return new Reporting.Error( - "Expected a JSON-able value, but got a null"); + // NOTE (mristin): + // A JSON null is a JSON-able value, but neither an array nor an object. + if (expectedShape != ExpectedShape.Any) + { + yield return new Reporting.Error( + $"Expected a Json{expectedShape}, but got a null"); + } yield break; } diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/types.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/types.cs index 8991b345a..4e969513e 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/types.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/types.cs @@ -63,6 +63,8 @@ public interface ISomething : IClass public Nodes.JsonArray Values { get; set; } public Nodes.JsonObject? OptionalMapping { get; set; } + + public Nodes.JsonNode? OptionalValue { get; set; } } public class Something : ISomething @@ -73,6 +75,8 @@ public class Something : ISomething public Nodes.JsonObject? OptionalMapping { get; set; } + public Nodes.JsonNode? OptionalValue { get; set; } + /// /// Iterate over all the class instances referenced from this instance /// without further recursion. @@ -135,11 +139,13 @@ public T Transform( public Something( Nodes.JsonObject mapping, Nodes.JsonArray values, - Nodes.JsonObject? optionalMapping = null) + Nodes.JsonObject? optionalMapping = null, + Nodes.JsonNode? optionalValue = null) { Mapping = mapping; Values = values; OptionalMapping = optionalMapping; + OptionalValue = optionalValue; } } } // namespace dummy diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/verification.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/verification.cs index a1a88f286..fc8514638 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/verification.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/verification.cs @@ -51,7 +51,7 @@ Nodes.JsonObject mapping /// a JSON-able object or array has somewhere to be handed over to. /// public static bool IsAcceptable( - Nodes.JsonNode value + Nodes.JsonNode? value ) { return true; @@ -77,9 +77,39 @@ private class Transformer Our.ISomething that ) { + if (!( + !(that.OptionalValue != null) + || Verification.IsAcceptable(that.OptionalValue))) + { + yield return new Reporting.Error( + "Invariant violated:\n" + + "The optional value must be acceptable"); + } + + if (!( + !(that.Values.Count > 0 + && (that.Values[0] == null)))) + { + yield return new Reporting.Error( + "Invariant violated:\n" + + "The first value must not be a JSON null"); + } + + if (!( + !(that.OptionalMapping != null) + || ( + !that.OptionalMapping.ContainsKey("type") + || (that.OptionalMapping["type"] != null) + ))) + { + yield return new Reporting.Error( + "Invariant violated:\n" + + "The type of the optional mapping must not be a JSON null"); + } + if (!( !(that.Values.Count >= 1) - || Verification.IsAcceptable(that.Values[^1]!))) + || Verification.IsAcceptable(that.Values[^1]))) { yield return new Reporting.Error( "Invariant violated:\n" + @@ -88,7 +118,7 @@ Our.ISomething that if (!( !(that.Values.Count > 0) - || Verification.IsAcceptable(that.Values[0]!))) + || Verification.IsAcceptable(that.Values[0]))) { yield return new Reporting.Error( "Invariant violated:\n" + @@ -111,7 +141,7 @@ Our.ISomething that if (!( !that.Mapping.ContainsKey("type") - || Verification.IsAcceptable(that.Mapping["type"]!))) + || Verification.IsAcceptable(that.Mapping["type"]))) { yield return new Reporting.Error( "Invariant violated:\n" + @@ -171,6 +201,18 @@ var error in JsonValueVerification.Verify( yield return error; } } + + if (that.OptionalValue != null) + { + foreach ( + var error in JsonValueVerification.Verify( + that.OptionalValue, JsonValueVerification.ExpectedShape.Any)) + { + error.PrependSegment( + new Reporting.NameSegment("optionalValue")); + yield return error; + } + } } } // private class Transformer diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlization.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlization.cs index 670c8447f..94a9900d4 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlization.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlization.cs @@ -177,7 +177,7 @@ private static Reporting.Error DuplicatePropertyError(string elementName) /// /// Read the content of an element typed as Nodes.JsonNode. /// - private static Nodes.JsonNode ReadJsonValueContent( + private static Nodes.JsonNode? ReadJsonValueContent( Xml.XmlReader reader, bool isEmpty, out Reporting.Error? error) @@ -185,7 +185,7 @@ private static Nodes.JsonNode ReadJsonValueContent( if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of the element, " + "but the element was self-closing"); return default!; @@ -198,9 +198,7 @@ private static Nodes.JsonNode ReadJsonValueContent( return default!; } - return result - ?? throw new System.InvalidOperationException( - "Unexpected result null when error is null"); + return result; } /// @@ -274,6 +272,9 @@ private static Nodes.JsonObject ReadJsonObjectContent( private static readonly ContentReader Read_jsonArray = ( ReadJsonArrayContent); + private static readonly ContentReader Read_jsonValue = ( + ReadJsonValueContent); + /// /// Deserialize an instance of class Something from a sequence of XML elements. /// @@ -292,6 +293,8 @@ internal static Our.Something SomethingFromSequence( Nodes.JsonObject? theMapping = null; Nodes.JsonArray? theValues = null; Nodes.JsonObject? theOptionalMapping = null; + Nodes.JsonNode? theOptionalValue = null; + bool seenOptionalValue = false; if (!isEmptySequence) { @@ -339,6 +342,16 @@ internal static Our.Something SomethingFromSequence( theOptionalMapping = Read_jsonObject( reader, isEmptyProperty, out error); break; + case "optionalValue": + if (seenOptionalValue) + { + error = DuplicatePropertyError(elementName); + break; + } + seenOptionalValue = true; + theOptionalValue = Read_jsonValue( + reader, isEmptyProperty, out error); + break; default: error = new Reporting.Error( "We expected properties of the class Something, " + @@ -400,7 +413,8 @@ internal static Our.Something SomethingFromSequence( theValues ?? throw new System.InvalidOperationException( "Unexpected null, had to be handled before"), - theOptionalMapping); + theOptionalMapping, + theOptionalValue); } // internal static Our.Something? SomethingFromSequence } // internal static class DeserializeImplementation @@ -603,6 +617,9 @@ internal static void WriteIClass( private static readonly ContentWriter Write_jsonArray = ( XmlRpc.SerializeArrayBodyTo); + private static readonly ContentWriter Write_jsonValue = ( + XmlRpc.SerializeValueTo); + private static void SomethingToSequence( Our.ISomething that, Xml.XmlWriter writer) @@ -622,6 +639,16 @@ private static void SomethingToSequence( writer, Write_jsonObject); } + + if (that.OptionalValue != null) + { + WriteProperty( + "optionalValue", + "OptionalValue", + that.OptionalValue, + writer, + Write_jsonValue); + } } // private static void SomethingToSequence public override void VisitSomething( diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.cs index bbf004467..90d6e95e9 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.cs @@ -182,7 +182,7 @@ public static class XmlRpc /// /// /// is expected to be positioned at the start - /// element of the discriminator (<boolean>, + /// element of the discriminator (<nil>, <boolean>, /// <double>, <string>, <array> or /// <struct>). On success, is left /// positioned at the node right after the discriminator's own end @@ -199,7 +199,7 @@ public static class XmlRpc if (reader.EOF || reader.NodeType != Xml.XmlNodeType.Element) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or , but got " + ( reader.EOF @@ -228,6 +228,13 @@ public static class XmlRpc Nodes.JsonNode? result = null; switch (discriminator) { + case "nil": + // NOTE (mristin): + // A has no content, which consuming its end element below + // checks. + result = null; + break; + case "boolean": if (isEmpty) { @@ -319,7 +326,7 @@ public static class XmlRpc // It is this very element which does not belong here, so naming it // in the path as well as in the message would say nothing more. error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + $" or , but got: <{discriminator}>"); return null; } @@ -358,7 +365,7 @@ public static class XmlRpc if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); return null; @@ -520,7 +527,7 @@ public static class XmlRpc if (isEmptyValue) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); error.PrependSegment( @@ -646,7 +653,7 @@ public static class XmlRpc if (isEmpty) { error = new Reporting.Error( - "Expected one of the elements , , , " + + "Expected one of the elements , , , , " + " or as the content of a element, " + "but got no content at all"); return null; @@ -675,11 +682,11 @@ public static class XmlRpc /// . /// /// - /// This writes exactly one discriminator element (<boolean>, - /// <double>, <string>, <array> or - /// <struct>). must be JSON-able - /// (recursively a boolean, a number, a string, an array or an object with - /// string keys), and never null, or a + /// This writes exactly one discriminator element (<nil>, + /// <boolean>, <double>, <string>, + /// <array> or <struct>). + /// must be JSON-able (recursively a null, a boolean, a number, a string, + /// an array or an object with string keys), or a /// is thrown, carrying /// the path to the culprit within . The enclosing /// serializer converts it -- does, @@ -694,10 +701,12 @@ public static void SerializeValueTo( switch (that) { case null: - throw new SerializationFailure( - new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + - "a string, an array or an object), but got null")); + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + writer.WriteStartElement("nil", ""); + writer.WriteEndElement(); + break; case Nodes.JsonArray jsonArray: writer.WriteStartElement("array", ""); @@ -754,7 +763,7 @@ public static void SerializeValueTo( { throw new SerializationFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + + "Expected a JSON-able value (a null, a boolean, a number, " + "a string, an array or an object), but got a value of " + $"an unexpected underlying type: {jsonValue}")); } @@ -763,7 +772,7 @@ public static void SerializeValueTo( default: throw new SerializationFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, " + + "Expected a JSON-able value (a null, a boolean, a number, " + "a string, an array or an object), but got: " + $"{that.GetType()}")); } diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/meta_model.py b/dev/test_data/main/csharp/expected/json_types_in_invariants/meta_model.py index b3b507d2d..a45501a44 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/meta_model.py +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/meta_model.py @@ -63,6 +63,29 @@ def is_acceptable(value: JSONValue) -> bool: or is_acceptable(self.values[-1]), "The last value must be acceptable", ) +@invariant( + lambda self: + not (self.optional_mapping is not None) + or ( + not ("type" in self.optional_mapping) + or self.optional_mapping["type"] is not None + ), + "The type of the optional mapping must not be a JSON null", +) +@invariant( + lambda self: + not ( + len(self.values) > 0 + and self.values[0] is None + ), + "The first value must not be a JSON null", +) +@invariant( + lambda self: + not (self.optional_value is not None) + or is_acceptable(self.optional_value), + "The optional value must be acceptable", +) class Something(DBC): #: Test the length of, the membership in and the indexing into a JSON-able #: object @@ -74,15 +97,21 @@ class Something(DBC): #: Test the membership in an optional JSON-able object optional_mapping: Optional[JSONObject[str]] + #: Test the nullness check over an optional JSON-able value, which is + #: treated as absent if it is a JSON null + optional_value: Optional[JSONValue] + def __init__( self, mapping: JSONObject[str], values: JSONArray, optional_mapping: Optional[JSONObject[str]] = None, + optional_value: Optional[JSONValue] = None, ) -> None: self.mapping = mapping self.values = values self.optional_mapping = optional_mapping + self.optional_value = optional_value __version__ = "dummy" diff --git a/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/method_references/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/method_references/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/method_references/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/method_references/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/sets/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/sets/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/sets/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/sets/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/string_operations/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/string_operations/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/string_operations/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/string_operations/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/tuples/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/tuples/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/tuples/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/tuples/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/unions/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/unions/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/unions/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/unions/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/variance_of_containers/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/variance_of_containers/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/variance_of_containers/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/variance_of_containers/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/csharp/expected/visibility/expected_output/dummy.Tests/CommonJson.cs b/dev/test_data/main/csharp/expected/visibility/expected_output/dummy.Tests/CommonJson.cs index 40a9dae4a..cbbd5cf8c 100644 --- a/dev/test_data/main/csharp/expected/visibility/expected_output/dummy.Tests/CommonJson.cs +++ b/dev/test_data/main/csharp/expected/visibility/expected_output/dummy.Tests/CommonJson.cs @@ -115,13 +115,27 @@ private static string GetNodeKind(Nodes.JsonNode node) } public static void CheckJsonNodesEqual( - Nodes.JsonNode that, - Nodes.JsonNode other, + Nodes.JsonNode? that, + Nodes.JsonNode? other, out Reporting.Error? error ) { error = null; + // NOTE (mristin): + // A JSON null is a C# null. + if (that == null || other == null) + { + if (that != other) + { + error = new Reporting.Error( + $"Mismatch in nulls: {that?.ToJsonString() ?? "null"} != " + + $"{other?.ToJsonString() ?? "null"}" + ); + } + return; + } + var thatNodeKind = GetNodeKind(that); var otherNodeKind = GetNodeKind(other); @@ -148,7 +162,7 @@ out Reporting.Error? error for (int i = 0; i < thatArray.Count; i++) { - CheckJsonNodesEqual(thatArray[i]!, otherArray[i]!, out error); + CheckJsonNodesEqual(thatArray[i], otherArray[i], out error); if (error != null) { error.PrependSegment(new Reporting.IndexSegment(i)); @@ -160,8 +174,8 @@ out Reporting.Error? error } case Nodes.JsonObject thatObject: { - var thatDictionary = thatObject as IDictionary; - var otherDictionary = (other as IDictionary)!; + var thatDictionary = thatObject as IDictionary; + var otherDictionary = (other as IDictionary)!; var thatKeys = thatDictionary.Keys.ToList(); thatKeys.Sort(); diff --git a/dev/test_data/main/golang/expected/json_types/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/json_types/expected_output/jsonization/jsonization.go index 65922005c..19584f1f0 100644 --- a/dev/test_data/main/golang/expected/json_types/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/json_types/expected_output/jsonization/jsonization.go @@ -711,10 +711,10 @@ func serializeArray[T any]( // Parse `jsonable` as a JSON-able value. // // A JSON-able value is, recursively, exactly as JSON itself is defined: -// a bool, a float64, a string, an array of JSON-able values or an object of -// JSON-able values with string keys. A nil is rejected at any depth, as -// the JSON null has no representation as a JSON-able value, and so are -// an infinity and a not-a-number, which JSON can not represent at all. +// a nil (JSON null), a bool, a float64, a string, an array of JSON-able +// values or an object of JSON-able values with string keys. An infinity and +// a not-a-number are rejected at any depth, as JSON can not represent them +// at all. // // The result is a new structure, and never `jsonable` itself, so that // the parsed instance does not alias the document it came from. @@ -725,9 +725,6 @@ func jsonValueFromJsonable( jsonable interface{}, ) (result ourtypes.JsonValue, err error) { if jsonable == nil { - err = newDeserializationError( - "Expected a JSON-able value, but got a nil", - ) return } @@ -843,8 +840,9 @@ func jsonObjectFromJsonable( // Serialize `value` as a JSON-able structure. // // A JSON-able value is almost JSON-able as it comes, but not quite: its type -// is an `any`, which rules out neither a nil nor a non-finite number, neither -// of which JSON can represent. Each is refused here, at any depth. +// is an `any`, which does not rule out a non-finite number, which JSON can not +// represent. It is refused here, at any depth. A nil is written as +// a JSON null. // // The result is a new structure, and never `value` itself, so that // the serialized document does not alias the instance it came from. @@ -852,9 +850,6 @@ func jsonValueToJsonable( value ourtypes.JsonValue, ) (result interface{}, err error) { if value == nil { - err = newSerializationError( - "Expected a JSON-able value, but got a nil", - ) return } diff --git a/dev/test_data/main/golang/expected/json_types/expected_output/types/types.go b/dev/test_data/main/golang/expected/json_types/expected_output/types/types.go index 0107b44f8..c5d0852c0 100644 --- a/dev/test_data/main/golang/expected/json_types/expected_output/types/types.go +++ b/dev/test_data/main/golang/expected/json_types/expected_output/types/types.go @@ -6,11 +6,16 @@ package types // Represent a value which JSON can carry. // -// This is, recursively, exactly as JSON itself is defined: a bool, a float64, -// a string, a [JsonArray] of JSON-able values or a [JsonObject] of JSON-able -// values with string keys -- never a nil, and never an infinity or +// This is, recursively, exactly as JSON itself is defined: a nil (JSON null), +// a bool, a float64, a string, a [JsonArray] of JSON-able values or +// a [JsonObject] of JSON-able values with string keys -- never an infinity or // a not-a-number, which JSON can not represent at all. // +// A nil might appear at any depth, including the value itself. Since +// an absent optional property is also nil, an optional JSON-able property +// which is a JSON null is indistinguishable from an absent one, and is treated +// as absent. +// // Go has no sum type, so this is an `any`, and what it actually holds is // checked at run-time -- by the verification, and again by the de/serialization. type JsonValue = interface{} diff --git a/dev/test_data/main/golang/expected/json_types/expected_output/verification/verification.go b/dev/test_data/main/golang/expected/json_types/expected_output/verification/verification.go index ed23b027b..5ff392c22 100644 --- a/dev/test_data/main/golang/expected/json_types/expected_output/verification/verification.go +++ b/dev/test_data/main/golang/expected/json_types/expected_output/verification/verification.go @@ -67,11 +67,7 @@ func verifyJsonValue( onError func(*VerificationError) bool, ) (abort bool) { if value == nil { - return onError( - newVerificationError( - "Expected a JSON-able value, but got a nil", - ), - ) + return false } switch casted := value.(type) { diff --git a/dev/test_data/main/golang/expected/json_types/expected_output/xmlization/xmlization.go b/dev/test_data/main/golang/expected/json_types/expected_output/xmlization/xmlization.go index e5eae1c63..dc15b0135 100644 --- a/dev/test_data/main/golang/expected/json_types/expected_output/xmlization/xmlization.go +++ b/dev/test_data/main/golang/expected/json_types/expected_output/xmlization/xmlization.go @@ -298,8 +298,9 @@ func namingElement[T any]( // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. diff --git a/dev/test_data/main/golang/expected/json_types/expected_output/xmlrpc/xmlrpc.go b/dev/test_data/main/golang/expected/json_types/expected_output/xmlrpc/xmlrpc.go index 634540bdb..9976acfba 100644 --- a/dev/test_data/main/golang/expected/json_types/expected_output/xmlrpc/xmlrpc.go +++ b/dev/test_data/main/golang/expected/json_types/expected_output/xmlrpc/xmlrpc.go @@ -2,8 +2,9 @@ // // JSON prescribes no XML representation of its own, so a JSON-able value is // de/serialized over the subset of XML-RPC which covers exactly what such -// a value can be: ``, ``, ``, `` and -// ``. +// a value can be: ``, ``, ``, ``, `` and +// ``. XML-RPC itself knows no null, so we follow the widespread +// extension, and write a JSON null as an empty ``. // // Every function reads from or writes to the decoder or the encoder which // the caller passes in, and consumes or produces exactly the content of one @@ -52,8 +53,9 @@ var doubleRe = regexp.MustCompile( // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. @@ -73,6 +75,8 @@ func readValueByLocal( local string, ) (value ourtypes.JsonValue, next xml.Token, err error) { switch local { + case "nil": + value, next, err = readNil(decoder, current) case "boolean": value, next, err = readBoolean(decoder, current) case "double": @@ -98,7 +102,7 @@ func readValueByLocal( // this very element which does not belong here. err = xmlcommon.NewDeserializationError( fmt.Sprintf( - "Expected a discriminator element (one of boolean, double, "+ + "Expected a discriminator element (one of nil, boolean, double, "+ "string, array or struct), but got: %s", local, ), @@ -112,6 +116,32 @@ func readValueByLocal( return } +// Read the content of a `` element as a JSON null. +// +// XML-RPC itself knows no null, so we follow the widespread extension, and +// read a JSON null from an empty ``. +func readNil( + decoder *xml.Decoder, + current xml.Token, +) (value ourtypes.JsonValue, next xml.Token, err error) { + var text string + text, next, err = xmlcommon.ReadText(decoder, current) + if err != nil { + return + } + + text = xmlcommon.CollapseWhitespace(text) + if text != "" { + err = xmlcommon.NewDeserializationError( + fmt.Sprintf( + "Expected no text in a nil element, but got: %s", + text, + ), + ) + } + return +} + // Read the content of a `` element. // // Real XML-RPC tooling writes and expects a strict `1`/`0`, and not @@ -456,9 +486,16 @@ func writeValueContent( undeclareNamespace bool, ) (err error) { if value == nil { - err = xmlcommon.NewSerializationError( - "Expected a JSON-able value, but got a nil", + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty ``. + err = xmlcommon.WriteStartElementInNoNamespace( + encoder, "nil", undeclareNamespace, ) + if err != nil { + return + } + err = xmlcommon.WriteEndElement(encoder, "nil", false) return } diff --git a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/jsonization/jsonization.go index 9b656a9ac..e639eeca8 100644 --- a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/jsonization/jsonization.go @@ -767,10 +767,10 @@ func jsonAbleToJsonable[T any](item T) (interface{}, error) { // Parse `jsonable` as a JSON-able value. // // A JSON-able value is, recursively, exactly as JSON itself is defined: -// a bool, a float64, a string, an array of JSON-able values or an object of -// JSON-able values with string keys. A nil is rejected at any depth, as -// the JSON null has no representation as a JSON-able value, and so are -// an infinity and a not-a-number, which JSON can not represent at all. +// a nil (JSON null), a bool, a float64, a string, an array of JSON-able +// values or an object of JSON-able values with string keys. An infinity and +// a not-a-number are rejected at any depth, as JSON can not represent them +// at all. // // The result is a new structure, and never `jsonable` itself, so that // the parsed instance does not alias the document it came from. @@ -781,9 +781,6 @@ func jsonValueFromJsonable( jsonable interface{}, ) (result ourtypes.JsonValue, err error) { if jsonable == nil { - err = newDeserializationError( - "Expected a JSON-able value, but got a nil", - ) return } @@ -899,8 +896,9 @@ func jsonObjectFromJsonable( // Serialize `value` as a JSON-able structure. // // A JSON-able value is almost JSON-able as it comes, but not quite: its type -// is an `any`, which rules out neither a nil nor a non-finite number, neither -// of which JSON can represent. Each is refused here, at any depth. +// is an `any`, which does not rule out a non-finite number, which JSON can not +// represent. It is refused here, at any depth. A nil is written as +// a JSON null. // // The result is a new structure, and never `value` itself, so that // the serialized document does not alias the instance it came from. @@ -908,9 +906,6 @@ func jsonValueToJsonable( value ourtypes.JsonValue, ) (result interface{}, err error) { if value == nil { - err = newSerializationError( - "Expected a JSON-able value, but got a nil", - ) return } diff --git a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/types/types.go b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/types/types.go index 975a30743..6bf3322a2 100644 --- a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/types/types.go +++ b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/types/types.go @@ -10,11 +10,16 @@ import ( // Represent a value which JSON can carry. // -// This is, recursively, exactly as JSON itself is defined: a bool, a float64, -// a string, a [JsonArray] of JSON-able values or a [JsonObject] of JSON-able -// values with string keys -- never a nil, and never an infinity or +// This is, recursively, exactly as JSON itself is defined: a nil (JSON null), +// a bool, a float64, a string, a [JsonArray] of JSON-able values or +// a [JsonObject] of JSON-able values with string keys -- never an infinity or // a not-a-number, which JSON can not represent at all. // +// A nil might appear at any depth, including the value itself. Since +// an absent optional property is also nil, an optional JSON-able property +// which is a JSON null is indistinguishable from an absent one, and is treated +// as absent. +// // Go has no sum type, so this is an `any`, and what it actually holds is // checked at run-time -- by the verification, and again by the de/serialization. type JsonValue = interface{} diff --git a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/verification/verification.go b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/verification/verification.go index 89d874403..41d676584 100644 --- a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/verification/verification.go +++ b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/verification/verification.go @@ -67,11 +67,7 @@ func verifyJsonValue( onError func(*VerificationError) bool, ) (abort bool) { if value == nil { - return onError( - newVerificationError( - "Expected a JSON-able value, but got a nil", - ), - ) + return false } switch casted := value.(type) { diff --git a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlization/xmlization.go b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlization/xmlization.go index feee81b20..478c01371 100644 --- a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlization/xmlization.go +++ b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlization/xmlization.go @@ -299,8 +299,9 @@ func namingElement[T any]( // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. diff --git a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlrpc/xmlrpc.go b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlrpc/xmlrpc.go index 634540bdb..9976acfba 100644 --- a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlrpc/xmlrpc.go +++ b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/xmlrpc/xmlrpc.go @@ -2,8 +2,9 @@ // // JSON prescribes no XML representation of its own, so a JSON-able value is // de/serialized over the subset of XML-RPC which covers exactly what such -// a value can be: ``, ``, ``, `` and -// ``. +// a value can be: ``, ``, ``, ``, `` and +// ``. XML-RPC itself knows no null, so we follow the widespread +// extension, and write a JSON null as an empty ``. // // Every function reads from or writes to the decoder or the encoder which // the caller passes in, and consumes or produces exactly the content of one @@ -52,8 +53,9 @@ var doubleRe = regexp.MustCompile( // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. @@ -73,6 +75,8 @@ func readValueByLocal( local string, ) (value ourtypes.JsonValue, next xml.Token, err error) { switch local { + case "nil": + value, next, err = readNil(decoder, current) case "boolean": value, next, err = readBoolean(decoder, current) case "double": @@ -98,7 +102,7 @@ func readValueByLocal( // this very element which does not belong here. err = xmlcommon.NewDeserializationError( fmt.Sprintf( - "Expected a discriminator element (one of boolean, double, "+ + "Expected a discriminator element (one of nil, boolean, double, "+ "string, array or struct), but got: %s", local, ), @@ -112,6 +116,32 @@ func readValueByLocal( return } +// Read the content of a `` element as a JSON null. +// +// XML-RPC itself knows no null, so we follow the widespread extension, and +// read a JSON null from an empty ``. +func readNil( + decoder *xml.Decoder, + current xml.Token, +) (value ourtypes.JsonValue, next xml.Token, err error) { + var text string + text, next, err = xmlcommon.ReadText(decoder, current) + if err != nil { + return + } + + text = xmlcommon.CollapseWhitespace(text) + if text != "" { + err = xmlcommon.NewDeserializationError( + fmt.Sprintf( + "Expected no text in a nil element, but got: %s", + text, + ), + ) + } + return +} + // Read the content of a `` element. // // Real XML-RPC tooling writes and expects a strict `1`/`0`, and not @@ -456,9 +486,16 @@ func writeValueContent( undeclareNamespace bool, ) (err error) { if value == nil { - err = xmlcommon.NewSerializationError( - "Expected a JSON-able value, but got a nil", + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty ``. + err = xmlcommon.WriteStartElementInNoNamespace( + encoder, "nil", undeclareNamespace, ) + if err != nil { + return + } + err = xmlcommon.WriteEndElement(encoder, "nil", false) return } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/enhancing/enhancing.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/enhancing/enhancing.go index 8519edfb7..0b9540e21 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/enhancing/enhancing.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/enhancing/enhancing.go @@ -72,6 +72,17 @@ func (es *enhancedSomething[E]) SetOptionalMapping( es.instance.SetOptionalMapping(value) } +func (es *enhancedSomething[E]) OptionalValue( +) ourtypes.JsonValue { + return es.instance.OptionalValue() +} + +func (es *enhancedSomething[E]) SetOptionalValue( + value ourtypes.JsonValue, +) { + es.instance.SetOptionalValue(value) +} + func (es *enhancedSomething[E]) getEnhancement( ) E { return es.enhancement diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/jsonization/jsonization.go index 1b3b32aee..f1ec67d28 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/jsonization/jsonization.go @@ -426,6 +426,7 @@ func somethingFromMapWithoutDispatch( var theMapping ourtypes.JsonObject var theValues ourtypes.JsonArray var theOptionalMapping ourtypes.JsonObject + var theOptionalValue ourtypes.JsonValue foundMapping := false foundValues := false @@ -443,6 +444,9 @@ func somethingFromMapWithoutDispatch( case "optionalMapping": theOptionalMapping, err = jsonObjectFromJsonable(v) + case "optionalValue": + theOptionalValue, err = jsonValueFromJsonable(v) + default: err = newDeserializationError( fmt.Sprintf( @@ -480,6 +484,9 @@ func somethingFromMapWithoutDispatch( result.SetOptionalMapping( theOptionalMapping, ) + result.SetOptionalValue( + theOptionalValue, + ) return } @@ -669,10 +676,10 @@ func serializeArray[T any]( // Parse `jsonable` as a JSON-able value. // // A JSON-able value is, recursively, exactly as JSON itself is defined: -// a bool, a float64, a string, an array of JSON-able values or an object of -// JSON-able values with string keys. A nil is rejected at any depth, as -// the JSON null has no representation as a JSON-able value, and so are -// an infinity and a not-a-number, which JSON can not represent at all. +// a nil (JSON null), a bool, a float64, a string, an array of JSON-able +// values or an object of JSON-able values with string keys. An infinity and +// a not-a-number are rejected at any depth, as JSON can not represent them +// at all. // // The result is a new structure, and never `jsonable` itself, so that // the parsed instance does not alias the document it came from. @@ -683,9 +690,6 @@ func jsonValueFromJsonable( jsonable interface{}, ) (result ourtypes.JsonValue, err error) { if jsonable == nil { - err = newDeserializationError( - "Expected a JSON-able value, but got a nil", - ) return } @@ -801,8 +805,9 @@ func jsonObjectFromJsonable( // Serialize `value` as a JSON-able structure. // // A JSON-able value is almost JSON-able as it comes, but not quite: its type -// is an `any`, which rules out neither a nil nor a non-finite number, neither -// of which JSON can represent. Each is refused here, at any depth. +// is an `any`, which does not rule out a non-finite number, which JSON can not +// represent. It is refused here, at any depth. A nil is written as +// a JSON null. // // The result is a new structure, and never `value` itself, so that // the serialized document does not alias the instance it came from. @@ -810,9 +815,6 @@ func jsonValueToJsonable( value ourtypes.JsonValue, ) (result interface{}, err error) { if value == nil { - err = newSerializationError( - "Expected a JSON-able value, but got a nil", - ) return } @@ -910,6 +912,14 @@ func somethingToMap( } } + if that.OptionalValue() != nil { + result["optionalValue"], err = jsonValueToJsonable(that.OptionalValue()) + if err != nil { + mustSerializationError(err).prependName("OptionalValue()") + return + } + } + return } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/ourtesting/deep_equal.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/ourtesting/deep_equal.go index f9f67f257..9bcb60515 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/ourtesting/deep_equal.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/ourtesting/deep_equal.go @@ -50,6 +50,22 @@ func deepEqualSomething( } } + thatOptionalValue := that.OptionalValue() + otherOptionalValue := other.OptionalValue() + if + (thatOptionalValue == nil && otherOptionalValue != nil) || + (thatOptionalValue != nil && otherOptionalValue == nil) { + return false + } + if thatOptionalValue != nil { + if !reflect.DeepEqual( + thatOptionalValue, + otherOptionalValue, + ) { + return false + } + } + return true } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/types/types.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/types/types.go index 2a30a043c..8ca4b8c77 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/types/types.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/types/types.go @@ -6,11 +6,16 @@ package types // Represent a value which JSON can carry. // -// This is, recursively, exactly as JSON itself is defined: a bool, a float64, -// a string, a [JsonArray] of JSON-able values or a [JsonObject] of JSON-able -// values with string keys -- never a nil, and never an infinity or +// This is, recursively, exactly as JSON itself is defined: a nil (JSON null), +// a bool, a float64, a string, a [JsonArray] of JSON-able values or +// a [JsonObject] of JSON-able values with string keys -- never an infinity or // a not-a-number, which JSON can not represent at all. // +// A nil might appear at any depth, including the value itself. Since +// an absent optional property is also nil, an optional JSON-able property +// which is a JSON null is indistinguishable from an absent one, and is treated +// as absent. +// // Go has no sum type, so this is an `any`, and what it actually holds is // checked at run-time -- by the verification, and again by the de/serialization. type JsonValue = interface{} @@ -78,6 +83,12 @@ type ISomething interface { SetOptionalMapping( value JsonObject, ); + + OptionalValue() JsonValue; + + SetOptionalValue( + value JsonValue, + ); } // Check whether the instance corresponds to [ourtypes.ISomething] @@ -97,6 +108,7 @@ type Something struct { mapping JsonObject values JsonArray optionalMapping JsonObject + optionalValue JsonValue } func (s *Something) Mapping( @@ -132,6 +144,17 @@ func (s *Something) SetOptionalMapping( s.optionalMapping = value } +func (s *Something) OptionalValue( +) JsonValue { + return s.optionalValue +} + +func (s *Something) SetOptionalValue( + value JsonValue, +) { + s.optionalValue = value +} + func (s *Something) ModelType( ) ModelType { return ModelTypeSomething @@ -190,6 +213,7 @@ func NewSomething( mapping: mapping, values: values, optionalMapping: nil, + optionalValue: nil, } } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/verification/verification.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/verification/verification.go index aa472571f..b305035b8 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/verification/verification.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/verification/verification.go @@ -88,11 +88,7 @@ func verifyJsonValue( onError func(*VerificationError) bool, ) (abort bool) { if value == nil { - return onError( - newVerificationError( - "Expected a JSON-able value, but got a nil", - ), - ) + return false } switch casted := value.(type) { @@ -216,6 +212,46 @@ func VerifySomething( ) (abort bool) { abort = false + if !( + !(that.OptionalValue() != nil) || + IsAcceptable(that.OptionalValue())) { + abort = onError( + newVerificationError( + "The optional value must be acceptable",), + ) + if abort { + return + } + } + + if !( + !(len(that.Values()) > 0 && + (that.Values()[0] == nil))) { + abort = onError( + newVerificationError( + "The first value must not be a JSON null",), + ) + if abort { + return + } + } + + if !( + !(that.OptionalMapping() != nil) || + (!ourcommon.MapContains( + that.OptionalMapping(), + "type", + ) || + (that.OptionalMapping()["type"] != nil))) { + abort = onError( + newVerificationError( + "The type of the optional mapping must not be a JSON null",), + ) + if abort { + return + } + } + if !( !(len(that.Values()) >= 1) || IsAcceptable(that.Values()[len(that.Values()) - 1])) { @@ -363,6 +399,23 @@ func VerifySomething( } } + if that.OptionalValue() != nil { + abort = verifyJsonValue( + that.OptionalValue(), + func(err *VerificationError) bool { + err.Path.PrependName( + &ourreporting.NameSegment{ + Name: "OptionalValue", + }, + ) + return onError(err) + }, + ) + if abort { + return + } + } + return } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlization/xmlization.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlization/xmlization.go index f75ea0c68..3f1a39d80 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlization/xmlization.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlization/xmlization.go @@ -298,8 +298,9 @@ func namingElement[T any]( // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. @@ -347,10 +348,12 @@ func readSomethingAsSequence( var theMapping ourtypes.JsonObject var theValues ourtypes.JsonArray var theOptionalMapping ourtypes.JsonObject + var theOptionalValue ourtypes.JsonValue foundMapping := false foundValues := false foundOptionalMapping := false + foundOptionalValue := false for { var local string @@ -392,6 +395,15 @@ func readSomethingAsSequence( decoder, current, ) foundOptionalMapping = true + case "optionalValue": + if foundOptionalValue { + valueErr = duplicatePropertyError(local) + break + } + theOptionalValue, current, valueErr = readJsonValue( + decoder, current, + ) + foundOptionalValue = true default: valueErr = xmlcommon.NewDeserializationError( "Unexpected property", @@ -421,6 +433,7 @@ func readSomethingAsSequence( theValues, ) instance.SetOptionalMapping(theOptionalMapping) + instance.SetOptionalValue(theOptionalValue) return } @@ -772,6 +785,16 @@ func writeSomethingAsSequence( return } + err = finishProperty( + "OptionalValue()", + writeOptionalJsonValue( + encoder, "optionalValue", that.OptionalValue(), xmlrpc.WriteValueContent, + ), + ) + if err != nil { + return + } + return } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlrpc/xmlrpc.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlrpc/xmlrpc.go index 634540bdb..9976acfba 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlrpc/xmlrpc.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/xmlrpc/xmlrpc.go @@ -2,8 +2,9 @@ // // JSON prescribes no XML representation of its own, so a JSON-able value is // de/serialized over the subset of XML-RPC which covers exactly what such -// a value can be: ``, ``, ``, `` and -// ``. +// a value can be: ``, ``, ``, ``, `` and +// ``. XML-RPC itself knows no null, so we follow the widespread +// extension, and write a JSON null as an empty ``. // // Every function reads from or writes to the decoder or the encoder which // the caller passes in, and consumes or produces exactly the content of one @@ -52,8 +53,9 @@ var doubleRe = regexp.MustCompile( // Read the content of an element holding a JSON-able value. // -// The content is a single discriminator element -- ``, ``, -// ``, `` or `` -- which says what the value is. +// The content is a single discriminator element -- ``, ``, +// ``, ``, `` or `` -- which says what +// the value is. // // The `current` token is expected to point to the content of the enclosing // element, and the resulting `next` token points to its end element. @@ -73,6 +75,8 @@ func readValueByLocal( local string, ) (value ourtypes.JsonValue, next xml.Token, err error) { switch local { + case "nil": + value, next, err = readNil(decoder, current) case "boolean": value, next, err = readBoolean(decoder, current) case "double": @@ -98,7 +102,7 @@ func readValueByLocal( // this very element which does not belong here. err = xmlcommon.NewDeserializationError( fmt.Sprintf( - "Expected a discriminator element (one of boolean, double, "+ + "Expected a discriminator element (one of nil, boolean, double, "+ "string, array or struct), but got: %s", local, ), @@ -112,6 +116,32 @@ func readValueByLocal( return } +// Read the content of a `` element as a JSON null. +// +// XML-RPC itself knows no null, so we follow the widespread extension, and +// read a JSON null from an empty ``. +func readNil( + decoder *xml.Decoder, + current xml.Token, +) (value ourtypes.JsonValue, next xml.Token, err error) { + var text string + text, next, err = xmlcommon.ReadText(decoder, current) + if err != nil { + return + } + + text = xmlcommon.CollapseWhitespace(text) + if text != "" { + err = xmlcommon.NewDeserializationError( + fmt.Sprintf( + "Expected no text in a nil element, but got: %s", + text, + ), + ) + } + return +} + // Read the content of a `` element. // // Real XML-RPC tooling writes and expects a strict `1`/`0`, and not @@ -456,9 +486,16 @@ func writeValueContent( undeclareNamespace bool, ) (err error) { if value == nil { - err = xmlcommon.NewSerializationError( - "Expected a JSON-able value, but got a nil", + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty ``. + err = xmlcommon.WriteStartElementInNoNamespace( + encoder, "nil", undeclareNamespace, ) + if err != nil { + return + } + err = xmlcommon.WriteEndElement(encoder, "nil", false) return } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/meta_model.py b/dev/test_data/main/golang/expected/json_types_in_invariants/meta_model.py index b3b507d2d..a45501a44 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/meta_model.py +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/meta_model.py @@ -63,6 +63,29 @@ def is_acceptable(value: JSONValue) -> bool: or is_acceptable(self.values[-1]), "The last value must be acceptable", ) +@invariant( + lambda self: + not (self.optional_mapping is not None) + or ( + not ("type" in self.optional_mapping) + or self.optional_mapping["type"] is not None + ), + "The type of the optional mapping must not be a JSON null", +) +@invariant( + lambda self: + not ( + len(self.values) > 0 + and self.values[0] is None + ), + "The first value must not be a JSON null", +) +@invariant( + lambda self: + not (self.optional_value is not None) + or is_acceptable(self.optional_value), + "The optional value must be acceptable", +) class Something(DBC): #: Test the length of, the membership in and the indexing into a JSON-able #: object @@ -74,15 +97,21 @@ class Something(DBC): #: Test the membership in an optional JSON-able object optional_mapping: Optional[JSONObject[str]] + #: Test the nullness check over an optional JSON-able value, which is + #: treated as absent if it is a JSON null + optional_value: Optional[JSONValue] + def __init__( self, mapping: JSONObject[str], values: JSONArray, optional_mapping: Optional[JSONObject[str]] = None, + optional_value: Optional[JSONValue] = None, ) -> None: self.mapping = mapping self.values = values self.optional_mapping = optional_mapping + self.optional_value = optional_value __version__ = "dummy" diff --git a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/jsonization/Jsonization.java index c684dd94d..f887ab977 100644 --- a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -185,10 +185,10 @@ private static Reporting.Result missingRequiredProperty(String name) { * Parse {@code node} as a JSON-able value. * *

A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys. A null is rejected at any depth, as - * the JSON null has no representation as a JSON-able value, and so are - * an infinity and a not-a-number, which JSON can not represent at all. + * a null node (JSON null), a boolean, a number, a string, an array of + * JSON-able values or an object of JSON-able values with string keys. + * An infinity and a not-a-number are rejected at any depth, as JSON can not + * represent them at all. * *

The result is a deep copy, and never {@code node} itself, so that * the parsed instance does not share a tree with the document it came from. @@ -199,11 +199,15 @@ private static Reporting.Result missingRequiredProperty(String name) { */ private static Reporting.Result tryJsonValueFromAt( JsonNode node, String path) { - if (node == null || node.isNull() || node.isMissingNode()) { + if (node == null || node.isMissingNode()) { return Reporting.Result.failure(new Reporting.Error( "Expected a JSON-able value at " + path + ", but got: " + node)); } + if (node.isNull()) { + return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); + } + if (node.isNumber()) { // NOTE (mristin): // JSON knows neither an infinity nor a not-a-number. Jackson's own @@ -253,7 +257,7 @@ private static Reporting.Result tryJsonValueFromAt( } return Reporting.Result.failure(new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object) at " + path + ", but got: " + node.getNodeType())); } @@ -601,8 +605,9 @@ private static void setOptionalProperty( * Serialize {@code that} as a JSON node. * *

A JSON-able value is almost a JSON node as it comes, but not quite: its - * type rules out neither a null nor a non-finite number, neither of which - * JSON can represent. Each is refused here, at any depth. + * type rules out neither a missing node nor a non-finite number, neither of + * which JSON can represent. Each is refused here, at any depth. A null node + * is written as a JSON null. * *

The result is a deep copy, and never {@code that} itself, so that * the serialized document does not share a tree with the instance it came @@ -612,12 +617,16 @@ private static void setOptionalProperty( * @return {@code that}, as a node of a JSON document */ private static JsonNode jsonValueToJsonNode(JsonNode that) { - if (that == null || that.isNull() || that.isMissingNode()) { + if (that == null || that.isMissingNode()) { throw new _SerializeFailure( new Reporting.Error( "Expected a JSON-able value, but got: " + that)); } + if (that.isNull()) { + return JsonNodeFactory.instance.nullNode(); + } + if (that.isNumber()) { final double value = that.doubleValue(); if (Double.isInfinite(value) || Double.isNaN(value)) { @@ -667,7 +676,7 @@ private static JsonNode jsonValueToJsonNode(JsonNode that) { throw new _SerializeFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object), but got: " + that.getNodeType())); } diff --git a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/types/impl/Something.java b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/types/impl/Something.java index da69f36fe..5442e2479 100644 --- a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/types/impl/Something.java +++ b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/types/impl/Something.java @@ -134,7 +134,8 @@ public void setMappingWithConstrainedKey(ObjectNode mappingWithConstrainedKey) { @Override public Optional getOptionalValue() { - return Optional.ofNullable(optionalValue); + return Optional.ofNullable(optionalValue) + .filter(value -> !value.isNull()); } @Override diff --git a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/verification/Verification.java b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/verification/Verification.java index aaca1a62d..ce079009e 100644 --- a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/verification/Verification.java +++ b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/verification/Verification.java @@ -63,13 +63,17 @@ private static Stream streamOfFieldNames(JsonNode node) { */ private static Stream verifyJsonValueAt( JsonNode node, String path) { - if (node == null || node.isNull() || node.isMissingNode()) { + if (node == null || node.isMissingNode()) { return Stream.of( new Reporting.Error( "Expected a JSON-able value at " + path + ", but got: " + node)); } + if (node.isNull()) { + return Stream.empty(); + } + if (node.isNumber()) { // NOTE (mristin): // JSON knows neither an infinity nor a not-a-number, so neither is @@ -106,7 +110,7 @@ private static Stream verifyJsonValueAt( return Stream.of( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object) at " + path + ", but got: " + node.getNodeType())); } diff --git a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java index bd0c4644a..d4763b8c0 100644 --- a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java +++ b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java @@ -24,8 +24,10 @@ * *

JSON prescribes no XML representation of its own, so a JSON-able value * is de/serialized over the subset of XML-RPC which covers exactly what such - * a value can be: {@code }, {@code }, {@code }, - * {@code } and {@code }. + * a value can be: {@code }, {@code }, {@code }, + * {@code }, {@code } and {@code }. XML-RPC itself + * knows no null, so we follow the widespread extension, and write a JSON null + * as an empty {@code }. * *

Every method reads from or writes to the reader or the writer which * the caller passes in, and consumes or produces exactly the content of one @@ -52,16 +54,16 @@ private XmlRpc() { /** * Read the content of an element holding a JSON-able value. * - *

The content is a single discriminator element -- {@code }, - * {@code }, {@code }, {@code } or - * {@code } -- which says what the value is. + *

The content is a single discriminator element -- {@code }, + * {@code }, {@code }, {@code }, + * {@code } or {@code } -- which says what the value is. */ public static Reporting.Result readValueContent( XMLEventReader reader, boolean isEmpty) { if (isEmpty) { return Reporting.Result.failure(new Reporting.Error( - "Expected one of the elements boolean, double, string, array or " + - "struct as the content of the element, but the element was empty")); + "Expected one of the elements nil, boolean, double, string, array " + + "or struct as the content of the element, but the element was empty")); } final Reporting.Result tryElementName = XmlCommon.peekElementNameInNoNamespace(reader); @@ -100,6 +102,7 @@ public static Reporting.Result readValueContent( */ private static boolean isUnexpectedDiscriminator(String local) { switch (local) { + case "nil": case "boolean": case "double": case "string": @@ -117,6 +120,8 @@ private static boolean isUnexpectedDiscriminator(String local) { private static Reporting.Result readXmlRpcDiscriminator( XMLEventReader reader, String local, boolean isEmpty) { switch (local) { + case "nil": + return readXmlRpcNil(reader, isEmpty); case "boolean": return readXmlRpcBoolean(reader, isEmpty); case "double": @@ -139,7 +144,7 @@ private static Reporting.Result readXmlRpcDiscriminator( } default: return Reporting.Result.failure(new Reporting.Error( - "Expected a discriminator element (one of boolean, double, " + + "Expected a discriminator element (one of nil, boolean, double, " + "string, array or struct), but got: " + local)); } } @@ -249,6 +254,25 @@ private static Reporting.Result readXmlRpcString( } } + /** + * Read the content of a {@code } element as a JSON null. + * + *

XML-RPC itself knows no null, so we follow the widespread extension, + * and read a JSON null from an empty {@code }. + */ + private static Reporting.Result readXmlRpcNil( + XMLEventReader reader, boolean isEmpty) { + if (!isEmpty) { + final String text = readXmlRpcCollapsedText(reader); + if (!text.isEmpty()) { + return Reporting.Result.failure(new Reporting.Error( + "Expected no text in a nil element, but got: " + text)); + } + } + + return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); + } + /** * Read the text of the element which the caller has opened, collapsed. * @@ -431,11 +455,21 @@ private static void writeNestedValueContent( private static void writeValueContent( JsonNode that, XMLStreamWriter writer, boolean undeclareNamespace) { - if (that == null || that.isNull() || that.isMissingNode()) { + if (that == null || that.isMissingNode()) { throw new XmlCommon.SerializeFailure(new Reporting.Error( "Expected a JSON-able value, but got: " + that)); } + if (that.isNull()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + XmlCommon.writeElementInNoNamespace( + "nil", that, writer, undeclareNamespace, + (node, aWriter) -> { }); + return; + } + if (that.isBoolean()) { // NOTE (mristin): // Real XML-RPC tooling writes and expects a strict 1/0, and not @@ -487,7 +521,7 @@ private static void writeValueContent( } throw new XmlCommon.SerializeFailure(new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object), but got: " + that.getNodeType())); } diff --git a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java index f8e0a346e..67169bbdd 100644 --- a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -289,10 +289,10 @@ private static Reporting.Result> parseTu * Parse {@code node} as a JSON-able value. * *

A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys. A null is rejected at any depth, as - * the JSON null has no representation as a JSON-able value, and so are - * an infinity and a not-a-number, which JSON can not represent at all. + * a null node (JSON null), a boolean, a number, a string, an array of + * JSON-able values or an object of JSON-able values with string keys. + * An infinity and a not-a-number are rejected at any depth, as JSON can not + * represent them at all. * *

The result is a deep copy, and never {@code node} itself, so that * the parsed instance does not share a tree with the document it came from. @@ -303,11 +303,15 @@ private static Reporting.Result> parseTu */ private static Reporting.Result tryJsonValueFromAt( JsonNode node, String path) { - if (node == null || node.isNull() || node.isMissingNode()) { + if (node == null || node.isMissingNode()) { return Reporting.Result.failure(new Reporting.Error( "Expected a JSON-able value at " + path + ", but got: " + node)); } + if (node.isNull()) { + return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); + } + if (node.isNumber()) { // NOTE (mristin): // JSON knows neither an infinity nor a not-a-number. Jackson's own @@ -357,7 +361,7 @@ private static Reporting.Result tryJsonValueFromAt( } return Reporting.Result.failure(new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object) at " + path + ", but got: " + node.getNodeType())); } @@ -711,8 +715,9 @@ private static JsonNode stringToJsonNode(String that) { * Serialize {@code that} as a JSON node. * *

A JSON-able value is almost a JSON node as it comes, but not quite: its - * type rules out neither a null nor a non-finite number, neither of which - * JSON can represent. Each is refused here, at any depth. + * type rules out neither a missing node nor a non-finite number, neither of + * which JSON can represent. Each is refused here, at any depth. A null node + * is written as a JSON null. * *

The result is a deep copy, and never {@code that} itself, so that * the serialized document does not share a tree with the instance it came @@ -722,12 +727,16 @@ private static JsonNode stringToJsonNode(String that) { * @return {@code that}, as a node of a JSON document */ private static JsonNode jsonValueToJsonNode(JsonNode that) { - if (that == null || that.isNull() || that.isMissingNode()) { + if (that == null || that.isMissingNode()) { throw new _SerializeFailure( new Reporting.Error( "Expected a JSON-able value, but got: " + that)); } + if (that.isNull()) { + return JsonNodeFactory.instance.nullNode(); + } + if (that.isNumber()) { final double value = that.doubleValue(); if (Double.isInfinite(value) || Double.isNaN(value)) { @@ -777,7 +786,7 @@ private static JsonNode jsonValueToJsonNode(JsonNode that) { throw new _SerializeFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object), but got: " + that.getNodeType())); } diff --git a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/verification/Verification.java b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/verification/Verification.java index 4fea04f38..0de25e3f8 100644 --- a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/verification/Verification.java +++ b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/verification/Verification.java @@ -63,13 +63,17 @@ private static Stream streamOfFieldNames(JsonNode node) { */ private static Stream verifyJsonValueAt( JsonNode node, String path) { - if (node == null || node.isNull() || node.isMissingNode()) { + if (node == null || node.isMissingNode()) { return Stream.of( new Reporting.Error( "Expected a JSON-able value at " + path + ", but got: " + node)); } + if (node.isNull()) { + return Stream.empty(); + } + if (node.isNumber()) { // NOTE (mristin): // JSON knows neither an infinity nor a not-a-number, so neither is @@ -106,7 +110,7 @@ private static Stream verifyJsonValueAt( return Stream.of( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object) at " + path + ", but got: " + node.getNodeType())); } diff --git a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java index bd0c4644a..d4763b8c0 100644 --- a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java +++ b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java @@ -24,8 +24,10 @@ * *

JSON prescribes no XML representation of its own, so a JSON-able value * is de/serialized over the subset of XML-RPC which covers exactly what such - * a value can be: {@code }, {@code }, {@code }, - * {@code } and {@code }. + * a value can be: {@code }, {@code }, {@code }, + * {@code }, {@code } and {@code }. XML-RPC itself + * knows no null, so we follow the widespread extension, and write a JSON null + * as an empty {@code }. * *

Every method reads from or writes to the reader or the writer which * the caller passes in, and consumes or produces exactly the content of one @@ -52,16 +54,16 @@ private XmlRpc() { /** * Read the content of an element holding a JSON-able value. * - *

The content is a single discriminator element -- {@code }, - * {@code }, {@code }, {@code } or - * {@code } -- which says what the value is. + *

The content is a single discriminator element -- {@code }, + * {@code }, {@code }, {@code }, + * {@code } or {@code } -- which says what the value is. */ public static Reporting.Result readValueContent( XMLEventReader reader, boolean isEmpty) { if (isEmpty) { return Reporting.Result.failure(new Reporting.Error( - "Expected one of the elements boolean, double, string, array or " + - "struct as the content of the element, but the element was empty")); + "Expected one of the elements nil, boolean, double, string, array " + + "or struct as the content of the element, but the element was empty")); } final Reporting.Result tryElementName = XmlCommon.peekElementNameInNoNamespace(reader); @@ -100,6 +102,7 @@ public static Reporting.Result readValueContent( */ private static boolean isUnexpectedDiscriminator(String local) { switch (local) { + case "nil": case "boolean": case "double": case "string": @@ -117,6 +120,8 @@ private static boolean isUnexpectedDiscriminator(String local) { private static Reporting.Result readXmlRpcDiscriminator( XMLEventReader reader, String local, boolean isEmpty) { switch (local) { + case "nil": + return readXmlRpcNil(reader, isEmpty); case "boolean": return readXmlRpcBoolean(reader, isEmpty); case "double": @@ -139,7 +144,7 @@ private static Reporting.Result readXmlRpcDiscriminator( } default: return Reporting.Result.failure(new Reporting.Error( - "Expected a discriminator element (one of boolean, double, " + + "Expected a discriminator element (one of nil, boolean, double, " + "string, array or struct), but got: " + local)); } } @@ -249,6 +254,25 @@ private static Reporting.Result readXmlRpcString( } } + /** + * Read the content of a {@code } element as a JSON null. + * + *

XML-RPC itself knows no null, so we follow the widespread extension, + * and read a JSON null from an empty {@code }. + */ + private static Reporting.Result readXmlRpcNil( + XMLEventReader reader, boolean isEmpty) { + if (!isEmpty) { + final String text = readXmlRpcCollapsedText(reader); + if (!text.isEmpty()) { + return Reporting.Result.failure(new Reporting.Error( + "Expected no text in a nil element, but got: " + text)); + } + } + + return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); + } + /** * Read the text of the element which the caller has opened, collapsed. * @@ -431,11 +455,21 @@ private static void writeNestedValueContent( private static void writeValueContent( JsonNode that, XMLStreamWriter writer, boolean undeclareNamespace) { - if (that == null || that.isNull() || that.isMissingNode()) { + if (that == null || that.isMissingNode()) { throw new XmlCommon.SerializeFailure(new Reporting.Error( "Expected a JSON-able value, but got: " + that)); } + if (that.isNull()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + XmlCommon.writeElementInNoNamespace( + "nil", that, writer, undeclareNamespace, + (node, aWriter) -> { }); + return; + } + if (that.isBoolean()) { // NOTE (mristin): // Real XML-RPC tooling writes and expects a strict 1/0, and not @@ -487,7 +521,7 @@ private static void writeValueContent( } throw new XmlCommon.SerializeFailure(new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object), but got: " + that.getNodeType())); } diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/copying/Copying.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/copying/Copying.java index 54f659ef7..b20773749 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/copying/Copying.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/copying/Copying.java @@ -61,7 +61,8 @@ public IClass transformSomething( return new Something( that.getMapping(), that.getValues(), - that.getOptionalMapping().orElse(null)); + that.getOptionalMapping().orElse(null), + that.getOptionalValue().orElse(null)); } } @@ -76,6 +77,9 @@ public IClass transformSomething ( that.getValues().deepCopy(), that.getOptionalMapping().isPresent() ? that.getOptionalMapping().get().deepCopy() + : null, + that.getOptionalValue().isPresent() + ? that.getOptionalValue().get().deepCopy() : null ); } diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/enhancing/EnhancedSomething.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/enhancing/EnhancedSomething.java index 069d68fb7..1bbbe0094 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/enhancing/EnhancedSomething.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/enhancing/EnhancedSomething.java @@ -63,6 +63,16 @@ public void setOptionalMapping(ObjectNode optionalMapping) { instance.setOptionalMapping(optionalMapping); } + @Override + public Optional getOptionalValue() { + return instance.getOptionalValue(); + } + + @Override + public void setOptionalValue(JsonNode optionalValue) { + instance.setOptionalValue(optionalValue); + } + public Iterable descendOnce() { return instance.descendOnce(); } diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/generation/SomethingBuilder.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/generation/SomethingBuilder.java index a56fa3f88..ed2266ca5 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/generation/SomethingBuilder.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/generation/SomethingBuilder.java @@ -21,6 +21,8 @@ public class SomethingBuilder { private ObjectNode optionalMapping; + private JsonNode optionalValue; + public SomethingBuilder( ObjectNode mapping, ArrayNode values) { @@ -37,10 +39,16 @@ public SomethingBuilder setOptionalMapping(ObjectNode optionalMapping) { return this; } + public SomethingBuilder setOptionalValue(JsonNode optionalValue) { + this.optionalValue = optionalValue; + return this; + } + public Something build() { return new Something( this.mapping, this.values, - this.optionalMapping); + this.optionalMapping, + this.optionalValue); } } diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 538d84d5b..3250bdb84 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -185,10 +185,10 @@ private static Reporting.Result missingRequiredProperty(String name) { * Parse {@code node} as a JSON-able value. * *

A JSON-able value is, recursively, exactly as JSON itself is defined: - * a boolean, a number, a string, an array of JSON-able values or an object - * of JSON-able values with string keys. A null is rejected at any depth, as - * the JSON null has no representation as a JSON-able value, and so are - * an infinity and a not-a-number, which JSON can not represent at all. + * a null node (JSON null), a boolean, a number, a string, an array of + * JSON-able values or an object of JSON-able values with string keys. + * An infinity and a not-a-number are rejected at any depth, as JSON can not + * represent them at all. * *

The result is a deep copy, and never {@code node} itself, so that * the parsed instance does not share a tree with the document it came from. @@ -199,11 +199,15 @@ private static Reporting.Result missingRequiredProperty(String name) { */ private static Reporting.Result tryJsonValueFromAt( JsonNode node, String path) { - if (node == null || node.isNull() || node.isMissingNode()) { + if (node == null || node.isMissingNode()) { return Reporting.Result.failure(new Reporting.Error( "Expected a JSON-able value at " + path + ", but got: " + node)); } + if (node.isNull()) { + return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); + } + if (node.isNumber()) { // NOTE (mristin): // JSON knows neither an infinity nor a not-a-number. Jackson's own @@ -253,7 +257,7 @@ private static Reporting.Result tryJsonValueFromAt( } return Reporting.Result.failure(new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object) at " + path + ", but got: " + node.getNodeType())); } @@ -317,6 +321,7 @@ private static Reporting.Result trySomethingFrom(JsonNode node) { ObjectNode theMapping = null; ArrayNode theValues = null; ObjectNode theOptionalMapping = null; + JsonNode theOptionalValue = null; for (Iterator> iterator = node.fields(); iterator.hasNext(); ) { final Map.Entry keyValue = iterator.next(); @@ -348,6 +353,14 @@ private static Reporting.Result trySomethingFrom(JsonNode node) { theOptionalMapping = parsed.getResult(); break; } + case "optionalValue": { + final Reporting.Result parsed = tryJsonValueFrom(value); + if (parsed.isError()) { + return prependName(parsed, key); + } + theOptionalValue = parsed.getResult(); + break; + } default: return unexpectedProperty(key); } @@ -364,7 +377,8 @@ private static Reporting.Result trySomethingFrom(JsonNode node) { return Reporting.Result.success(new Something( theMapping, theValues, - theOptionalMapping)); + theOptionalMapping, + theOptionalValue)); } } @@ -553,8 +567,9 @@ private static void setOptionalProperty( * Serialize {@code that} as a JSON node. * *

A JSON-able value is almost a JSON node as it comes, but not quite: its - * type rules out neither a null nor a non-finite number, neither of which - * JSON can represent. Each is refused here, at any depth. + * type rules out neither a missing node nor a non-finite number, neither of + * which JSON can represent. Each is refused here, at any depth. A null node + * is written as a JSON null. * *

The result is a deep copy, and never {@code that} itself, so that * the serialized document does not share a tree with the instance it came @@ -564,12 +579,16 @@ private static void setOptionalProperty( * @return {@code that}, as a node of a JSON document */ private static JsonNode jsonValueToJsonNode(JsonNode that) { - if (that == null || that.isNull() || that.isMissingNode()) { + if (that == null || that.isMissingNode()) { throw new _SerializeFailure( new Reporting.Error( "Expected a JSON-able value, but got: " + that)); } + if (that.isNull()) { + return JsonNodeFactory.instance.nullNode(); + } + if (that.isNumber()) { final double value = that.doubleValue(); if (Double.isInfinite(value) || Double.isNaN(value)) { @@ -619,7 +638,7 @@ private static JsonNode jsonValueToJsonNode(JsonNode that) { throw new _SerializeFailure( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object), but got: " + that.getNodeType())); } @@ -641,6 +660,10 @@ public JsonNode transformSomething( result, "optionalMapping", "getOptionalMapping()", that.getOptionalMapping(), _Transformer::jsonValueToJsonNode); + setOptionalProperty( + result, "optionalValue", "getOptionalValue()", + that.getOptionalValue(), _Transformer::jsonValueToJsonNode); + return result; } } diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/impl/Something.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/impl/Something.java index 4f6bdc65d..0ce2222ff 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/impl/Something.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/impl/Something.java @@ -32,6 +32,8 @@ public class Something implements ISomething { private ObjectNode optionalMapping; + private JsonNode optionalValue; + public Something( ObjectNode mapping, ArrayNode values) { @@ -46,7 +48,8 @@ public Something( public Something( ObjectNode mapping, ArrayNode values, - ObjectNode optionalMapping) { + ObjectNode optionalMapping, + JsonNode optionalValue) { this.mapping = Objects.requireNonNull( mapping, "Argument \"mapping\" must be non-null."); @@ -54,6 +57,7 @@ public Something( values, "Argument \"values\" must be non-null."); this.optionalMapping = optionalMapping; + this.optionalValue = optionalValue; } @Override @@ -90,6 +94,17 @@ public void setOptionalMapping(ObjectNode optionalMapping) { this.optionalMapping = optionalMapping; } + @Override + public Optional getOptionalValue() { + return Optional.ofNullable(optionalValue) + .filter(value -> !value.isNull()); + } + + @Override + public void setOptionalValue(JsonNode optionalValue) { + this.optionalValue = optionalValue; + } + /** * Iterate recursively over all the class instances referenced from this instance. */ diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/model/ISomething.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/model/ISomething.java index 21f9240c1..0fb8bc3a2 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/model/ISomething.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/types/model/ISomething.java @@ -28,6 +28,10 @@ public interface ISomething extends IClass { Optional getOptionalMapping(); void setOptionalMapping(ObjectNode optionalMapping); + + Optional getOptionalValue(); + + void setOptionalValue(JsonNode optionalValue); } /* diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/verification/Verification.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/verification/Verification.java index b8a83a947..02bf4dfc8 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/verification/Verification.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/verification/Verification.java @@ -83,13 +83,17 @@ private static Stream streamOfFieldNames(JsonNode node) { */ private static Stream verifyJsonValueAt( JsonNode node, String path) { - if (node == null || node.isNull() || node.isMissingNode()) { + if (node == null || node.isMissingNode()) { return Stream.of( new Reporting.Error( "Expected a JSON-able value at " + path + ", but got: " + node)); } + if (node.isNull()) { + return Stream.empty(); + } + if (node.isNumber()) { // NOTE (mristin): // JSON knows neither an infinity nor a not-a-number, so neither is @@ -126,7 +130,7 @@ private static Stream verifyJsonValueAt( return Stream.of( new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object) at " + path + ", but got: " + node.getNodeType())); } @@ -179,6 +183,36 @@ public Stream transformSomething( ISomething that) { Stream errorStream = Stream.empty(); + if (!( + !(that.getOptionalValue().isPresent()) + || isAcceptable(that.getOptionalValue().orElse(null)))) { + errorStream = Stream.concat(errorStream, + Stream.of(new Reporting.Error( + "Invariant violated:\n" + + "The optional value must be acceptable"))); + } + + if (!( + !(that.getValues().size() > 0 + && (that.getValues().get(0).isNull())))) { + errorStream = Stream.concat(errorStream, + Stream.of(new Reporting.Error( + "Invariant violated:\n" + + "The first value must not be a JSON null"))); + } + + if (!( + !(that.getOptionalMapping().isPresent()) + || ( + !that.getOptionalMapping().get().has("type") + || (!that.getOptionalMapping().get().get("type").isNull()) + ))) { + errorStream = Stream.concat(errorStream, + Stream.of(new Reporting.Error( + "Invariant violated:\n" + + "The type of the optional mapping must not be a JSON null"))); + } + if (!( !(that.getValues().size() >= 1) || isAcceptable(that.getValues().get(that.getValues().size() - 1)))) { @@ -273,6 +307,17 @@ public Stream transformSomething( })); } + if (that.getOptionalValue().isPresent()) { + errorStream = Stream.concat(errorStream, + Stream.of(that.getOptionalValue().get()) + .flatMap(Verification::verifyJsonValue) + .map(error -> { + error.prependSegment( + new Reporting.NameSegment("optionalValue")); + return error; + })); + } + return errorStream; } } diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlization/Xmlization.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlization/Xmlization.java index 9a0fb1f26..ff1e6a6f7 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlization/Xmlization.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlization/Xmlization.java @@ -201,6 +201,7 @@ private static Reporting.Result readSomethingFromSequence( ObjectNode theMapping = null; ArrayNode theValues = null; ObjectNode theOptionalMapping = null; + JsonNode theOptionalValue = null; if (!isEmptySequence) { while (!atEndOfSequence(reader)) { @@ -260,6 +261,21 @@ private static Reporting.Result readSomethingFromSequence( } break; } + case "optionalValue": { + if (theOptionalValue != null) { + valueError = duplicatePropertyError(elementName); + break; + } + + final Reporting.Result value = + readTextAs_jsonValue(reader, isEmptyProperty); + if (value.isError()) { + valueError = value.getError(); + } else { + theOptionalValue = value.getResult(); + } + break; + } default: return unexpectedProperty("Something", elementName); } @@ -295,7 +311,8 @@ private static Reporting.Result readSomethingFromSequence( return Reporting.Result.success(new Something( theMapping, theValues, - theOptionalMapping)); + theOptionalMapping, + theOptionalValue)); } /** @@ -495,6 +512,13 @@ private static void writeSomethingAsSequence( that.getOptionalMapping(), writer, _VisitorWithWriter::writeJsonObjectContent); + + writeOptionalProperty( + "optionalValue", + "getOptionalValue()", + that.getOptionalValue(), + writer, + _VisitorWithWriter::writeJsonValueContent); } @Override diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java index bd0c4644a..d4763b8c0 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/xmlrpc/XmlRpc.java @@ -24,8 +24,10 @@ * *

JSON prescribes no XML representation of its own, so a JSON-able value * is de/serialized over the subset of XML-RPC which covers exactly what such - * a value can be: {@code }, {@code }, {@code }, - * {@code } and {@code }. + * a value can be: {@code }, {@code }, {@code }, + * {@code }, {@code } and {@code }. XML-RPC itself + * knows no null, so we follow the widespread extension, and write a JSON null + * as an empty {@code }. * *

Every method reads from or writes to the reader or the writer which * the caller passes in, and consumes or produces exactly the content of one @@ -52,16 +54,16 @@ private XmlRpc() { /** * Read the content of an element holding a JSON-able value. * - *

The content is a single discriminator element -- {@code }, - * {@code }, {@code }, {@code } or - * {@code } -- which says what the value is. + *

The content is a single discriminator element -- {@code }, + * {@code }, {@code }, {@code }, + * {@code } or {@code } -- which says what the value is. */ public static Reporting.Result readValueContent( XMLEventReader reader, boolean isEmpty) { if (isEmpty) { return Reporting.Result.failure(new Reporting.Error( - "Expected one of the elements boolean, double, string, array or " + - "struct as the content of the element, but the element was empty")); + "Expected one of the elements nil, boolean, double, string, array " + + "or struct as the content of the element, but the element was empty")); } final Reporting.Result tryElementName = XmlCommon.peekElementNameInNoNamespace(reader); @@ -100,6 +102,7 @@ public static Reporting.Result readValueContent( */ private static boolean isUnexpectedDiscriminator(String local) { switch (local) { + case "nil": case "boolean": case "double": case "string": @@ -117,6 +120,8 @@ private static boolean isUnexpectedDiscriminator(String local) { private static Reporting.Result readXmlRpcDiscriminator( XMLEventReader reader, String local, boolean isEmpty) { switch (local) { + case "nil": + return readXmlRpcNil(reader, isEmpty); case "boolean": return readXmlRpcBoolean(reader, isEmpty); case "double": @@ -139,7 +144,7 @@ private static Reporting.Result readXmlRpcDiscriminator( } default: return Reporting.Result.failure(new Reporting.Error( - "Expected a discriminator element (one of boolean, double, " + + "Expected a discriminator element (one of nil, boolean, double, " + "string, array or struct), but got: " + local)); } } @@ -249,6 +254,25 @@ private static Reporting.Result readXmlRpcString( } } + /** + * Read the content of a {@code } element as a JSON null. + * + *

XML-RPC itself knows no null, so we follow the widespread extension, + * and read a JSON null from an empty {@code }. + */ + private static Reporting.Result readXmlRpcNil( + XMLEventReader reader, boolean isEmpty) { + if (!isEmpty) { + final String text = readXmlRpcCollapsedText(reader); + if (!text.isEmpty()) { + return Reporting.Result.failure(new Reporting.Error( + "Expected no text in a nil element, but got: " + text)); + } + } + + return Reporting.Result.success(JsonNodeFactory.instance.nullNode()); + } + /** * Read the text of the element which the caller has opened, collapsed. * @@ -431,11 +455,21 @@ private static void writeNestedValueContent( private static void writeValueContent( JsonNode that, XMLStreamWriter writer, boolean undeclareNamespace) { - if (that == null || that.isNull() || that.isMissingNode()) { + if (that == null || that.isMissingNode()) { throw new XmlCommon.SerializeFailure(new Reporting.Error( "Expected a JSON-able value, but got: " + that)); } + if (that.isNull()) { + // NOTE (mristin): + // XML-RPC itself knows no null, so we follow the widespread extension, + // and write a JSON null as an empty . + XmlCommon.writeElementInNoNamespace( + "nil", that, writer, undeclareNamespace, + (node, aWriter) -> { }); + return; + } + if (that.isBoolean()) { // NOTE (mristin): // Real XML-RPC tooling writes and expects a strict 1/0, and not @@ -487,7 +521,7 @@ private static void writeValueContent( } throw new XmlCommon.SerializeFailure(new Reporting.Error( - "Expected a JSON-able value (a boolean, a number, a string, " + + "Expected a JSON-able value (a null, a boolean, a number, a string, " + "an array or an object), but got: " + that.getNodeType())); } diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/test/java/TestCopying.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/test/java/TestCopying.java index 91fca4930..a752bac58 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/test/java/TestCopying.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/test/java/TestCopying.java @@ -35,7 +35,8 @@ public Boolean transformSomething(ISomething that, IClass other) { return ( that.getMapping().equals(casted.getMapping()) && that.getValues().equals(casted.getValues()) - && that.getOptionalMapping().equals(casted.getOptionalMapping())); + && that.getOptionalMapping().equals(casted.getOptionalMapping()) + && that.getOptionalValue().equals(casted.getOptionalValue())); } } // class _DeepEqualiser @@ -47,7 +48,8 @@ private static Boolean SomethingShallowEquals( return ( that.getMapping().equals(other.getMapping()) && that.getValues().equals(other.getValues()) - && that.getOptionalMapping().equals(other.getOptionalMapping())); + && that.getOptionalMapping().equals(other.getOptionalMapping()) + && that.getOptionalValue().equals(other.getOptionalValue())); } private static Boolean SomethingDeepEquals(Something that, Something other) { diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/meta_model.py b/dev/test_data/main/java/expected/json_types_in_invariants/meta_model.py index b3b507d2d..a45501a44 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/meta_model.py +++ b/dev/test_data/main/java/expected/json_types_in_invariants/meta_model.py @@ -63,6 +63,29 @@ def is_acceptable(value: JSONValue) -> bool: or is_acceptable(self.values[-1]), "The last value must be acceptable", ) +@invariant( + lambda self: + not (self.optional_mapping is not None) + or ( + not ("type" in self.optional_mapping) + or self.optional_mapping["type"] is not None + ), + "The type of the optional mapping must not be a JSON null", +) +@invariant( + lambda self: + not ( + len(self.values) > 0 + and self.values[0] is None + ), + "The first value must not be a JSON null", +) +@invariant( + lambda self: + not (self.optional_value is not None) + or is_acceptable(self.optional_value), + "The optional value must be acceptable", +) class Something(DBC): #: Test the length of, the membership in and the indexing into a JSON-able #: object @@ -74,15 +97,21 @@ class Something(DBC): #: Test the membership in an optional JSON-able object optional_mapping: Optional[JSONObject[str]] + #: Test the nullness check over an optional JSON-able value, which is + #: treated as absent if it is a JSON null + optional_value: Optional[JSONValue] + def __init__( self, mapping: JSONObject[str], values: JSONArray, optional_mapping: Optional[JSONObject[str]] = None, + optional_value: Optional[JSONValue] = None, ) -> None: self.mapping = mapping self.values = values self.optional_mapping = optional_mapping + self.optional_value = optional_value __version__ = "dummy" diff --git a/dev/test_data/main/jsonschema/expected/json_types/examples/Expected/Something/nulls.json b/dev/test_data/main/jsonschema/expected/json_types/examples/Expected/Something/nulls.json new file mode 100644 index 000000000..5a89d1b5f --- /dev/null +++ b/dev/test_data/main/jsonschema/expected/json_types/examples/Expected/Something/nulls.json @@ -0,0 +1,19 @@ +{ + "value": null, + "values": [ + 1.5, + null + ], + "mapping": { + "a": null, + "b": [ + null, + { + "c": null + } + ] + }, + "mappingWithConstrainedKey": { + "nonEmptyKey": null + } +} diff --git a/dev/test_data/main/jsonschema/expected/json_types/examples/Unexpected/Unserializable/NullNotAValidJsonValue/Something/null_as_bare_value.json b/dev/test_data/main/jsonschema/expected/json_types/examples/Unexpected/Unserializable/NullNotAValidJsonValue/Something/null_as_bare_value.json deleted file mode 100644 index efab57e96..000000000 --- a/dev/test_data/main/jsonschema/expected/json_types/examples/Unexpected/Unserializable/NullNotAValidJsonValue/Something/null_as_bare_value.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "value": null, - "values": [], - "mapping": {}, - "mappingWithConstrainedKey": { - "a": 1 - } -} diff --git a/dev/test_data/main/jsonschema/expected/json_types/examples/Unexpected/Unserializable/NullNotAValidJsonValue/Something/null_nested_in_array.json b/dev/test_data/main/jsonschema/expected/json_types/examples/Unexpected/Unserializable/NullNotAValidJsonValue/Something/null_nested_in_array.json deleted file mode 100644 index 337546102..000000000 --- a/dev/test_data/main/jsonschema/expected/json_types/examples/Unexpected/Unserializable/NullNotAValidJsonValue/Something/null_nested_in_array.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "value": true, - "values": [1, null, 3], - "mapping": {}, - "mappingWithConstrainedKey": { - "a": 1 - } -} diff --git a/dev/test_data/main/jsonschema/expected/json_types/expected_output/schema.json b/dev/test_data/main/jsonschema/expected/json_types/expected_output/schema.json index 5c6f2b59d..3e7b570c5 100644 --- a/dev/test_data/main/jsonschema/expected/json_types/expected_output/schema.json +++ b/dev/test_data/main/jsonschema/expected/json_types/expected_output/schema.json @@ -11,6 +11,9 @@ "definitions": { "JSONValue": { "anyOf": [ + { + "type": "null" + }, { "type": "boolean" }, diff --git a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/jsonization.py b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/jsonization.py index b974f9393..c6e5422fd 100644 --- a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/jsonization.py +++ b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/dev/tests/common_jsonization.py index 842d97e88..48d07f713 100644 --- a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dev/tests/common_jsonization.py index e1e619063..d7e77faeb 100644 --- a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/jsonization.py index cafcaf60a..cb5bd5a26 100644 --- a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/arithmetic/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/arithmetic/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/arithmetic/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/arithmetic/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/jsonization.py index d7beb8fbe..a67a25d0c 100644 --- a/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/assert_statements/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/assert_statements/expected_output/dev/tests/common_jsonization.py index beb809acb..893ccc58e 100644 --- a/dev/test_data/main/python/expected/assert_statements/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/assert_statements/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/jsonization.py index 064cb6649..e2619fcd5 100644 --- a/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/assignment_targets/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/assignment_targets/expected_output/dev/tests/common_jsonization.py index 98ad1e250..c84e412ec 100644 --- a/dev/test_data/main/python/expected/assignment_targets/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/assignment_targets/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/jsonization.py index 193d5e899..095760e25 100644 --- a/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dev/tests/common_jsonization.py index 204fbbcb7..f5068e2bf 100644 --- a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/jsonization.py index 61f821d99..5a04c3c80 100644 --- a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/comparisons/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/comparisons/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/comparisons/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/comparisons/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/comparisons/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/comparisons/expected_output/dummy/jsonization.py index af0080e8e..1d6d248a8 100644 --- a/dev/test_data/main/python/expected/comparisons/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/comparisons/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/jsonization.py index a0b09e88c..1f3d3ec0e 100644 --- a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/jsonization.py @@ -222,6 +222,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -232,6 +233,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dev/tests/common_jsonization.py index f451bd069..8ef83d25f 100644 --- a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/jsonization.py index 7d7a2dd12..cfaa7df8e 100644 --- a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dev/tests/common_jsonization.py index ee59eb2c6..d3139b827 100644 --- a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/jsonization.py index cf1b92398..f1de19f72 100644 --- a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/empty_class/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/empty_class/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/empty_class/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/empty_class/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/empty_class/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/empty_class/expected_output/dummy/jsonization.py index 483c757da..df454f4dd 100644 --- a/dev/test_data/main/python/expected/empty_class/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/empty_class/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/enum/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/enum/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/enum/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/enum/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/enum/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/enum/expected_output/dummy/jsonization.py index c5c7a6294..98fd90c08 100644 --- a/dev/test_data/main/python/expected/enum/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/enum/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/float_only/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/float_only/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/float_only/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/float_only/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/float_only/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/float_only/expected_output/dummy/jsonization.py index 933b7eff6..6b494047d 100644 --- a/dev/test_data/main/python/expected/float_only/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/float_only/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/for_statements/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/for_statements/expected_output/dev/tests/common_jsonization.py index 98ad1e250..c84e412ec 100644 --- a/dev/test_data/main/python/expected/for_statements/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/for_statements/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/for_statements/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/for_statements/expected_output/dummy/jsonization.py index 5d621c5cc..c4e8fcba7 100644 --- a/dev/test_data/main/python/expected/for_statements/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/for_statements/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/if_statements/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/if_statements/expected_output/dev/tests/common_jsonization.py index e5a4d0aff..ea2d7d901 100644 --- a/dev/test_data/main/python/expected/if_statements/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/if_statements/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/if_statements/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/if_statements/expected_output/dummy/jsonization.py index 2803d8ec2..c105a4491 100644 --- a/dev/test_data/main/python/expected/if_statements/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/if_statements/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/implementation_specific/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/implementation_specific/expected_output/dev/tests/common_jsonization.py index 65560cec6..1f868a062 100644 --- a/dev/test_data/main/python/expected/implementation_specific/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/implementation_specific/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/jsonization.py index eed1795bd..54e704949 100644 --- a/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/jsonization.py index 2460a795d..d60ae2529 100644 --- a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/int_parsing/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/int_parsing/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/int_parsing/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/int_parsing/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/int_parsing/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/int_parsing/expected_output/dummy/jsonization.py index 8a45d1799..331dcb4b7 100644 --- a/dev/test_data/main/python/expected/int_parsing/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/int_parsing/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/is_instance/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/is_instance/expected_output/dev/tests/common_jsonization.py index c2d290063..3293ffffe 100644 --- a/dev/test_data/main/python/expected/is_instance/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/is_instance/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/is_instance/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/is_instance/expected_output/dummy/jsonization.py index 535b19748..0beae0f7f 100644 --- a/dev/test_data/main/python/expected/is_instance/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/is_instance/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_json_value_verification.py b/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_json_value_verification.py index 6e84417a7..f93fbfa2f 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_json_value_verification.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_json_value_verification.py @@ -139,6 +139,16 @@ def test_an_integer_too_large_for_a_float_is_refused(self) -> None: paths_and_causes[0][1] ) + def test_a_null_is_accepted_at_any_depth(self) -> None: + for value in (None, [None], {"a": [None, {"b": None}]}): + self.assertEqual( + [], + _paths_and_causes( + our_json_value_verification.verify_json_value(value) + ), + f"for the value {value!r}" + ) + def test_a_value_of_an_unexpected_type_is_refused(self) -> None: paths_and_causes = _paths_and_causes( our_json_value_verification.verify_json_value({"a": object()}) diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_xml_rpc.py b/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_xml_rpc.py index 6e0693d5c..e98029df7 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_xml_rpc.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dev/tests/test_xml_rpc.py @@ -50,17 +50,30 @@ def test_a_value_round_trips_through_every_discriminator(self) -> None: f'n1' f'f1.5' f'sx' + f'z' f'a' f'0' + f'' f'' f'' ) self.assertEqual( - {"b": True, "n": 1.0, "f": 1.5, "s": "x", "a": [0.0]}, + {"b": True, "n": 1.0, "f": 1.5, "s": "x", "z": None, "a": [0.0, None]}, our_xmlrpc.read_value_content(element, iterator) ) + def test_a_nil_with_text_is_refused(self) -> None: + element, iterator = _enter( + f'x' + ) + + with self.assertRaises(our_xmlcommon.DeserializationException) as context: + our_xmlrpc.read_value_content(element, iterator) + + self.assertEqual('nil', str(context.exception.path)) + self.assertIn("Expected no text in a element", context.exception.cause) + def test_a_discriminator_in_the_document_namespace_is_refused(self) -> None: # NOTE (mristin): # The XML-RPC elements reside in no namespace at all, so a discriminator @@ -248,6 +261,10 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: [0.0], '0.0' ), + ( + [None], + '' + ), ( {'k': 0.0}, ( @@ -272,6 +289,14 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: f"for the value {value!r}" ) + def test_a_null_is_written_as_an_empty_nil(self) -> None: + stream = io.StringIO() + writer = our_xmlcommon.Writer(stream) + + our_xmlrpc.write_discriminator(None, writer) + + self.assertEqual('', stream.getvalue()) + def test_the_path_of_a_failure_points_into_the_value(self) -> None: stream = io.StringIO() writer = our_xmlcommon.Writer(stream) diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonization.py index a3c731c8c..891b9c432 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, @@ -290,11 +292,10 @@ def _json_value_from_jsonable( Parse :paramref:`jsonable` as a JSON-able value. A JSON-able value is, recursively, exactly as JSON itself is defined: - a boolean, a number, a string, an array of JSON-able values or an object - of JSON-able values with string keys. A ``None`` is rejected at any - depth, as the JSON ``null`` has no representation as a JSON-able value, - and so are an infinity and a not-a-number, which JSON can not represent - at all. + a ``None`` (JSON null), a boolean, a number, a string, an array of + JSON-able values or an object of JSON-able values with string keys. + An infinity and a not-a-number are rejected at any depth, as JSON can not + represent them at all. The result is a new structure, and never :paramref:`jsonable` itself, so that the parsed instance does not alias the document it came from. @@ -303,6 +304,9 @@ def _json_value_from_jsonable( :return: parsed JSON-able value :raise: :py:class:`DeserializationException` if unexpected :paramref:`jsonable` """ + if jsonable is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -370,8 +374,8 @@ def _json_value_from_jsonable( return items raise DeserializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(jsonable)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(jsonable)}" ) @@ -470,7 +474,7 @@ def something_from_jsonable( f"Unexpected property: {unexpected_key}" ) - if the_value is None: + if 'value' not in mapping: raise DeserializationException( "The required property 'value' is missing" ) @@ -565,10 +569,9 @@ def _json_value_to_jsonable( Serialize :paramref:`value` as a JSON-able structure. A JSON-able value is almost JSON-able as it comes, but not quite: its type - rules out neither a ``None``, which the JSON ``null`` gives no - representation for, nor an infinity and a not-a-number, which JSON can not + rules out neither an infinity and a not-a-number, which JSON can not represent at all, nor a non-string key in an object. Each is refused here, - at any depth. + at any depth. A ``None`` is written out as a JSON null. The result is a new structure, and never :paramref:`value` itself, so that the serialized document does not alias the instance it came from. @@ -577,6 +580,9 @@ def _json_value_to_jsonable( :return: :paramref:`value`, as a JSON-able structure :raise: :py:class:`SerializationException` if :paramref:`value` is not JSON-able """ + if value is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -640,8 +646,8 @@ def _json_value_to_jsonable( return items raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonvalueverification.py b/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonvalueverification.py index cec368c6b..0f3360f10 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonvalueverification.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dummy/jsonvalueverification.py @@ -2,8 +2,8 @@ Verify that a value is JSON-able. A JSON-able value is, recursively, exactly as JSON itself is defined: -a boolean, a finite number, a string, an array of JSON-able values or -an object of JSON-able values with string keys. Python offers no type which +a ``None`` (JSON null), a boolean, a finite number, a string, an array of +JSON-able values or an object of JSON-able values with string keys. Python offers no type which says exactly that, so a value has to be walked to make sure of it. This module depends on no class of the meta-model. It is invoked from @@ -49,6 +49,9 @@ def verify_json_value( :param value: to be verified :yield: errors, if any """ + if value is None: + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be taken for a number. @@ -114,8 +117,8 @@ def verify_json_value( return yield Error( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dummy/types.py b/dev/test_data/main/python/expected/json_types/expected_output/dummy/types.py index a3abb3fbc..9a4258eae 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dummy/types.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dummy/types.py @@ -23,10 +23,15 @@ # NOTE (mristin): # A JSON-able value is, recursively, exactly as JSON itself is defined: -# a boolean, a number, a string, an array of JSON-able values or an object -# of JSON-able values with string keys -- never a ``None``, and never +# a ``None`` (JSON null), a boolean, a number, a string, an array of JSON-able +# values or an object of JSON-able values with string keys -- never # an infinity or a not-a-number, neither of which JSON can represent. # +# A ``None`` might appear at any depth, including the value itself. Since +# an absent optional property is also ``None``, an optional JSON-able property +# which is a JSON null is indistinguishable from an absent one, and is treated +# as absent. +# # JSON knows a single numeric type. Python tells an ``int`` from a ``float``, # and a caller will naturally write either, so both are admitted here -- but # an ``int`` only so long as a ``float`` can hold it exactly. The @@ -38,6 +43,7 @@ # (see https://github.com/python/mypy/issues/731), so the item and the value # types have to be spelled ``Any`` instead of ``JsonValue``. JsonValue = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlcommon.py index 4dfc2fadd..944707e33 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlcommon.py @@ -764,6 +764,23 @@ def write_end_element_in_no_namespace( self._open_in_no_namespace -= 1 self.stream.write(f'') + def write_empty_element_in_no_namespace( + self, + name: str + ) -> None: + """ + Write the empty element with the tag name :paramref:`name`, which resides + in no namespace at all. + + If it is the outermost such element, it undeclares the default namespace. + + :param name: of the element tag. Expected to contain no XML special characters. + """ + if self._open_in_no_namespace == 0: + self.stream.write(f'<{name} xmlns=""/>') + else: + self.stream.write(f'<{name}/>') + def _write_first_empty_element_with_namespace( self, name: str diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlization.py b/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlization.py index a63697495..b3b449f73 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlization.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlization.py @@ -822,7 +822,7 @@ def _read_something_as_sequence( the_optional_values: Optional[our_types.JsonArray] = values.get('optionalValues') the_optional_mapping: Optional[our_types.JsonObject] = values.get('optionalMapping') - if the_value is None: + if 'value' not in values: raise DeserializationException( "The required property 'value' is missing" ) diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlrpc.py b/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlrpc.py index 2de48b34f..489ee824e 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlrpc.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dummy/xmlrpc.py @@ -3,8 +3,10 @@ JSON prescribes no XML representation of its own, so we borrow the one of XML-RPC for it: a value is written as a single discriminator element -- -````, ````, ````, ```` or ```` -- -which says what the value is. +````, ````, ````, ````, ```` or +```` -- which says what the value is. XML-RPC itself knows no null, +so we follow the widespread extension, and write a JSON null as an empty +````. This module knows nothing of the meta-model beyond the JSON-able types, and it drives no document of its own. The caller hands over the very stream of @@ -66,9 +68,9 @@ def read_value_content( """ Read the content of :paramref:`element` as a JSON-able value. - The content is a single discriminator element -- ````, - ````, ````, ```` or ```` -- which says what - the value is. + The content is a single discriminator element -- ````, + ````, ````, ````, ```` or ```` -- + which says what the value is. The end element corresponding to :paramref:`element` will be read as well. @@ -86,7 +88,7 @@ def read_value_content( discriminator = read_next_start_element( iterator, 'a discriminator ' - '(one of , , , or )' + '(one of , , , , or )' ) result = _read_discriminator(discriminator, iterator) @@ -117,7 +119,7 @@ def _read_discriminator( iterator: Iterator[Tuple[str, Element]] ) -> our_types.JsonValue: """ - Read :paramref:`element` as one of the five XML-RPC discriminators. + Read :paramref:`element` as one of the six XML-RPC discriminators. The end element corresponding to :paramref:`element` will be read as well. @@ -138,6 +140,19 @@ def _read_discriminator( # very element which does not belong here, so naming it in the path as well # as in the message would say nothing more. try: + if tag_wo_ns == 'nil': + read_end_element(element, iterator) + + # NOTE (mristin): + # The text of an element is only complete at its end, so we can check + # it only now. + if element.text is not None and len(element.text.strip()) != 0: + raise DeserializationException( + f"Expected no text in a element, but got: {element.text!r}" + ) + + return None + if tag_wo_ns == 'boolean': # NOTE (mristin): # Real XML-RPC tooling writes and expects a strict ``1``/``0``, and not @@ -204,7 +219,7 @@ def _read_discriminator( raise DeserializationException( f"Expected a discriminator element " - f"(one of , , , or ), " + f"(one of , , , , or ), " f"but got: {tag_wo_ns!r}" ) @@ -442,12 +457,16 @@ def write_discriminator( writer: Writer ) -> None: """ - Write :paramref:`value` as one of the five XML-RPC discriminators. + Write :paramref:`value` as one of the six XML-RPC discriminators. :param value: to be serialized :param writer: to write to :raise: :py:class:`SerializationException` if the value could not be written """ + if value is None: + writer.write_empty_element_in_no_namespace('nil') + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be written as a number. @@ -524,8 +543,8 @@ def write_discriminator( return raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_json_value_verification.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_json_value_verification.py index 6e84417a7..f93fbfa2f 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_json_value_verification.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_json_value_verification.py @@ -139,6 +139,16 @@ def test_an_integer_too_large_for_a_float_is_refused(self) -> None: paths_and_causes[0][1] ) + def test_a_null_is_accepted_at_any_depth(self) -> None: + for value in (None, [None], {"a": [None, {"b": None}]}): + self.assertEqual( + [], + _paths_and_causes( + our_json_value_verification.verify_json_value(value) + ), + f"for the value {value!r}" + ) + def test_a_value_of_an_unexpected_type_is_refused(self) -> None: paths_and_causes = _paths_and_causes( our_json_value_verification.verify_json_value({"a": object()}) diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_xml_rpc.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_xml_rpc.py index 6e0693d5c..e98029df7 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_xml_rpc.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dev/tests/test_xml_rpc.py @@ -50,17 +50,30 @@ def test_a_value_round_trips_through_every_discriminator(self) -> None: f'n1' f'f1.5' f'sx' + f'z' f'a' f'0' + f'' f'' f'' ) self.assertEqual( - {"b": True, "n": 1.0, "f": 1.5, "s": "x", "a": [0.0]}, + {"b": True, "n": 1.0, "f": 1.5, "s": "x", "z": None, "a": [0.0, None]}, our_xmlrpc.read_value_content(element, iterator) ) + def test_a_nil_with_text_is_refused(self) -> None: + element, iterator = _enter( + f'x' + ) + + with self.assertRaises(our_xmlcommon.DeserializationException) as context: + our_xmlrpc.read_value_content(element, iterator) + + self.assertEqual('nil', str(context.exception.path)) + self.assertIn("Expected no text in a element", context.exception.cause) + def test_a_discriminator_in_the_document_namespace_is_refused(self) -> None: # NOTE (mristin): # The XML-RPC elements reside in no namespace at all, so a discriminator @@ -248,6 +261,10 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: [0.0], '0.0' ), + ( + [None], + '' + ), ( {'k': 0.0}, ( @@ -272,6 +289,14 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: f"for the value {value!r}" ) + def test_a_null_is_written_as_an_empty_nil(self) -> None: + stream = io.StringIO() + writer = our_xmlcommon.Writer(stream) + + our_xmlrpc.write_discriminator(None, writer) + + self.assertEqual('', stream.getvalue()) + def test_the_path_of_a_failure_points_into_the_value(self) -> None: stream = io.StringIO() writer = our_xmlcommon.Writer(stream) diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonization.py index b43ef8b15..ebb02eb39 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, @@ -339,11 +341,10 @@ def _json_value_from_jsonable( Parse :paramref:`jsonable` as a JSON-able value. A JSON-able value is, recursively, exactly as JSON itself is defined: - a boolean, a number, a string, an array of JSON-able values or an object - of JSON-able values with string keys. A ``None`` is rejected at any - depth, as the JSON ``null`` has no representation as a JSON-able value, - and so are an infinity and a not-a-number, which JSON can not represent - at all. + a ``None`` (JSON null), a boolean, a number, a string, an array of + JSON-able values or an object of JSON-able values with string keys. + An infinity and a not-a-number are rejected at any depth, as JSON can not + represent them at all. The result is a new structure, and never :paramref:`jsonable` itself, so that the parsed instance does not alias the document it came from. @@ -352,6 +353,9 @@ def _json_value_from_jsonable( :return: parsed JSON-able value :raise: :py:class:`DeserializationException` if unexpected :paramref:`jsonable` """ + if jsonable is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -419,8 +423,8 @@ def _json_value_from_jsonable( return items raise DeserializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(jsonable)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(jsonable)}" ) @@ -731,10 +735,9 @@ def _json_value_to_jsonable( Serialize :paramref:`value` as a JSON-able structure. A JSON-able value is almost JSON-able as it comes, but not quite: its type - rules out neither a ``None``, which the JSON ``null`` gives no - representation for, nor an infinity and a not-a-number, which JSON can not + rules out neither an infinity and a not-a-number, which JSON can not represent at all, nor a non-string key in an object. Each is refused here, - at any depth. + at any depth. A ``None`` is written out as a JSON null. The result is a new structure, and never :paramref:`value` itself, so that the serialized document does not alias the instance it came from. @@ -743,6 +746,9 @@ def _json_value_to_jsonable( :return: :paramref:`value`, as a JSON-able structure :raise: :py:class:`SerializationException` if :paramref:`value` is not JSON-able """ + if value is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -806,8 +812,8 @@ def _json_value_to_jsonable( return items raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.py index cec368c6b..0f3360f10 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/jsonvalueverification.py @@ -2,8 +2,8 @@ Verify that a value is JSON-able. A JSON-able value is, recursively, exactly as JSON itself is defined: -a boolean, a finite number, a string, an array of JSON-able values or -an object of JSON-able values with string keys. Python offers no type which +a ``None`` (JSON null), a boolean, a finite number, a string, an array of +JSON-able values or an object of JSON-able values with string keys. Python offers no type which says exactly that, so a value has to be walked to make sure of it. This module depends on no class of the meta-model. It is invoked from @@ -49,6 +49,9 @@ def verify_json_value( :param value: to be verified :yield: errors, if any """ + if value is None: + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be taken for a number. @@ -114,8 +117,8 @@ def verify_json_value( return yield Error( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/types.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/types.py index 0b838bf1b..26cc8bbd8 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/types.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/types.py @@ -23,10 +23,15 @@ # NOTE (mristin): # A JSON-able value is, recursively, exactly as JSON itself is defined: -# a boolean, a number, a string, an array of JSON-able values or an object -# of JSON-able values with string keys -- never a ``None``, and never +# a ``None`` (JSON null), a boolean, a number, a string, an array of JSON-able +# values or an object of JSON-able values with string keys -- never # an infinity or a not-a-number, neither of which JSON can represent. # +# A ``None`` might appear at any depth, including the value itself. Since +# an absent optional property is also ``None``, an optional JSON-able property +# which is a JSON null is indistinguishable from an absent one, and is treated +# as absent. +# # JSON knows a single numeric type. Python tells an ``int`` from a ``float``, # and a caller will naturally write either, so both are admitted here -- but # an ``int`` only so long as a ``float`` can hold it exactly. The @@ -38,6 +43,7 @@ # (see https://github.com/python/mypy/issues/731), so the item and the value # types have to be spelled ``Any`` instead of ``JsonValue``. JsonValue = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlcommon.py index 4dfc2fadd..944707e33 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlcommon.py @@ -764,6 +764,23 @@ def write_end_element_in_no_namespace( self._open_in_no_namespace -= 1 self.stream.write(f'') + def write_empty_element_in_no_namespace( + self, + name: str + ) -> None: + """ + Write the empty element with the tag name :paramref:`name`, which resides + in no namespace at all. + + If it is the outermost such element, it undeclares the default namespace. + + :param name: of the element tag. Expected to contain no XML special characters. + """ + if self._open_in_no_namespace == 0: + self.stream.write(f'<{name} xmlns=""/>') + else: + self.stream.write(f'<{name}/>') + def _write_first_empty_element_with_namespace( self, name: str diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlrpc.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlrpc.py index 2de48b34f..489ee824e 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlrpc.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/xmlrpc.py @@ -3,8 +3,10 @@ JSON prescribes no XML representation of its own, so we borrow the one of XML-RPC for it: a value is written as a single discriminator element -- -````, ````, ````, ```` or ```` -- -which says what the value is. +````, ````, ````, ````, ```` or +```` -- which says what the value is. XML-RPC itself knows no null, +so we follow the widespread extension, and write a JSON null as an empty +````. This module knows nothing of the meta-model beyond the JSON-able types, and it drives no document of its own. The caller hands over the very stream of @@ -66,9 +68,9 @@ def read_value_content( """ Read the content of :paramref:`element` as a JSON-able value. - The content is a single discriminator element -- ````, - ````, ````, ```` or ```` -- which says what - the value is. + The content is a single discriminator element -- ````, + ````, ````, ````, ```` or ```` -- + which says what the value is. The end element corresponding to :paramref:`element` will be read as well. @@ -86,7 +88,7 @@ def read_value_content( discriminator = read_next_start_element( iterator, 'a discriminator ' - '(one of , , , or )' + '(one of , , , , or )' ) result = _read_discriminator(discriminator, iterator) @@ -117,7 +119,7 @@ def _read_discriminator( iterator: Iterator[Tuple[str, Element]] ) -> our_types.JsonValue: """ - Read :paramref:`element` as one of the five XML-RPC discriminators. + Read :paramref:`element` as one of the six XML-RPC discriminators. The end element corresponding to :paramref:`element` will be read as well. @@ -138,6 +140,19 @@ def _read_discriminator( # very element which does not belong here, so naming it in the path as well # as in the message would say nothing more. try: + if tag_wo_ns == 'nil': + read_end_element(element, iterator) + + # NOTE (mristin): + # The text of an element is only complete at its end, so we can check + # it only now. + if element.text is not None and len(element.text.strip()) != 0: + raise DeserializationException( + f"Expected no text in a element, but got: {element.text!r}" + ) + + return None + if tag_wo_ns == 'boolean': # NOTE (mristin): # Real XML-RPC tooling writes and expects a strict ``1``/``0``, and not @@ -204,7 +219,7 @@ def _read_discriminator( raise DeserializationException( f"Expected a discriminator element " - f"(one of , , , or ), " + f"(one of , , , , or ), " f"but got: {tag_wo_ns!r}" ) @@ -442,12 +457,16 @@ def write_discriminator( writer: Writer ) -> None: """ - Write :paramref:`value` as one of the five XML-RPC discriminators. + Write :paramref:`value` as one of the six XML-RPC discriminators. :param value: to be serialized :param writer: to write to :raise: :py:class:`SerializationException` if the value could not be written """ + if value is None: + writer.write_empty_element_in_no_namespace('nil') + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be written as a number. @@ -524,8 +543,8 @@ def write_discriminator( return raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_json_value_verification.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_json_value_verification.py index 6e84417a7..f93fbfa2f 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_json_value_verification.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_json_value_verification.py @@ -139,6 +139,16 @@ def test_an_integer_too_large_for_a_float_is_refused(self) -> None: paths_and_causes[0][1] ) + def test_a_null_is_accepted_at_any_depth(self) -> None: + for value in (None, [None], {"a": [None, {"b": None}]}): + self.assertEqual( + [], + _paths_and_causes( + our_json_value_verification.verify_json_value(value) + ), + f"for the value {value!r}" + ) + def test_a_value_of_an_unexpected_type_is_refused(self) -> None: paths_and_causes = _paths_and_causes( our_json_value_verification.verify_json_value({"a": object()}) diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_xml_rpc.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_xml_rpc.py index 6e0693d5c..e98029df7 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_xml_rpc.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dev/tests/test_xml_rpc.py @@ -50,17 +50,30 @@ def test_a_value_round_trips_through_every_discriminator(self) -> None: f'n1' f'f1.5' f'sx' + f'z' f'a' f'0' + f'' f'' f'' ) self.assertEqual( - {"b": True, "n": 1.0, "f": 1.5, "s": "x", "a": [0.0]}, + {"b": True, "n": 1.0, "f": 1.5, "s": "x", "z": None, "a": [0.0, None]}, our_xmlrpc.read_value_content(element, iterator) ) + def test_a_nil_with_text_is_refused(self) -> None: + element, iterator = _enter( + f'x' + ) + + with self.assertRaises(our_xmlcommon.DeserializationException) as context: + our_xmlrpc.read_value_content(element, iterator) + + self.assertEqual('nil', str(context.exception.path)) + self.assertIn("Expected no text in a element", context.exception.cause) + def test_a_discriminator_in_the_document_namespace_is_refused(self) -> None: # NOTE (mristin): # The XML-RPC elements reside in no namespace at all, so a discriminator @@ -248,6 +261,10 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: [0.0], '0.0' ), + ( + [None], + '' + ), ( {'k': 0.0}, ( @@ -272,6 +289,14 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: f"for the value {value!r}" ) + def test_a_null_is_written_as_an_empty_nil(self) -> None: + stream = io.StringIO() + writer = our_xmlcommon.Writer(stream) + + our_xmlrpc.write_discriminator(None, writer) + + self.assertEqual('', stream.getvalue()) + def test_the_path_of_a_failure_points_into_the_value(self) -> None: stream = io.StringIO() writer = our_xmlcommon.Writer(stream) diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonization.py index 9ff01ee4e..a6d0035dc 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, @@ -290,11 +292,10 @@ def _json_value_from_jsonable( Parse :paramref:`jsonable` as a JSON-able value. A JSON-able value is, recursively, exactly as JSON itself is defined: - a boolean, a number, a string, an array of JSON-able values or an object - of JSON-able values with string keys. A ``None`` is rejected at any - depth, as the JSON ``null`` has no representation as a JSON-able value, - and so are an infinity and a not-a-number, which JSON can not represent - at all. + a ``None`` (JSON null), a boolean, a number, a string, an array of + JSON-able values or an object of JSON-able values with string keys. + An infinity and a not-a-number are rejected at any depth, as JSON can not + represent them at all. The result is a new structure, and never :paramref:`jsonable` itself, so that the parsed instance does not alias the document it came from. @@ -303,6 +304,9 @@ def _json_value_from_jsonable( :return: parsed JSON-able value :raise: :py:class:`DeserializationException` if unexpected :paramref:`jsonable` """ + if jsonable is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -370,8 +374,8 @@ def _json_value_from_jsonable( return items raise DeserializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(jsonable)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(jsonable)}" ) @@ -431,6 +435,7 @@ def something_from_jsonable( the_mapping: Optional[our_types.JsonObject] = None the_values: Optional[our_types.JsonArray] = None the_optional_mapping: Optional[our_types.JsonObject] = None + the_optional_value: Optional[our_types.JsonValue] = None unexpected_key: Optional[str] = None @@ -442,6 +447,8 @@ def something_from_jsonable( the_values = _json_array_from_jsonable(jsonable_value) elif key == 'optionalMapping': the_optional_mapping = _json_object_from_jsonable(jsonable_value) + elif key == 'optionalValue': + the_optional_value = _json_value_from_jsonable(jsonable_value) else: unexpected_key = key break @@ -469,7 +476,8 @@ def something_from_jsonable( return our_types.Something( the_mapping, the_values, - the_optional_mapping + the_optional_mapping, + the_optional_value ) @@ -537,10 +545,9 @@ def _json_value_to_jsonable( Serialize :paramref:`value` as a JSON-able structure. A JSON-able value is almost JSON-able as it comes, but not quite: its type - rules out neither a ``None``, which the JSON ``null`` gives no - representation for, nor an infinity and a not-a-number, which JSON can not + rules out neither an infinity and a not-a-number, which JSON can not represent at all, nor a non-string key in an object. Each is refused here, - at any depth. + at any depth. A ``None`` is written out as a JSON null. The result is a new structure, and never :paramref:`value` itself, so that the serialized document does not alias the instance it came from. @@ -549,6 +556,9 @@ def _json_value_to_jsonable( :return: :paramref:`value`, as a JSON-able structure :raise: :py:class:`SerializationException` if :paramref:`value` is not JSON-able """ + if value is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -612,8 +622,8 @@ def _json_value_to_jsonable( return items raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) @@ -644,6 +654,14 @@ def _something_to_jsonable( except SerializationException as exception: exception._prepend_property('optional_mapping') raise + if that.optional_value is not None: + try: + jsonable['optionalValue'] = _json_value_to_jsonable( + that.optional_value + ) + except SerializationException as exception: + exception._prepend_property('optional_value') + raise return jsonable diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.py index cec368c6b..0f3360f10 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/jsonvalueverification.py @@ -2,8 +2,8 @@ Verify that a value is JSON-able. A JSON-able value is, recursively, exactly as JSON itself is defined: -a boolean, a finite number, a string, an array of JSON-able values or -an object of JSON-able values with string keys. Python offers no type which +a ``None`` (JSON null), a boolean, a finite number, a string, an array of +JSON-able values or an object of JSON-able values with string keys. Python offers no type which says exactly that, so a value has to be walked to make sure of it. This module depends on no class of the meta-model. It is invoked from @@ -49,6 +49,9 @@ def verify_json_value( :param value: to be verified :yield: errors, if any """ + if value is None: + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be taken for a number. @@ -114,8 +117,8 @@ def verify_json_value( return yield Error( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/types.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/types.py index 8d549a238..08d1ef249 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/types.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/types.py @@ -23,10 +23,15 @@ # NOTE (mristin): # A JSON-able value is, recursively, exactly as JSON itself is defined: -# a boolean, a number, a string, an array of JSON-able values or an object -# of JSON-able values with string keys -- never a ``None``, and never +# a ``None`` (JSON null), a boolean, a number, a string, an array of JSON-able +# values or an object of JSON-able values with string keys -- never # an infinity or a not-a-number, neither of which JSON can represent. # +# A ``None`` might appear at any depth, including the value itself. Since +# an absent optional property is also ``None``, an optional JSON-able property +# which is a JSON null is indistinguishable from an absent one, and is treated +# as absent. +# # JSON knows a single numeric type. Python tells an ``int`` from a ``float``, # and a caller will naturally write either, so both are admitted here -- but # an ``int`` only so long as a ``float`` can hold it exactly. The @@ -38,6 +43,7 @@ # (see https://github.com/python/mypy/issues/731), so the item and the value # types have to be spelled ``Any`` instead of ``JsonValue``. JsonValue = Union[ + None, bool, int, float, @@ -130,6 +136,8 @@ class Something(Class): optional_mapping: Optional[JsonObject] + optional_value: Optional[JsonValue] + def descend_once(self) -> Iterator[Class]: """ Iterate over the instances referenced from this instance. @@ -197,12 +205,14 @@ def __init__( self, mapping: JsonObject, values: JsonArray, - optional_mapping: Optional[JsonObject] = None + optional_mapping: Optional[JsonObject] = None, + optional_value: Optional[JsonValue] = None ) -> None: """Initialize with the given values.""" self.mapping = mapping self.values = values self.optional_mapping = optional_mapping + self.optional_value = optional_value class AbstractVisitor: diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/verification.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/verification.py index 93f66fa1e..04023b13b 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/verification.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/verification.py @@ -93,6 +93,35 @@ def transform_something( self, that: our_types.Something ) -> Iterator[Error]: + if not ( + not (that.optional_value is not None) + or is_acceptable(that.optional_value) + ): + yield Error( + 'The optional value must be acceptable' + ) + + if not ( + not (( + len(that.values) > 0 + and (that.values[0] is None) + )) + ): + yield Error( + 'The first value must not be a JSON null' + ) + + if not ( + not (that.optional_mapping is not None) + or ( + not ('type' in that.optional_mapping) + or (that.optional_mapping['type'] is not None) + ) + ): + yield Error( + 'The type of the optional mapping must not be a JSON null' + ) + if not ( not (len(that.values) >= 1) or is_acceptable(that.values[-1]) @@ -180,6 +209,18 @@ def transform_something( ) yield error + if that.optional_value is not None: + for error in our_json_value_verification.verify_json_value( + that.optional_value + ): + error.path._prepend( + PropertySegment( + that, + 'optional_value' + ) + ) + yield error + _TRANSFORMER = _Transformer() diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlcommon.py index 4dfc2fadd..944707e33 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlcommon.py @@ -764,6 +764,23 @@ def write_end_element_in_no_namespace( self._open_in_no_namespace -= 1 self.stream.write(f'') + def write_empty_element_in_no_namespace( + self, + name: str + ) -> None: + """ + Write the empty element with the tag name :paramref:`name`, which resides + in no namespace at all. + + If it is the outermost such element, it undeclares the default namespace. + + :param name: of the element tag. Expected to contain no XML special characters. + """ + if self._open_in_no_namespace == 0: + self.stream.write(f'<{name} xmlns=""/>') + else: + self.stream.write(f'<{name}/>') + def _write_first_empty_element_with_namespace( self, name: str diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlization.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlization.py index e75fe1c60..d5b96d549 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlization.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlization.py @@ -733,6 +733,24 @@ def _read_instance_from_iterparse( raise +def _read_json_value_content( + element: Element, + iterator: Iterator[Tuple[str, Element]] +) -> our_types.JsonValue: + """ + Read the content of :paramref:`element` as a JSON-able value. + + :param element: start element enclosing the discriminator + :param iterator: + Input stream of ``(event, element)`` coming from + :py:func:`xml.etree.ElementTree.iterparse` with the argument + ``events=["start", "end"]`` + :raise: :py:class:`DeserializationException` if unexpected input + :return: parsed JSON-able value + """ + return our_xmlrpc.read_value_content(element, iterator) + + def _read_json_array_body( element: Element, iterator: Iterator[Tuple[str, Element]] @@ -797,6 +815,7 @@ def _read_something_as_sequence( the_mapping: Optional[our_types.JsonObject] = values.get('mapping') the_values: Optional[our_types.JsonArray] = values.get('values') the_optional_mapping: Optional[our_types.JsonObject] = values.get('optionalMapping') + the_optional_value: Optional[our_types.JsonValue] = values.get('optionalValue') if the_mapping is None: raise DeserializationException( @@ -811,7 +830,8 @@ def _read_something_as_sequence( return our_types.Something( the_mapping, the_values, - the_optional_mapping + the_optional_mapping, + the_optional_value ) @@ -887,6 +907,7 @@ def _read_as_element( 'mapping': _read_json_object_body, 'values': _read_json_array_body, 'optionalMapping': _read_json_object_body, + 'optionalValue': _read_json_value_content, } @@ -979,6 +1000,32 @@ def _attribute_to_item( raise failure from exception +def _write_json_value_as_element( + name: str, + prop_name: Optional[str], + value: our_types.JsonValue, + serializer: '_Serializer' +) -> None: + """ + Write the :paramref:`value` of a JSON-able value enclosed in + the :paramref:`name` element. + + :param name: of the corresponding element tag + :param prop_name: + name of the property, as spelled in Python, whose value is written, or + ``None`` if the access to the value is recorded by an enclosing writer + :param value: to be serialized + :param serializer: to write to + :raise: :py:class:`SerializationException` if the value could not be written + """ + try: + serializer.writer.write_start_element(name) + our_xmlrpc.write_discriminator(value, serializer.writer) + serializer.writer.write_end_element(name) + except Exception as exception: + _attribute_to_property(exception, prop_name) + + def _write_json_array_as_element( name: str, prop_name: Optional[str], @@ -1062,6 +1109,10 @@ def _write_something_as_element( _write_json_object_as_element( 'optionalMapping', 'optional_mapping', that.optional_mapping, serializer ) + if that.optional_value is not None: + _write_json_value_as_element( + 'optionalValue', 'optional_value', that.optional_value, serializer + ) serializer.writer.write_end_element(name) except Exception as exception: _attribute_to_property(exception, prop_name) diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.py index 2de48b34f..489ee824e 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/xmlrpc.py @@ -3,8 +3,10 @@ JSON prescribes no XML representation of its own, so we borrow the one of XML-RPC for it: a value is written as a single discriminator element -- -````, ````, ````, ```` or ```` -- -which says what the value is. +````, ````, ````, ````, ```` or +```` -- which says what the value is. XML-RPC itself knows no null, +so we follow the widespread extension, and write a JSON null as an empty +````. This module knows nothing of the meta-model beyond the JSON-able types, and it drives no document of its own. The caller hands over the very stream of @@ -66,9 +68,9 @@ def read_value_content( """ Read the content of :paramref:`element` as a JSON-able value. - The content is a single discriminator element -- ````, - ````, ````, ```` or ```` -- which says what - the value is. + The content is a single discriminator element -- ````, + ````, ````, ````, ```` or ```` -- + which says what the value is. The end element corresponding to :paramref:`element` will be read as well. @@ -86,7 +88,7 @@ def read_value_content( discriminator = read_next_start_element( iterator, 'a discriminator ' - '(one of , , , or )' + '(one of , , , , or )' ) result = _read_discriminator(discriminator, iterator) @@ -117,7 +119,7 @@ def _read_discriminator( iterator: Iterator[Tuple[str, Element]] ) -> our_types.JsonValue: """ - Read :paramref:`element` as one of the five XML-RPC discriminators. + Read :paramref:`element` as one of the six XML-RPC discriminators. The end element corresponding to :paramref:`element` will be read as well. @@ -138,6 +140,19 @@ def _read_discriminator( # very element which does not belong here, so naming it in the path as well # as in the message would say nothing more. try: + if tag_wo_ns == 'nil': + read_end_element(element, iterator) + + # NOTE (mristin): + # The text of an element is only complete at its end, so we can check + # it only now. + if element.text is not None and len(element.text.strip()) != 0: + raise DeserializationException( + f"Expected no text in a element, but got: {element.text!r}" + ) + + return None + if tag_wo_ns == 'boolean': # NOTE (mristin): # Real XML-RPC tooling writes and expects a strict ``1``/``0``, and not @@ -204,7 +219,7 @@ def _read_discriminator( raise DeserializationException( f"Expected a discriminator element " - f"(one of , , , or ), " + f"(one of , , , , or ), " f"but got: {tag_wo_ns!r}" ) @@ -442,12 +457,16 @@ def write_discriminator( writer: Writer ) -> None: """ - Write :paramref:`value` as one of the five XML-RPC discriminators. + Write :paramref:`value` as one of the six XML-RPC discriminators. :param value: to be serialized :param writer: to write to :raise: :py:class:`SerializationException` if the value could not be written """ + if value is None: + writer.write_empty_element_in_no_namespace('nil') + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be written as a number. @@ -524,8 +543,8 @@ def write_discriminator( return raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/meta_model.py b/dev/test_data/main/python/expected/json_types_in_invariants/meta_model.py index b3b507d2d..a45501a44 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/meta_model.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/meta_model.py @@ -63,6 +63,29 @@ def is_acceptable(value: JSONValue) -> bool: or is_acceptable(self.values[-1]), "The last value must be acceptable", ) +@invariant( + lambda self: + not (self.optional_mapping is not None) + or ( + not ("type" in self.optional_mapping) + or self.optional_mapping["type"] is not None + ), + "The type of the optional mapping must not be a JSON null", +) +@invariant( + lambda self: + not ( + len(self.values) > 0 + and self.values[0] is None + ), + "The first value must not be a JSON null", +) +@invariant( + lambda self: + not (self.optional_value is not None) + or is_acceptable(self.optional_value), + "The optional value must be acceptable", +) class Something(DBC): #: Test the length of, the membership in and the indexing into a JSON-able #: object @@ -74,15 +97,21 @@ class Something(DBC): #: Test the membership in an optional JSON-able object optional_mapping: Optional[JSONObject[str]] + #: Test the nullness check over an optional JSON-able value, which is + #: treated as absent if it is a JSON null + optional_value: Optional[JSONValue] + def __init__( self, mapping: JSONObject[str], values: JSONArray, optional_mapping: Optional[JSONObject[str]] = None, + optional_value: Optional[JSONValue] = None, ) -> None: self.mapping = mapping self.values = values self.optional_mapping = optional_mapping + self.optional_value = optional_value __version__ = "dummy" diff --git a/dev/test_data/main/python/expected/list_of_classes/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/list_of_classes/expected_output/dev/tests/common_jsonization.py index 25c828049..2d5f0b17c 100644 --- a/dev/test_data/main/python/expected/list_of_classes/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/list_of_classes/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/jsonization.py index bcbb11421..81c6f2ce3 100644 --- a/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/jsonization.py index 42cdfd802..7e73d99a9 100644 --- a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/list_of_enums/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/list_of_enums/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/list_of_enums/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/list_of_enums/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/jsonization.py index c72e0dbee..e053eca30 100644 --- a/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/jsonization.py index 0b0ebfcd8..474c48164 100644 --- a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/jsonization.py @@ -222,6 +222,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -232,6 +233,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/method_references/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/method_references/expected_output/dev/tests/common_jsonization.py index 5c4f18fa9..07e661442 100644 --- a/dev/test_data/main/python/expected/method_references/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/method_references/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/method_references/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/method_references/expected_output/dummy/jsonization.py index 371836d71..323b3f845 100644 --- a/dev/test_data/main/python/expected/method_references/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/method_references/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dev/tests/common_jsonization.py index 49c627282..07b2b6a1a 100644 --- a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/jsonization.py index 75512cf5a..bc90869df 100644 --- a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/nested_collections/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/nested_collections/expected_output/dev/tests/common_jsonization.py index 17647a835..d3aff9b10 100644 --- a/dev/test_data/main/python/expected/nested_collections/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/nested_collections/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/jsonization.py index 1f137c9ea..198963a7c 100644 --- a/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/optional_arguments/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/optional_arguments/expected_output/dev/tests/common_jsonization.py index 98ad1e250..c84e412ec 100644 --- a/dev/test_data/main/python/expected/optional_arguments/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/optional_arguments/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/jsonization.py index 548de32dc..24f0b6f03 100644 --- a/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/primitive_types/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/primitive_types/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/primitive_types/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/primitive_types/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/jsonization.py index a0b09e88c..1f3d3ec0e 100644 --- a/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/jsonization.py @@ -222,6 +222,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -232,6 +233,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/jsonization.py index 1108367bd..40752f04d 100644 --- a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/sets/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/sets/expected_output/dev/tests/common_jsonization.py index 34aa816a7..8e9356007 100644 --- a/dev/test_data/main/python/expected/sets/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/sets/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/sets/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/sets/expected_output/dummy/jsonization.py index 656dafaf1..c13bd4b13 100644 --- a/dev/test_data/main/python/expected/sets/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/sets/expected_output/dummy/jsonization.py @@ -221,6 +221,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -231,6 +232,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/string_operations/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/string_operations/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/string_operations/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/string_operations/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/string_operations/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/string_operations/expected_output/dummy/jsonization.py index 3ef1616d4..ad5707085 100644 --- a/dev/test_data/main/python/expected/string_operations/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/string_operations/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/switch_statements/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/switch_statements/expected_output/dev/tests/common_jsonization.py index 582d6f8ca..99eda8fb0 100644 --- a/dev/test_data/main/python/expected/switch_statements/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/switch_statements/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/jsonization.py index c13195fbb..68af1383b 100644 --- a/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dev/tests/common_jsonization.py index d1eaa812d..88f28c00f 100644 --- a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/jsonization.py index ccecd4dcc..5dafde536 100644 --- a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/tuples/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/tuples/expected_output/dev/tests/common_jsonization.py index 17647a835..d3aff9b10 100644 --- a/dev/test_data/main/python/expected/tuples/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/tuples/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/tuples/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/tuples/expected_output/dummy/jsonization.py index 0c4a7f1cd..2f0711200 100644 --- a/dev/test_data/main/python/expected/tuples/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/tuples/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/unions/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/unions/expected_output/dev/tests/common_jsonization.py index ee7969b3d..2d45dc4ff 100644 --- a/dev/test_data/main/python/expected/unions/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/unions/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/unions/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/unions/expected_output/dummy/jsonization.py index 202fadaf4..3de24b7a7 100644 --- a/dev/test_data/main/python/expected/unions/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/unions/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/variance_of_containers/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/variance_of_containers/expected_output/dev/tests/common_jsonization.py index 30385a73f..9affa7818 100644 --- a/dev/test_data/main/python/expected/variance_of_containers/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/variance_of_containers/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/variance_of_containers/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/variance_of_containers/expected_output/dummy/jsonization.py index 4ffcec54b..f86121271 100644 --- a/dev/test_data/main/python/expected/variance_of_containers/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/variance_of_containers/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/python/expected/visibility/expected_output/dev/tests/common_jsonization.py b/dev/test_data/main/python/expected/visibility/expected_output/dev/tests/common_jsonization.py index 5c4f18fa9..07e661442 100644 --- a/dev/test_data/main/python/expected/visibility/expected_output/dev/tests/common_jsonization.py +++ b/dev/test_data/main/python/expected/visibility/expected_output/dev/tests/common_jsonization.py @@ -53,7 +53,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/test_data/main/python/expected/visibility/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/visibility/expected_output/dummy/jsonization.py index 170ee99f7..1947ecd5c 100644 --- a/dev/test_data/main/python/expected/visibility/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/visibility/expected_output/dummy/jsonization.py @@ -220,6 +220,7 @@ def __init__( # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. Jsonable = Union[ + None, bool, int, float, @@ -230,6 +231,7 @@ def __init__( MutableJsonable = Union[ + None, bool, int, float, diff --git a/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/jsonization.ts index 0ca27a30a..92cf5d76b 100644 --- a/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/aas_core_meta.v3/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/common.ts index 63a624502..561a6c2dd 100644 --- a/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/jsonization.ts index e4f078566..833071287 100644 --- a/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/annotated_assignments/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/common.ts index cafe67e68..2e04d4ae3 100644 --- a/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/jsonization.ts index 57108265f..e2afc3079 100644 --- a/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/arithmetic/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/jsonization.ts index 4fca7f86c..d05bc9243 100644 --- a/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/assert_statements/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/jsonization.ts index bbea90181..ee3f9111c 100644 --- a/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/assignment_targets/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/common.ts index d082db63c..250f4b1de 100644 --- a/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/jsonization.ts index bc76beacb..69a474b15 100644 --- a/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/collections_in_returns/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/comparisons/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/comparisons/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/comparisons/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/comparisons/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/comparisons/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/comparisons/expected_output/src/jsonization.ts index 26a7de14d..fce453fb9 100644 --- a/dev/test_data/main/typescript/expected/comparisons/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/comparisons/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/jsonization.ts index 78d52a0c3..4bc351e85 100644 --- a/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/conflict_with_utility_types/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/jsonization.ts index 86da09db8..cc576d099 100644 --- a/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/constrained_primitives/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/jsonization.ts index 69bc616b1..7e6ca61ac 100644 --- a/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/custom_serialization_names/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/jsonization.ts index add9b32d8..bf5908812 100644 --- a/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/deep_class_hierarchy/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/empty_class/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/empty_class/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/empty_class/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/empty_class/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/empty_class/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/empty_class/expected_output/src/jsonization.ts index 1e7f3871c..484b33d95 100644 --- a/dev/test_data/main/typescript/expected/empty_class/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/empty_class/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/enum/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/enum/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/enum/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/enum/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/enum/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/enum/expected_output/src/jsonization.ts index 4ddd86240..41e2bec91 100644 --- a/dev/test_data/main/typescript/expected/enum/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/enum/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/float_only/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/float_only/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/float_only/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/float_only/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/float_only/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/float_only/expected_output/src/jsonization.ts index af81105c5..f35152e46 100644 --- a/dev/test_data/main/typescript/expected/float_only/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/float_only/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/for_statements/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/for_statements/expected_output/src/common.ts index cafe67e68..2e04d4ae3 100644 --- a/dev/test_data/main/typescript/expected/for_statements/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/for_statements/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/for_statements/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/for_statements/expected_output/src/jsonization.ts index 3922ee8ac..9dc76ef19 100644 --- a/dev/test_data/main/typescript/expected/for_statements/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/for_statements/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/if_statements/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/if_statements/expected_output/src/common.ts index cafe67e68..2e04d4ae3 100644 --- a/dev/test_data/main/typescript/expected/if_statements/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/if_statements/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/if_statements/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/if_statements/expected_output/src/jsonization.ts index 8380db9bb..4e7a79db6 100644 --- a/dev/test_data/main/typescript/expected/if_statements/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/if_statements/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/jsonization.ts index 16ded221c..aa51f35d1 100644 --- a/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/implementation_specific/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/common.ts index 92572453b..4062f3795 100644 --- a/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/jsonization.ts index 688425b5e..3f50a7375 100644 --- a/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/int_length_interplay/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/common.ts index c7d87157c..40011a62e 100644 --- a/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/jsonization.ts index 9844db592..c7a47228b 100644 --- a/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/int_parsing/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/is_instance/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/is_instance/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/is_instance/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/is_instance/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/is_instance/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/is_instance/expected_output/src/jsonization.ts index 001d1bdac..bd55532fd 100644 --- a/dev/test_data/main/typescript/expected/is_instance/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/is_instance/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/src/jsonization.ts index 2e6501c44..b72c77aa4 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; @@ -542,7 +548,7 @@ function parsePropertiesOfSomething( OurTypes.Something, DeserializationError > { - let theValue: OurTypes.JsonValue | null = null; + let theValue: OurTypes.JsonValue | undefined = undefined; let theValues: OurTypes.JsonArray | null = null; let theMapping: OurTypes.JsonObject | null = null; let theMappingWithConstrainedKey: OurTypes.JsonObject | null = null; @@ -641,7 +647,7 @@ function parsePropertiesOfSomething( } } - if (theValue === null) { + if (theValue === undefined) { return newDeserializationError< OurTypes.Something >( @@ -791,7 +797,13 @@ export class SerializationError extends Error { function jsonValueFromJsonable( jsonable: JsonValue ): OurCommon.Either { - if (jsonable === null || jsonable === undefined) { + if (jsonable === null) { + return new OurCommon.Either( + null, null + ); + } + + if (jsonable === undefined) { return newDeserializationError( `Expected a JSON-able value, but got: ${jsonable}` ); @@ -824,7 +836,7 @@ function jsonValueFromJsonable( default: return newDeserializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof jsonable}` ); } @@ -938,7 +950,11 @@ function jsonValueToJsonable(that: OurTypes.JsonArray): Array; function jsonValueToJsonable(that: OurTypes.JsonObject): JsonObject; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue { - if (that === null || that === undefined) { + if (that === null) { + return null; + } + + if (that === undefined) { throw new SerializationError( `Expected a JSON-able value, but got: ${that}` ); @@ -966,7 +982,7 @@ function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue { default: throw new SerializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof that}` ); } diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/src/types.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/src/types.ts index d3b053d5b..68454411f 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/src/types.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/src/types.ts @@ -4,12 +4,18 @@ /** * Represent a value which JSON can carry. * - * This is, recursively, exactly as JSON itself is defined: a boolean, - * a number, a string, an array of JSON-able values or an object of JSON-able - * values with string keys -- never a `null`, and never a non-finite number, - * which JSON can not represent at all. + * This is, recursively, exactly as JSON itself is defined: a `null`, + * a boolean, a number, a string, an array of JSON-able values or an object of + * JSON-able values with string keys -- never a non-finite number, which JSON + * can not represent at all. + * + * A `null` might appear at any depth, including the value itself. Since + * an absent optional property is also `null`, an optional JSON-able property + * which is a JSON null is indistinguishable from an absent one, and is treated + * as absent. */ export type JsonValue = + | null | boolean | number | string diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/src/verification.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/src/verification.ts index 2d28ba3c3..2bd29ae2e 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/src/verification.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/src/verification.ts @@ -208,7 +208,11 @@ export class VerificationError { export function *verifyJsonValue( value: OurTypes.JsonValue ): IterableIterator { - if (value === null || value === undefined) { + if (value === null) { + return; + } + + if (value === undefined) { yield new VerificationError( `Expected a JSON-able value, but got: ${value}` ); @@ -237,7 +241,7 @@ export function *verifyJsonValue( default: yield new VerificationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof value}` ); return; diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlization.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlization.ts index 0630b08c0..a309936a6 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlization.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlization.ts @@ -219,11 +219,11 @@ function parse_bytes( function parseSomethingFromSequence( cursor: XmlCursor ): OurCommon.Either { - let theValue: OurTypes.JsonValue | null = null; + let theValue: OurTypes.JsonValue | undefined = undefined; let theValues: OurTypes.JsonArray | null = null; let theMapping: OurTypes.JsonObject | null = null; let theMappingWithConstrainedKey: OurTypes.JsonObject | null = null; - let theOptionalValue: OurTypes.JsonValue | null = null; + let theOptionalValue: OurTypes.JsonValue | null | undefined = undefined; let theOptionalValues: OurTypes.JsonArray | null = null; let theOptionalMapping: OurTypes.JsonObject | null = null; @@ -248,7 +248,7 @@ function parseSomethingFromSequence( let propertyError: DeserializationError | null = null; switch (propertyLocalName) { case "value": { - if (theValue !== null) { + if (theValue !== undefined) { propertyError = duplicatePropertyError(propertyLocalName); break; } @@ -312,7 +312,7 @@ function parseSomethingFromSequence( } case "optionalValue": { - if (theOptionalValue !== null) { + if (theOptionalValue !== undefined) { propertyError = duplicatePropertyError(propertyLocalName); break; } @@ -378,7 +378,7 @@ function parseSomethingFromSequence( cursor.skipIgnorable(); } - if (theValue === null) { + if (theValue === undefined) { return newDeserializationError( "The required property 'value' is missing" ); @@ -407,7 +407,7 @@ function parseSomethingFromSequence( theValues, theMapping, theMappingWithConstrainedKey, - theOptionalValue, + theOptionalValue ?? null, theOptionalValues, theOptionalMapping ); diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlrpc.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlrpc.ts index f9365e3dd..9bcaec43f 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlrpc.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/src/xmlrpc.ts @@ -5,7 +5,9 @@ * * JSON prescribes no XML representation of its own, so a JSON-able value is * written as the subset of XML-RPC which covers exactly what such a value can - * be: ``, ``, ``, `` and ``. + * be: ``, ``, ``, ``, `` and ``. + * XML-RPC itself knows no null, so we follow the widespread extension, and + * write a JSON null as an empty ``. * * These elements reside in no namespace at all, as the XML-RPC specification * prescribes, and not in the namespace of the enclosing document. The @@ -55,8 +57,9 @@ const DOUBLE_RE = new RegExp( * Parse the content of an element holding a JSON-able value. * * @remarks - * The content is a single discriminator element -- ``, ``, - * ``, `` or `` -- which says what the value is. + * The content is a single discriminator element -- ``, ``, + * ``, ``, `` or `` -- which says what + * the value is. * * @param cursor - to read from * @returns parsed JSON-able value, or an error @@ -80,8 +83,8 @@ export function parseValueContent( // it is that very element which does not belong here, so naming it in // the path as well as in the message would say nothing more. return newDeserializationError( - `Expected a discriminator element (one of 'boolean', 'double', ` + - `'string', 'array' or 'struct'), but got: '${localName}'` + `Expected a discriminator element (one of 'nil', 'boolean', ` + + `'double', 'string', 'array' or 'struct'), but got: '${localName}'` ); } @@ -109,6 +112,8 @@ function parserForDiscriminator( localName: string ): ContentParser | null { switch (localName) { + case "nil": + return parseNilContent; case "boolean": return parseBooleanContent; case "double": @@ -124,6 +129,31 @@ function parserForDiscriminator( } } +/** + * Parse the content of a `` element. + * + * @remarks + * XML-RPC itself knows no null, so we follow the widespread extension, and + * read a JSON null from an empty ``. + * + * @param cursor - to read from + * @returns JSON null, or an error + */ +function parseNilContent( + cursor: XmlCursor +): OurCommon.Either { + const text = parseTextContent(cursor); + if (text !== "") { + return newDeserializationError( + `Expected no text in a 'nil' element, but got: '${text}'` + ); + } + + return new OurCommon.Either( + null, null + ); +} + /** * Parse the content of a `` element. * @@ -434,7 +464,12 @@ function writeValue( ): void { const xmlns = undeclareNamespace ? ' xmlns=""' : ""; - if (value === null || value === undefined) { + if (value === null) { + parts.push(``); + return; + } + + if (value === undefined) { throw new SerializationError( `Expected a JSON-able value, but got: ${value}` ); @@ -476,7 +511,7 @@ function writeValue( default: throw new SerializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof value}` ); } diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/test/verification.jsonValue.spec.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/test/verification.jsonValue.spec.ts index 6d2c18736..47fd254b2 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/test/verification.jsonValue.spec.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/test/verification.jsonValue.spec.ts @@ -113,8 +113,22 @@ test("a value of an unexpected type is refused", () => { expect(pathsAndCauses[0][1]).toContain("but got: function"); }); -test("a null is refused at any depth", () => { - const value = { a: [null] } as unknown as OurTypes.JsonValue; +test("a null is accepted at any depth", () => { + const values: Array = [ + null, + [null], + { a: [null, { b: null }] } + ]; + + for (const value of values) { + expect( + pathsAndMessages(OurVerification.verifyJsonValue(value)) + ).toStrictEqual([]); + } +}); + +test("an undefined is refused at any depth", () => { + const value = { a: [undefined] } as unknown as OurTypes.JsonValue; const pathsAndCauses = pathsAndMessages( OurVerification.verifyJsonValue(value) @@ -122,7 +136,7 @@ test("a null is refused at any depth", () => { expect(pathsAndCauses.length).toStrictEqual(1); expect(pathsAndCauses[0][0]).toStrictEqual('["a"][0]'); - expect(pathsAndCauses[0][1]).toContain("but got: null"); + expect(pathsAndCauses[0][1]).toContain("but got: undefined"); }); test("a JSON-able array has to be an array", () => { diff --git a/dev/test_data/main/typescript/expected/json_types/expected_output/test/xmlrpc.spec.ts b/dev/test_data/main/typescript/expected/json_types/expected_output/test/xmlrpc.spec.ts index 36107d25b..a7e0d4f19 100644 --- a/dev/test_data/main/typescript/expected/json_types/expected_output/test/xmlrpc.spec.ts +++ b/dev/test_data/main/typescript/expected/json_types/expected_output/test/xmlrpc.spec.ts @@ -60,8 +60,10 @@ test("a value round-trips through every discriminator", () => { "b1" + "f1.5" + "sx" + + "z" + "a" + "0" + + "" + "" + "" ); @@ -73,10 +75,21 @@ test("a value round-trips through every discriminator", () => { b: true, f: 1.5, s: "x", - a: [0] + z: null, + a: [0, null] }); }); +test("a nil with text is refused", () => { + const cursor = enter(`x`); + + const parsed = OurXmlRpc.parseValueContent(cursor); + + const error = mustError(parsed); + expect(error.path.toString()).toStrictEqual("nil"); + expect(error.message).toContain("Expected no text in a 'nil' element"); +}); + test("the discriminator is a step of the path", () => { const cursor = enter( `yes` @@ -206,6 +219,8 @@ test("a value is written with its discriminator", () => { [1, '1'], [1.5, '1.5'], ["aa<b&c'], + [null, ''], + [[null], ''], [ [0], '0' diff --git a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/jsonization.ts index f0176095a..fab07e218 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; @@ -879,7 +885,13 @@ export class SerializationError extends Error { function jsonValueFromJsonable( jsonable: JsonValue ): OurCommon.Either { - if (jsonable === null || jsonable === undefined) { + if (jsonable === null) { + return new OurCommon.Either( + null, null + ); + } + + if (jsonable === undefined) { return newDeserializationError( `Expected a JSON-able value, but got: ${jsonable}` ); @@ -912,7 +924,7 @@ function jsonValueFromJsonable( default: return newDeserializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof jsonable}` ); } @@ -1026,7 +1038,11 @@ function jsonValueToJsonable(that: OurTypes.JsonArray): Array; function jsonValueToJsonable(that: OurTypes.JsonObject): JsonObject; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue { - if (that === null || that === undefined) { + if (that === null) { + return null; + } + + if (that === undefined) { throw new SerializationError( `Expected a JSON-able value, but got: ${that}` ); @@ -1054,7 +1070,7 @@ function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue { default: throw new SerializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof that}` ); } diff --git a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/types.ts b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/types.ts index e39e9f0cf..488868e54 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/types.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/types.ts @@ -4,12 +4,18 @@ /** * Represent a value which JSON can carry. * - * This is, recursively, exactly as JSON itself is defined: a boolean, - * a number, a string, an array of JSON-able values or an object of JSON-able - * values with string keys -- never a `null`, and never a non-finite number, - * which JSON can not represent at all. + * This is, recursively, exactly as JSON itself is defined: a `null`, + * a boolean, a number, a string, an array of JSON-able values or an object of + * JSON-able values with string keys -- never a non-finite number, which JSON + * can not represent at all. + * + * A `null` might appear at any depth, including the value itself. Since + * an absent optional property is also `null`, an optional JSON-able property + * which is a JSON null is indistinguishable from an absent one, and is treated + * as absent. */ export type JsonValue = + | null | boolean | number | string diff --git a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/verification.ts b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/verification.ts index dd5f7a02b..217294a17 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/verification.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/verification.ts @@ -208,7 +208,11 @@ export class VerificationError { export function *verifyJsonValue( value: OurTypes.JsonValue ): IterableIterator { - if (value === null || value === undefined) { + if (value === null) { + return; + } + + if (value === undefined) { yield new VerificationError( `Expected a JSON-able value, but got: ${value}` ); @@ -237,7 +241,7 @@ export function *verifyJsonValue( default: yield new VerificationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof value}` ); return; diff --git a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/xmlrpc.ts b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/xmlrpc.ts index f9365e3dd..9bcaec43f 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/xmlrpc.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/src/xmlrpc.ts @@ -5,7 +5,9 @@ * * JSON prescribes no XML representation of its own, so a JSON-able value is * written as the subset of XML-RPC which covers exactly what such a value can - * be: ``, ``, ``, `` and ``. + * be: ``, ``, ``, ``, `` and ``. + * XML-RPC itself knows no null, so we follow the widespread extension, and + * write a JSON null as an empty ``. * * These elements reside in no namespace at all, as the XML-RPC specification * prescribes, and not in the namespace of the enclosing document. The @@ -55,8 +57,9 @@ const DOUBLE_RE = new RegExp( * Parse the content of an element holding a JSON-able value. * * @remarks - * The content is a single discriminator element -- ``, ``, - * ``, `` or `` -- which says what the value is. + * The content is a single discriminator element -- ``, ``, + * ``, ``, `` or `` -- which says what + * the value is. * * @param cursor - to read from * @returns parsed JSON-able value, or an error @@ -80,8 +83,8 @@ export function parseValueContent( // it is that very element which does not belong here, so naming it in // the path as well as in the message would say nothing more. return newDeserializationError( - `Expected a discriminator element (one of 'boolean', 'double', ` + - `'string', 'array' or 'struct'), but got: '${localName}'` + `Expected a discriminator element (one of 'nil', 'boolean', ` + + `'double', 'string', 'array' or 'struct'), but got: '${localName}'` ); } @@ -109,6 +112,8 @@ function parserForDiscriminator( localName: string ): ContentParser | null { switch (localName) { + case "nil": + return parseNilContent; case "boolean": return parseBooleanContent; case "double": @@ -124,6 +129,31 @@ function parserForDiscriminator( } } +/** + * Parse the content of a `` element. + * + * @remarks + * XML-RPC itself knows no null, so we follow the widespread extension, and + * read a JSON null from an empty ``. + * + * @param cursor - to read from + * @returns JSON null, or an error + */ +function parseNilContent( + cursor: XmlCursor +): OurCommon.Either { + const text = parseTextContent(cursor); + if (text !== "") { + return newDeserializationError( + `Expected no text in a 'nil' element, but got: '${text}'` + ); + } + + return new OurCommon.Either( + null, null + ); +} + /** * Parse the content of a `` element. * @@ -434,7 +464,12 @@ function writeValue( ): void { const xmlns = undeclareNamespace ? ' xmlns=""' : ""; - if (value === null || value === undefined) { + if (value === null) { + parts.push(``); + return; + } + + if (value === undefined) { throw new SerializationError( `Expected a JSON-able value, but got: ${value}` ); @@ -476,7 +511,7 @@ function writeValue( default: throw new SerializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof value}` ); } diff --git a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/verification.jsonValue.spec.ts b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/verification.jsonValue.spec.ts index 6d2c18736..47fd254b2 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/verification.jsonValue.spec.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/verification.jsonValue.spec.ts @@ -113,8 +113,22 @@ test("a value of an unexpected type is refused", () => { expect(pathsAndCauses[0][1]).toContain("but got: function"); }); -test("a null is refused at any depth", () => { - const value = { a: [null] } as unknown as OurTypes.JsonValue; +test("a null is accepted at any depth", () => { + const values: Array = [ + null, + [null], + { a: [null, { b: null }] } + ]; + + for (const value of values) { + expect( + pathsAndMessages(OurVerification.verifyJsonValue(value)) + ).toStrictEqual([]); + } +}); + +test("an undefined is refused at any depth", () => { + const value = { a: [undefined] } as unknown as OurTypes.JsonValue; const pathsAndCauses = pathsAndMessages( OurVerification.verifyJsonValue(value) @@ -122,7 +136,7 @@ test("a null is refused at any depth", () => { expect(pathsAndCauses.length).toStrictEqual(1); expect(pathsAndCauses[0][0]).toStrictEqual('["a"][0]'); - expect(pathsAndCauses[0][1]).toContain("but got: null"); + expect(pathsAndCauses[0][1]).toContain("but got: undefined"); }); test("a JSON-able array has to be an array", () => { diff --git a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/xmlrpc.spec.ts b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/xmlrpc.spec.ts index 36107d25b..a7e0d4f19 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/xmlrpc.spec.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_collections/expected_output/test/xmlrpc.spec.ts @@ -60,8 +60,10 @@ test("a value round-trips through every discriminator", () => { "b1" + "f1.5" + "sx" + + "z" + "a" + "0" + + "" + "" + "" ); @@ -73,10 +75,21 @@ test("a value round-trips through every discriminator", () => { b: true, f: 1.5, s: "x", - a: [0] + z: null, + a: [0, null] }); }); +test("a nil with text is refused", () => { + const cursor = enter(`x`); + + const parsed = OurXmlRpc.parseValueContent(cursor); + + const error = mustError(parsed); + expect(error.path.toString()).toStrictEqual("nil"); + expect(error.message).toContain("Expected no text in a 'nil' element"); +}); + test("the discriminator is a step of the path", () => { const cursor = enter( `yes` @@ -206,6 +219,8 @@ test("a value is written with its discriminator", () => { [1, '1'], [1.5, '1.5'], ["aa<b&c'], + [null, ''], + [[null], ''], [ [0], '0' diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/jsonization.ts index c630df775..e18466f31 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; @@ -545,6 +551,7 @@ function parsePropertiesOfSomething( let theMapping: OurTypes.JsonObject | null = null; let theValues: OurTypes.JsonArray | null = null; let theOptionalMapping: OurTypes.JsonObject | null = null; + let theOptionalValue: OurTypes.JsonValue | null = null; for (const key in jsonObject) { const jsonableValue = jsonObject[key]; @@ -578,6 +585,15 @@ function parsePropertiesOfSomething( break; } + case "optionalValue": { + const parsed = jsonValueFromJsonable( + jsonableValue + ); + propertyError = parsed.error; + theOptionalValue = parsed.value; + break; + } + // NOTE (mristin): // Since we conflate here a JavaScript object with a JSON object, we ignore // properties which we do not know how to de-serialize and assume they are @@ -624,7 +640,8 @@ function parsePropertiesOfSomething( new OurTypes.Something( theMapping, theValues, - theOptionalMapping + theOptionalMapping, + theOptionalValue ), null ); @@ -731,7 +748,13 @@ export class SerializationError extends Error { function jsonValueFromJsonable( jsonable: JsonValue ): OurCommon.Either { - if (jsonable === null || jsonable === undefined) { + if (jsonable === null) { + return new OurCommon.Either( + null, null + ); + } + + if (jsonable === undefined) { return newDeserializationError( `Expected a JSON-able value, but got: ${jsonable}` ); @@ -764,7 +787,7 @@ function jsonValueFromJsonable( default: return newDeserializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof jsonable}` ); } @@ -878,7 +901,11 @@ function jsonValueToJsonable(that: OurTypes.JsonArray): Array; function jsonValueToJsonable(that: OurTypes.JsonObject): JsonObject; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue; function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue { - if (that === null || that === undefined) { + if (that === null) { + return null; + } + + if (that === undefined) { throw new SerializationError( `Expected a JSON-able value, but got: ${that}` ); @@ -906,7 +933,7 @@ function jsonValueToJsonable(that: OurTypes.JsonValue): JsonValue { default: throw new SerializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof that}` ); } @@ -968,6 +995,12 @@ function serializeSomething( jsonable["optionalMapping"] = jsonValueToJsonable(that.optionalMapping); } + + if (that.optionalValue !== null) { + prop = "optionalValue"; + jsonable["optionalValue"] = + jsonValueToJsonable(that.optionalValue); + } } catch (error) { if (error instanceof SerializationError) { error.prependProperty(prop); diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/types.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/types.ts index 52a13e085..84c058ba2 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/types.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/types.ts @@ -4,12 +4,18 @@ /** * Represent a value which JSON can carry. * - * This is, recursively, exactly as JSON itself is defined: a boolean, - * a number, a string, an array of JSON-able values or an object of JSON-able - * values with string keys -- never a `null`, and never a non-finite number, - * which JSON can not represent at all. + * This is, recursively, exactly as JSON itself is defined: a `null`, + * a boolean, a number, a string, an array of JSON-able values or an object of + * JSON-able values with string keys -- never a non-finite number, which JSON + * can not represent at all. + * + * A `null` might appear at any depth, including the value itself. Since + * an absent optional property is also `null`, an optional JSON-able property + * which is a JSON null is indistinguishable from an absent one, and is treated + * as absent. */ export type JsonValue = + | null | boolean | number | string @@ -128,6 +134,8 @@ export class Something extends Class { optionalMapping: JsonObject | null; + optionalValue: JsonValue | null; + /** * Iterate over the instances referenced from this instance. * @@ -203,12 +211,14 @@ export class Something extends Class { constructor( mapping: JsonObject, values: JsonArray, - optionalMapping: JsonObject | null = null + optionalMapping: JsonObject | null = null, + optionalValue: JsonValue | null = null ) { super(); this.mapping = mapping; this.values = values; this.optionalMapping = optionalMapping; + this.optionalValue = optionalValue; } } diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/verification.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/verification.ts index 75175883e..f4ef36855 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/verification.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/verification.ts @@ -233,7 +233,11 @@ export function isAcceptable( export function *verifyJsonValue( value: OurTypes.JsonValue ): IterableIterator { - if (value === null || value === undefined) { + if (value === null) { + return; + } + + if (value === undefined) { yield new VerificationError( `Expected a JSON-able value, but got: ${value}` ); @@ -262,7 +266,7 @@ export function *verifyJsonValue( default: yield new VerificationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof value}` ); return; @@ -340,6 +344,38 @@ class Verifier that: OurTypes.Something, context: boolean ): IterableIterator { + if (!( + !(that.optionalValue !== null) + || isAcceptable(that.optionalValue) + )) { + yield new VerificationError( + "The optional value must be acceptable" + ) + } + + if (!( + !(( + that.values.length > 0 + && (OurCommon.at(that.values, 0) === null) + )) + )) { + yield new VerificationError( + "The first value must not be a JSON null" + ) + } + + if (!( + !(that.optionalMapping !== null) + || ( + !(Object.prototype.hasOwnProperty.call(that.optionalMapping, "type")) + || (that.optionalMapping["type"] !== null) + ) + )) { + yield new VerificationError( + "The type of the optional mapping must not be a JSON null" + ) + } + if (!( !(that.values.length >= 1) || isAcceptable(OurCommon.at(that.values, -1)) @@ -435,6 +471,18 @@ class Verifier yield error; } } + + if (that.optionalValue !== null) { + for (const error of verifyJsonValue(that.optionalValue)) { + error.path.prepend( + new PropertySegment( + that, + "optionalValue" + ) + ); + yield error; + } + } } } } diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlization.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlization.ts index 6a5d6e6de..78cae5fc1 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlization.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlization.ts @@ -46,8 +46,10 @@ import { import { parseArrayBody as parse_jsonArray, parseStructBody as parse_jsonObject, + parseValueContent as parse_jsonValue, writeArrayBody as write_jsonArray, - writeStructBody as write_jsonObject + writeStructBody as write_jsonObject, + writeValueContent as write_jsonValue } from "./xmlrpc"; export { @@ -220,6 +222,7 @@ function parseSomethingFromSequence( let theMapping: OurTypes.JsonObject | null = null; let theValues: OurTypes.JsonArray | null = null; let theOptionalMapping: OurTypes.JsonObject | null = null; + let theOptionalValue: OurTypes.JsonValue | null | undefined = undefined; const className = OurTypes.Something.name; @@ -289,6 +292,22 @@ function parseSomethingFromSequence( break; } + case "optionalValue": { + if (theOptionalValue !== undefined) { + propertyError = duplicatePropertyError(propertyLocalName); + break; + } + + const parsed = parseElementContent( + cursor, + propertyLocalName, + parse_jsonValue + ); + propertyError = parsed.error; + theOptionalValue = parsed.value; + break; + } + default: { propertyError = new DeserializationError( `Unexpected XML property: ${propertyLocalName}` @@ -323,7 +342,8 @@ function parseSomethingFromSequence( const instance = new OurTypes.Something( theMapping, theValues, - theOptionalMapping + theOptionalMapping, + theOptionalValue ?? null ); return new OurCommon.Either( instance, @@ -359,6 +379,12 @@ function writeSomethingAsSequence( that.optionalMapping, write_jsonObject ); + writeOptionalProperty( + parts, + "optionalValue", + that.optionalValue, + write_jsonValue + ); } const ROOT_DISPATCH_BY_LOCAL_NAME = new Map< diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlrpc.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlrpc.ts index f9365e3dd..9bcaec43f 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlrpc.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/src/xmlrpc.ts @@ -5,7 +5,9 @@ * * JSON prescribes no XML representation of its own, so a JSON-able value is * written as the subset of XML-RPC which covers exactly what such a value can - * be: ``, ``, ``, `` and ``. + * be: ``, ``, ``, ``, `` and ``. + * XML-RPC itself knows no null, so we follow the widespread extension, and + * write a JSON null as an empty ``. * * These elements reside in no namespace at all, as the XML-RPC specification * prescribes, and not in the namespace of the enclosing document. The @@ -55,8 +57,9 @@ const DOUBLE_RE = new RegExp( * Parse the content of an element holding a JSON-able value. * * @remarks - * The content is a single discriminator element -- ``, ``, - * ``, `` or `` -- which says what the value is. + * The content is a single discriminator element -- ``, ``, + * ``, ``, `` or `` -- which says what + * the value is. * * @param cursor - to read from * @returns parsed JSON-able value, or an error @@ -80,8 +83,8 @@ export function parseValueContent( // it is that very element which does not belong here, so naming it in // the path as well as in the message would say nothing more. return newDeserializationError( - `Expected a discriminator element (one of 'boolean', 'double', ` + - `'string', 'array' or 'struct'), but got: '${localName}'` + `Expected a discriminator element (one of 'nil', 'boolean', ` + + `'double', 'string', 'array' or 'struct'), but got: '${localName}'` ); } @@ -109,6 +112,8 @@ function parserForDiscriminator( localName: string ): ContentParser | null { switch (localName) { + case "nil": + return parseNilContent; case "boolean": return parseBooleanContent; case "double": @@ -124,6 +129,31 @@ function parserForDiscriminator( } } +/** + * Parse the content of a `` element. + * + * @remarks + * XML-RPC itself knows no null, so we follow the widespread extension, and + * read a JSON null from an empty ``. + * + * @param cursor - to read from + * @returns JSON null, or an error + */ +function parseNilContent( + cursor: XmlCursor +): OurCommon.Either { + const text = parseTextContent(cursor); + if (text !== "") { + return newDeserializationError( + `Expected no text in a 'nil' element, but got: '${text}'` + ); + } + + return new OurCommon.Either( + null, null + ); +} + /** * Parse the content of a `` element. * @@ -434,7 +464,12 @@ function writeValue( ): void { const xmlns = undeclareNamespace ? ' xmlns=""' : ""; - if (value === null || value === undefined) { + if (value === null) { + parts.push(``); + return; + } + + if (value === undefined) { throw new SerializationError( `Expected a JSON-able value, but got: ${value}` ); @@ -476,7 +511,7 @@ function writeValue( default: throw new SerializationError( - `Expected a JSON-able value (a boolean, a number, a string, ` + + `Expected a JSON-able value (a null, a boolean, a number, a string, ` + `an array or an object), but got: ${typeof value}` ); } diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/verification.jsonValue.spec.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/verification.jsonValue.spec.ts index 6d2c18736..47fd254b2 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/verification.jsonValue.spec.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/verification.jsonValue.spec.ts @@ -113,8 +113,22 @@ test("a value of an unexpected type is refused", () => { expect(pathsAndCauses[0][1]).toContain("but got: function"); }); -test("a null is refused at any depth", () => { - const value = { a: [null] } as unknown as OurTypes.JsonValue; +test("a null is accepted at any depth", () => { + const values: Array = [ + null, + [null], + { a: [null, { b: null }] } + ]; + + for (const value of values) { + expect( + pathsAndMessages(OurVerification.verifyJsonValue(value)) + ).toStrictEqual([]); + } +}); + +test("an undefined is refused at any depth", () => { + const value = { a: [undefined] } as unknown as OurTypes.JsonValue; const pathsAndCauses = pathsAndMessages( OurVerification.verifyJsonValue(value) @@ -122,7 +136,7 @@ test("a null is refused at any depth", () => { expect(pathsAndCauses.length).toStrictEqual(1); expect(pathsAndCauses[0][0]).toStrictEqual('["a"][0]'); - expect(pathsAndCauses[0][1]).toContain("but got: null"); + expect(pathsAndCauses[0][1]).toContain("but got: undefined"); }); test("a JSON-able array has to be an array", () => { diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/xmlrpc.spec.ts b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/xmlrpc.spec.ts index 36107d25b..a7e0d4f19 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/xmlrpc.spec.ts +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/expected_output/test/xmlrpc.spec.ts @@ -60,8 +60,10 @@ test("a value round-trips through every discriminator", () => { "b1" + "f1.5" + "sx" + + "z" + "a" + "0" + + "" + "" + "" ); @@ -73,10 +75,21 @@ test("a value round-trips through every discriminator", () => { b: true, f: 1.5, s: "x", - a: [0] + z: null, + a: [0, null] }); }); +test("a nil with text is refused", () => { + const cursor = enter(`x`); + + const parsed = OurXmlRpc.parseValueContent(cursor); + + const error = mustError(parsed); + expect(error.path.toString()).toStrictEqual("nil"); + expect(error.message).toContain("Expected no text in a 'nil' element"); +}); + test("the discriminator is a step of the path", () => { const cursor = enter( `yes` @@ -206,6 +219,8 @@ test("a value is written with its discriminator", () => { [1, '1'], [1.5, '1.5'], ["aa<b&c'], + [null, ''], + [[null], ''], [ [0], '0' diff --git a/dev/test_data/main/typescript/expected/json_types_in_invariants/meta_model.py b/dev/test_data/main/typescript/expected/json_types_in_invariants/meta_model.py index b3b507d2d..a45501a44 100644 --- a/dev/test_data/main/typescript/expected/json_types_in_invariants/meta_model.py +++ b/dev/test_data/main/typescript/expected/json_types_in_invariants/meta_model.py @@ -63,6 +63,29 @@ def is_acceptable(value: JSONValue) -> bool: or is_acceptable(self.values[-1]), "The last value must be acceptable", ) +@invariant( + lambda self: + not (self.optional_mapping is not None) + or ( + not ("type" in self.optional_mapping) + or self.optional_mapping["type"] is not None + ), + "The type of the optional mapping must not be a JSON null", +) +@invariant( + lambda self: + not ( + len(self.values) > 0 + and self.values[0] is None + ), + "The first value must not be a JSON null", +) +@invariant( + lambda self: + not (self.optional_value is not None) + or is_acceptable(self.optional_value), + "The optional value must be acceptable", +) class Something(DBC): #: Test the length of, the membership in and the indexing into a JSON-able #: object @@ -74,15 +97,21 @@ class Something(DBC): #: Test the membership in an optional JSON-able object optional_mapping: Optional[JSONObject[str]] + #: Test the nullness check over an optional JSON-able value, which is + #: treated as absent if it is a JSON null + optional_value: Optional[JSONValue] + def __init__( self, mapping: JSONObject[str], values: JSONArray, optional_mapping: Optional[JSONObject[str]] = None, + optional_value: Optional[JSONValue] = None, ) -> None: self.mapping = mapping self.values = values self.optional_mapping = optional_mapping + self.optional_value = optional_value __version__ = "dummy" diff --git a/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/jsonization.ts index cc5334f34..69b2049a6 100644 --- a/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/list_of_classes/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/common.ts index 2c811e0b2..4d4e1e52b 100644 --- a/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/jsonization.ts index 11e76a70b..136b2f867 100644 --- a/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/list_of_constrained_primitives/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/jsonization.ts index 2ac246c22..bf9441252 100644 --- a/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/list_of_enums/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/jsonization.ts index 6eeeb5a47..61683afc0 100644 --- a/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/list_of_primitives/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/method_references/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/method_references/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/method_references/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/method_references/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/method_references/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/method_references/expected_output/src/jsonization.ts index 08550eca0..77265a9bb 100644 --- a/dev/test_data/main/typescript/expected/method_references/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/method_references/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/jsonization.ts index c2843006f..c2ea5d09b 100644 --- a/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/nested_any_in_all/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/common.ts index d40d09ff6..0456045ec 100644 --- a/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/jsonization.ts index 9b55f192d..831a60fde 100644 --- a/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/nested_collections/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/jsonization.ts index b7cafe292..0baf1f20d 100644 --- a/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/optional_arguments/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/jsonization.ts index 86da09db8..cc576d099 100644 --- a/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/primitive_types/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/jsonization.ts index 7cd0e556a..8c54b0789 100644 --- a/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/problematic_keywords/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/sets/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/sets/expected_output/src/common.ts index 61f9ea879..37b2ddc15 100644 --- a/dev/test_data/main/typescript/expected/sets/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/sets/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/sets/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/sets/expected_output/src/jsonization.ts index f0b5bfa24..177c51d2b 100644 --- a/dev/test_data/main/typescript/expected/sets/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/sets/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/string_operations/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/string_operations/expected_output/src/common.ts index 837f39ba1..91c972806 100644 --- a/dev/test_data/main/typescript/expected/string_operations/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/string_operations/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/string_operations/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/string_operations/expected_output/src/jsonization.ts index e5deb0271..dbe06527d 100644 --- a/dev/test_data/main/typescript/expected/string_operations/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/string_operations/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/jsonization.ts index 826f4353d..039279a2c 100644 --- a/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/switch_statements/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/common.ts index 2be9b5039..beb459d10 100644 --- a/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/jsonization.ts index 849232336..981c61df2 100644 --- a/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/transpiled_methods/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/tuples/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/tuples/expected_output/src/common.ts index 2ddf59dc5..1ad845117 100644 --- a/dev/test_data/main/typescript/expected/tuples/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/tuples/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/tuples/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/tuples/expected_output/src/jsonization.ts index fb0402683..37238041e 100644 --- a/dev/test_data/main/typescript/expected/tuples/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/tuples/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/unions/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/unions/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/unions/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/unions/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/unions/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/unions/expected_output/src/jsonization.ts index b293331eb..aa9bc901c 100644 --- a/dev/test_data/main/typescript/expected/unions/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/unions/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/jsonization.ts index 9329b5ee1..581408f5d 100644 --- a/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/variance_of_containers/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/typescript/expected/visibility/expected_output/src/common.ts b/dev/test_data/main/typescript/expected/visibility/expected_output/src/common.ts index 4a665a7c2..11a40830a 100644 --- a/dev/test_data/main/typescript/expected/visibility/expected_output/src/common.ts +++ b/dev/test_data/main/typescript/expected/visibility/expected_output/src/common.ts @@ -149,6 +149,9 @@ export function *map( /** * Represent either a result, or an error. * + * Only the error tells the two apart: the result itself might be `null`, + * as is, for example, a JSON null. + * * @typeParam ValueT - type of the resulting value * @typeParam ErrorT - type of the error */ @@ -164,22 +167,18 @@ export class Either { readonly error: ErrorT | null; /** - * Assert that value is set and return it. + * Assert that there is no error and return the value. * - * @returns {@link value}, or throw if `null` + * @returns {@link value}, or throw if there is an error */ mustValue(): ValueT { - if (this.value === null) { - throw new Error("Expected value to be set, but it was null"); + if (this.error !== null) { + throw new Error("Expected no error, but got one"); } - return this.value; + return this.value as ValueT; } constructor(value: ValueT | null, error: ErrorT | null) { - if (value === null && error === null) { - throw new Error("Unexpected both value and error null in an Either"); - } - if (value !== null && error !== null) { throw new Error("Unexpected both value and error non-null in an Either"); } diff --git a/dev/test_data/main/typescript/expected/visibility/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/visibility/expected_output/src/jsonization.ts index a968e495d..c2d22cd7d 100644 --- a/dev/test_data/main/typescript/expected/visibility/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/visibility/expected_output/src/jsonization.ts @@ -13,7 +13,13 @@ import * as OurCommon from "./common"; import * as OurTypes from "./types"; import * as OurStringification from "./stringification"; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; +export type JsonValue = + | null + | string + | number + | boolean + | JsonObject + | JsonArray; export type JsonArray = Iterable; export type JsonObject = { [prop: string]: JsonValue }; diff --git a/dev/test_data/main/xsd/expected/json_types/examples/Expected/something/nulls.xml b/dev/test_data/main/xsd/expected/json_types/examples/Expected/something/nulls.xml new file mode 100644 index 000000000..33e9bd791 --- /dev/null +++ b/dev/test_data/main/xsd/expected/json_types/examples/Expected/something/nulls.xml @@ -0,0 +1 @@ +1.5abcnonEmptyKey \ No newline at end of file diff --git a/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilNotAValidJsonValue/something/nil_as_bare_value.xml b/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilNotAValidJsonValue/something/nil_as_bare_value.xml deleted file mode 100644 index 8fe69ed81..000000000 --- a/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilNotAValidJsonValue/something/nil_as_bare_value.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - a - 1 - - - diff --git a/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilNotAValidJsonValue/something/nil_nested_in_array.xml b/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilNotAValidJsonValue/something/nil_nested_in_array.xml deleted file mode 100644 index 38fcdd289..000000000 --- a/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilNotAValidJsonValue/something/nil_nested_in_array.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - 1 - - - 1 - - 3 - - - - - - a - 1 - - - diff --git a/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilWithContent/something/nil_with_content.xml b/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilWithContent/something/nil_with_content.xml new file mode 100644 index 000000000..1d2d104a0 --- /dev/null +++ b/dev/test_data/main/xsd/expected/json_types/examples/Unexpected/Unserializable/NilWithContent/something/nil_with_content.xml @@ -0,0 +1 @@ +unexpected1.5a1.5nonEmptyKeysome value \ No newline at end of file diff --git a/dev/test_data/main/xsd/expected/json_types/expected_output/schema.xsd b/dev/test_data/main/xsd/expected/json_types/expected_output/schema.xsd index 2069bb731..cccb8ebd3 100644 --- a/dev/test_data/main/xsd/expected/json_types/expected_output/schema.xsd +++ b/dev/test_data/main/xsd/expected/json_types/expected_output/schema.xsd @@ -55,6 +55,9 @@ + + + diff --git a/dev/tests/intermediate/test_type_inference.py b/dev/tests/intermediate/test_type_inference.py index 5a094d5c4..d50121048 100644 --- a/dev/tests/intermediate/test_type_inference.py +++ b/dev/tests/intermediate/test_type_inference.py @@ -559,11 +559,41 @@ def __init__(self, something: str) -> None: self.expect_type_inference_to_fail( source=source, expected_joined_message=( - "Expected the value to be of an optional type " + "Expected the value to be of an optional type, or a JSONValue, " "for a nullness check (``is None``), but got str" ), ) + def test_is_none_on_unguarded_json_index_fails(self) -> None: + source = """\ +@invariant( + lambda self: + self.values[0] is None, + "Dummy invariant description" +) +class Some_class: + values: JSONArray + + def __init__(self, values: JSONArray) -> None: + self.values = values + +__version__ = "dummy" +__xml_namespace__ = "https://dummy.com" +""" + + self.expect_type_inference_to_fail( + source=source, + expected_joined_message=( + "An index into a JSON-able object or array might be missing, " + "or hold a JSON null, so ``is None`` over an unguarded index " + "conflates the two. Please first ask whether the key or " + "the position is there, as `` in `` and as " + "``len() > ``, which strips the optionality " + "of the index in the guarded expression. Then ``is None`` over " + "the guarded index asks whether it is a JSON null." + ), + ) + def test_is_not_none_fails_on_non_optional(self) -> None: source = """\ @invariant( @@ -584,7 +614,7 @@ def __init__(self, something: str) -> None: self.expect_type_inference_to_fail( source=source, expected_joined_message=( - "Expected the value to be of an optional type " + "Expected the value to be of an optional type, or a JSONValue, " "for a non-nullness check (``is not None``), but got str" ), )