diff --git a/aas_core_codegen/cpp/lib/_generate_jsonization.py b/aas_core_codegen/cpp/lib/_generate_jsonization.py index c699fb845..dbbcfd882 100644 --- a/aas_core_codegen/cpp/lib/_generate_jsonization.py +++ b/aas_core_codegen/cpp/lib/_generate_jsonization.py @@ -205,6 +205,29 @@ class ISegment {{ ), Stripped( f"""\ +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment {{ +{I}/** +{I} * Key of the member in a JSON object +{I} */ +{I}std::wstring key; + +{I}explicit KeySegment( +{II}std::wstring a_key +{I}); + +{I}std::wstring ToWstring() const override; + +{I}std::unique_ptr Clone() const override; + +{I}~KeySegment() override = default; +}}; // struct KeySegment""" + ), + Stripped( + f"""\ /** * Represent a JSON path to some value. */ @@ -306,6 +329,101 @@ class SerializationException : public std::exception {{ return writer.getvalue() +def _generate_json_path_helpers() -> List[Stripped]: + """Generate the functions shared by the segments of a JSON path.""" + return [ + Stripped("// region JSON path helpers"), + Stripped( + f"""\ +/** + * \\brief Escape \\p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( +{I}const std::wstring& text +) {{ +{I}static const wchar_t* const kHexDigits = L"0123456789abcdef"; + +{I}std::wstring escaped; +{I}escaped.reserve(text.size()); +{I}for (const wchar_t character : text) {{ +{II}switch (character) {{ +{III}case L'\\\\': +{IIII}escaped.append(L"\\\\\\\\"); +{IIII}break; +{III}case L'"': +{IIII}escaped.append(L"\\\\\\""); +{IIII}break; +{III}case L'\\b': +{IIII}escaped.append(L"\\\\b"); +{IIII}break; +{III}case L'\\f': +{IIII}escaped.append(L"\\\\f"); +{IIII}break; +{III}case L'\\n': +{IIII}escaped.append(L"\\\\n"); +{IIII}break; +{III}case L'\\r': +{IIII}escaped.append(L"\\\\r"); +{IIII}break; +{III}case L'\\t': +{IIII}escaped.append(L"\\\\t"); +{IIII}break; +{III}default: +{IIII}if (static_cast(character) < 0x20) {{ +{IIIII}escaped.append(L"\\\\u00"); +{IIIII}escaped.push_back(kHexDigits[(character >> 4) & 0xF]); +{IIIII}escaped.push_back(kHexDigits[character & 0xF]); +{IIII}}} else {{ +{IIIII}escaped.push_back(character); +{IIII}}} +{IIII}break; +{II}}} +{I}}} + +{I}return escaped; +}}""" + ), + Stripped( + f"""\ +/** + * \\brief Check whether a JSON path writes the property \\p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( +{I}const std::wstring& name +) {{ +{I}if (name.empty()) {{ +{II}return false; +{I}}} + +{I}for (size_t i = 0; i < name.size(); ++i) {{ +{II}const wchar_t character = name[i]; + +{II}const bool is_letter_or_underscore = ( +{III}(character >= L'a' && character <= L'z') +{III}|| (character >= L'A' && character <= L'Z') +{III}|| character == L'_' +{II}); + +{II}const bool is_digit = character >= L'0' && character <= L'9'; + +{II}if (!(is_letter_or_underscore || (i > 0 && is_digit))) {{ +{III}return false; +{II}}} +{I}}} + +{I}return true; +}}""" + ), + Stripped("// endregion JSON path helpers"), + ] + + def _generate_property_segment_implementation() -> List[Stripped]: """Generate the implementation of the struct ``PropertySegment``.""" return [ @@ -322,9 +440,17 @@ def _generate_property_segment_implementation() -> List[Stripped]: Stripped( f"""\ std::wstring PropertySegment::ToWstring() const {{ +{I}if (IsVariableName(name)) {{ +{II}return common::Concat( +{III}L".", +{III}name +{II}); +{I}}} + {I}return common::Concat( -{II}L".", -{II}name +{II}L"[\\"", +{II}EscapeForJsonString(name), +{II}L"\\"]" {I}); }}""" ), @@ -371,6 +497,42 @@ def _generate_index_segment_implementation() -> List[Stripped]: ] +def _generate_key_segment_implementation() -> List[Stripped]: + """Generate the implementation of the struct ``KeySegment``.""" + return [ + Stripped("// region KeySegment"), + Stripped( + f"""\ +KeySegment::KeySegment( +{I}std::wstring a_key +) : +{I}key(std::move(a_key)) {{ +{I}// Intentionally empty. +}}""" + ), + Stripped( + f"""\ +std::wstring KeySegment::ToWstring() const {{ +{I}// NOTE (mristin): +{I}// A key can be any string at all, so we always write it as a JSON string in +{I}// brackets, as in the other SDKs. +{I}return common::Concat( +{II}L"[\\"", +{II}EscapeForJsonString(key), +{II}L"\\"]" +{I}); +}}""" + ), + Stripped( + f"""\ +std::unique_ptr KeySegment::Clone() const {{ +{I}return common::make_unique(*this); +}}""" + ), + Stripped("// endregion KeySegment"), + ] + + def _generate_path_implementation() -> List[Stripped]: """Generate the implementation of the struct ``Path``.""" return [ @@ -426,6 +588,16 @@ def _generate_path_implementation() -> List[Stripped]: {II}parts.emplace_back(segment->ToWstring()); {I}}} +{I}// NOTE (mristin): +{I}// The path starts with the name of the first property without a dot, as in +{I}// the other SDKs, *e.g.*, ``items[0].name``. +{I}const PropertySegment* first_property( +{II}dynamic_cast(segments.front().get()) +{I}); +{I}if (first_property != nullptr && IsVariableName(first_property->name)) {{ +{II}parts.front() = first_property->name; +{I}}} + {I}size_t out_len = 0; {I}for (const std::wstring& part : parts) {{ {II}out_len += part.size(); @@ -903,7 +1075,7 @@ def _generate_deserialize_json_value() -> Stripped: {III}if (error.has_value()) {{ {IIII}error->path.segments.emplace_front( -{IIIII}common::make_unique( +{IIIII}common::make_unique( {IIIIII}common::Utf8ToWstring(item.key()) {IIIII}) {IIII}); @@ -5596,8 +5768,10 @@ def generate_implementation( #pragma warning(pop)""" ), cpp_common.generate_namespace_opening(namespace), + *_generate_json_path_helpers(), *_generate_property_segment_implementation(), *_generate_index_segment_implementation(), + *_generate_key_segment_implementation(), *_generate_path_implementation(), Stripped("// region De-serialization"), *_generate_deserialization_error_implementation(), diff --git a/aas_core_codegen/csharp/lib/_generate_jsonization.py b/aas_core_codegen/csharp/lib/_generate_jsonization.py index be2ae8d61..aaf90180e 100644 --- a/aas_core_codegen/csharp/lib/_generate_jsonization.py +++ b/aas_core_codegen/csharp/lib/_generate_jsonization.py @@ -336,7 +336,17 @@ def _generate_model_type_from_helper() -> Stripped: {II}return default!; {I}}} -{I}return StringFrom(modelTypeNode, out error); +{I}string modelType = StringFrom(modelTypeNode, out error); +{I}if (error != null) +{I}{{ +{II}// NOTE (mristin): +{II}// A model type of a wrong type is reported at the property, as the type +{II}// errors of all the other properties are, and as in the other SDKs. +{II}error.PrependSegment( +{III}new Reporting.NameSegment("modelType")); +{II}return default!; +{I}}} +{I}return modelType; }}""" ) @@ -683,6 +693,8 @@ def _generate_from_method_for_named_union( {I}string modelType = StringFrom(modelTypeNode, out error); {I}if (error != null) {I}{{ +{II}error.PrependSegment( +{III}new Reporting.NameSegment("modelType")); {II}return default!; {I}}} @@ -1033,9 +1045,14 @@ def _generate_from_method_for_class( {II}keyValue.Value, out error); {I}if (error == null && modelType != "{model_type}") {I}{{ +{II}// NOTE (mristin): +{II}// A wrong model type is reported at the instance, as an unknown one is, +{II}// and as in the other SDKs, so we return before the property is +{II}// prepended to the path. {II}error = new Reporting.Error( {III}"Expected the model type '{model_type}', " + {III}$"but got {{modelType}}"); +{II}return default!; {I}}} {I}break;""" ) diff --git a/aas_core_codegen/csharp/lib/_generate_reporting.py b/aas_core_codegen/csharp/lib/_generate_reporting.py index 613f74108..a528c2088 100644 --- a/aas_core_codegen/csharp/lib/_generate_reporting.py +++ b/aas_core_codegen/csharp/lib/_generate_reporting.py @@ -96,16 +96,48 @@ def generate(namespace: csharp_common.NamespaceIdentifier) -> str: private static string EscapeForJsonString( {I}string text) {{ -{I}return ( -{II}text -{III}.Replace("\\\\", "\\\\\\\\") -{III}.Replace("\\"", "\\\\\\"") -{III}.Replace("\\b", "\\\\b") -{III}.Replace("\\f", "\\\\f") -{III}.Replace("\\n", "\\\\n") -{III}.Replace("\\r", "\\\\r") -{III}.Replace("\\t", "\\\\t") -{I}); +{I}var builder = new System.Text.StringBuilder(text.Length); +{I}foreach (char character in text) +{I}{{ +{II}switch (character) +{II}{{ +{III}case '\\\\': +{IIII}builder.Append("\\\\\\\\"); +{IIII}break; +{III}case '"': +{IIII}builder.Append("\\\\\\""); +{IIII}break; +{III}case '\\b': +{IIII}builder.Append("\\\\b"); +{IIII}break; +{III}case '\\f': +{IIII}builder.Append("\\\\f"); +{IIII}break; +{III}case '\\n': +{IIII}builder.Append("\\\\n"); +{IIII}break; +{III}case '\\r': +{IIII}builder.Append("\\\\r"); +{IIII}break; +{III}case '\\t': +{IIII}builder.Append("\\\\t"); +{IIII}break; +{III}default: +{IIII}// NOTE (mristin): +{IIII}// The other control characters can not stand in a JSON string as they +{IIII}// are, so we write them as Unicode escapes, as JSON.stringify does. +{IIII}if (character < 0x20) +{IIII}{{ +{IIIII}builder.Append($"\\\\u{{(int)character:x4}}"); +{IIII}}} +{IIII}else +{IIII}{{ +{IIIII}builder.Append(character); +{IIII}}} +{IIII}break; +{II}}} +{I}}} +{I}return builder.ToString(); }}""" ), Stripped( diff --git a/aas_core_codegen/golang/lib/_generate_jsonization.py b/aas_core_codegen/golang/lib/_generate_jsonization.py index d688eb7fe..e1424d7b0 100644 --- a/aas_core_codegen/golang/lib/_generate_jsonization.py +++ b/aas_core_codegen/golang/lib/_generate_jsonization.py @@ -345,13 +345,16 @@ def _generate_check_model_type() -> Stripped: {I}}} {I}if modelType != expected {{ +{II}// NOTE (mristin): +{II}// A wrong model type is reported at the instance, as an unknown one is, +{II}// and as in the other SDKs. {II}err = newDeserializationError( {III}fmt.Sprintf( {IIII}"Expected the model type '%s', but got %s", {IIII}expected, {IIII}modelType, {III}), -{II}).prependName("modelType") +{II}) {I}}} {I}return }}""" diff --git a/aas_core_codegen/golang/lib/_generate_reporting.py b/aas_core_codegen/golang/lib/_generate_reporting.py index 2f9060f24..3c4c86683 100644 --- a/aas_core_codegen/golang/lib/_generate_reporting.py +++ b/aas_core_codegen/golang/lib/_generate_reporting.py @@ -37,6 +37,7 @@ def generate() -> str: f"""\ import ( {I}"fmt" +{I}"regexp" {I}"strings" {I}"strconv" )""" @@ -127,24 +128,34 @@ def generate() -> str: {II}callback(p.segments[i]) {I}}} }}""" + ), + Stripped( + """\ +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`)""" ), Stripped( f"""\ -// Translate the path to a JSON path. +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string {{ +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string {{ {I}var b strings.Builder {I}i := 0 {I}p.OverSegments(func(s interface{{}}) {{ {II}switch v := s.(type) {{ {II}case *NameSegment: -{III}if i == 0 {{ -{III}{I}b.WriteString(v.Name) +{III}if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) {{ +{IIII}if i > 0 {{ +{IIIII}b.WriteString(".") +{IIII}}} +{IIII}b.WriteString(v.Name) {III}}} else {{ -{III}{I}b.WriteString(".") -{III}{I}b.WriteString(v.Name) +{IIII}b.WriteString(`["`) +{IIII}b.WriteString(escapeForJSONString(v.Name)) +{IIII}b.WriteString(`"]`) {III}}} {II}case *IndexSegment: {III}b.WriteString("[") @@ -166,6 +177,16 @@ def generate() -> str: {II}i++ {I}}}) {I}return b.String() +}}""" + ), + Stripped( + f"""\ +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string {{ +{I}return renderPath(p, true) }}""" ), Stripped( @@ -176,32 +197,47 @@ def generate() -> str: // in Golang, not JSON property names. func ToGolangPath(p *Path) string {{ {I}// NOTE(mristin): -{I}// We re-use JSON path formatting as implementation, but introduce -{I}// a separate function to signal to the reader in which form -{I}// the name segments are expected (Golang property names instead -{I}// of JSON property names). -{I}return ToJSONPath(p) +{I}// The name segments of a Golang path are accessors such as `Value()`, +{I}// which we always write after a dot. +{I}return renderPath(p, false) }}""" ), Stripped( f"""\ -var replacerForJSONString = strings.NewReplacer( -{I}"\\\\", "\\\\\\\\", -{I}"\\"", "\\\\\\"", -{I}"\\b", "\\\\b", -{I}"\\f", "\\\\f", -{I}"\\n", "\\\\n", -{I}"\\r", "\\\\r", -{I}"\\t", "\\\\t", -)""" - ), - Stripped( - f"""\ // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( {I}text string, ) string {{ -{I}return replacerForJSONString.Replace(text) +{I}var b strings.Builder +{I}b.Grow(len(text)) +{I}for _, r := range text {{ +{II}switch r {{ +{II}case '\\\\': +{III}b.WriteString(`\\\\`) +{II}case '"': +{III}b.WriteString(`\\"`) +{II}case '\\b': +{III}b.WriteString(`\\b`) +{II}case '\\f': +{III}b.WriteString(`\\f`) +{II}case '\\n': +{III}b.WriteString(`\\n`) +{II}case '\\r': +{III}b.WriteString(`\\r`) +{II}case '\\t': +{III}b.WriteString(`\\t`) +{II}default: +{III}if r < 0x20 {{ +{IIII}fmt.Fprintf(&b, `\\u%04x`, r) +{III}}} else {{ +{IIII}b.WriteRune(r) +{III}}} +{II}}} +{I}}} +{I}return b.String() }}""" ), Stripped( diff --git a/aas_core_codegen/java/lib/_generate_jsonization.py b/aas_core_codegen/java/lib/_generate_jsonization.py index 73ae11e42..f3e2bf72d 100644 --- a/aas_core_codegen/java/lib/_generate_jsonization.py +++ b/aas_core_codegen/java/lib/_generate_jsonization.py @@ -364,7 +364,9 @@ def _generate_check_model_type() -> Stripped: {II}final Reporting.Error error = new Reporting.Error( {III}"Expected the model type '" + expected + "', " + {III}"but got '" + modelType + "'"); -{II}error.prependSegment(new Reporting.NameSegment("modelType")); +{II}// NOTE (mristin): +{II}// A wrong model type is reported at the instance, as an unknown one is, +{II}// and as in the other SDKs. {II}return error; {I}}} diff --git a/aas_core_codegen/java/lib/_generate_reporting.py b/aas_core_codegen/java/lib/_generate_reporting.py index 8b1538a94..6a6f6d704 100644 --- a/aas_core_codegen/java/lib/_generate_reporting.py +++ b/aas_core_codegen/java/lib/_generate_reporting.py @@ -15,6 +15,7 @@ INDENT2 as II, INDENT3 as III, INDENT4 as IIII, + INDENT5 as IIIII, ) @@ -132,18 +133,45 @@ def generate(package: java_common.PackageIdentifier) -> List[java_common.JavaFil /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {{@code JSON.stringify}} does. */ private static String escapeForJsonString(String text) {{ -{I}return text -{II}.replace("\\\\", "\\\\\\\\") -{II}.replace("\\"", "\\\\\\"") -{II}.replace("\\b", "\\\\b") -{II}.replace("\\f", "\\\\f") -{II}.replace("\\n", "\\\\n") -{II}.replace("\\r", "\\\\r") -{II}.replace("\\t", "\\\\t"); +{I}StringBuilder builder = new StringBuilder(text.length()); +{I}for (int i = 0; i < text.length(); i++) {{ +{II}char character = text.charAt(i); +{II}switch (character) {{ +{III}case '\\\\': +{IIII}builder.append("\\\\\\\\"); +{IIII}break; +{III}case '"': +{IIII}builder.append("\\\\\\""); +{IIII}break; +{III}case '\\b': +{IIII}builder.append("\\\\b"); +{IIII}break; +{III}case '\\f': +{IIII}builder.append("\\\\f"); +{IIII}break; +{III}case '\\n': +{IIII}builder.append("\\\\n"); +{IIII}break; +{III}case '\\r': +{IIII}builder.append("\\\\r"); +{IIII}break; +{III}case '\\t': +{IIII}builder.append("\\\\t"); +{IIII}break; +{III}default: +{IIII}if (character < 0x20) {{ +{IIIII}builder.append(String.format("\\\\u%04x", (int) character)); +{IIII}}} else {{ +{IIIII}builder.append(character); +{IIII}}} +{IIII}break; +{II}}} +{I}}} +{I}return builder.toString(); }}""" ), Stripped( diff --git a/aas_core_codegen/python/lib/_generate_jsonization.py b/aas_core_codegen/python/lib/_generate_jsonization.py index d95b5af2c..2eea3ed2d 100644 --- a/aas_core_codegen/python/lib/_generate_jsonization.py +++ b/aas_core_codegen/python/lib/_generate_jsonization.py @@ -457,10 +457,15 @@ def _dispatch_from_jsonable( {II}) {I}if not isinstance(model_type, str): -{II}raise DeserializationException( +{II}# NOTE (mristin): +{II}# A model type of a wrong type is reported at the property, as the type +{II}# errors of all the other properties are. +{II}exception = DeserializationException( {III}f"Expected the property modelType to be a str, " {III}f"but got: {{type(model_type)}}" {II}) +{II}exception.path._prepend(PropertySegment(mapping, "modelType")) +{II}raise exception {I}parse = dispatch.get(model_type, None) {I}if parse is None: @@ -1342,6 +1347,17 @@ def _generate_concrete_class_from_jsonable( Stripped( f"""\ model_type = mapping.get('modelType', None) +if model_type is not None and not isinstance(model_type, str): +{I}# NOTE (mristin): +{I}# A model type of a wrong type is reported at the property, as the type +{I}# errors of all the other properties are. +{I}exception = DeserializationException( +{II}f"Expected the property modelType to be a str, " +{II}f"but got: {{type(model_type)}}" +{I}) +{I}exception.path._prepend(PropertySegment(mapping, 'modelType')) +{I}raise exception + if model_type != {model_type_literal}: {I}raise DeserializationException( {II}f"Expected modelType to be {model_type_literal}, " @@ -1379,26 +1395,26 @@ def _generate_concrete_class_from_jsonable( # which carries one, while any other property may or may not be there, so it is # the branch most often taken. It has been dealt with before the loop -- either # above, or by the dispatch which got us here -- so we only have to accept it. + # A class which carries no model type refuses it as an unexpected property, as + # in the other SDKs. branches = [] # type: List[Stripped] + keys = [] # type: List[Stripped] + model_type_note: Optional[str] = None if checks_model_type: model_type_note = "# The model type has already been checked above." elif len(cls.concrete_descendants) > 0: model_type_note = "# The dispatch has already matched the model type." - else: - model_type_note = ( - "# The model type is redundant for this class, and we simply accept it." - ) - branches.append( - Stripped( - f"""\ + if model_type_note is not None: + branches.append( + Stripped( + f"""\ {model_type_note} pass""" + ) ) - ) - - keys = [Stripped("'modelType'")] # type: List[Stripped] + keys.append(Stripped("'modelType'")) for prop in cls.properties: keys.append(Stripped(python_common.string_literal(prop.json_name))) @@ -1423,39 +1439,59 @@ def _generate_concrete_class_from_jsonable( ) ) - switch_writer = io.StringIO() - for i, (key, branch) in enumerate(zip(keys, branches)): - keyword = "if" if i == 0 else "elif" - switch_writer.write( - f"""\ + # NOTE (mristin): + # An unexpected property is reported at the object which holds it, not at + # the property itself, as in the other SDKs. Hence we raise it only after + # the loop, where the property is not prepended to the path. + if len(keys) == 0: + # NOTE (mristin): + # A class without properties and without a model type expects an empty + # object, so that any property at all is unexpected. + blocks.append( + Stripped( + f"""\ +for key in mapping: +{I}raise DeserializationException( +{II}f"Unexpected property: {{key}}" +{I})""" + ) + ) + else: + switch_writer = io.StringIO() + for i, (key, branch) in enumerate(zip(keys, branches)): + keyword = "if" if i == 0 else "elif" + switch_writer.write( + f"""\ {II}{keyword} key == {key}: {III}{indent_but_first_line(branch, III)} """ - ) + ) - switch_writer.write( - f"""\ + switch_writer.write( + f"""\ {II}else: -{III}raise DeserializationException( -{IIII}f"Unexpected property: {{key}}" -{III})""" - ) +{III}unexpected_key = key +{III}break""" + ) - # NOTE (mristin): - # The property is marked on the error path here, once for all the properties, - # instead of in every branch: ``key`` *is* the JSON name of the property which - # we are parsing. We wrap the loop rather than its body, as the two are - # equivalent -- ``key`` still holds the culprit -- and the enclosing form is - # cheaper on the Python versions which do not have a zero-cost ``try``. - iteration = ( - "for key, jsonable_value in mapping.items():" - if len(cls.properties) > 0 - else "for key in mapping:" - ) + # NOTE (mristin): + # The property is marked on the error path here, once for all + # the properties, instead of in every branch: ``key`` *is* the JSON name of + # the property which we are parsing. We wrap the loop rather than its body, + # as the two are equivalent -- ``key`` still holds the culprit -- and + # the enclosing form is cheaper on the Python versions which do not have + # a zero-cost ``try``. + iteration = ( + "for key, jsonable_value in mapping.items():" + if len(cls.properties) > 0 + else "for key in mapping:" + ) + + blocks.append( + Stripped( + f"""\ +unexpected_key: Optional[str] = None - blocks.append( - Stripped( - f"""\ try: {I}{iteration} {switch_writer.getvalue()} @@ -1463,9 +1499,14 @@ def _generate_concrete_class_from_jsonable( {I}exception.path._prepend( {II}PropertySegment(mapping, key) {I}) -{I}raise""" +{I}raise + +if unexpected_key is not None: +{I}raise DeserializationException( +{II}f"Unexpected property: {{unexpected_key}}" +{I})""" + ) ) - ) # endregion @@ -2273,7 +2314,7 @@ class SerializationException(Exception): {II}object is known only at run time and can be any string at all, so it is {II}always rendered as a subscript. {II}""" -{II}self._segments.insert(0, f'[{{key!r}}]') +{II}self._segments.insert(0, f'[{{json.dumps(key, ensure_ascii=False)}}]') {I}def __str__(self) -> str: {II}if len(self._segments) == 0: @@ -2570,7 +2611,9 @@ def generate( f"""\ {base64_import}\ import collections.abc +import json {math_import}\ +import re import sys from typing import ( {I}Any, @@ -2670,6 +2713,12 @@ class KeySegment: Stripped( """\ Segment = Union[PropertySegment, IndexSegment, KeySegment]""" + ), + Stripped( + """\ +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$")""" ), Stripped( f"""\ @@ -2698,21 +2747,27 @@ class Path: {II}iterator = iter(self._segments) {II}first = next(iterator) {II}if isinstance(first, PropertySegment): -{III}parts.append(f"{{first.name}}") +{III}if _VARIABLE_NAME_RE.match(first.name): +{IIII}parts.append(first.name) +{III}else: +{IIII}parts.append(f"[{{json.dumps(first.name, ensure_ascii=False)}}]") {II}elif isinstance(first, IndexSegment): {III}parts.append(f"[{{first.index}}]") {II}elif isinstance(first, KeySegment): -{III}parts.append(f"[{{first.key!r}}]") +{III}parts.append(f"[{{json.dumps(first.key, ensure_ascii=False)}}]") {II}else: {III}our_common.assert_never(first) {II}for segment in iterator: {III}if isinstance(segment, PropertySegment): -{IIII}parts.append(f".{{segment.name}}") +{IIII}if _VARIABLE_NAME_RE.match(segment.name): +{IIIII}parts.append(f".{{segment.name}}") +{IIII}else: +{IIIII}parts.append(f"[{{json.dumps(segment.name, ensure_ascii=False)}}]") {III}elif isinstance(segment, IndexSegment): {IIII}parts.append(f"[{{segment.index}}]") {III}elif isinstance(segment, KeySegment): -{IIII}parts.append(f"[{{segment.key!r}}]") +{IIII}parts.append(f"[{{json.dumps(segment.key, ensure_ascii=False)}}]") {III}else: {IIII}our_common.assert_never(segment) diff --git a/aas_core_codegen/python/lib/_generate_reporting.py b/aas_core_codegen/python/lib/_generate_reporting.py index 0d503b796..b2d594cca 100644 --- a/aas_core_codegen/python/lib/_generate_reporting.py +++ b/aas_core_codegen/python/lib/_generate_reporting.py @@ -58,6 +58,7 @@ def generate(qualified_module_name: python_common.QualifiedModuleName) -> str: Stripped( f"""\ import collections +import json import sys from typing import ( {I}Any, @@ -151,7 +152,7 @@ class KeySegment: {II}self.key = key {I}def __str__(self) -> str: -{II}return f'[{{self.key!r}}]'""" +{II}return f'[{{json.dumps(self.key, ensure_ascii=False)}}]'""" ), Stripped("Segment = Union[PropertySegment, IndexSegment, KeySegment]"), Stripped( diff --git a/aas_core_codegen/python/lib/_generate_xml_common.py b/aas_core_codegen/python/lib/_generate_xml_common.py index 93ed81ecd..d72417807 100644 --- a/aas_core_codegen/python/lib/_generate_xml_common.py +++ b/aas_core_codegen/python/lib/_generate_xml_common.py @@ -196,6 +196,7 @@ def parse_unqualified_element_tag( # pylint: disable=line-too-long Stripped( f"""\ +import json import re import sys from typing import ( @@ -448,7 +449,7 @@ class SerializationException(Exception): {II}object is known only at run time and can be any string at all, so it is {II}always rendered as a subscript. {II}\"\"\" -{II}self._segments.insert(0, f'[{{key!r}}]') +{II}self._segments.insert(0, f'[{{json.dumps(key, ensure_ascii=False)}}]') {I}def __str__(self) -> str: {II}if len(self._segments) == 0: 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 263966991..ff877bb12 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 @@ -74,8 +74,8 @@ class TestJsonValue(unittest.TestCase): {I}def test_the_walk_is_depth_first(self) -> None: {II}# NOTE (mristin): -{II}# A breadth-first walk would report the shallow ``['z']`` before -{II}# the deeper ``['a']['deep']``, although the latter comes first in +{II}# A breadth-first walk would report the shallow ``["z"]`` before +{II}# the deeper ``["a"]["deep"]``, although the latter comes first in {II}# the value. {II}value = {{ {III}"a": {{"deep": math.nan}}, @@ -83,7 +83,7 @@ class TestJsonValue(unittest.TestCase): {II}}} {II}self.assertListEqual( -{III}["['a']['deep']", "['z']"], +{III}['["a"]["deep"]', '["z"]'], {III}[ {IIII}path {IIII}for path, _ in _paths_and_causes( @@ -101,7 +101,7 @@ class TestJsonValue(unittest.TestCase): {II}self.assertEqual(1, len(paths_and_causes)) {II}self.assertEqual( -{III}"['a b'][1][\\"c'd\\"]", +{III}'["a b"][1]["c\\'d"]', {III}paths_and_causes[0][0] {II}) @@ -113,7 +113,7 @@ class TestJsonValue(unittest.TestCase): {II}) {II}self.assertEqual(1, len(paths_and_causes)) -{II}self.assertEqual("['a']", paths_and_causes[0][0]) +{II}self.assertEqual('["a"]', paths_and_causes[0][0]) {II}self.assertIn("but got a key of type", paths_and_causes[0][1]) {I}def test_the_segments_carry_their_container(self) -> None: @@ -184,7 +184,7 @@ class TestJsonValue(unittest.TestCase): {II}) {II}self.assertEqual(1, len(paths_and_causes)) -{II}self.assertEqual("['a']", paths_and_causes[0][0]) +{II}self.assertEqual('["a"]', paths_and_causes[0][0]) {II}self.assertIn("but got: ", paths_and_causes[0][1])""" ), Stripped( @@ -231,7 +231,7 @@ class TestJsonArrayAndJsonObject(unittest.TestCase): {II}) {II}self.assertEqual(1, len(paths_and_causes)) -{II}self.assertEqual("['a'][0]['b']", paths_and_causes[0][0])""" +{II}self.assertEqual('["a"][0]["b"]', paths_and_causes[0][0])""" ), Stripped( f"""\ 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 b2dcce862..f950b85aa 100644 --- a/aas_core_codegen/python/tests/_generate_test_xml_rpc.py +++ b/aas_core_codegen/python/tests/_generate_test_xml_rpc.py @@ -324,7 +324,7 @@ class TestWriting(unittest.TestCase): {III}) {II}self.assertEqual( -{III}\"['a b'][1]\", context.exception.path +{III}'["a b"][1]', context.exception.path {II}) {I}def test_an_integer_too_large_for_a_float_is_not_written(self) -> None: diff --git a/aas_core_codegen/typescript/lib/_generate_jsonization.py b/aas_core_codegen/typescript/lib/_generate_jsonization.py index 18a787c61..01233dbeb 100644 --- a/aas_core_codegen/typescript/lib/_generate_jsonization.py +++ b/aas_core_codegen/typescript/lib/_generate_jsonization.py @@ -26,6 +26,7 @@ INDENT3 as III, INDENT4 as IIII, INDENT5 as IIIII, + INDENT6 as IIIIII, ) @@ -175,10 +176,15 @@ def _generate_extract_model_type() -> Stripped: {II}); {I}}} {I}if (typeof modelType !== "string") {{ -{II}return newDeserializationError( +{II}// NOTE (mristin): +{II}// A model type of a wrong type is reported at the property, as the type +{II}// errors of all the other properties are, and as in the other SDKs. +{II}const error = new DeserializationError( {III}`Expected the property modelType to be a string, ` + {III}`but got: ${{typeof modelType}}` {II}); +{II}error.path.prepend(new PropertySegment(jsonObject, "modelType")); +{II}return new OurCommon.Either(null, error); {I}}} {I}return new OurCommon.Either(modelType, null); @@ -2827,6 +2833,14 @@ def generate( Stripped( """\ export type Segment = PropertySegment | IndexSegment | KeySegment;""" + ), + Stripped( + """\ +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/;""" ), Stripped( f"""\ @@ -2862,7 +2876,11 @@ def generate( {II}let segment = this._segments[0]; {II}if (segment instanceof PropertySegment) {{ -{III}parts.push(segment.name); +{III}parts.push( +{IIII}VARIABLE_NAME_RE.test(segment.name) +{IIIII}? segment.name +{IIIII}: `[${{JSON.stringify(segment.name)}}]` +{III}); {II}}} else if (segment instanceof IndexSegment) {{ {III}parts.push(`[${{segment.index}}]`); {II}}} else if (segment instanceof KeySegment) {{ @@ -2874,7 +2892,11 @@ def generate( {II}for (let i = 1; i < this._segments.length; i++) {{ {III}segment = this._segments[i]; {III}if (segment instanceof PropertySegment) {{ -{IIII}parts.push(`.${{segment.name}}`); +{IIII}parts.push( +{IIIII}VARIABLE_NAME_RE.test(segment.name) +{IIIIII}? `.${{segment.name}}` +{IIIIII}: `[${{JSON.stringify(segment.name)}}]` +{IIII}); {III}}} else if (segment instanceof IndexSegment) {{ {IIII}parts.push(`[${{segment.index}}]`); {III}}} else if (segment instanceof KeySegment) {{ diff --git a/dev/test_data/live_tests/common_test_data/deep_class_hierarchy/Json/Unexpected/Unserializable/TypeViolation/Container/node_model_type_not_a_string.json b/dev/test_data/live_tests/common_test_data/deep_class_hierarchy/Json/Unexpected/Unserializable/TypeViolation/Container/node_model_type_not_a_string.json new file mode 100644 index 000000000..d0c445338 --- /dev/null +++ b/dev/test_data/live_tests/common_test_data/deep_class_hierarchy/Json/Unexpected/Unserializable/TypeViolation/Container/node_model_type_not_a_string.json @@ -0,0 +1,19 @@ +{ + "node": { + "modelType": 42, + "identifier": "some-identifier", + "description": "some-description" + }, + "something": { + "someChoice": { + "modelType": "Branch", + "identifier": "some-identifier", + "description": "some-description" + }, + "somethingWithoutChoice": { + "modelType": "Branch", + "identifier": "some-identifier", + "description": "some-description" + } + } +} \ No newline at end of file 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 index d90061f61..426f35e4f 100644 --- 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 @@ -1 +1 @@ -.mapping: Expected a JSON object, but got a value of type: array \ No newline at end of file +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 index a5f3352d0..10100dbc7 100644 --- 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 @@ -1 +1 @@ -.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 +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 index 5c74ec067..b080aaed2 100644 --- 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 @@ -1 +1 @@ -.values: Expected a JSON array, but got a value of type: object \ No newline at end of file +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/tuples/Json/Unexpected/Unserializable/TypeViolation/Something/tricky_second_item_model_type_not_a_string.json b/dev/test_data/live_tests/common_test_data/tuples/Json/Unexpected/Unserializable/TypeViolation/Something/tricky_second_item_model_type_not_a_string.json new file mode 100644 index 000000000..0f0999009 --- /dev/null +++ b/dev/test_data/live_tests/common_test_data/tuples/Json/Unexpected/Unserializable/TypeViolation/Something/tricky_second_item_model_type_not_a_string.json @@ -0,0 +1,33 @@ +{ + "pair": [ + "hello", + 3 + ], + "items": [ + { + "modelType": "SomeItem", + "name": "something" + }, + { + "modelType": "AnotherItem", + "serialNumber": 42 + } + ], + "tricky": [ + 7, + { + "modelType": 42, + "name": "something" + }, + { + "modelType": "AnotherItem", + "serialNumber": 99 + }, + { + "modelType": "SomeItem", + "name": "second" + }, + 5, + "ok" + ] +} diff --git a/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/include/aas_core/aas_3_0/jsonization.hpp b/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/include/aas_core/aas_3_0/jsonization.hpp index 4186ad409..f919966fb 100644 --- a/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/include/aas_core/aas_3_0/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/include/aas_core/aas_3_0/jsonization.hpp @@ -80,6 +80,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/src/jsonization.cpp index aaa45f076..6a2753c99 100644 --- a/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/aas_core_meta.v3/expected_output/src/jsonization.cpp @@ -16,6 +16,93 @@ namespace aas_core { namespace aas_3_0 { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -26,9 +113,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -61,6 +156,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -102,6 +223,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/include/dummy/jsonization.hpp index d077cf8c8..dc50d23f6 100644 --- a/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/src/jsonization.cpp index f7aea6854..fa5a2d1b5 100644 --- a/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/annotated_assignments/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/arithmetic/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/arithmetic/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/arithmetic/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/arithmetic/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/arithmetic/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/arithmetic/expected_output/src/jsonization.cpp index 9053d41bf..d59406fdb 100644 --- a/dev/test_data/main/cpp/expected/arithmetic/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/arithmetic/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/assert_statements/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/assert_statements/expected_output/include/dummy/jsonization.hpp index 346bcc278..22daf4e5c 100644 --- a/dev/test_data/main/cpp/expected/assert_statements/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/assert_statements/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/assert_statements/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/assert_statements/expected_output/src/jsonization.cpp index 00a27ee2b..aa98e9886 100644 --- a/dev/test_data/main/cpp/expected/assert_statements/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/assert_statements/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/assignment_targets/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/assignment_targets/expected_output/include/dummy/jsonization.hpp index 31b58cb30..f955d7b3b 100644 --- a/dev/test_data/main/cpp/expected/assignment_targets/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/assignment_targets/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/assignment_targets/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/assignment_targets/expected_output/src/jsonization.cpp index 96b03a113..3f3e4b714 100644 --- a/dev/test_data/main/cpp/expected/assignment_targets/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/assignment_targets/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/include/dummy/jsonization.hpp index 370f788f0..3b4ef4bc1 100644 --- a/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/src/jsonization.cpp index 788989b12..9f1bc24b9 100644 --- a/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/collections_in_returns/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/src/jsonization.cpp index d3b880ac0..fd9aa0dae 100644 --- a/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/constrained_primitives/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/include/dummy/jsonization.hpp index b67d3a123..3e4654a72 100644 --- a/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/src/jsonization.cpp index bbe44ef68..356d00dd1 100644 --- a/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/custom_serialization_names/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/include/dummy/jsonization.hpp index d6a466864..e4e005236 100644 --- a/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/src/jsonization.cpp index d6bad41e7..24fa7f2cb 100644 --- a/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/deep_class_hierarchy/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/empty_class/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/empty_class/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/empty_class/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/empty_class/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/empty_class/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/empty_class/expected_output/src/jsonization.cpp index 9c0fe8d1a..c44ae2dc0 100644 --- a/dev/test_data/main/cpp/expected/empty_class/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/empty_class/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/enum/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/enum/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/enum/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/enum/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/enum/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/enum/expected_output/src/jsonization.cpp index 2a82355ae..b0a92afd7 100644 --- a/dev/test_data/main/cpp/expected/enum/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/enum/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/float_only/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/float_only/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/float_only/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/float_only/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/float_only/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/float_only/expected_output/src/jsonization.cpp index 213cdf3bd..7d57dcedb 100644 --- a/dev/test_data/main/cpp/expected/float_only/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/float_only/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/for_statements/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/for_statements/expected_output/include/dummy/jsonization.hpp index 31b58cb30..f955d7b3b 100644 --- a/dev/test_data/main/cpp/expected/for_statements/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/for_statements/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/for_statements/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/for_statements/expected_output/src/jsonization.cpp index 8f153742a..3809d4a3c 100644 --- a/dev/test_data/main/cpp/expected/for_statements/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/for_statements/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/if_statements/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/if_statements/expected_output/include/dummy/jsonization.hpp index dfb8dd63c..9eea9813f 100644 --- a/dev/test_data/main/cpp/expected/if_statements/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/if_statements/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/if_statements/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/if_statements/expected_output/src/jsonization.cpp index bc3a2635f..0d41c427e 100644 --- a/dev/test_data/main/cpp/expected/if_statements/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/if_statements/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/implementation_specific/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/implementation_specific/expected_output/include/dummy/jsonization.hpp index bb0dc6abd..e9483a07e 100644 --- a/dev/test_data/main/cpp/expected/implementation_specific/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/implementation_specific/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/implementation_specific/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/implementation_specific/expected_output/src/jsonization.cpp index 2a2b950b3..9d317cc0e 100644 --- a/dev/test_data/main/cpp/expected/implementation_specific/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/implementation_specific/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/include/dummy/jsonization.hpp index 6a0e3b187..224141f8a 100644 --- a/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/src/jsonization.cpp index a728d3bfd..e01994705 100644 --- a/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/infallible_dispatch_among_fallible_classes/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/src/jsonization.cpp index 4196da2e1..42011c4f4 100644 --- a/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/int_length_interplay/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/src/jsonization.cpp index 75c8922ba..f2695409d 100644 --- a/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/int_parsing_and_lstrip/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/is_instance/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/is_instance/expected_output/include/dummy/jsonization.hpp index 8e69456bc..9f364bd0f 100644 --- a/dev/test_data/main/cpp/expected/is_instance/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/is_instance/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/is_instance/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/is_instance/expected_output/src/jsonization.cpp index a42cdee93..2714ad4d4 100644 --- a/dev/test_data/main/cpp/expected/is_instance/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/is_instance/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/json_types/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/json_types/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/json_types/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/json_types/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ 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 7e1446328..694fa5c5a 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 @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); @@ -622,7 +753,7 @@ std::pair< if (error.has_value()) { error->path.segments.emplace_front( - common::make_unique( + common::make_unique( common::Utf8ToWstring(item.key()) ) ); diff --git a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_collections/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ 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 285dc7428..d4fb55adf 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 @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); @@ -622,7 +753,7 @@ std::pair< if (error.has_value()) { error->path.segments.emplace_front( - common::make_unique( + common::make_unique( common::Utf8ToWstring(item.key()) ) ); diff --git a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/json_types_in_invariants/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ 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 81298048f..bf1b477fc 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 @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); @@ -622,7 +753,7 @@ std::pair< if (error.has_value()) { error->path.segments.emplace_front( - common::make_unique( + common::make_unique( common::Utf8ToWstring(item.key()) ) ); diff --git a/dev/test_data/main/cpp/expected/list_of_classes/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/list_of_classes/expected_output/include/dummy/jsonization.hpp index ee123adaf..732b387b0 100644 --- a/dev/test_data/main/cpp/expected/list_of_classes/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/list_of_classes/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/list_of_classes/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/list_of_classes/expected_output/src/jsonization.cpp index a765a62be..6288ee477 100644 --- a/dev/test_data/main/cpp/expected/list_of_classes/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/list_of_classes/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/src/jsonization.cpp index ba854de01..743ad9bdb 100644 --- a/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/list_of_constrained_primitives/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/list_of_enums/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/list_of_enums/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/list_of_enums/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/list_of_enums/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/list_of_enums/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/list_of_enums/expected_output/src/jsonization.cpp index 7b25e81ce..708255c31 100644 --- a/dev/test_data/main/cpp/expected/list_of_enums/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/list_of_enums/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/src/jsonization.cpp index dd2f83979..c2d834aac 100644 --- a/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/list_of_primitives/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/method_references/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/method_references/expected_output/include/dummy/jsonization.hpp index 8df79d229..ecefa7a05 100644 --- a/dev/test_data/main/cpp/expected/method_references/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/method_references/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/method_references/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/method_references/expected_output/src/jsonization.cpp index c3ddaa17e..dde1265a3 100644 --- a/dev/test_data/main/cpp/expected/method_references/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/method_references/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/include/dummy/jsonization.hpp index 818a9f5fb..b4a3f82df 100644 --- a/dev/test_data/main/cpp/expected/naming_collisions/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/naming_collisions/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ 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 2318410d6..d90d2e092 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 @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); @@ -622,7 +753,7 @@ std::pair< if (error.has_value()) { error->path.segments.emplace_front( - common::make_unique( + common::make_unique( common::Utf8ToWstring(item.key()) ) ); diff --git a/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/include/dummy/jsonization.hpp index 7a0096120..9b2b829e4 100644 --- a/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/src/jsonization.cpp index ddeaeacd9..bcbc61873 100644 --- a/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/nested_any_in_all/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/nested_collections/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/nested_collections/expected_output/include/dummy/jsonization.hpp index a2ea18a8e..921d10fc5 100644 --- a/dev/test_data/main/cpp/expected/nested_collections/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/nested_collections/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/nested_collections/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/nested_collections/expected_output/src/jsonization.cpp index b62b3b15c..1568be190 100644 --- a/dev/test_data/main/cpp/expected/nested_collections/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/nested_collections/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/optional_arguments/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/optional_arguments/expected_output/include/dummy/jsonization.hpp index 31b58cb30..f955d7b3b 100644 --- a/dev/test_data/main/cpp/expected/optional_arguments/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/optional_arguments/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/optional_arguments/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/optional_arguments/expected_output/src/jsonization.cpp index 4cdd602e4..0d0750588 100644 --- a/dev/test_data/main/cpp/expected/optional_arguments/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/optional_arguments/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/optional_lists/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/optional_lists/expected_output/include/dummy/jsonization.hpp index 31b58cb30..f955d7b3b 100644 --- a/dev/test_data/main/cpp/expected/optional_lists/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/optional_lists/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ 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 99634666e..f057568c5 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 @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); @@ -622,7 +753,7 @@ std::pair< if (error.has_value()) { error->path.segments.emplace_front( - common::make_unique( + common::make_unique( common::Utf8ToWstring(item.key()) ) ); diff --git a/dev/test_data/main/cpp/expected/primitive_types/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/primitive_types/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/primitive_types/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/primitive_types/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/primitive_types/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/primitive_types/expected_output/src/jsonization.cpp index d3b880ac0..fd9aa0dae 100644 --- a/dev/test_data/main/cpp/expected/primitive_types/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/primitive_types/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/src/jsonization.cpp index d77002ca1..9552950cb 100644 --- a/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/problematic_keywords/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/sets/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/sets/expected_output/include/dummy/jsonization.hpp index 16394ffc2..9043d7ef6 100644 --- a/dev/test_data/main/cpp/expected/sets/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/sets/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/sets/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/sets/expected_output/src/jsonization.cpp index 751295391..24d847d39 100644 --- a/dev/test_data/main/cpp/expected/sets/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/sets/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/src/jsonization.cpp index 7c50006b7..004248d3a 100644 --- a/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/string_slicing_and_find/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/switch_statements/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/switch_statements/expected_output/include/dummy/jsonization.hpp index 8bd2e4405..4904280c6 100644 --- a/dev/test_data/main/cpp/expected/switch_statements/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/switch_statements/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/switch_statements/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/switch_statements/expected_output/src/jsonization.cpp index 1759f2120..4a31b33e8 100644 --- a/dev/test_data/main/cpp/expected/switch_statements/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/switch_statements/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/include/dummy/jsonization.hpp index d957fdd0d..bb3e83127 100644 --- a/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/src/jsonization.cpp index f349432d7..15e526ef2 100644 --- a/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/transpiled_methods/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/tuples/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/tuples/expected_output/include/dummy/jsonization.hpp index 177fabe96..a8d9b9432 100644 --- a/dev/test_data/main/cpp/expected/tuples/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/tuples/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/tuples/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/tuples/expected_output/src/jsonization.cpp index 492782df9..1493958bf 100644 --- a/dev/test_data/main/cpp/expected/tuples/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/tuples/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/unions/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/unions/expected_output/include/dummy/jsonization.hpp index 254289ba0..b906d0622 100644 --- a/dev/test_data/main/cpp/expected/unions/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/unions/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/unions/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/unions/expected_output/src/jsonization.cpp index 557791d3a..b8e7584d2 100644 --- a/dev/test_data/main/cpp/expected/unions/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/unions/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/cpp/expected/visibility/expected_output/include/dummy/jsonization.hpp b/dev/test_data/main/cpp/expected/visibility/expected_output/include/dummy/jsonization.hpp index 8df79d229..ecefa7a05 100644 --- a/dev/test_data/main/cpp/expected/visibility/expected_output/include/dummy/jsonization.hpp +++ b/dev/test_data/main/cpp/expected/visibility/expected_output/include/dummy/jsonization.hpp @@ -79,6 +79,27 @@ struct IndexSegment : public ISegment { ~IndexSegment() override = default; }; // struct IndexSegment +/** + * Represent an access to a member of a JSON object whose key is known only at + * run time, such as a member of a JSON-able object, on a JSON path. + */ +struct KeySegment : public ISegment { + /** + * Key of the member in a JSON object + */ + std::wstring key; + + explicit KeySegment( + std::wstring a_key + ); + + std::wstring ToWstring() const override; + + std::unique_ptr Clone() const override; + + ~KeySegment() override = default; +}; // struct KeySegment + /** * Represent a JSON path to some value. */ diff --git a/dev/test_data/main/cpp/expected/visibility/expected_output/src/jsonization.cpp b/dev/test_data/main/cpp/expected/visibility/expected_output/src/jsonization.cpp index 7db61f0c8..074dd98f4 100644 --- a/dev/test_data/main/cpp/expected/visibility/expected_output/src/jsonization.cpp +++ b/dev/test_data/main/cpp/expected/visibility/expected_output/src/jsonization.cpp @@ -15,6 +15,93 @@ namespace dummy { namespace jsonization { +// region JSON path helpers + +/** + * \brief Escape \p text as the content of a JSON string. + * + * The control characters without a short escape are written as Unicode + * escapes, as JSON.stringify does. + */ +std::wstring EscapeForJsonString( + const std::wstring& text +) { + static const wchar_t* const kHexDigits = L"0123456789abcdef"; + + std::wstring escaped; + escaped.reserve(text.size()); + for (const wchar_t character : text) { + switch (character) { + case L'\\': + escaped.append(L"\\\\"); + break; + case L'"': + escaped.append(L"\\\""); + break; + case L'\b': + escaped.append(L"\\b"); + break; + case L'\f': + escaped.append(L"\\f"); + break; + case L'\n': + escaped.append(L"\\n"); + break; + case L'\r': + escaped.append(L"\\r"); + break; + case L'\t': + escaped.append(L"\\t"); + break; + default: + if (static_cast(character) < 0x20) { + escaped.append(L"\\u00"); + escaped.push_back(kHexDigits[(character >> 4) & 0xF]); + escaped.push_back(kHexDigits[character & 0xF]); + } else { + escaped.push_back(character); + } + break; + } + } + + return escaped; +} + +/** + * \brief Check whether a JSON path writes the property \p name after a dot. + * + * The other names are written in brackets as JSON strings, + * e.g., ["$eq"]. + */ +bool IsVariableName( + const std::wstring& name +) { + if (name.empty()) { + return false; + } + + for (size_t i = 0; i < name.size(); ++i) { + const wchar_t character = name[i]; + + const bool is_letter_or_underscore = ( + (character >= L'a' && character <= L'z') + || (character >= L'A' && character <= L'Z') + || character == L'_' + ); + + const bool is_digit = character >= L'0' && character <= L'9'; + + if (!(is_letter_or_underscore || (i > 0 && is_digit))) { + return false; + } + } + + return true; +} + +// endregion JSON path helpers + // region PropertySegment PropertySegment::PropertySegment( @@ -25,9 +112,17 @@ PropertySegment::PropertySegment( } std::wstring PropertySegment::ToWstring() const { + if (IsVariableName(name)) { + return common::Concat( + L".", + name + ); + } + return common::Concat( - L".", - name + L"[\"", + EscapeForJsonString(name), + L"\"]" ); } @@ -60,6 +155,32 @@ std::unique_ptr IndexSegment::Clone() const { // endregion IndexSegment +// region KeySegment + +KeySegment::KeySegment( + std::wstring a_key +) : + key(std::move(a_key)) { + // Intentionally empty. +} + +std::wstring KeySegment::ToWstring() const { + // NOTE (mristin): + // A key can be any string at all, so we always write it as a JSON string in + // brackets, as in the other SDKs. + return common::Concat( + L"[\"", + EscapeForJsonString(key), + L"\"]" + ); +} + +std::unique_ptr KeySegment::Clone() const { + return common::make_unique(*this); +} + +// endregion KeySegment + // region struct Path Path::Path() { @@ -101,6 +222,16 @@ std::wstring Path::ToWstring() const { parts.emplace_back(segment->ToWstring()); } + // NOTE (mristin): + // The path starts with the name of the first property without a dot, as in + // the other SDKs, *e.g.*, ``items[0].name``. + const PropertySegment* first_property( + dynamic_cast(segments.front().get()) + ); + if (first_property != nullptr && IsVariableName(first_property->name)) { + parts.front() = first_property->name; + } + size_t out_len = 0; for (const std::wstring& part : parts) { out_len += part.size(); diff --git a/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/jsonization.cs b/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/jsonization.cs index fc1404f8d..c352c3d84 100644 --- a/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/jsonization.cs +++ b/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_IReference = ( @@ -1344,9 +1354,14 @@ internal static Our.AssetAdministrationShell AssetAdministrationShellFrom( keyValue.Value, out error); if (error == null && modelType != "AssetAdministrationShell") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'AssetAdministrationShell', " + $"but got {modelType}"); + return default!; } break; default: @@ -1759,9 +1774,14 @@ internal static Our.Submodel SubmodelFrom( keyValue.Value, out error); if (error == null && modelType != "Submodel") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Submodel', " + $"but got {modelType}"); + return default!; } break; default: @@ -2013,9 +2033,14 @@ internal static Our.RelationshipElement RelationshipElementFrom( keyValue.Value, out error); if (error == null && modelType != "RelationshipElement") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'RelationshipElement', " + $"but got {modelType}"); + return default!; } break; default: @@ -2199,9 +2224,14 @@ internal static Our.SubmodelElementList SubmodelElementListFrom( keyValue.Value, out error); if (error == null && modelType != "SubmodelElementList") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'SubmodelElementList', " + $"but got {modelType}"); + return default!; } break; default: @@ -2333,9 +2363,14 @@ internal static Our.SubmodelElementCollection SubmodelElementCollectionFrom( keyValue.Value, out error); if (error == null && modelType != "SubmodelElementCollection") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'SubmodelElementCollection', " + $"but got {modelType}"); + return default!; } break; default: @@ -2516,9 +2551,14 @@ internal static Our.Property PropertyFrom( keyValue.Value, out error); if (error == null && modelType != "Property") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Property', " + $"but got {modelType}"); + return default!; } break; default: @@ -2653,9 +2693,14 @@ internal static Our.MultiLanguageProperty MultiLanguagePropertyFrom( keyValue.Value, out error); if (error == null && modelType != "MultiLanguageProperty") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'MultiLanguageProperty', " + $"but got {modelType}"); + return default!; } break; default: @@ -2785,9 +2830,14 @@ internal static Our.Range RangeFrom( keyValue.Value, out error); if (error == null && modelType != "Range") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Range', " + $"but got {modelType}"); + return default!; } break; default: @@ -2917,9 +2967,14 @@ internal static Our.ReferenceElement ReferenceElementFrom( keyValue.Value, out error); if (error == null && modelType != "ReferenceElement") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ReferenceElement', " + $"but got {modelType}"); + return default!; } break; default: @@ -3043,9 +3098,14 @@ internal static Our.Blob BlobFrom( keyValue.Value, out error); if (error == null && modelType != "Blob") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Blob', " + $"but got {modelType}"); + return default!; } break; default: @@ -3179,9 +3239,14 @@ internal static Our.File FileFrom( keyValue.Value, out error); if (error == null && modelType != "File") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'File', " + $"but got {modelType}"); + return default!; } break; default: @@ -3320,9 +3385,14 @@ internal static Our.AnnotatedRelationshipElement AnnotatedRelationshipElementFro keyValue.Value, out error); if (error == null && modelType != "AnnotatedRelationshipElement") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'AnnotatedRelationshipElement', " + $"but got {modelType}"); + return default!; } break; default: @@ -3476,9 +3546,14 @@ internal static Our.Entity EntityFrom( keyValue.Value, out error); if (error == null && modelType != "Entity") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Entity', " + $"but got {modelType}"); + return default!; } break; default: @@ -3876,9 +3951,14 @@ internal static Our.BasicEventElement BasicEventElementFrom( keyValue.Value, out error); if (error == null && modelType != "BasicEventElement") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'BasicEventElement', " + $"but got {modelType}"); + return default!; } break; default: @@ -4041,9 +4121,14 @@ internal static Our.Operation OperationFrom( keyValue.Value, out error); if (error == null && modelType != "Operation") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Operation', " + $"but got {modelType}"); + return default!; } break; default: @@ -4216,9 +4301,14 @@ internal static Our.Capability CapabilityFrom( keyValue.Value, out error); if (error == null && modelType != "Capability") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Capability', " + $"but got {modelType}"); + return default!; } break; default: @@ -4331,9 +4421,14 @@ internal static Our.ConceptDescription ConceptDescriptionFrom( keyValue.Value, out error); if (error == null && modelType != "ConceptDescription") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ConceptDescription', " + $"but got {modelType}"); + return default!; } break; default: @@ -5533,9 +5628,14 @@ internal static Our.DataSpecificationIec61360 DataSpecificationIec61360From( keyValue.Value, out error); if (error == null && modelType != "DataSpecificationIec61360") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'DataSpecificationIec61360', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/reporting.cs b/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/reporting.cs index 290874c4d..c31d38944 100644 --- a/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/reporting.cs +++ b/dev/test_data/main/csharp/expected/aas_core_meta.v3/expected_output/AasCore.Aas3_0/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/jsonization.cs index f09a9ace4..2563f6a9b 100644 --- a/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_IParent = ( @@ -516,9 +526,14 @@ internal static Our.ChildA ChildAFrom( keyValue.Value, out error); if (error == null && modelType != "ChildA") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ChildA', " + $"but got {modelType}"); + return default!; } break; default: @@ -598,9 +613,14 @@ internal static Our.ChildB ChildBFrom( keyValue.Value, out error); if (error == null && modelType != "ChildB") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ChildB', " + $"but got {modelType}"); + return default!; } break; default: @@ -666,6 +686,8 @@ public static Our.ParentOrItem ParentOrItemFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } @@ -742,6 +764,8 @@ public static Our.WideUnion WideUnionFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } diff --git a/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/annotated_assignments/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/arithmetic/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/jsonization.cs index 599804a25..56c25f459 100644 --- a/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_string = ( @@ -429,9 +439,14 @@ internal static Our.ChildA ChildAFrom( keyValue.Value, out error); if (error == null && modelType != "ChildA") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ChildA', " + $"but got {modelType}"); + return default!; } break; default: @@ -515,9 +530,14 @@ internal static Our.ChildB ChildBFrom( keyValue.Value, out error); if (error == null && modelType != "ChildB") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ChildB', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/assert_statements/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/assignment_targets/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/collections_in_returns/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/constrained_primitives/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/custom_serialization_names/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/jsonization.cs index 4c476b813..3a6311af2 100644 --- a/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/jsonization.cs @@ -257,7 +257,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } ///

@@ -387,9 +397,14 @@ internal static Our.Branch BranchFrom( keyValue.Value, out error); if (error == null && modelType != "Branch") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Branch', " + $"but got {modelType}"); + return default!; } break; default: @@ -523,9 +538,14 @@ internal static Our.Leaf LeafFrom( keyValue.Value, out error); if (error == null && modelType != "Leaf") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Leaf', " + $"but got {modelType}"); + return default!; } break; default: @@ -634,9 +654,14 @@ internal static Our.Blossom BlossomFrom( keyValue.Value, out error); if (error == null && modelType != "Blossom") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Blossom', " + $"but got {modelType}"); + return default!; } break; default: @@ -771,9 +796,14 @@ internal static Our.PlainMarker PlainMarkerFrom( keyValue.Value, out error); if (error == null && modelType != "PlainMarker") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'PlainMarker', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/deep_class_hierarchy/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/empty_class/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/enum/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/enum/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/enum/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/enum/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/float_only/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/float_only/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/float_only/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/float_only/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/for_statements/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/jsonization.cs index dcae74802..06bc71e5d 100644 --- a/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_IParent = ( @@ -521,9 +531,14 @@ internal static Our.ChildA ChildAFrom( keyValue.Value, out error); if (error == null && modelType != "ChildA") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ChildA', " + $"but got {modelType}"); + return default!; } break; default: @@ -603,9 +618,14 @@ internal static Our.ChildB ChildBFrom( keyValue.Value, out error); if (error == null && modelType != "ChildB") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ChildB', " + $"but got {modelType}"); + return default!; } break; default: @@ -685,9 +705,14 @@ internal static Our.Container ContainerFrom( keyValue.Value, out error); if (error == null && modelType != "Container") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Container', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/if_statements/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/jsonization.cs index d64c27dcb..26e95c032 100644 --- a/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_string = ( @@ -459,9 +469,14 @@ internal static Our.Box BoxFrom( keyValue.Value, out error); if (error == null && modelType != "Box") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Box', " + $"but got {modelType}"); + return default!; } break; default: @@ -541,9 +556,14 @@ internal static Our.Bag BagFrom( keyValue.Value, out error); if (error == null && modelType != "Bag") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Bag', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/implementation_specific/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/int_length_interplay/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/jsonization.cs index 728da3449..f731f2fe8 100644 --- a/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_IElement = ( @@ -436,9 +446,14 @@ internal static Our.Leaf LeafFrom( keyValue.Value, out error); if (error == null && modelType != "Leaf") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Leaf', " + $"but got {modelType}"); + return default!; } break; default: @@ -572,9 +587,14 @@ internal static Our.OrderedContainer OrderedContainerFrom( keyValue.Value, out error); if (error == null && modelType != "OrderedContainer") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'OrderedContainer', " + $"but got {modelType}"); + return default!; } break; default: @@ -673,9 +693,14 @@ internal static Our.UnorderedContainer UnorderedContainerFrom( keyValue.Value, out error); if (error == null && modelType != "UnorderedContainer") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'UnorderedContainer', " + $"but got {modelType}"); + return default!; } break; default: @@ -759,9 +784,14 @@ internal static Our.GlobalAttribute GlobalAttributeFrom( keyValue.Value, out error); if (error == null && modelType != "GlobalAttribute") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'GlobalAttribute', " + $"but got {modelType}"); + return default!; } break; default: @@ -835,9 +865,14 @@ internal static Our.LocalAttribute LocalAttributeFrom( keyValue.Value, out error); if (error == null && modelType != "LocalAttribute") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'LocalAttribute', " + $"but got {modelType}"); + return default!; } break; default: @@ -902,6 +937,8 @@ public static Our.AttributeItem AttributeItemFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } @@ -976,9 +1013,14 @@ internal static Our.AttributeOperand AttributeOperandFrom( keyValue.Value, out error); if (error == null && modelType != "AttributeOperand") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'AttributeOperand', " + $"but got {modelType}"); + return default!; } break; default: @@ -1052,9 +1094,14 @@ internal static Our.StringLiteral StringLiteralFrom( keyValue.Value, out error); if (error == null && modelType != "StringLiteral") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'StringLiteral', " + $"but got {modelType}"); + return default!; } break; default: @@ -1128,9 +1175,14 @@ internal static Our.NumberLiteral NumberLiteralFrom( keyValue.Value, out error); if (error == null && modelType != "NumberLiteral") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'NumberLiteral', " + $"but got {modelType}"); + return default!; } break; default: @@ -1195,6 +1247,8 @@ public static Our.StringValue StringValueFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } @@ -1260,6 +1314,8 @@ public static Our.Value ValueFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } diff --git a/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/is_instance/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/json_types/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_collections/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/json_types_in_invariants/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/jsonization.cs index 4345eca08..5517e6bc6 100644 --- a/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_IAbstractItem = ( @@ -428,9 +438,14 @@ internal static Our.SomeItem SomeItemFrom( keyValue.Value, out error); if (error == null && modelType != "SomeItem") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'SomeItem', " + $"but got {modelType}"); + return default!; } break; default: @@ -504,9 +519,14 @@ internal static Our.AnotherItem AnotherItemFrom( keyValue.Value, out error); if (error == null && modelType != "AnotherItem") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'AnotherItem', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/list_of_classes/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/list_of_constrained_primitives/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/list_of_enums/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/list_of_primitives/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/jsonization.cs index a8f710240..db9a7b39b 100644 --- a/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/jsonization.cs @@ -257,7 +257,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } /// @@ -338,9 +348,14 @@ internal static Our.Box BoxFrom( keyValue.Value, out error); if (error == null && modelType != "Box") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Box', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/method_references/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/jsonization.cs index 4557a25d5..31420cbaf 100644 --- a/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_ILangString = ( @@ -565,9 +575,14 @@ internal static Our.IecContent IecContentFrom( keyValue.Value, out error); if (error == null && modelType != "IecContent") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'IecContent', " + $"but got {modelType}"); + return default!; } break; default: @@ -626,9 +641,14 @@ internal static Our.OtherContent OtherContentFrom( keyValue.Value, out error); if (error == null && modelType != "OtherContent") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'OtherContent', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/nested_any_in_all/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/jsonization.cs index bb891b31b..2cfcf07e7 100644 --- a/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/jsonization.cs @@ -519,7 +519,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_long = ( @@ -696,9 +706,14 @@ internal static Our.SomeItem SomeItemFrom( keyValue.Value, out error); if (error == null && modelType != "SomeItem") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'SomeItem', " + $"but got {modelType}"); + return default!; } break; default: @@ -772,9 +787,14 @@ internal static Our.AnotherItem AnotherItemFrom( keyValue.Value, out error); if (error == null && modelType != "AnotherItem") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'AnotherItem', " + $"but got {modelType}"); + return default!; } break; default: @@ -839,6 +859,8 @@ public static Our.SomeUnion SomeUnionFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } diff --git a/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/nested_collections/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/optional_arguments/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/primitive_types/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/problematic_keywords/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/sets/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/sets/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/sets/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/sets/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/string_slicing_and_find/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/string_slicing_and_find/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/string_slicing_and_find/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/string_slicing_and_find/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/switch_statements/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/jsonization.cs index b0b2165cc..f34f0da74 100644 --- a/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/jsonization.cs @@ -341,7 +341,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer> Parse_ListOf_string = ( @@ -460,9 +470,14 @@ internal static Our.First FirstFrom( keyValue.Value, out error); if (error == null && modelType != "First") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'First', " + $"but got {modelType}"); + return default!; } break; default: @@ -562,9 +577,14 @@ internal static Our.Second SecondFrom( keyValue.Value, out error); if (error == null && modelType != "Second") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Second', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/transpiled_methods/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/jsonization.cs index a9386fbd6..31f953f17 100644 --- a/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/jsonization.cs @@ -447,7 +447,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer<(string, long)> Parse_TupleOf2_string_long = ( @@ -582,9 +592,14 @@ internal static Our.SomeItem SomeItemFrom( keyValue.Value, out error); if (error == null && modelType != "SomeItem") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'SomeItem', " + $"but got {modelType}"); + return default!; } break; default: @@ -658,9 +673,14 @@ internal static Our.AnotherItem AnotherItemFrom( keyValue.Value, out error); if (error == null && modelType != "AnotherItem") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'AnotherItem', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/tuples/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/unions/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/unions/expected_output/dummy/jsonization.cs index 3b022ba0a..4dfbd549e 100644 --- a/dev/test_data/main/csharp/expected/unions/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/unions/expected_output/dummy/jsonization.cs @@ -409,7 +409,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } private static readonly Deserializer< @@ -835,9 +845,14 @@ internal static Our.MixedConcreteWithDescendants MixedConcreteWithDescendantsFro keyValue.Value, out error); if (error == null && modelType != "MixedConcreteWithDescendants") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'MixedConcreteWithDescendants', " + $"but got {modelType}"); + return default!; } break; default: @@ -916,9 +931,14 @@ internal static Our.MixedConcreteWithDescendantsChild MixedConcreteWithDescendan keyValue.Value, out error); if (error == null && modelType != "MixedConcreteWithDescendantsChild") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'MixedConcreteWithDescendantsChild', " + $"but got {modelType}"); + return default!; } break; default: @@ -1050,6 +1070,8 @@ public static Our.MixedUnion MixedUnionFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } @@ -1157,9 +1179,14 @@ internal static Our.ModelTypedFirst ModelTypedFirstFrom( keyValue.Value, out error); if (error == null && modelType != "ModelTypedFirst") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ModelTypedFirst', " + $"but got {modelType}"); + return default!; } break; default: @@ -1233,9 +1260,14 @@ internal static Our.ModelTypedSecond ModelTypedSecondFrom( keyValue.Value, out error); if (error == null && modelType != "ModelTypedSecond") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'ModelTypedSecond', " + $"but got {modelType}"); + return default!; } break; default: @@ -1300,6 +1332,8 @@ public static Our.ModelTypedUnion ModelTypedUnionFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } @@ -1365,6 +1399,8 @@ public static Our.OverlappingUnion OverlappingUnionFrom( string modelType = StringFrom(modelTypeNode, out error); if (error != null) { + error.PrependSegment( + new Reporting.NameSegment("modelType")); return default!; } diff --git a/dev/test_data/main/csharp/expected/unions/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/unions/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/unions/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/unions/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/jsonization.cs b/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/jsonization.cs index 2af3fb1a8..e4394a957 100644 --- a/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/jsonization.cs +++ b/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/jsonization.cs @@ -257,7 +257,17 @@ private static string ModelTypeFrom( return default!; } - return StringFrom(modelTypeNode, out error); + string modelType = StringFrom(modelTypeNode, out error); + if (error != null) + { + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + error.PrependSegment( + new Reporting.NameSegment("modelType")); + return default!; + } + return modelType; } /// @@ -338,9 +348,14 @@ internal static Our.Box BoxFrom( keyValue.Value, out error); if (error == null && modelType != "Box") { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs, so we return before the property is + // prepended to the path. error = new Reporting.Error( "Expected the model type 'Box', " + $"but got {modelType}"); + return default!; } break; default: diff --git a/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/reporting.cs b/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/reporting.cs index da9cc728d..4c2c4a848 100644 --- a/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/reporting.cs +++ b/dev/test_data/main/csharp/expected/visibility/expected_output/dummy/reporting.cs @@ -65,16 +65,48 @@ public KeySegment(string key) private static string EscapeForJsonString( string text) { - return ( - text - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\b", "\\b") - .Replace("\f", "\\f") - .Replace("\n", "\\n") - .Replace("\r", "\\r") - .Replace("\t", "\\t") - ); + var builder = new System.Text.StringBuilder(text.Length); + foreach (char character in text) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '"': + builder.Append("\\\""); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + // NOTE (mristin): + // The other control characters can not stand in a JSON string as they + // are, so we write them as Unicode escapes, as JSON.stringify does. + if (character < 0x20) + { + builder.Append($"\\u{(int)character:x4}"); + } + else + { + builder.Append(character); + } + break; + } + } + return builder.ToString(); } private static readonly System.Text.RegularExpressions.Regex VariableNameRe = ( diff --git a/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/jsonization/jsonization.go index f03a53e6f..7ac3d5939 100644 --- a/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/aas_core_meta.v3/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/annotated_assignments/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/annotated_assignments/expected_output/jsonization/jsonization.go index 9201fa5df..af34ca4af 100644 --- a/dev/test_data/main/golang/expected/annotated_assignments/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/annotated_assignments/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/annotated_assignments/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/annotated_assignments/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/annotated_assignments/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/annotated_assignments/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/arithmetic/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/arithmetic/expected_output/jsonization/jsonization.go index a096852f6..7333dd563 100644 --- a/dev/test_data/main/golang/expected/arithmetic/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/arithmetic/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/arithmetic/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/arithmetic/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/arithmetic/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/arithmetic/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/assert_statements/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/assert_statements/expected_output/jsonization/jsonization.go index b73d20a8d..5ddec405b 100644 --- a/dev/test_data/main/golang/expected/assert_statements/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/assert_statements/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/assert_statements/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/assert_statements/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/assert_statements/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/assert_statements/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/assignment_targets/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/assignment_targets/expected_output/jsonization/jsonization.go index bfd7dd9ee..a9b9981bb 100644 --- a/dev/test_data/main/golang/expected/assignment_targets/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/assignment_targets/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/assignment_targets/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/assignment_targets/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/assignment_targets/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/assignment_targets/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/collections_in_returns/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/collections_in_returns/expected_output/jsonization/jsonization.go index e7eb16e3f..0b4fba705 100644 --- a/dev/test_data/main/golang/expected/collections_in_returns/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/collections_in_returns/expected_output/jsonization/jsonization.go @@ -356,13 +356,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/collections_in_returns/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/collections_in_returns/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/collections_in_returns/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/collections_in_returns/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/constrained_primitives/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/constrained_primitives/expected_output/jsonization/jsonization.go index 3311b16db..4d7dfdb9f 100644 --- a/dev/test_data/main/golang/expected/constrained_primitives/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/constrained_primitives/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/constrained_primitives/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/constrained_primitives/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/constrained_primitives/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/constrained_primitives/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/jsonization/jsonization.go index 85adc42e9..8bf6feafd 100644 --- a/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/custom_serialization_names/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/jsonization/jsonization.go index 901888611..a7e7275f2 100644 --- a/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/deep_class_hierarchy/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/empty_class/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/empty_class/expected_output/jsonization/jsonization.go index f341c1b6f..19678b154 100644 --- a/dev/test_data/main/golang/expected/empty_class/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/empty_class/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/empty_class/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/empty_class/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/empty_class/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/empty_class/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/enum/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/enum/expected_output/jsonization/jsonization.go index 18080f72e..7f669cc94 100644 --- a/dev/test_data/main/golang/expected/enum/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/enum/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/enum/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/enum/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/enum/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/enum/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/float_only/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/float_only/expected_output/jsonization/jsonization.go index 11e6bdce1..726c3c17f 100644 --- a/dev/test_data/main/golang/expected/float_only/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/float_only/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/float_only/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/float_only/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/float_only/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/float_only/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/for_statements/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/for_statements/expected_output/jsonization/jsonization.go index 5aba0c6a2..84808a245 100644 --- a/dev/test_data/main/golang/expected/for_statements/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/for_statements/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/for_statements/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/for_statements/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/for_statements/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/for_statements/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/if_statements/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/if_statements/expected_output/jsonization/jsonization.go index 11817a3ac..520e6f573 100644 --- a/dev/test_data/main/golang/expected/if_statements/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/if_statements/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/if_statements/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/if_statements/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/if_statements/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/if_statements/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/implementation_specific/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/implementation_specific/expected_output/jsonization/jsonization.go index 10f072db1..a15c96c31 100644 --- a/dev/test_data/main/golang/expected/implementation_specific/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/implementation_specific/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/implementation_specific/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/implementation_specific/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/implementation_specific/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/implementation_specific/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/int_length_interplay/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/int_length_interplay/expected_output/jsonization/jsonization.go index fa780036d..c1cfc7838 100644 --- a/dev/test_data/main/golang/expected/int_length_interplay/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/int_length_interplay/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/int_length_interplay/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/int_length_interplay/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/int_length_interplay/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/int_length_interplay/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/jsonization/jsonization.go index 0d547d7ee..a7d9524f3 100644 --- a/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/int_parsing_and_lstrip/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/is_instance/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/is_instance/expected_output/jsonization/jsonization.go index 2556effc5..159a47637 100644 --- a/dev/test_data/main/golang/expected/is_instance/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/is_instance/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/is_instance/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/is_instance/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/is_instance/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/is_instance/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( 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 6b9c42270..65922005c 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 @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/json_types/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/json_types/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/json_types/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/json_types/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( 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 526130413..3a7389be7 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 @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/json_types_in_collections/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( 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 e3f27faba..1b3b32aee 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 @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/json_types_in_invariants/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/list_of_classes/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/list_of_classes/expected_output/jsonization/jsonization.go index f5336b543..76e54a709 100644 --- a/dev/test_data/main/golang/expected/list_of_classes/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/list_of_classes/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/list_of_classes/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/list_of_classes/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/list_of_classes/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/list_of_classes/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/jsonization/jsonization.go index 002f949d9..62fd2c254 100644 --- a/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/list_of_constrained_primitives/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/list_of_enums/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/list_of_enums/expected_output/jsonization/jsonization.go index 05b9d65cb..41300eed2 100644 --- a/dev/test_data/main/golang/expected/list_of_enums/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/list_of_enums/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/list_of_enums/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/list_of_enums/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/list_of_enums/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/list_of_enums/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/list_of_primitives/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/list_of_primitives/expected_output/jsonization/jsonization.go index 2f2dec411..2cfa3462e 100644 --- a/dev/test_data/main/golang/expected/list_of_primitives/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/list_of_primitives/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/list_of_primitives/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/list_of_primitives/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/list_of_primitives/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/list_of_primitives/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/method_references/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/method_references/expected_output/jsonization/jsonization.go index ad21872e5..f43f03439 100644 --- a/dev/test_data/main/golang/expected/method_references/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/method_references/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/method_references/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/method_references/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/method_references/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/method_references/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/jsonization/jsonization.go index d3ff1dd17..9a2691191 100644 --- a/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/nested_any_in_all/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/nested_collections/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/nested_collections/expected_output/jsonization/jsonization.go index 58aae086c..9ea1d1337 100644 --- a/dev/test_data/main/golang/expected/nested_collections/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/nested_collections/expected_output/jsonization/jsonization.go @@ -356,13 +356,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/nested_collections/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/nested_collections/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/nested_collections/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/nested_collections/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/optional_arguments/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/optional_arguments/expected_output/jsonization/jsonization.go index 25944983a..4427dce0e 100644 --- a/dev/test_data/main/golang/expected/optional_arguments/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/optional_arguments/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/optional_arguments/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/optional_arguments/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/optional_arguments/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/optional_arguments/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/primitive_types/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/primitive_types/expected_output/jsonization/jsonization.go index 3311b16db..4d7dfdb9f 100644 --- a/dev/test_data/main/golang/expected/primitive_types/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/primitive_types/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/primitive_types/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/primitive_types/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/primitive_types/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/primitive_types/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/problematic_keywords/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/problematic_keywords/expected_output/jsonization/jsonization.go index d42b77094..3ddf76b4e 100644 --- a/dev/test_data/main/golang/expected/problematic_keywords/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/problematic_keywords/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/problematic_keywords/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/problematic_keywords/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/problematic_keywords/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/problematic_keywords/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/sets/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/sets/expected_output/jsonization/jsonization.go index 2020e38dc..3ea20a217 100644 --- a/dev/test_data/main/golang/expected/sets/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/sets/expected_output/jsonization/jsonization.go @@ -356,13 +356,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/sets/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/sets/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/sets/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/sets/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/jsonization/jsonization.go index 1f272da45..c9a888093 100644 --- a/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/string_slicing_and_find/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/switch_statements/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/switch_statements/expected_output/jsonization/jsonization.go index 076785784..142559b75 100644 --- a/dev/test_data/main/golang/expected/switch_statements/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/switch_statements/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/switch_statements/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/switch_statements/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/switch_statements/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/switch_statements/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/transpiled_methods/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/transpiled_methods/expected_output/jsonization/jsonization.go index e8e4cf96c..9e9f81c32 100644 --- a/dev/test_data/main/golang/expected/transpiled_methods/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/transpiled_methods/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/transpiled_methods/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/transpiled_methods/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/transpiled_methods/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/transpiled_methods/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/tuples/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/tuples/expected_output/jsonization/jsonization.go index ed8177c7b..6802f3890 100644 --- a/dev/test_data/main/golang/expected/tuples/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/tuples/expected_output/jsonization/jsonization.go @@ -356,13 +356,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/tuples/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/tuples/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/tuples/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/tuples/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/unions/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/unions/expected_output/jsonization/jsonization.go index 42ed74c5f..e923e7304 100644 --- a/dev/test_data/main/golang/expected/unions/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/unions/expected_output/jsonization/jsonization.go @@ -355,13 +355,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/unions/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/unions/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/unions/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/unions/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/golang/expected/visibility/expected_output/jsonization/jsonization.go b/dev/test_data/main/golang/expected/visibility/expected_output/jsonization/jsonization.go index 919f5be47..95b0cb4aa 100644 --- a/dev/test_data/main/golang/expected/visibility/expected_output/jsonization/jsonization.go +++ b/dev/test_data/main/golang/expected/visibility/expected_output/jsonization/jsonization.go @@ -354,13 +354,16 @@ func checkModelType( } if modelType != expected { + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. err = newDeserializationError( fmt.Sprintf( "Expected the model type '%s', but got %s", expected, modelType, ), - ).prependName("modelType") + ) } return } diff --git a/dev/test_data/main/golang/expected/visibility/expected_output/reporting/reporting.go b/dev/test_data/main/golang/expected/visibility/expected_output/reporting/reporting.go index f24ba0cbe..86698a098 100644 --- a/dev/test_data/main/golang/expected/visibility/expected_output/reporting/reporting.go +++ b/dev/test_data/main/golang/expected/visibility/expected_output/reporting/reporting.go @@ -6,6 +6,7 @@ package reporting import ( "fmt" + "regexp" "strings" "strconv" ) @@ -79,21 +80,29 @@ func (p *Path) OverSegments(callback func(interface{})) { } } -// Translate the path to a JSON path. +// Match the property names which a JSON path writes after a dot; the other +// names are written in brackets as JSON strings, *e.g.*, `["$eq"]`. +var variableNameRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +// Render the path with dots and subscripts. // -// The name segments are expected to denote the names of the properties -// in JSON property names, not Golang property names. -func ToJSONPath(p *Path) string { +// If `bracketNonVariableNames` is set, the names which are no variable names +// are written in brackets as JSON strings. +func renderPath(p *Path, bracketNonVariableNames bool) string { var b strings.Builder i := 0 p.OverSegments(func(s interface{}) { switch v := s.(type) { case *NameSegment: - if i == 0 { + if !bracketNonVariableNames || variableNameRe.MatchString(v.Name) { + if i > 0 { + b.WriteString(".") + } b.WriteString(v.Name) } else { - b.WriteString(".") - b.WriteString(v.Name) + b.WriteString(`["`) + b.WriteString(escapeForJSONString(v.Name)) + b.WriteString(`"]`) } case *IndexSegment: b.WriteString("[") @@ -117,34 +126,59 @@ func ToJSONPath(p *Path) string { return b.String() } +// Translate the path to a JSON path. +// +// The name segments are expected to denote the names of the properties +// in JSON property names, not Golang property names. +func ToJSONPath(p *Path) string { + return renderPath(p, true) +} + // Translate the path to a Golang access path. // // The name segments are expected to denote the names of the properties // in Golang, not JSON property names. func ToGolangPath(p *Path) string { // NOTE(mristin): - // We re-use JSON path formatting as implementation, but introduce - // a separate function to signal to the reader in which form - // the name segments are expected (Golang property names instead - // of JSON property names). - return ToJSONPath(p) + // The name segments of a Golang path are accessors such as `Value()`, + // which we always write after a dot. + return renderPath(p, false) } -var replacerForJSONString = strings.NewReplacer( - "\\", "\\\\", - "\"", "\\\"", - "\b", "\\b", - "\f", "\\f", - "\n", "\\n", - "\r", "\\r", - "\t", "\\t", -) - // Escape the characters which a JSON string may not hold as they are. +// +// The control characters without a short escape are written as Unicode +// escapes, as JSON.stringify does. func escapeForJSONString( text string, ) string { - return replacerForJSONString.Replace(text) + var b strings.Builder + b.Grow(len(text)) + for _, r := range text { + switch r { + case '\\': + b.WriteString(`\\`) + case '"': + b.WriteString(`\"`) + case '\b': + b.WriteString(`\b`) + case '\f': + b.WriteString(`\f`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case '\t': + b.WriteString(`\t`) + default: + if r < 0x20 { + fmt.Fprintf(&b, `\u%04x`, r) + } else { + b.WriteRune(r) + } + } + } + return b.String() } var replacerForXPath = strings.NewReplacer( diff --git a/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/jsonization/Jsonization.java b/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/jsonization/Jsonization.java index b73ca856b..32155d805 100644 --- a/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/reporting/Reporting.java b/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/reporting/Reporting.java index c111ccd04..5db0b0aa9 100644 --- a/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/aas_core_meta.v3/expected_output/src/main/java/aas_core/aas3_0/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/jsonization/Jsonization.java index c59499cf4..4f037f678 100644 --- a/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/annotated_assignments/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/arithmetic/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/arithmetic/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/arithmetic/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/arithmetic/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 6b9394b85..c2c7ae29d 100644 --- a/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/assert_statements/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/assignment_targets/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/assignment_targets/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/assignment_targets/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/assignment_targets/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/collections_in_returns/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/collections_in_returns/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/collections_in_returns/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/collections_in_returns/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/constants/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/constants/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/constants/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/constants/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/custom_serialization_names/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/custom_serialization_names/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/custom_serialization_names/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/custom_serialization_names/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/jsonization/Jsonization.java index d14af9f5b..a9f33e6e3 100644 --- a/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -225,7 +225,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/deep_class_hierarchy/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/empty_class/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/empty_class/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/empty_class/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/empty_class/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/enum/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/enum/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/enum/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/enum/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/float_only/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/float_only/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/float_only/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/float_only/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/for_statements/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/for_statements/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/for_statements/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/for_statements/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 5edf08fa0..0931950af 100644 --- a/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/if_statements/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 44b027892..80c7f842f 100644 --- a/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/implementation_specific/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/int_length_interplay/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/int_length_interplay/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/int_length_interplay/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/int_length_interplay/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/int_parsing_and_lstrip/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/int_parsing_and_lstrip/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/int_parsing_and_lstrip/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/int_parsing_and_lstrip/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 5239f838c..e70a51dd0 100644 --- a/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/is_instance/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/json_types/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/json_types_in_collections/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/json_types_in_invariants/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/jsonization/Jsonization.java index ce7abf20d..cf21b314c 100644 --- a/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/list_of_classes/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/list_of_constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/list_of_constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/list_of_constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/list_of_constrained_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/list_of_enums/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/list_of_enums/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/list_of_enums/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/list_of_enums/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/list_of_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/list_of_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/list_of_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/list_of_primitives/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/jsonization/Jsonization.java index a2505aa75..ce618fb36 100644 --- a/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -225,7 +225,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/method_references/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/jsonization/Jsonization.java index b79972996..3261c6249 100644 --- a/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/nested_any_in_all/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 95eaf8ab0..16752b36c 100644 --- a/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/nested_collections/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/optional_arguments/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/optional_arguments/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/optional_arguments/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/optional_arguments/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/primitive_types/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/primitive_types/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/primitive_types/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/primitive_types/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/problematic_keywords/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/problematic_keywords/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/problematic_keywords/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/problematic_keywords/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/sets/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/sets/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/sets/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/sets/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/string_slicing_and_find/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/string_slicing_and_find/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/string_slicing_and_find/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/string_slicing_and_find/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/switch_statements/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/switch_statements/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/switch_statements/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/switch_statements/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/jsonization/Jsonization.java index b22b02f10..140df4cb2 100644 --- a/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/transpiled_methods/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 2c67f5b98..505a50372 100644 --- a/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/tuples/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 5adab87e4..2d1bd2198 100644 --- a/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -246,7 +246,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/unions/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** diff --git a/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/jsonization/Jsonization.java b/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/jsonization/Jsonization.java index 6c105dffe..10d9dc511 100644 --- a/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/jsonization/Jsonization.java +++ b/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/jsonization/Jsonization.java @@ -225,7 +225,9 @@ private static Reporting.Error checkModelType(JsonNode node, String expected) { final Reporting.Error error = new Reporting.Error( "Expected the model type '" + expected + "', " + "but got '" + modelType + "'"); - error.prependSegment(new Reporting.NameSegment("modelType")); + // NOTE (mristin): + // A wrong model type is reported at the instance, as an unknown one is, + // and as in the other SDKs. return error; } diff --git a/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/reporting/Reporting.java b/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/reporting/Reporting.java index 248fd865a..647e421f2 100644 --- a/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/reporting/Reporting.java +++ b/dev/test_data/main/java/expected/visibility/expected_output/src/main/java/dummy/reporting/Reporting.java @@ -115,18 +115,45 @@ public static String generateJsonPath(Collection segments) { /** * Escape the characters which a JSON string may not hold as they are. * - *

Mind the order: the backslash has to go first, or the backslash which - * the replacements below introduce would be escaped a second time. + *

The control characters without a short escape are written as Unicode + * escapes, as {@code JSON.stringify} does. */ private static String escapeForJsonString(String text) { - return text - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\b", "\\b") - .replace("\f", "\\f") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("\t", "\\t"); + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char character = text.charAt(i); + switch (character) { + case '\\': + builder.append("\\\\"); + break; + case '"': + builder.append("\\\""); + break; + case '\b': + builder.append("\\b"); + break; + case '\f': + builder.append("\\f"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + case '\t': + builder.append("\\t"); + break; + default: + if (character < 0x20) { + builder.append(String.format("\\u%04x", (int) character)); + } else { + builder.append(character); + } + break; + } + } + return builder.toString(); } /** 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 6a2729d71..af5d0357f 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 @@ -13,6 +13,8 @@ import base64 import collections.abc +import json +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -293,10 +306,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -737,12 +755,11 @@ def extension_from_jsonable( the_value: Optional[str] = None the_refers_to: Optional[List[our_types.Reference]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'semanticId': + if key == 'semanticId': the_semantic_id = reference_from_jsonable(jsonable_value) elif key == 'supplementalSemanticIds': the_supplemental_semantic_ids = ( @@ -757,15 +774,19 @@ def extension_from_jsonable( elif key == 'refersTo': the_refers_to = _list_of__reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -916,12 +937,11 @@ def administrative_information_from_jsonable( the_creator: Optional[our_types.Reference] = None the_template_id: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'embeddedDataSpecifications': + if key == 'embeddedDataSpecifications': the_embedded_data_specifications = ( _list_of__embedded_data_specification_from_jsonable(jsonable_value) ) @@ -934,15 +954,19 @@ def administrative_information_from_jsonable( elif key == 'templateId': the_template_id = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.AdministrativeInformation( the_embedded_data_specifications, the_version, @@ -1017,12 +1041,11 @@ def qualifier_from_jsonable( the_value: Optional[str] = None the_value_id: Optional[our_types.Reference] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'semanticId': + if key == 'semanticId': the_semantic_id = reference_from_jsonable(jsonable_value) elif key == 'supplementalSemanticIds': the_supplemental_semantic_ids = ( @@ -1039,15 +1062,19 @@ def qualifier_from_jsonable( elif key == 'valueId': the_value_id = reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_type is None: raise DeserializationException( "The required property 'type' is missing" @@ -1083,6 +1110,17 @@ def asset_administration_shell_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'AssetAdministrationShell': raise DeserializationException( f"Expected modelType to be 'AssetAdministrationShell', " @@ -1101,6 +1139,8 @@ def asset_administration_shell_from_jsonable( the_asset_information: Optional[our_types.AssetInformation] = None the_submodels: Optional[List[our_types.Reference]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -1137,15 +1177,19 @@ def asset_administration_shell_from_jsonable( elif key == 'submodels': the_submodels = _list_of__reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_id is None: raise DeserializationException( "The required property 'id' is missing" @@ -1190,12 +1234,11 @@ def asset_information_from_jsonable( the_asset_type: Optional[str] = None the_default_thumbnail: Optional[our_types.Resource] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'assetKind': + if key == 'assetKind': the_asset_kind = asset_kind_from_jsonable(jsonable_value) elif key == 'globalAssetId': the_global_asset_id = _str_from_jsonable(jsonable_value) @@ -1208,15 +1251,19 @@ def asset_information_from_jsonable( elif key == 'defaultThumbnail': the_default_thumbnail = resource_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_asset_kind is None: raise DeserializationException( "The required property 'assetKind' is missing" @@ -1247,25 +1294,28 @@ def resource_from_jsonable( the_path: Optional[str] = None the_content_type: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'path': + if key == 'path': the_path = _str_from_jsonable(jsonable_value) elif key == 'contentType': the_content_type = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_path is None: raise DeserializationException( "The required property 'path' is missing" @@ -1322,12 +1372,11 @@ def specific_asset_id_from_jsonable( the_value: Optional[str] = None the_external_subject_id: Optional[our_types.Reference] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'semanticId': + if key == 'semanticId': the_semantic_id = reference_from_jsonable(jsonable_value) elif key == 'supplementalSemanticIds': the_supplemental_semantic_ids = ( @@ -1340,15 +1389,19 @@ def specific_asset_id_from_jsonable( elif key == 'externalSubjectId': the_external_subject_id = reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -1382,6 +1435,17 @@ def submodel_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Submodel': raise DeserializationException( f"Expected modelType to be 'Submodel', " @@ -1402,6 +1466,8 @@ def submodel_from_jsonable( the_embedded_data_specifications: Optional[List[our_types.EmbeddedDataSpecification]] = None the_submodel_elements: Optional[List[our_types.SubmodelElement]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -1446,15 +1512,19 @@ def submodel_from_jsonable( _list_of__submodel_element_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_id is None: raise DeserializationException( "The required property 'id' is missing" @@ -1545,6 +1615,8 @@ def _relationship_element_from_jsonable_without_dispatch( the_first: Optional[our_types.Reference] = None the_second: Optional[our_types.Reference] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -1581,15 +1653,19 @@ def _relationship_element_from_jsonable_without_dispatch( elif key == 'second': the_second = reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_first is None: raise DeserializationException( "The required property 'first' is missing" @@ -1655,6 +1731,17 @@ def submodel_element_list_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'SubmodelElementList': raise DeserializationException( f"Expected modelType to be 'SubmodelElementList', " @@ -1676,6 +1763,8 @@ def submodel_element_list_from_jsonable( the_value_type_list_element: Optional[our_types.DataTypeDefXSD] = None the_value: Optional[List[our_types.SubmodelElement]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -1722,15 +1811,19 @@ def submodel_element_list_from_jsonable( elif key == 'value': the_value = _list_of__submodel_element_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_type_value_list_element is None: raise DeserializationException( "The required property 'typeValueListElement' is missing" @@ -1768,6 +1861,17 @@ def submodel_element_collection_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'SubmodelElementCollection': raise DeserializationException( f"Expected modelType to be 'SubmodelElementCollection', " @@ -1785,6 +1889,8 @@ def submodel_element_collection_from_jsonable( the_embedded_data_specifications: Optional[List[our_types.EmbeddedDataSpecification]] = None the_value: Optional[List[our_types.SubmodelElement]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -1819,15 +1925,19 @@ def submodel_element_collection_from_jsonable( elif key == 'value': the_value = _list_of__submodel_element_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.SubmodelElementCollection( the_extensions, the_category, @@ -1874,6 +1984,17 @@ def property_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Property': raise DeserializationException( f"Expected modelType to be 'Property', " @@ -1893,6 +2014,8 @@ def property_from_jsonable( the_value: Optional[str] = None the_value_id: Optional[our_types.Reference] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -1931,15 +2054,19 @@ def property_from_jsonable( elif key == 'valueId': the_value_id = reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_value_type is None: raise DeserializationException( "The required property 'valueType' is missing" @@ -1975,6 +2102,17 @@ def multi_language_property_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'MultiLanguageProperty': raise DeserializationException( f"Expected modelType to be 'MultiLanguageProperty', " @@ -1993,6 +2131,8 @@ def multi_language_property_from_jsonable( the_value: Optional[List[our_types.LangStringTextType]] = None the_value_id: Optional[our_types.Reference] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2029,15 +2169,19 @@ def multi_language_property_from_jsonable( elif key == 'valueId': the_value_id = reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.MultiLanguageProperty( the_extensions, the_category, @@ -2067,6 +2211,17 @@ def range_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Range': raise DeserializationException( f"Expected modelType to be 'Range', " @@ -2086,6 +2241,8 @@ def range_from_jsonable( the_min: Optional[str] = None the_max: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2124,15 +2281,19 @@ def range_from_jsonable( elif key == 'max': the_max = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_value_type is None: raise DeserializationException( "The required property 'valueType' is missing" @@ -2168,6 +2329,17 @@ def reference_element_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ReferenceElement': raise DeserializationException( f"Expected modelType to be 'ReferenceElement', " @@ -2185,6 +2357,8 @@ def reference_element_from_jsonable( the_embedded_data_specifications: Optional[List[our_types.EmbeddedDataSpecification]] = None the_value: Optional[our_types.Reference] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2219,15 +2393,19 @@ def reference_element_from_jsonable( elif key == 'value': the_value = reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.ReferenceElement( the_extensions, the_category, @@ -2256,6 +2434,17 @@ def blob_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Blob': raise DeserializationException( f"Expected modelType to be 'Blob', " @@ -2274,6 +2463,8 @@ def blob_from_jsonable( the_value: Optional[bytes] = None the_content_type: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2310,15 +2501,19 @@ def blob_from_jsonable( elif key == 'contentType': the_content_type = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_content_type is None: raise DeserializationException( "The required property 'contentType' is missing" @@ -2353,6 +2548,17 @@ def file_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'File': raise DeserializationException( f"Expected modelType to be 'File', " @@ -2371,6 +2577,8 @@ def file_from_jsonable( the_value: Optional[str] = None the_content_type: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2407,15 +2615,19 @@ def file_from_jsonable( elif key == 'contentType': the_content_type = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_content_type is None: raise DeserializationException( "The required property 'contentType' is missing" @@ -2450,6 +2662,17 @@ def annotated_relationship_element_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'AnnotatedRelationshipElement': raise DeserializationException( f"Expected modelType to be 'AnnotatedRelationshipElement', " @@ -2469,6 +2692,8 @@ def annotated_relationship_element_from_jsonable( the_second: Optional[our_types.Reference] = None the_annotations: Optional[List[our_types.DataElement]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2507,15 +2732,19 @@ def annotated_relationship_element_from_jsonable( elif key == 'annotations': the_annotations = _list_of__data_element_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_first is None: raise DeserializationException( "The required property 'first' is missing" @@ -2556,6 +2785,17 @@ def entity_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Entity': raise DeserializationException( f"Expected modelType to be 'Entity', " @@ -2576,6 +2816,8 @@ def entity_from_jsonable( the_global_asset_id: Optional[str] = None the_specific_asset_ids: Optional[List[our_types.SpecificAssetID]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2618,15 +2860,19 @@ def entity_from_jsonable( _list_of__specific_asset_id_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_entity_type is None: raise DeserializationException( "The required property 'entityType' is missing" @@ -2749,12 +2995,11 @@ def event_payload_from_jsonable( the_time_stamp: Optional[str] = None the_payload: Optional[bytes] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'source': + if key == 'source': the_source = reference_from_jsonable(jsonable_value) elif key == 'sourceSemanticId': the_source_semantic_id = reference_from_jsonable(jsonable_value) @@ -2771,15 +3016,19 @@ def event_payload_from_jsonable( elif key == 'payload': the_payload = _bytes_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_source is None: raise DeserializationException( "The required property 'source' is missing" @@ -2839,6 +3088,17 @@ def basic_event_element_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'BasicEventElement': raise DeserializationException( f"Expected modelType to be 'BasicEventElement', " @@ -2863,6 +3123,8 @@ def basic_event_element_from_jsonable( the_min_interval: Optional[str] = None the_max_interval: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -2911,15 +3173,19 @@ def basic_event_element_from_jsonable( elif key == 'maxInterval': the_max_interval = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_observed is None: raise DeserializationException( "The required property 'observed' is missing" @@ -2970,6 +3236,17 @@ def operation_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Operation': raise DeserializationException( f"Expected modelType to be 'Operation', " @@ -2989,6 +3266,8 @@ def operation_from_jsonable( the_output_variables: Optional[List[our_types.OperationVariable]] = None the_inoutput_variables: Optional[List[our_types.OperationVariable]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -3033,15 +3312,19 @@ def operation_from_jsonable( _list_of__operation_variable_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.Operation( the_extensions, the_category, @@ -3073,23 +3356,26 @@ def operation_variable_from_jsonable( the_value: Optional[our_types.SubmodelElement] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'value': + if key == 'value': the_value = submodel_element_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_value is None: raise DeserializationException( "The required property 'value' is missing" @@ -3114,6 +3400,17 @@ def capability_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Capability': raise DeserializationException( f"Expected modelType to be 'Capability', " @@ -3130,6 +3427,8 @@ def capability_from_jsonable( the_qualifiers: Optional[List[our_types.Qualifier]] = None the_embedded_data_specifications: Optional[List[our_types.EmbeddedDataSpecification]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -3162,15 +3461,19 @@ def capability_from_jsonable( _list_of__embedded_data_specification_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.Capability( the_extensions, the_category, @@ -3198,6 +3501,17 @@ def concept_description_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ConceptDescription': raise DeserializationException( f"Expected modelType to be 'ConceptDescription', " @@ -3214,6 +3528,8 @@ def concept_description_from_jsonable( the_embedded_data_specifications: Optional[List[our_types.EmbeddedDataSpecification]] = None the_is_case_of: Optional[List[our_types.Reference]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -3246,15 +3562,19 @@ def concept_description_from_jsonable( elif key == 'isCaseOf': the_is_case_of = _list_of__reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_id is None: raise DeserializationException( "The required property 'id' is missing" @@ -3316,27 +3636,30 @@ def reference_from_jsonable( the_referred_semantic_id: Optional[our_types.Reference] = None the_keys: Optional[List[our_types.Key]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'type': + if key == 'type': the_type = reference_types_from_jsonable(jsonable_value) elif key == 'referredSemanticId': the_referred_semantic_id = reference_from_jsonable(jsonable_value) elif key == 'keys': the_keys = _list_of__key_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_type is None: raise DeserializationException( "The required property 'type' is missing" @@ -3370,25 +3693,28 @@ def key_from_jsonable( the_type: Optional[our_types.KeyTypes] = None the_value: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'type': + if key == 'type': the_type = key_types_from_jsonable(jsonable_value) elif key == 'value': the_value = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_type is None: raise DeserializationException( "The required property 'type' is missing" @@ -3491,25 +3817,28 @@ def lang_string_name_type_from_jsonable( the_language: Optional[str] = None the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'language': + if key == 'language': the_language = _str_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_language is None: raise DeserializationException( "The required property 'language' is missing" @@ -3542,25 +3871,28 @@ def lang_string_text_type_from_jsonable( the_language: Optional[str] = None the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'language': + if key == 'language': the_language = _str_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_language is None: raise DeserializationException( "The required property 'language' is missing" @@ -3594,12 +3926,11 @@ def environment_from_jsonable( the_submodels: Optional[List[our_types.Submodel]] = None the_concept_descriptions: Optional[List[our_types.ConceptDescription]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'assetAdministrationShells': + if key == 'assetAdministrationShells': the_asset_administration_shells = ( _list_of__asset_administration_shell_from_jsonable(jsonable_value) ) @@ -3610,15 +3941,19 @@ def environment_from_jsonable( _list_of__concept_description_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.Environment( the_asset_administration_shells, the_submodels, @@ -3660,27 +3995,30 @@ def embedded_data_specification_from_jsonable( the_data_specification: Optional[our_types.Reference] = None the_data_specification_content: Optional[our_types.DataSpecificationContent] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'dataSpecification': + if key == 'dataSpecification': the_data_specification = reference_from_jsonable(jsonable_value) elif key == 'dataSpecificationContent': the_data_specification_content = ( data_specification_content_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_data_specification is None: raise DeserializationException( "The required property 'dataSpecification' is missing" @@ -3741,12 +4079,11 @@ def level_type_from_jsonable( the_typ: Optional[bool] = None the_max: Optional[bool] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'min': + if key == 'min': the_min = _bool_from_jsonable(jsonable_value) elif key == 'nom': the_nom = _bool_from_jsonable(jsonable_value) @@ -3755,15 +4092,19 @@ def level_type_from_jsonable( elif key == 'max': the_max = _bool_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_min is None: raise DeserializationException( "The required property 'min' is missing" @@ -3808,25 +4149,28 @@ def value_reference_pair_from_jsonable( the_value: Optional[str] = None the_value_id: Optional[our_types.Reference] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'value': + if key == 'value': the_value = _str_from_jsonable(jsonable_value) elif key == 'valueId': the_value_id = reference_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_value is None: raise DeserializationException( "The required property 'value' is missing" @@ -3858,25 +4202,28 @@ def value_list_from_jsonable( the_value_reference_pairs: Optional[List[our_types.ValueReferencePair]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'valueReferencePairs': + if key == 'valueReferencePairs': the_value_reference_pairs = ( _list_of__value_reference_pair_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_value_reference_pairs is None: raise DeserializationException( "The required property 'valueReferencePairs' is missing" @@ -3903,25 +4250,28 @@ def lang_string_preferred_name_type_iec_61360_from_jsonable( the_language: Optional[str] = None the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'language': + if key == 'language': the_language = _str_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_language is None: raise DeserializationException( "The required property 'language' is missing" @@ -3954,25 +4304,28 @@ def lang_string_short_name_type_iec_61360_from_jsonable( the_language: Optional[str] = None the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'language': + if key == 'language': the_language = _str_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_language is None: raise DeserializationException( "The required property 'language' is missing" @@ -4005,25 +4358,28 @@ def lang_string_definition_type_iec_61360_from_jsonable( the_language: Optional[str] = None the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'language': + if key == 'language': the_language = _str_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_language is None: raise DeserializationException( "The required property 'language' is missing" @@ -4054,6 +4410,17 @@ def data_specification_iec_61360_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'DataSpecificationIec61360': raise DeserializationException( f"Expected modelType to be 'DataSpecificationIec61360', " @@ -4073,6 +4440,8 @@ def data_specification_iec_61360_from_jsonable( the_value: Optional[str] = None the_level_type: Optional[our_types.LevelType] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -4109,15 +4478,19 @@ def data_specification_iec_61360_from_jsonable( elif key == 'levelType': the_level_type = level_type_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_preferred_name is None: raise DeserializationException( "The required property 'preferredName' is missing" @@ -4422,7 +4795,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/reporting.py b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/reporting.py index 470ba39ef..d72fe1438 100644 --- a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/reporting.py +++ b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/xmlcommon.py b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/xmlcommon.py index 73ca121f1..c01bf0995 100644 --- a/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/xmlcommon.py +++ b/dev/test_data/main/python/expected/aas_core_meta.v3/expected_output/aas_core3/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 28364384a..1ccebcd52 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 @@ -13,6 +13,8 @@ import base64 import collections.abc +import json +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -293,10 +306,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -488,23 +506,26 @@ def item_from_jsonable( the_name: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'name': + if key == 'name': the_name = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -547,6 +568,17 @@ def child_a_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ChildA': raise DeserializationException( f"Expected modelType to be 'ChildA', " @@ -556,6 +588,8 @@ def child_a_from_jsonable( the_optional_text: Optional[str] = None the_a_only: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -566,15 +600,19 @@ def child_a_from_jsonable( elif key == 'aOnly': the_a_only = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_a_only is None: raise DeserializationException( "The required property 'aOnly' is missing" @@ -600,6 +638,17 @@ def child_b_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ChildB': raise DeserializationException( f"Expected modelType to be 'ChildB', " @@ -609,6 +658,8 @@ def child_b_from_jsonable( the_optional_text: Optional[str] = None the_b_only: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -619,15 +670,19 @@ def child_b_from_jsonable( elif key == 'bOnly': the_b_only = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_b_only is None: raise DeserializationException( "The required property 'bOnly' is missing" @@ -726,12 +781,11 @@ def something_from_jsonable( the_optional_data: Optional[bytes] = None the_optional_member: Optional[our_types.ParentOrItem] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'text': + if key == 'text': the_text = _str_from_jsonable(jsonable_value) elif key == 'number': the_number = _int_from_jsonable(jsonable_value) @@ -762,15 +816,19 @@ def something_from_jsonable( elif key == 'optionalMember': the_optional_member = parent_or_item_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_text is None: raise DeserializationException( "The required property 'text' is missing" @@ -916,7 +974,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/annotated_assignments/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 bfffd09bb..ffeae427e 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 @@ -12,7 +12,9 @@ import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -375,12 +388,11 @@ def something_from_jsonable( the_optional_number: Optional[int] = None the_optional_ratio: Optional[float] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'even': + if key == 'even': the_even = _int_from_jsonable(jsonable_value) elif key == 'offset': the_offset = _int_from_jsonable(jsonable_value) @@ -403,15 +415,19 @@ def something_from_jsonable( elif key == 'optionalRatio': the_optional_ratio = _float_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_even is None: raise DeserializationException( "The required property 'even' is missing" @@ -520,7 +536,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/arithmetic/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 f4d60a9ca..6a4aa55af 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -424,6 +442,17 @@ def child_a_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ChildA': raise DeserializationException( f"Expected modelType to be 'ChildA', " @@ -433,6 +462,8 @@ def child_a_from_jsonable( the_number: Optional[int] = None the_a_only: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -443,15 +474,19 @@ def child_a_from_jsonable( elif key == 'aOnly': the_a_only = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_number is None: raise DeserializationException( "The required property 'number' is missing" @@ -482,6 +517,17 @@ def child_b_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ChildB': raise DeserializationException( f"Expected modelType to be 'ChildB', " @@ -490,6 +536,8 @@ def child_b_from_jsonable( the_number: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -498,15 +546,19 @@ def child_b_from_jsonable( elif key == 'number': the_number = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_number is None: raise DeserializationException( "The required property 'number' is missing" @@ -536,12 +588,11 @@ def something_from_jsonable( the_parent: Optional[our_types.Parent] = None the_optional_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'number': + if key == 'number': the_number = _int_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) @@ -552,15 +603,19 @@ def something_from_jsonable( elif key == 'optionalText': the_optional_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_number is None: raise DeserializationException( "The required property 'number' is missing" @@ -649,7 +704,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/assert_statements/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 2aa7c2626..705108446 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -397,12 +410,11 @@ def item_from_jsonable( the_texts: Optional[List[str]] = None the_maybe_texts: Optional[List[str]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'text': + if key == 'text': the_text = _str_from_jsonable(jsonable_value) elif key == 'maybeText': the_maybe_text = _str_from_jsonable(jsonable_value) @@ -413,15 +425,19 @@ def item_from_jsonable( elif key == 'maybeTexts': the_maybe_texts = _list_of__str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_text is None: raise DeserializationException( "The required property 'text' is missing" @@ -457,25 +473,28 @@ def something_from_jsonable( the_items: Optional[List[our_types.Item]] = None the_maybe_item: Optional[our_types.Item] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'items': + if key == 'items': the_items = _list_of__item_from_jsonable(jsonable_value) elif key == 'maybeItem': the_maybe_item = item_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_items is None: raise DeserializationException( "The required property 'items' is missing" @@ -535,7 +554,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/assignment_targets/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 04eafe37a..302bedde1 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -472,27 +485,30 @@ def something_from_jsonable( the_kinds: Optional[List[our_types.Kind]] = None the_optional_texts: Optional[List[str]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'texts': + if key == 'texts': the_texts = _list_of__str_from_jsonable(jsonable_value) elif key == 'kinds': the_kinds = _list_of__kind_from_jsonable(jsonable_value) elif key == 'optionalTexts': the_optional_texts = _list_of__str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_texts is None: raise DeserializationException( "The required property 'texts' is missing" @@ -525,23 +541,26 @@ def grouped_from_jsonable( the_groups: Optional[List[Set[str]]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'groups': + if key == 'groups': the_groups = _list_of__set_of__str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_groups is None: raise DeserializationException( "The required property 'groups' is missing" @@ -600,7 +619,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/collections_in_returns/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 98b5e9b55..2fd6f7c80 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 @@ -13,7 +13,9 @@ import base64 import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -140,6 +142,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -165,21 +172,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -407,12 +420,11 @@ def something_from_jsonable( the_some_string: Optional[str] = None the_some_bytes: Optional[bytes] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someBool': + if key == 'someBool': the_some_bool = _bool_from_jsonable(jsonable_value) elif key == 'someInt': the_some_int = _int_from_jsonable(jsonable_value) @@ -423,15 +435,19 @@ def something_from_jsonable( elif key == 'someBytes': the_some_bytes = _bytes_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_bool is None: raise DeserializationException( "The required property 'someBool' is missing" @@ -514,7 +530,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/constrained_primitives/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 d8912c556..b127de4e9 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -302,25 +315,28 @@ def query_condition_from_jsonable( the_eq: Optional[str] = None the_not_eq: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == '$eq': + if key == '$eq': the_eq = _str_from_jsonable(jsonable_value) elif key == '$ne': the_not_eq = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.QueryCondition( the_eq, the_not_eq @@ -375,7 +391,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/custom_serialization_names/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 f5c48a42a..97434a435 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -402,6 +420,8 @@ def _branch_from_jsonable_without_dispatch( the_identifier: Optional[str] = None the_description: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -412,15 +432,19 @@ def _branch_from_jsonable_without_dispatch( elif key == 'description': the_description = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_identifier is None: raise DeserializationException( "The required property 'identifier' is missing" @@ -479,6 +503,8 @@ def _leaf_from_jsonable_without_dispatch( the_description: Optional[str] = None the_value: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -491,15 +517,19 @@ def _leaf_from_jsonable_without_dispatch( elif key == 'value': the_value = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_identifier is None: raise DeserializationException( "The required property 'identifier' is missing" @@ -536,6 +566,17 @@ def blossom_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Blossom': raise DeserializationException( f"Expected modelType to be 'Blossom', " @@ -547,6 +588,8 @@ def blossom_from_jsonable( the_value: Optional[int] = None the_details: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -561,15 +604,19 @@ def blossom_from_jsonable( elif key == 'details': the_details = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_identifier is None: raise DeserializationException( "The required property 'identifier' is missing" @@ -630,27 +677,44 @@ def plain_marker_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'PlainMarker': raise DeserializationException( f"Expected modelType to be 'PlainMarker', " f"but got: {model_type!r}" ) + unexpected_key: Optional[str] = None + try: for key in mapping: if key == 'modelType': # The model type has already been checked above. pass else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.PlainMarker() @@ -670,25 +734,28 @@ def something_from_jsonable( the_some_choice: Optional[our_types.Node] = None the_something_without_choice: Optional[our_types.Branch] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someChoice': + if key == 'someChoice': the_some_choice = node_from_jsonable(jsonable_value) elif key == 'somethingWithoutChoice': the_something_without_choice = branch_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_choice is None: raise DeserializationException( "The required property 'someChoice' is missing" @@ -721,25 +788,28 @@ def container_from_jsonable( the_node: Optional[our_types.Node] = None the_something: Optional[our_types.Something] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'node': + if key == 'node': the_node = node_from_jsonable(jsonable_value) elif key == 'something': the_something = something_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_node is None: raise DeserializationException( "The required property 'node' is missing" @@ -849,7 +919,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/deep_class_hierarchy/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 af8478df5..74d7b84d9 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -282,20 +295,10 @@ def something_from_jsonable( """ mapping = _as_mapping(jsonable) - try: - for key in mapping: - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - else: - raise DeserializationException( - f"Unexpected property: {key}" - ) - except DeserializationException as exception: - exception.path._prepend( - PropertySegment(mapping, key) + for key in mapping: + raise DeserializationException( + f"Unexpected property: {key}" ) - raise return our_types.Something() @@ -348,7 +351,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/empty_class/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/empty_class/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/empty_class/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/empty_class/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/empty_class/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/empty_class/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/empty_class/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/empty_class/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 e45efbfc3..5a27bf4dd 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -310,23 +323,26 @@ def something_from_jsonable( the_some_result: Optional[our_types.Result] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someResult': + if key == 'someResult': the_some_result = result_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_result is None: raise DeserializationException( "The required property 'someResult' is missing" @@ -385,7 +401,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/enum/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/enum/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/enum/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/enum/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/enum/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/enum/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/enum/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/enum/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 4931b5732..c140b3c86 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 @@ -12,7 +12,9 @@ import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -473,12 +486,11 @@ def something_from_jsonable( the_some_floats: Optional[List[float]] = None the_some_pair: Optional[Tuple[str, float]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someFloat': + if key == 'someFloat': the_some_float = _float_from_jsonable(jsonable_value) elif key == 'someOptionalFloat': the_some_optional_float = _float_from_jsonable(jsonable_value) @@ -487,15 +499,19 @@ def something_from_jsonable( elif key == 'somePair': the_some_pair = _tuple2_of__str__float_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_float is None: raise DeserializationException( "The required property 'someFloat' is missing" @@ -567,7 +583,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/float_only/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/float_only/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/float_only/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/float_only/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/float_only/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/float_only/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/float_only/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/float_only/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 bc874e53a..e272d5162 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -430,23 +443,26 @@ def item_from_jsonable( the_texts: Optional[List[str]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'texts': + if key == 'texts': the_texts = _list_of__str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_texts is None: raise DeserializationException( "The required property 'texts' is missing" @@ -475,12 +491,11 @@ def something_from_jsonable( the_texts: Optional[List[str]] = None the_items: Optional[List[our_types.Item]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'kind': + if key == 'kind': the_kind = kind_from_jsonable(jsonable_value) elif key == 'numbers': the_numbers = _list_of__int_from_jsonable(jsonable_value) @@ -489,15 +504,19 @@ def something_from_jsonable( elif key == 'items': the_items = _list_of__item_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_kind is None: raise DeserializationException( "The required property 'kind' is missing" @@ -574,7 +593,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/for_statements/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/for_statements/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/for_statements/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/for_statements/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/for_statements/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/for_statements/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/for_statements/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/for_statements/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 a66c740f2..ba95b75de 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -451,25 +469,28 @@ def item_from_jsonable( the_name: Optional[str] = None the_optional_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'name': + if key == 'name': the_name = _str_from_jsonable(jsonable_value) elif key == 'optionalText': the_optional_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -513,6 +534,17 @@ def child_a_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ChildA': raise DeserializationException( f"Expected modelType to be 'ChildA', " @@ -522,6 +554,8 @@ def child_a_from_jsonable( the_optional_text: Optional[str] = None the_a_only: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -532,15 +566,19 @@ def child_a_from_jsonable( elif key == 'aOnly': the_a_only = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_a_only is None: raise DeserializationException( "The required property 'aOnly' is missing" @@ -566,6 +604,17 @@ def child_b_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ChildB': raise DeserializationException( f"Expected modelType to be 'ChildB', " @@ -575,6 +624,8 @@ def child_b_from_jsonable( the_optional_text: Optional[str] = None the_b_only: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -585,15 +636,19 @@ def child_b_from_jsonable( elif key == 'bOnly': the_b_only = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_b_only is None: raise DeserializationException( "The required property 'bOnly' is missing" @@ -619,6 +674,17 @@ def container_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Container': raise DeserializationException( f"Expected modelType to be 'Container', " @@ -628,6 +694,8 @@ def container_from_jsonable( the_optional_text: Optional[str] = None the_children: Optional[List[our_types.Parent]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -638,15 +706,19 @@ def container_from_jsonable( elif key == 'children': the_children = _list_of__parent_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.Container( the_optional_text, the_children @@ -674,12 +746,11 @@ def something_from_jsonable( the_optional_parent: Optional[our_types.Parent] = None the_parents: Optional[List[our_types.Parent]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'kind': + if key == 'kind': the_kind = kind_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) @@ -694,15 +765,19 @@ def something_from_jsonable( elif key == 'parents': the_parents = _list_of__parent_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_kind is None: raise DeserializationException( "The required property 'kind' is missing" @@ -799,7 +874,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/if_statements/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/if_statements/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/if_statements/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/if_statements/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/if_statements/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/if_statements/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/if_statements/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/if_statements/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 d94ecadca..ba0a2f499 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -447,6 +465,17 @@ def box_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Box': raise DeserializationException( f"Expected modelType to be 'Box', " @@ -456,6 +485,8 @@ def box_from_jsonable( the_label: Optional[str] = None the_color: Optional[our_types.Color] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -466,15 +497,19 @@ def box_from_jsonable( elif key == 'color': the_color = color_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_label is None: raise DeserializationException( "The required property 'label' is missing" @@ -500,6 +535,17 @@ def bag_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Bag': raise DeserializationException( f"Expected modelType to be 'Bag', " @@ -509,6 +555,8 @@ def bag_from_jsonable( the_label: Optional[str] = None the_tags: Optional[List[str]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -519,15 +567,19 @@ def bag_from_jsonable( elif key == 'tags': the_tags = _list_of__str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_label is None: raise DeserializationException( "The required property 'label' is missing" @@ -560,25 +612,28 @@ def container_from_jsonable( the_names: Optional[List[str]] = None the_items: Optional[List[our_types.Item]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'names': + if key == 'names': the_names = _list_of__str_from_jsonable(jsonable_value) elif key == 'items': the_items = _list_of__item_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_names is None: raise DeserializationException( "The required property 'names' is missing" @@ -654,7 +709,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/implementation_specific/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 1887cd635..4048af6ea 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -390,12 +403,11 @@ def something_from_jsonable( the_count: Optional[int] = None the_maybe_count: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'numbers': + if key == 'numbers': the_numbers = _list_of__int_from_jsonable(jsonable_value) elif key == 'texts': the_texts = _list_of__str_from_jsonable(jsonable_value) @@ -404,15 +416,19 @@ def something_from_jsonable( elif key == 'maybeCount': the_maybe_count = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_numbers is None: raise DeserializationException( "The required property 'numbers' is missing" @@ -484,7 +500,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/int_length_interplay/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/jsonization.py index 0c8730fd2..7ecc40330 100644 --- a/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/jsonization.py @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -305,12 +318,11 @@ def something_from_jsonable( the_long_text: Optional[str] = None the_maybe_number: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'number': + if key == 'number': the_number = _str_from_jsonable(jsonable_value) elif key == 'padded': the_padded = _str_from_jsonable(jsonable_value) @@ -321,15 +333,19 @@ def something_from_jsonable( elif key == 'maybeNumber': the_maybe_number = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_number is None: raise DeserializationException( "The required property 'number' is missing" @@ -407,7 +423,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/int_parsing_and_lstrip/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 4812eb061..ea87d04b2 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 @@ -12,7 +12,9 @@ import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -293,10 +306,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -482,6 +500,17 @@ def leaf_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Leaf': raise DeserializationException( f"Expected modelType to be 'Leaf', " @@ -491,6 +520,8 @@ def leaf_from_jsonable( the_identifier: Optional[str] = None the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -501,15 +532,19 @@ def leaf_from_jsonable( elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_identifier is None: raise DeserializationException( "The required property 'identifier' is missing" @@ -558,6 +593,17 @@ def ordered_container_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'OrderedContainer': raise DeserializationException( f"Expected modelType to be 'OrderedContainer', " @@ -568,6 +614,8 @@ def ordered_container_from_jsonable( the_children: Optional[List[our_types.Element]] = None the_is_sorted: Optional[bool] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -580,15 +628,19 @@ def ordered_container_from_jsonable( elif key == 'isSorted': the_is_sorted = _bool_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_identifier is None: raise DeserializationException( "The required property 'identifier' is missing" @@ -625,6 +677,17 @@ def unordered_container_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'UnorderedContainer': raise DeserializationException( f"Expected modelType to be 'UnorderedContainer', " @@ -634,6 +697,8 @@ def unordered_container_from_jsonable( the_identifier: Optional[str] = None the_children: Optional[List[our_types.Element]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -644,15 +709,19 @@ def unordered_container_from_jsonable( elif key == 'children': the_children = _list_of__element_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_identifier is None: raise DeserializationException( "The required property 'identifier' is missing" @@ -683,6 +752,17 @@ def global_attribute_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'GlobalAttribute': raise DeserializationException( f"Expected modelType to be 'GlobalAttribute', " @@ -691,6 +771,8 @@ def global_attribute_from_jsonable( the_kind: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -699,15 +781,19 @@ def global_attribute_from_jsonable( elif key == 'kind': the_kind = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_kind is None: raise DeserializationException( "The required property 'kind' is missing" @@ -732,6 +818,17 @@ def local_attribute_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'LocalAttribute': raise DeserializationException( f"Expected modelType to be 'LocalAttribute', " @@ -740,6 +837,8 @@ def local_attribute_from_jsonable( the_name: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -748,15 +847,19 @@ def local_attribute_from_jsonable( elif key == 'name': the_name = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -807,6 +910,17 @@ def attribute_operand_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'AttributeOperand': raise DeserializationException( f"Expected modelType to be 'AttributeOperand', " @@ -815,6 +929,8 @@ def attribute_operand_from_jsonable( the_attribute: Optional[our_types.AttributeItem] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -823,15 +939,19 @@ def attribute_operand_from_jsonable( elif key == 'attribute': the_attribute = attribute_item_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_attribute is None: raise DeserializationException( "The required property 'attribute' is missing" @@ -856,6 +976,17 @@ def string_literal_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'StringLiteral': raise DeserializationException( f"Expected modelType to be 'StringLiteral', " @@ -864,6 +995,8 @@ def string_literal_from_jsonable( the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -872,15 +1005,19 @@ def string_literal_from_jsonable( elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_text is None: raise DeserializationException( "The required property 'text' is missing" @@ -905,6 +1042,17 @@ def number_literal_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'NumberLiteral': raise DeserializationException( f"Expected modelType to be 'NumberLiteral', " @@ -913,6 +1061,8 @@ def number_literal_from_jsonable( the_number: Optional[float] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -921,15 +1071,19 @@ def number_literal_from_jsonable( elif key == 'number': the_number = _float_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_number is None: raise DeserializationException( "The required property 'number' is missing" @@ -1010,12 +1164,11 @@ def something_from_jsonable( the_value: Optional[our_types.Value] = None the_values: Optional[List[our_types.Value]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'root': + if key == 'root': the_root = element_from_jsonable(jsonable_value) elif key == 'optionalElement': the_optional_element = element_from_jsonable(jsonable_value) @@ -1024,15 +1177,19 @@ def something_from_jsonable( elif key == 'values': the_values = _list_of__value_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_root is None: raise DeserializationException( "The required property 'root' is missing" @@ -1161,7 +1318,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/is_instance/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/is_instance/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/is_instance/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/is_instance/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/is_instance/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/is_instance/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/is_instance/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/is_instance/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 ab385bd54..7835f53b1 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 @@ -38,8 +38,8 @@ def test_a_deeply_nested_value_is_json_able(self) -> None: def test_the_walk_is_depth_first(self) -> None: # NOTE (mristin): - # A breadth-first walk would report the shallow ``['z']`` before - # the deeper ``['a']['deep']``, although the latter comes first in + # A breadth-first walk would report the shallow ``["z"]`` before + # the deeper ``["a"]["deep"]``, although the latter comes first in # the value. value = { "a": {"deep": math.nan}, @@ -47,7 +47,7 @@ def test_the_walk_is_depth_first(self) -> None: } self.assertListEqual( - ["['a']['deep']", "['z']"], + ['["a"]["deep"]', '["z"]'], [ path for path, _ in _paths_and_causes( @@ -65,7 +65,7 @@ def test_an_index_and_a_key_render_as_a_subscript(self) -> None: self.assertEqual(1, len(paths_and_causes)) self.assertEqual( - "['a b'][1][\"c'd\"]", + '["a b"][1]["c\'d"]', paths_and_causes[0][0] ) @@ -77,7 +77,7 @@ def test_a_non_string_key_stops_at_the_object_which_holds_it(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a']", paths_and_causes[0][0]) + self.assertEqual('["a"]', paths_and_causes[0][0]) self.assertIn("but got a key of type", paths_and_causes[0][1]) def test_the_segments_carry_their_container(self) -> None: @@ -148,7 +148,7 @@ def test_a_value_of_an_unexpected_type_is_refused(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a']", paths_and_causes[0][0]) + self.assertEqual('["a"]', paths_and_causes[0][0]) self.assertIn("but got: ", paths_and_causes[0][1]) @@ -194,7 +194,7 @@ def test_the_content_is_verified_beneath_the_shape(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a'][0]['b']", paths_and_causes[0][0]) + self.assertEqual('["a"][0]["b"]', paths_and_causes[0][0]) if __name__ == "__main__": 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 491ee0249..b78d062c0 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 @@ -285,7 +285,7 @@ def test_the_path_of_a_failure_points_into_the_value(self) -> None: ) self.assertEqual( - "['a b'][1]", context.exception.path + '["a b"][1]', context.exception.path ) def test_an_integer_too_large_for_a_float_is_not_written(self) -> None: 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 ac0e9de47..f9a7969b6 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 @@ -12,7 +12,9 @@ import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -423,12 +436,11 @@ def something_from_jsonable( the_optional_values: Optional[our_types.JsonArray] = None the_optional_mapping: Optional[our_types.JsonObject] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'value': + if key == 'value': the_value = _json_value_from_jsonable(jsonable_value) elif key == 'values': the_values = _json_array_from_jsonable(jsonable_value) @@ -445,15 +457,19 @@ def something_from_jsonable( elif key == 'optionalMapping': the_optional_mapping = _json_object_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_value is None: raise DeserializationException( "The required property 'value' is missing" @@ -533,7 +549,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/json_types/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/json_types/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/json_types/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/json_types/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] 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 564bbfffa..5d367e479 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 @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 ab385bd54..7835f53b1 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 @@ -38,8 +38,8 @@ def test_a_deeply_nested_value_is_json_able(self) -> None: def test_the_walk_is_depth_first(self) -> None: # NOTE (mristin): - # A breadth-first walk would report the shallow ``['z']`` before - # the deeper ``['a']['deep']``, although the latter comes first in + # A breadth-first walk would report the shallow ``["z"]`` before + # the deeper ``["a"]["deep"]``, although the latter comes first in # the value. value = { "a": {"deep": math.nan}, @@ -47,7 +47,7 @@ def test_the_walk_is_depth_first(self) -> None: } self.assertListEqual( - ["['a']['deep']", "['z']"], + ['["a"]["deep"]', '["z"]'], [ path for path, _ in _paths_and_causes( @@ -65,7 +65,7 @@ def test_an_index_and_a_key_render_as_a_subscript(self) -> None: self.assertEqual(1, len(paths_and_causes)) self.assertEqual( - "['a b'][1][\"c'd\"]", + '["a b"][1]["c\'d"]', paths_and_causes[0][0] ) @@ -77,7 +77,7 @@ def test_a_non_string_key_stops_at_the_object_which_holds_it(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a']", paths_and_causes[0][0]) + self.assertEqual('["a"]', paths_and_causes[0][0]) self.assertIn("but got a key of type", paths_and_causes[0][1]) def test_the_segments_carry_their_container(self) -> None: @@ -148,7 +148,7 @@ def test_a_value_of_an_unexpected_type_is_refused(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a']", paths_and_causes[0][0]) + self.assertEqual('["a"]', paths_and_causes[0][0]) self.assertIn("but got: ", paths_and_causes[0][1]) @@ -194,7 +194,7 @@ def test_the_content_is_verified_beneath_the_shape(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a'][0]['b']", paths_and_causes[0][0]) + self.assertEqual('["a"][0]["b"]', paths_and_causes[0][0]) if __name__ == "__main__": 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 491ee0249..b78d062c0 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 @@ -285,7 +285,7 @@ def test_the_path_of_a_failure_points_into_the_value(self) -> None: ) self.assertEqual( - "['a b'][1]", context.exception.path + '["a b"][1]', context.exception.path ) def test_an_integer_too_large_for_a_float_is_not_written(self) -> None: 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 741559c72..cff71567e 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 @@ -12,7 +12,9 @@ import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -589,27 +602,30 @@ def something_from_jsonable( ] ] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'values': + if key == 'values': the_values = _list_of__json_value_from_jsonable(jsonable_value) elif key == 'tupleWithJson': the_tuple_with_json = ( _tuple4_of__str__json_value__json_array__json_object_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_values is None: raise DeserializationException( "The required property 'values' is missing" @@ -674,7 +690,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/json_types_in_collections/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] 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 564bbfffa..5d367e479 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 @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 ab385bd54..7835f53b1 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 @@ -38,8 +38,8 @@ def test_a_deeply_nested_value_is_json_able(self) -> None: def test_the_walk_is_depth_first(self) -> None: # NOTE (mristin): - # A breadth-first walk would report the shallow ``['z']`` before - # the deeper ``['a']['deep']``, although the latter comes first in + # A breadth-first walk would report the shallow ``["z"]`` before + # the deeper ``["a"]["deep"]``, although the latter comes first in # the value. value = { "a": {"deep": math.nan}, @@ -47,7 +47,7 @@ def test_the_walk_is_depth_first(self) -> None: } self.assertListEqual( - ["['a']['deep']", "['z']"], + ['["a"]["deep"]', '["z"]'], [ path for path, _ in _paths_and_causes( @@ -65,7 +65,7 @@ def test_an_index_and_a_key_render_as_a_subscript(self) -> None: self.assertEqual(1, len(paths_and_causes)) self.assertEqual( - "['a b'][1][\"c'd\"]", + '["a b"][1]["c\'d"]', paths_and_causes[0][0] ) @@ -77,7 +77,7 @@ def test_a_non_string_key_stops_at_the_object_which_holds_it(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a']", paths_and_causes[0][0]) + self.assertEqual('["a"]', paths_and_causes[0][0]) self.assertIn("but got a key of type", paths_and_causes[0][1]) def test_the_segments_carry_their_container(self) -> None: @@ -148,7 +148,7 @@ def test_a_value_of_an_unexpected_type_is_refused(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a']", paths_and_causes[0][0]) + self.assertEqual('["a"]', paths_and_causes[0][0]) self.assertIn("but got: ", paths_and_causes[0][1]) @@ -194,7 +194,7 @@ def test_the_content_is_verified_beneath_the_shape(self) -> None: ) self.assertEqual(1, len(paths_and_causes)) - self.assertEqual("['a'][0]['b']", paths_and_causes[0][0]) + self.assertEqual('["a"][0]["b"]', paths_and_causes[0][0]) if __name__ == "__main__": 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 491ee0249..b78d062c0 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 @@ -285,7 +285,7 @@ def test_the_path_of_a_failure_points_into_the_value(self) -> None: ) self.assertEqual( - "['a b'][1]", context.exception.path + '["a b"][1]', context.exception.path ) def test_an_integer_too_large_for_a_float_is_not_written(self) -> None: 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 bab788316..ba47013ab 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 @@ -12,7 +12,9 @@ import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -419,27 +432,30 @@ def something_from_jsonable( the_values: Optional[our_types.JsonArray] = None the_optional_mapping: Optional[our_types.JsonObject] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'mapping': + if key == 'mapping': the_mapping = _json_object_from_jsonable(jsonable_value) elif key == 'values': the_values = _json_array_from_jsonable(jsonable_value) elif key == 'optionalMapping': the_optional_mapping = _json_object_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_mapping is None: raise DeserializationException( "The required property 'mapping' is missing" @@ -505,7 +521,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/json_types_in_invariants/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] 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 564bbfffa..5d367e479 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 @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 2ad767c50..1d5575a30 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -441,6 +459,17 @@ def some_item_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'SomeItem': raise DeserializationException( f"Expected modelType to be 'SomeItem', " @@ -449,6 +478,8 @@ def some_item_from_jsonable( the_name: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -457,15 +488,19 @@ def some_item_from_jsonable( elif key == 'name': the_name = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -490,6 +525,17 @@ def another_item_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'AnotherItem': raise DeserializationException( f"Expected modelType to be 'AnotherItem', " @@ -498,6 +544,8 @@ def another_item_from_jsonable( the_serial_number: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -506,15 +554,19 @@ def another_item_from_jsonable( elif key == 'serialNumber': the_serial_number = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_serial_number is None: raise DeserializationException( "The required property 'serialNumber' is missing" @@ -540,23 +592,26 @@ def simple_from_jsonable( the_name: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'name': + if key == 'name': the_name = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -584,27 +639,30 @@ def something_from_jsonable( the_some_items: Optional[List[our_types.AbstractItem]] = None the_some_simples: Optional[List[our_types.Simple]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'item': + if key == 'item': the_item = simple_from_jsonable(jsonable_value) elif key == 'someItems': the_some_items = _list_of__abstract_item_from_jsonable(jsonable_value) elif key == 'someSimples': the_some_simples = _list_of__simple_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_item is None: raise DeserializationException( "The required property 'item' is missing" @@ -686,7 +744,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/list_of_classes/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 97ef2d8f9..371d283e9 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -350,23 +363,26 @@ def something_from_jsonable( the_some_names: Optional[List[str]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someNames': + if key == 'someNames': the_some_names = _list_of__str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_names is None: raise DeserializationException( "The required property 'someNames' is missing" @@ -425,7 +441,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/list_of_constrained_primitives/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 9d7247811..391167092 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -359,23 +372,26 @@ def something_from_jsonable( the_some_results: Optional[List[our_types.Result]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someResults': + if key == 'someResults': the_some_results = _list_of__result_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_results is None: raise DeserializationException( "The required property 'someResults' is missing" @@ -434,7 +450,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/list_of_enums/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 f4ffb30e1..587a6eeca 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 @@ -13,7 +13,9 @@ import base64 import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -140,6 +142,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -165,21 +172,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -524,12 +537,11 @@ def something_from_jsonable( the_some_strings: Optional[List[str]] = None the_some_bytes: Optional[List[bytes]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someBools': + if key == 'someBools': the_some_bools = _list_of__bool_from_jsonable(jsonable_value) elif key == 'someInts': the_some_ints = _list_of__int_from_jsonable(jsonable_value) @@ -540,15 +552,19 @@ def something_from_jsonable( elif key == 'someBytes': the_some_bytes = _list_of__bytes_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_bools is None: raise DeserializationException( "The required property 'someBools' is missing" @@ -631,7 +647,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/list_of_primitives/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 690dbe210..9d3298ada 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -375,6 +393,17 @@ def box_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Box': raise DeserializationException( f"Expected modelType to be 'Box', " @@ -384,6 +413,8 @@ def box_from_jsonable( the_label: Optional[str] = None the_size: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -394,15 +425,19 @@ def box_from_jsonable( elif key == 'size': the_size = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_label is None: raise DeserializationException( "The required property 'label' is missing" @@ -477,7 +512,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/method_references/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/method_references/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/method_references/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/method_references/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/method_references/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/method_references/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/method_references/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/method_references/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 8982cebfe..5da18bbf8 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -422,25 +440,28 @@ def lang_string_from_jsonable( the_language: Optional[str] = None the_text: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'language': + if key == 'language': the_language = _str_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_language is None: raise DeserializationException( "The required property 'language' is missing" @@ -472,23 +493,26 @@ def lang_string_set_from_jsonable( the_lang_strings: Optional[List[our_types.LangString]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'langStrings': + if key == 'langStrings': the_lang_strings = _list_of__lang_string_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_lang_strings is None: raise DeserializationException( "The required property 'langStrings' is missing" @@ -531,6 +555,17 @@ def iec_content_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'IecContent': raise DeserializationException( f"Expected modelType to be 'IecContent', " @@ -539,6 +574,8 @@ def iec_content_from_jsonable( the_definition: Optional[List[our_types.LangString]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -547,15 +584,19 @@ def iec_content_from_jsonable( elif key == 'definition': the_definition = _list_of__lang_string_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.IecContent( the_definition ) @@ -575,27 +616,44 @@ def other_content_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'OtherContent': raise DeserializationException( f"Expected modelType to be 'OtherContent', " f"but got: {model_type!r}" ) + unexpected_key: Optional[str] = None + try: for key in mapping: if key == 'modelType': # The model type has already been checked above. pass else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + return our_types.OtherContent() @@ -614,23 +672,26 @@ def specification_from_jsonable( the_content: Optional[our_types.Content] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'content': + if key == 'content': the_content = content_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_content is None: raise DeserializationException( "The required property 'content' is missing" @@ -658,12 +719,11 @@ def something_from_jsonable( the_lang_string_sets: Optional[List[our_types.LangStringSet]] = None the_specifications: Optional[List[our_types.Specification]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'defaultLanguage': + if key == 'defaultLanguage': the_default_language = _str_from_jsonable(jsonable_value) elif key == 'langStringSets': the_lang_string_sets = ( @@ -672,15 +732,19 @@ def something_from_jsonable( elif key == 'specifications': the_specifications = _list_of__specification_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_default_language is None: raise DeserializationException( "The required property 'defaultLanguage' is missing" @@ -757,7 +821,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/nested_any_in_all/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 b63d98d64..7bf921748 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -293,10 +306,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -839,6 +857,17 @@ def some_item_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'SomeItem': raise DeserializationException( f"Expected modelType to be 'SomeItem', " @@ -847,6 +876,8 @@ def some_item_from_jsonable( the_name: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -855,15 +886,19 @@ def some_item_from_jsonable( elif key == 'name': the_name = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -888,6 +923,17 @@ def another_item_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'AnotherItem': raise DeserializationException( f"Expected modelType to be 'AnotherItem', " @@ -896,6 +942,8 @@ def another_item_from_jsonable( the_serial_number: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -904,15 +952,19 @@ def another_item_from_jsonable( elif key == 'serialNumber': the_serial_number = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_serial_number is None: raise DeserializationException( "The required property 'serialNumber' is missing" @@ -975,12 +1027,11 @@ def something_from_jsonable( the_grid_of_unions: Optional[List[List[our_types.SomeUnion]]] = None the_optional_grid_of_items: Optional[List[List[our_types.AbstractItem]]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'grid': + if key == 'grid': the_grid = _list_of__list_of__int_from_jsonable(jsonable_value) elif key == 'rowsOfPairs': the_rows_of_pairs = ( @@ -1001,15 +1052,19 @@ def something_from_jsonable( _list_of__list_of__abstract_item_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_grid is None: raise DeserializationException( "The required property 'grid' is missing" @@ -1115,7 +1170,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/nested_collections/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 c16706a32..c4c203730 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -397,25 +410,28 @@ def item_from_jsonable( the_name: Optional[str] = None the_note: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'name': + if key == 'name': the_name = _str_from_jsonable(jsonable_value) elif key == 'note': the_note = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -447,12 +463,11 @@ def something_from_jsonable( the_optional_texts: Optional[List[str]] = None the_optional_kind: Optional[our_types.Kind] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'item': + if key == 'item': the_item = item_from_jsonable(jsonable_value) elif key == 'optionalText': the_optional_text = _str_from_jsonable(jsonable_value) @@ -465,15 +480,19 @@ def something_from_jsonable( elif key == 'optionalKind': the_optional_kind = kind_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_item is None: raise DeserializationException( "The required property 'item' is missing" @@ -537,7 +556,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/optional_arguments/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 98b5e9b55..2fd6f7c80 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 @@ -13,7 +13,9 @@ import base64 import collections.abc +import json import math +import re import sys from typing import ( Any, @@ -140,6 +142,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -165,21 +172,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -407,12 +420,11 @@ def something_from_jsonable( the_some_string: Optional[str] = None the_some_bytes: Optional[bytes] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'someBool': + if key == 'someBool': the_some_bool = _bool_from_jsonable(jsonable_value) elif key == 'someInt': the_some_int = _int_from_jsonable(jsonable_value) @@ -423,15 +435,19 @@ def something_from_jsonable( elif key == 'someBytes': the_some_bytes = _bytes_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_bool is None: raise DeserializationException( "The required property 'someBool' is missing" @@ -514,7 +530,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/primitive_types/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 9570cb91f..81184c5ed 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -304,12 +317,11 @@ def something_from_jsonable( the_range: Optional[str] = None the_void: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'interface': + if key == 'interface': the_interface = _str_from_jsonable(jsonable_value) elif key == 'type': the_type = _str_from_jsonable(jsonable_value) @@ -318,15 +330,19 @@ def something_from_jsonable( elif key == 'void': the_void = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_interface is None: raise DeserializationException( "The required property 'interface' is missing" @@ -403,7 +419,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/problematic_keywords/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 18a95596e..057982ab6 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -139,6 +141,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -164,21 +171,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -610,12 +623,11 @@ def something_from_jsonable( the_optional_texts: Optional[List[str]] = None the_optional_kind: Optional[our_types.Kind] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'text': + if key == 'text': the_text = _str_from_jsonable(jsonable_value) elif key == 'number': the_number = _int_from_jsonable(jsonable_value) @@ -636,15 +648,19 @@ def something_from_jsonable( elif key == 'optionalKind': the_optional_kind = kind_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_text is None: raise DeserializationException( "The required property 'text' is missing" @@ -720,12 +736,11 @@ def collection_from_jsonable( the_optional_texts: Optional[Set[str]] = None the_optional_directions: Optional[Set[our_types.Direction]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'texts': + if key == 'texts': the_texts = _set_of__str_from_jsonable(jsonable_value) elif key == 'numbers': the_numbers = _set_of__int_from_jsonable(jsonable_value) @@ -740,15 +755,19 @@ def collection_from_jsonable( elif key == 'optionalDirections': the_optional_directions = _set_of__direction_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_texts is None: raise DeserializationException( "The required property 'texts' is missing" @@ -833,7 +852,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/sets/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/sets/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/sets/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/sets/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/sets/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/sets/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/sets/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/sets/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/jsonization.py b/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/jsonization.py index 8bcddec9f..c41a15bf6 100644 --- a/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/jsonization.py +++ b/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/jsonization.py @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -302,25 +315,28 @@ def something_from_jsonable( the_text: Optional[str] = None the_name: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'text': + if key == 'text': the_text = _str_from_jsonable(jsonable_value) elif key == 'name': the_name = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_text is None: raise DeserializationException( "The required property 'text' is missing" @@ -385,7 +401,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/string_slicing_and_find/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 ec44dc66d..0522d40b0 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -350,12 +363,11 @@ def something_from_jsonable( the_name: Optional[str] = None the_number: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'kind': + if key == 'kind': the_kind = kind_from_jsonable(jsonable_value) elif key == 'text': the_text = _str_from_jsonable(jsonable_value) @@ -364,15 +376,19 @@ def something_from_jsonable( elif key == 'number': the_number = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_kind is None: raise DeserializationException( "The required property 'kind' is missing" @@ -449,7 +465,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/switch_statements/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 839e16093..e0d62baa4 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -450,6 +468,17 @@ def first_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'First': raise DeserializationException( f"Expected modelType to be 'First', " @@ -460,6 +489,8 @@ def first_from_jsonable( the_count: Optional[int] = None the_kind: Optional[our_types.Kind] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -472,15 +503,19 @@ def first_from_jsonable( elif key == 'kind': the_kind = kind_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_texts is None: raise DeserializationException( "The required property 'texts' is missing" @@ -512,6 +547,17 @@ def second_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Second': raise DeserializationException( f"Expected modelType to be 'Second', " @@ -523,6 +569,8 @@ def second_from_jsonable( the_kind: Optional[our_types.Kind] = None the_note: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -537,15 +585,19 @@ def second_from_jsonable( elif key == 'note': the_note = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_texts is None: raise DeserializationException( "The required property 'texts' is missing" @@ -628,7 +680,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/transpiled_methods/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 bcc4424bf..c0b170e38 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -634,6 +652,17 @@ def some_item_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'SomeItem': raise DeserializationException( f"Expected modelType to be 'SomeItem', " @@ -642,6 +671,8 @@ def some_item_from_jsonable( the_name: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -650,15 +681,19 @@ def some_item_from_jsonable( elif key == 'name': the_name = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_name is None: raise DeserializationException( "The required property 'name' is missing" @@ -683,6 +718,17 @@ def another_item_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'AnotherItem': raise DeserializationException( f"Expected modelType to be 'AnotherItem', " @@ -691,6 +737,8 @@ def another_item_from_jsonable( the_serial_number: Optional[int] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -699,15 +747,19 @@ def another_item_from_jsonable( elif key == 'serialNumber': the_serial_number = _int_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_serial_number is None: raise DeserializationException( "The required property 'serialNumber' is missing" @@ -745,12 +797,11 @@ def something_from_jsonable( ] = None the_optional_pair: Optional[Tuple[str, our_types.AbstractItem]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'pair': + if key == 'pair': the_pair = _tuple2_of__str__int_from_jsonable(jsonable_value) elif key == 'items': the_items = ( @@ -765,15 +816,19 @@ def something_from_jsonable( _tuple2_of__str__abstract_item_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_pair is None: raise DeserializationException( "The required property 'pair' is missing" @@ -856,7 +911,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/tuples/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/tuples/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/tuples/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/tuples/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/tuples/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/tuples/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/tuples/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/tuples/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 c88ac9c80..d2dc965b0 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -525,23 +543,26 @@ def structural_first_from_jsonable( the_unique_to_first: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'uniqueToFirst': + if key == 'uniqueToFirst': the_unique_to_first = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_unique_to_first is None: raise DeserializationException( "The required property 'uniqueToFirst' is missing" @@ -567,23 +588,26 @@ def structural_second_from_jsonable( the_unique_to_second: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'uniqueToSecond': + if key == 'uniqueToSecond': the_unique_to_second = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_unique_to_second is None: raise DeserializationException( "The required property 'uniqueToSecond' is missing" @@ -652,23 +676,26 @@ def mixed_abstract_descendant_one_from_jsonable( the_unique_to_abstract_descendant_one: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'uniqueToAbstractDescendantOne': + if key == 'uniqueToAbstractDescendantOne': the_unique_to_abstract_descendant_one = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_unique_to_abstract_descendant_one is None: raise DeserializationException( "The required property 'uniqueToAbstractDescendantOne' is missing" @@ -694,23 +721,26 @@ def mixed_abstract_descendant_two_from_jsonable( the_unique_to_abstract_descendant_two: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'uniqueToAbstractDescendantTwo': + if key == 'uniqueToAbstractDescendantTwo': the_unique_to_abstract_descendant_two = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_unique_to_abstract_descendant_two is None: raise DeserializationException( "The required property 'uniqueToAbstractDescendantTwo' is missing" @@ -761,6 +791,8 @@ def _mixed_concrete_with_descendants_from_jsonable_without_dispatch( the_some_base_property: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -769,15 +801,19 @@ def _mixed_concrete_with_descendants_from_jsonable_without_dispatch( elif key == 'someBaseProperty': the_some_base_property = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_base_property is None: raise DeserializationException( "The required property 'someBaseProperty' is missing" @@ -802,6 +838,17 @@ def mixed_concrete_with_descendants_child_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'MixedConcreteWithDescendantsChild': raise DeserializationException( f"Expected modelType to be 'MixedConcreteWithDescendantsChild', " @@ -811,6 +858,8 @@ def mixed_concrete_with_descendants_child_from_jsonable( the_some_base_property: Optional[str] = None the_some_child_property: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -821,15 +870,19 @@ def mixed_concrete_with_descendants_child_from_jsonable( elif key == 'someChildProperty': the_some_child_property = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_base_property is None: raise DeserializationException( "The required property 'someBaseProperty' is missing" @@ -861,23 +914,26 @@ def mixed_concrete_leaf_from_jsonable( the_unique_to_concrete_leaf: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'uniqueToConcreteLeaf': + if key == 'uniqueToConcreteLeaf': the_unique_to_concrete_leaf = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_unique_to_concrete_leaf is None: raise DeserializationException( "The required property 'uniqueToConcreteLeaf' is missing" @@ -937,6 +993,17 @@ def model_typed_first_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ModelTypedFirst': raise DeserializationException( f"Expected modelType to be 'ModelTypedFirst', " @@ -945,6 +1012,8 @@ def model_typed_first_from_jsonable( the_some_property: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -953,15 +1022,19 @@ def model_typed_first_from_jsonable( elif key == 'someProperty': the_some_property = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_property is None: raise DeserializationException( "The required property 'someProperty' is missing" @@ -986,6 +1059,17 @@ def model_typed_second_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'ModelTypedSecond': raise DeserializationException( f"Expected modelType to be 'ModelTypedSecond', " @@ -994,6 +1078,8 @@ def model_typed_second_from_jsonable( the_some_property: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -1002,15 +1088,19 @@ def model_typed_second_from_jsonable( elif key == 'someProperty': the_some_property = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_some_property is None: raise DeserializationException( "The required property 'someProperty' is missing" @@ -1104,12 +1194,11 @@ def something_from_jsonable( the_optional_model_typed_property: Optional[our_types.ModelTypedUnion] = None the_optional_list_overlapping_property: Optional[List[our_types.OverlappingUnion]] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): - if key == 'modelType': - # The model type is redundant for this class, and we simply accept it. - pass - elif key == 'structuralProperty': + if key == 'structuralProperty': the_structural_property = structural_union_from_jsonable(jsonable_value) elif key == 'mixedProperty': the_mixed_property = mixed_union_from_jsonable(jsonable_value) @@ -1146,15 +1235,19 @@ def something_from_jsonable( _list_of__overlapping_union_from_jsonable(jsonable_value) ) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_structural_property is None: raise DeserializationException( "The required property 'structuralProperty' is missing" @@ -1310,7 +1403,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/unions/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/unions/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/unions/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/unions/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/unions/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/unions/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/unions/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/unions/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 01cddbbdc..fd56ab1a3 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 @@ -12,6 +12,8 @@ import collections.abc +import json +import re import sys from typing import ( Any, @@ -138,6 +140,11 @@ def __init__( Segment = Union[PropertySegment, IndexSegment, KeySegment] +#: Match the property names which a path writes after a dot; the other names +#: are written in brackets as JSON strings, *e.g.*, ``["$eq"]``. +_VARIABLE_NAME_RE = re.compile(r"^[a-zA-Z_][a-zA-Z_0-9]*$") + + class Path: """Represent the relative path to the erroneous value.""" @@ -163,21 +170,27 @@ def __str__(self) -> str: iterator = iter(self._segments) first = next(iterator) if isinstance(first, PropertySegment): - parts.append(f"{first.name}") + if _VARIABLE_NAME_RE.match(first.name): + parts.append(first.name) + else: + parts.append(f"[{json.dumps(first.name, ensure_ascii=False)}]") elif isinstance(first, IndexSegment): parts.append(f"[{first.index}]") elif isinstance(first, KeySegment): - parts.append(f"[{first.key!r}]") + parts.append(f"[{json.dumps(first.key, ensure_ascii=False)}]") else: our_common.assert_never(first) for segment in iterator: if isinstance(segment, PropertySegment): - parts.append(f".{segment.name}") + if _VARIABLE_NAME_RE.match(segment.name): + parts.append(f".{segment.name}") + else: + parts.append(f"[{json.dumps(segment.name, ensure_ascii=False)}]") elif isinstance(segment, IndexSegment): parts.append(f"[{segment.index}]") elif isinstance(segment, KeySegment): - parts.append(f"[{segment.key!r}]") + parts.append(f"[{json.dumps(segment.key, ensure_ascii=False)}]") else: our_common.assert_never(segment) @@ -292,10 +305,15 @@ def _dispatch_from_jsonable( ) if not isinstance(model_type, str): - raise DeserializationException( + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( f"Expected the property modelType to be a str, " f"but got: {type(model_type)}" ) + exception.path._prepend(PropertySegment(mapping, "modelType")) + raise exception parse = dispatch.get(model_type, None) if parse is None: @@ -355,6 +373,17 @@ def box_from_jsonable( mapping = _as_mapping(jsonable) model_type = mapping.get('modelType', None) + if model_type is not None and not isinstance(model_type, str): + # NOTE (mristin): + # A model type of a wrong type is reported at the property, as the type + # errors of all the other properties are. + exception = DeserializationException( + f"Expected the property modelType to be a str, " + f"but got: {type(model_type)}" + ) + exception.path._prepend(PropertySegment(mapping, 'modelType')) + raise exception + if model_type != 'Box': raise DeserializationException( f"Expected modelType to be 'Box', " @@ -364,6 +393,8 @@ def box_from_jsonable( the_number: Optional[str] = None the_code: Optional[str] = None + unexpected_key: Optional[str] = None + try: for key, jsonable_value in mapping.items(): if key == 'modelType': @@ -374,15 +405,19 @@ def box_from_jsonable( elif key == 'code': the_code = _str_from_jsonable(jsonable_value) else: - raise DeserializationException( - f"Unexpected property: {key}" - ) + unexpected_key = key + break except DeserializationException as exception: exception.path._prepend( PropertySegment(mapping, key) ) raise + if unexpected_key is not None: + raise DeserializationException( + f"Unexpected property: {unexpected_key}" + ) + if the_number is None: raise DeserializationException( "The required property 'number' is missing" @@ -457,7 +492,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: diff --git a/dev/test_data/main/python/expected/visibility/expected_output/dummy/reporting.py b/dev/test_data/main/python/expected/visibility/expected_output/dummy/reporting.py index 34836e1cc..c36fbc039 100644 --- a/dev/test_data/main/python/expected/visibility/expected_output/dummy/reporting.py +++ b/dev/test_data/main/python/expected/visibility/expected_output/dummy/reporting.py @@ -21,6 +21,7 @@ import collections +import json import sys from typing import ( Any, @@ -141,7 +142,7 @@ def __init__( self.key = key def __str__(self) -> str: - return f'[{self.key!r}]' + return f'[{json.dumps(self.key, ensure_ascii=False)}]' Segment = Union[PropertySegment, IndexSegment, KeySegment] diff --git a/dev/test_data/main/python/expected/visibility/expected_output/dummy/xmlcommon.py b/dev/test_data/main/python/expected/visibility/expected_output/dummy/xmlcommon.py index 6d3f75e8f..a1d73d712 100644 --- a/dev/test_data/main/python/expected/visibility/expected_output/dummy/xmlcommon.py +++ b/dev/test_data/main/python/expected/visibility/expected_output/dummy/xmlcommon.py @@ -19,6 +19,7 @@ # Do NOT edit or append. +import json import re import sys from typing import ( @@ -318,7 +319,7 @@ def _prepend_key(self, key: str) -> None: object is known only at run time and can be any string at all, so it is always rendered as a subscript. """ - self._segments.insert(0, f'[{key!r}]') + self._segments.insert(0, f'[{json.dumps(key, ensure_ascii=False)}]') def __str__(self) -> str: if len(self._segments) == 0: 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 659adfbf9..0ca27a30a 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 59275ab5c..e4f078566 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 5a91e755c..57108265f 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 9d3f891ac..4fca7f86c 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 893758853..bbea90181 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 1b8b0dd76..bc76beacb 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 44be6c5fe..78d52a0c3 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 803a960ca..86da09db8 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 bb128f28c..69bc616b1 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 f10829d16..add9b32d8 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 4550a8366..1e7f3871c 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 90cc2ed1c..4ddd86240 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 06bff9c3f..af81105c5 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 8953f3fea..3922ee8ac 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 ecf8a7038..8380db9bb 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 bdef98131..16ded221c 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 3887b427c..688425b5e 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); diff --git a/dev/test_data/main/typescript/expected/int_parsing_and_lstrip/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/int_parsing_and_lstrip/expected_output/src/jsonization.ts index 773174d90..9844db592 100644 --- a/dev/test_data/main/typescript/expected/int_parsing_and_lstrip/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/int_parsing_and_lstrip/expected_output/src/jsonization.ts @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 e10937235..001d1bdac 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 fa58604e0..798e2e9e3 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 7272d7be9..148e8cbb0 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 e06ecea41..f2d79f34b 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 7ffffdd0b..cc5334f34 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 6bf1f3aef..11e76a70b 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 b801de30c..2ac246c22 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 e4ba5ac50..6eeeb5a47 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 ba9402b54..08550eca0 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 ee129ad4e..c2843006f 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 f162c95b3..9b55f192d 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 bd28d483c..b7cafe292 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 803a960ca..86da09db8 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 42aeff954..7cd0e556a 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 ea29f17fb..f0b5bfa24 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); diff --git a/dev/test_data/main/typescript/expected/string_slicing_and_find/expected_output/src/jsonization.ts b/dev/test_data/main/typescript/expected/string_slicing_and_find/expected_output/src/jsonization.ts index dd9267e43..264a16af1 100644 --- a/dev/test_data/main/typescript/expected/string_slicing_and_find/expected_output/src/jsonization.ts +++ b/dev/test_data/main/typescript/expected/string_slicing_and_find/expected_output/src/jsonization.ts @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 cc2a81fc3..826f4353d 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 03338a784..849232336 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 95272829c..fb0402683 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 2c9de25f3..b293331eb 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null); 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 4c2be5a41..a968e495d 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 @@ -123,6 +123,12 @@ export class KeySegment { export type Segment = PropertySegment | IndexSegment | KeySegment; +/** + * Match the property names which a path writes after a dot; the other names + * are written in brackets as JSON strings, *e.g.*, `["$eq"]`. + */ +const VARIABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z_0-9]*$/; + /** * Represent the relative path to the erroneous value. */ @@ -155,7 +161,11 @@ export class Path { let segment = this._segments[0]; if (segment instanceof PropertySegment) { - parts.push(segment.name); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? segment.name + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -167,7 +177,11 @@ export class Path { for (let i = 1; i < this._segments.length; i++) { segment = this._segments[i]; if (segment instanceof PropertySegment) { - parts.push(`.${segment.name}`); + parts.push( + VARIABLE_NAME_RE.test(segment.name) + ? `.${segment.name}` + : `[${JSON.stringify(segment.name)}]` + ); } else if (segment instanceof IndexSegment) { parts.push(`[${segment.index}]`); } else if (segment instanceof KeySegment) { @@ -261,10 +275,15 @@ function extractModelType( ); } if (typeof modelType !== "string") { - return newDeserializationError( + // NOTE (mristin): + // A model type of a wrong type is reported at the property, as the type + // errors of all the other properties are, and as in the other SDKs. + const error = new DeserializationError( `Expected the property modelType to be a string, ` + `but got: ${typeof modelType}` ); + error.path.prepend(new PropertySegment(jsonObject, "modelType")); + return new OurCommon.Either(null, error); } return new OurCommon.Either(modelType, null);