From f43c7ed5c6f8dd2e682dbeb7bebfa1cd8fe9adcb Mon Sep 17 00:00:00 2001 From: Tnirpps <87409111+Tnirpps@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:29:26 +0300 Subject: [PATCH 1/2] feat chaotic: Add default values for array fields --- .../chaotic/back/cpp/templates/type.cpp.jinja | 19 ++++++++ .../chaotic/back/cpp/templates/type.hpp.jinja | 4 ++ .../back/cpp/templates/type_parsers.ipp.jinja | 2 +- chaotic/chaotic/back/cpp/translator.py | 1 + chaotic/chaotic/back/cpp/types.py | 22 ++++++++- chaotic/chaotic/front/types.py | 1 + chaotic/include/userver/chaotic/object.hpp | 13 ++++++ .../userver/chaotic/sax_parser/object.hpp | 24 ++++++---- .../schemas/object_single_field.yaml | 46 +++++++++++++++++++ .../tests/render/object_single_field.cpp | 34 ++++++++++++++ chaotic/tests/front/test_array.py | 11 +++++ scripts/docs/en/userver/chaotic.md | 8 +++- 12 files changed, 173 insertions(+), 12 deletions(-) diff --git a/chaotic/chaotic/back/cpp/templates/type.cpp.jinja b/chaotic/chaotic/back/cpp/templates/type.cpp.jinja index 434bec88a486..606b351b9d5f 100644 --- a/chaotic/chaotic/back/cpp/templates/type.cpp.jinja +++ b/chaotic/chaotic/back/cpp/templates/type.cpp.jinja @@ -117,6 +117,23 @@ {% endif %} {% endmacro %} +{% macro _generate_array_default_definitions(type) %} + {% for schema in type.subtypes() %} + {{- _generate_array_default_definitions(schema) -}} + {% endfor %} + + {% if type.get_py_type() == 'CppStruct' %} + {% for field in type.fields.values() if field.has_array_default() %} + const {{ field.schema.cpp_user_name() }}& {{ type.cpp_global_name() }}::{{ field.array_default_getter() }}() { + static const auto value = {{ userver }}::formats::json::FromString({{ field.array_default_json() }}).As< + {{ field.schema.parser_type('', '') }} + >(); + return value; + } + {% endfor %} + {% endif %} +{% endmacro %} + {% macro _generate_cpp_struct_properties_serializer_definition(type) %} {% for fname, field in type.fields.items() %} @@ -339,6 +356,8 @@ {{- _generate_string_parser_definition(type) -}} + {{- _generate_array_default_definitions(type) -}} + {% if generate_serializer %} {{- _generate_serializer_definition(type) -}} {% if generate_stream_writer %} diff --git a/chaotic/chaotic/back/cpp/templates/type.hpp.jinja b/chaotic/chaotic/back/cpp/templates/type.hpp.jinja index 7ac283254b5f..1811cc81e50f 100644 --- a/chaotic/chaotic/back/cpp/templates/type.hpp.jinja +++ b/chaotic/chaotic/back/cpp/templates/type.hpp.jinja @@ -32,7 +32,11 @@ {%- set ns = namespace(requires_newlines=false) %} {% for field in type.fields.values() if field.get_default() != '' %} {% set ns.requires_newlines = true %} + {% if field.has_array_default() %} + static const {{ field.schema.cpp_user_name() }}& {{ field.array_default_getter() }}(); + {% else %} static constexpr auto kFieldDefault{{ field.cpp_field_name() }} = {{ field.get_default_raw() }}; + {% endif %} {% endfor %} {% if ns.requires_newlines %} diff --git a/chaotic/chaotic/back/cpp/templates/type_parsers.ipp.jinja b/chaotic/chaotic/back/cpp/templates/type_parsers.ipp.jinja index 0af1c66f757d..6cfa2c0b302b 100644 --- a/chaotic/chaotic/back/cpp/templates/type_parsers.ipp.jinja +++ b/chaotic/chaotic/back/cpp/templates/type_parsers.ipp.jinja @@ -51,7 +51,7 @@ {% for property_name, field in type.fields.items() %} .{{ field.cpp_field_name() }} = value["{{ property_name }}"].template As< {{ field.cpp_field_parse_type() }} - >({{ field.get_default() }}), + >({{ field.get_default(type.cpp_global_name()) }}), {% endfor %} {% if type.extra_type %} .extra={{ extra_variable }}, diff --git a/chaotic/chaotic/back/cpp/translator.py b/chaotic/chaotic/back/cpp/translator.py index 36ecb3b2662a..ffa5fa7eaf07 100644 --- a/chaotic/chaotic/back/cpp/translator.py +++ b/chaotic/chaotic/back/cpp/translator.py @@ -613,6 +613,7 @@ def _gen_array( json_schema=schema, nullable=schema.nullable, user_cpp_type=user_cpp_type, + default=schema.default, items=items, container=container, validators=cpp_types.CppArrayValidator( diff --git a/chaotic/chaotic/back/cpp/types.py b/chaotic/chaotic/back/cpp/types.py index 2c0873f47d75..cd1618239a67 100644 --- a/chaotic/chaotic/back/cpp/types.py +++ b/chaotic/chaotic/back/cpp/types.py @@ -2,6 +2,7 @@ from collections.abc import Callable import dataclasses import itertools +import json from typing import Any from chaotic.back.cpp import type_name @@ -685,6 +686,15 @@ class CppStructField: def _default(self) -> Any | None: return getattr(self.schema, 'default', None) + def has_array_default(self) -> bool: + return isinstance(self.schema, CppArray) and self.schema.default is not None + + def array_default_getter(self) -> str: + return f'GetFieldDefault{self.name}' + + def array_default_json(self) -> str: + return json.dumps(json.dumps(self._default(), ensure_ascii=True)) + def get_default_cpp_type(self) -> str: default = self._default() assert default is not None @@ -727,7 +737,10 @@ def get_default_raw(self) -> Any: else: return f'{default}' - def get_default(self) -> str: + def get_default(self, object_type: str | None = None) -> str: + if self.has_array_default(): + getter = self.array_default_getter() + return f'{object_type}::{getter}()' if object_type else f'{getter}()' default = self._get_default() if not default: return default @@ -788,7 +801,11 @@ def descriptor_type(self, object_type: str) -> str: mode = f'{ch}::Optional<{type_}>' else: default_var = f'{object_type}::kFieldDefault{name}' - mode = f'{ch}::Defaulted<{type_}, {self.get_default_cpp_type()}, {default_var}>' + if self.has_array_default(): + getter = f'&{object_type}::{self.array_default_getter()}' + mode = f'{ch}::DefaultedBy<{type_}, {getter}>' + else: + mode = f'{ch}::Defaulted<{type_}, {self.get_default_cpp_type()}, {default_var}>' name_var = self.property_name_holder_variable(object_type) return f'{ch}::Field<{object_type}, {mode}, &{object_type}::{name}, {name_var}>' @@ -973,6 +990,7 @@ class CppArray(CppType): items: CppType container: str validators: CppArrayValidator + default: list[Any] | None = None KNOWN_X_PROPERTIES = [ 'x-usrv-cpp-type', diff --git a/chaotic/chaotic/front/types.py b/chaotic/chaotic/front/types.py index 854b34c5f0c0..8bf03aa8913c 100644 --- a/chaotic/chaotic/front/types.py +++ b/chaotic/chaotic/front/types.py @@ -250,6 +250,7 @@ class Array(Schema): # validated in SchemaParser._parse_array() items: Schema type: str = 'array' + default: list[Any] | None = None nullable: bool = False minItems: int | None = None maxItems: int | None = None diff --git a/chaotic/include/userver/chaotic/object.hpp b/chaotic/include/userver/chaotic/object.hpp index e0b2ef23dff3..480c5727e6f7 100644 --- a/chaotic/include/userver/chaotic/object.hpp +++ b/chaotic/include/userver/chaotic/object.hpp @@ -29,6 +29,16 @@ struct Defaulted { using Descriptor = DescriptorType; static constexpr const T& DefaultValue = Default; + + static constexpr const T& GetDefaultValue() { return Default; } +}; + +template +struct DefaultedBy { + using Value = TypeOfDescriptor; + using Descriptor = DescriptorType; + + static const Value& GetDefaultValue() { return DefaultGetter(); } }; template @@ -37,6 +47,9 @@ struct IsDefaulted : std::false_type {}; template struct IsDefaulted> : std::true_type {}; +template +struct IsDefaulted> : std::true_type {}; + template < typename StructType, typename ModeDescriptor, diff --git a/chaotic/include/userver/chaotic/sax_parser/object.hpp b/chaotic/include/userver/chaotic/sax_parser/object.hpp index e5187cf82079..0f6ad73d2f9f 100644 --- a/chaotic/include/userver/chaotic/sax_parser/object.hpp +++ b/chaotic/include/userver/chaotic/sax_parser/object.hpp @@ -52,6 +52,11 @@ struct FieldModeSubscriber> { using Type = formats::json::parser::SubscriberSink>; }; +template +struct FieldModeSubscriber> { + using Type = formats::json::parser::SubscriberSink>; +}; + template using FieldSubscriber = typename FieldModeSubscriber::Type; @@ -221,14 +226,17 @@ class FieldSubparser { ); } } else if constexpr (IsDefaulted::value) { - using TargetType = std::decay_t; - using DefaultType = std::decay_t; - if constexpr (std::is_same_v) { - object_.*FieldType::kField = ModeDescriptorType::DefaultValue; - } else if constexpr (std::is_arithmetic_v && std::is_arithmetic_v) { - object_.*FieldType::kField = TargetType{ModeDescriptorType::DefaultValue}; - } else { - object_.*FieldType::kField = chaotic::ConvertTo(ModeDescriptorType::DefaultValue); + if (!seen_[Idx]) { + const auto& default_value = ModeDescriptorType::GetDefaultValue(); + using TargetType = std::decay_t; + using DefaultType = std::decay_t; + if constexpr (std::is_same_v) { + object_.*FieldType::kField = default_value; + } else if constexpr (std::is_arithmetic_v && std::is_arithmetic_v) { + object_.*FieldType::kField = TargetType{default_value}; + } else { + object_.*FieldType::kField = chaotic::ConvertTo(default_value); + } } } } diff --git a/chaotic/integration_tests/schemas/object_single_field.yaml b/chaotic/integration_tests/schemas/object_single_field.yaml index 249b0d4884ae..2f0868d34af5 100644 --- a/chaotic/integration_tests/schemas/object_single_field.yaml +++ b/chaotic/integration_tests/schemas/object_single_field.yaml @@ -107,6 +107,52 @@ definitions: items: type: integer + ObjectWithDefaultArrays: + type: object + additionalProperties: false + properties: + # /// [chaotic docs - array default] + # yaml + numbers: + type: array + items: + type: integer + default: [1, 2] + # /// [chaotic docs - array default] + empty: + type: array + items: + type: string + default: [] + text: + type: array + items: + type: string + default: ['a"b', 'c\d'] + nested: + type: array + items: + type: array + items: + type: integer + default: [[3, 4], [5]] + records: + type: array + items: + type: object + additionalProperties: false + required: [name] + properties: + name: + type: string + enabled: + type: boolean + default: true + default: + - name: first + - name: second + enabled: false + ObjectWithAdditionalPropertiesInt: type: object additionalProperties: diff --git a/chaotic/integration_tests/tests/render/object_single_field.cpp b/chaotic/integration_tests/tests/render/object_single_field.cpp index 1a87bdcf05b2..fc83cb5ef562 100644 --- a/chaotic/integration_tests/tests/render/object_single_field.cpp +++ b/chaotic/integration_tests/tests/render/object_single_field.cpp @@ -38,6 +38,40 @@ TEST(Simple, DefaultFieldValue) { EXPECT_EQ(obj.int_, 1); } +TEST(Simple, DefaultArrays) { + const ns::ObjectWithDefaultArrays defaults; + EXPECT_EQ(defaults.numbers, (std::vector{1, 2})); + EXPECT_TRUE(defaults.empty.empty()); + EXPECT_EQ(defaults.text, (std::vector{"a\"b", "c\\d"})); + EXPECT_EQ(defaults.nested, (std::vector>{{3, 4}, {5}})); + ASSERT_EQ(defaults.records.size(), 2); + EXPECT_EQ(defaults.records[0].name, "first"); + EXPECT_TRUE(defaults.records[0].enabled); + EXPECT_EQ(defaults.records[1].name, "second"); + EXPECT_FALSE(defaults.records[1].enabled); + + const auto dom = formats::json::MakeObject().As(); + const auto sax = CallSaxParser("{}"); + EXPECT_EQ(dom, defaults); + EXPECT_EQ(sax, defaults); +} + +TEST(Simple, ExplicitArraysOverrideDefaults) { + const auto json = formats::json::MakeObject( + "numbers", formats::json::MakeArray(9), + "empty", formats::json::MakeArray("present"), + "nested", formats::json::MakeArray(formats::json::MakeArray(8)), + "records", formats::json::MakeArray(formats::json::MakeObject("name", "override")) + ); + const auto dom = json.As(); + const auto sax = CallSaxParser(ToString(json)); + EXPECT_EQ(dom, sax); + EXPECT_EQ(dom.numbers, (std::vector{9})); + EXPECT_EQ(dom.empty, (std::vector{"present"})); + ASSERT_EQ(dom.records.size(), 1); + EXPECT_EQ(dom.records[0].name, "override"); +} + TEST(Simple, IntegerMinimum) { auto json = formats::json::MakeObject("int3", 1, "int", -10); UEXPECT_THROW_MSG( diff --git a/chaotic/tests/front/test_array.py b/chaotic/tests/front/test_array.py index f24a442d2c39..d46ee40fd578 100644 --- a/chaotic/tests/front/test_array.py +++ b/chaotic/tests/front/test_array.py @@ -14,6 +14,17 @@ def test_int_array(simple_parse): simple_parse({'type': 'array', 'items': {'type': 'integer'}}) +def test_array_default(simple_parse): + simple_parse({'type': 'array', 'items': {'type': 'integer'}, 'default': [1, 2]}) + simple_parse({'type': 'array', 'items': {'type': 'integer'}, 'default': []}) + + +def test_array_default_must_be_array(simple_parse): + with pytest.raises(front_parser.ParserError) as exc: + simple_parse({'type': 'array', 'items': {'type': 'integer'}, 'default': 'invalid'}) + assert exc.value.infile_path == '/definitions/type/items/default' + + def test_full_array(simple_parse): simple_parse({ 'type': 'array', diff --git a/scripts/docs/en/userver/chaotic.md b/scripts/docs/en/userver/chaotic.md index b2aafc01c18d..bc54ca439c7d 100644 --- a/scripts/docs/en/userver/chaotic.md +++ b/scripts/docs/en/userver/chaotic.md @@ -248,7 +248,13 @@ Array supports the following validators: * `maxItems` * `uniqueItems` -Example: +An optional array field may specify `default`, including an empty array or an array of objects. The generated C++ field receives that value when the property is absent. Chaotic parses the default with +the field's schema, so item conversions and validators apply to it as they do to a JSON input value. +For example: + +@snippet chaotic/integration_tests/schemas/object_single_field.yaml chaotic docs - array default + +Another array example: @snippet chaotic/golden_tests/schemas/docs/docs.yaml chaotic docs - type array From 4267395f3479b420ce8ad0d9898cfb8e33a60653 Mon Sep 17 00:00:00 2001 From: Tnirpps <87409111+Tnirpps@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:11:54 +0300 Subject: [PATCH 2/2] fix chaotic: Handle array defaults through refs --- chaotic/chaotic/back/cpp/types.py | 2 +- .../schemas/object_single_field.yaml | 16 ++++++++++++++++ .../tests/render/object_single_field.cpp | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/chaotic/chaotic/back/cpp/types.py b/chaotic/chaotic/back/cpp/types.py index d8c1c8505c86..a1676662f0a5 100644 --- a/chaotic/chaotic/back/cpp/types.py +++ b/chaotic/chaotic/back/cpp/types.py @@ -687,7 +687,7 @@ def _default(self) -> Any | None: return getattr(self.schema, 'default', None) def has_array_default(self) -> bool: - return isinstance(self.schema, CppArray) and self.schema.default is not None + return isinstance(self._resolved_schema(), CppArray) and self._default() is not None def array_default_getter(self) -> str: return f'GetFieldDefault{self.name}' diff --git a/chaotic/integration_tests/schemas/object_single_field.yaml b/chaotic/integration_tests/schemas/object_single_field.yaml index 2f0868d34af5..62ed4389ce9e 100644 --- a/chaotic/integration_tests/schemas/object_single_field.yaml +++ b/chaotic/integration_tests/schemas/object_single_field.yaml @@ -107,6 +107,22 @@ definitions: items: type: integer + DefaultNumbers: + type: array + items: + type: integer + default: [1, 2] + + ObjectWithDefaultArrayRefs: + type: object + additionalProperties: false + properties: + numbers: + $ref: '#/definitions/DefaultNumbers' + indirect: + $ref: '#/definitions/DefaultNumbers' + x-usrv-cpp-indirect: true + ObjectWithDefaultArrays: type: object additionalProperties: false diff --git a/chaotic/integration_tests/tests/render/object_single_field.cpp b/chaotic/integration_tests/tests/render/object_single_field.cpp index fc83cb5ef562..1732636fbe4a 100644 --- a/chaotic/integration_tests/tests/render/object_single_field.cpp +++ b/chaotic/integration_tests/tests/render/object_single_field.cpp @@ -72,6 +72,22 @@ TEST(Simple, ExplicitArraysOverrideDefaults) { EXPECT_EQ(dom.records[0].name, "override"); } +TEST(Simple, DefaultArrayRefs) { + const ns::ObjectWithDefaultArrayRefs defaults; + EXPECT_EQ(defaults.numbers, (std::vector{1, 2})); + EXPECT_EQ(*defaults.indirect, defaults.numbers); + EXPECT_EQ(formats::json::MakeObject().As(), defaults); + EXPECT_EQ(CallSaxParser("{}"), defaults); + + const auto json = formats::json::MakeObject( + "numbers", formats::json::MakeArray(9), "indirect", formats::json::MakeArray() + ); + const auto dom = json.As(); + EXPECT_EQ(dom.numbers, (std::vector{9})); + EXPECT_TRUE(dom.indirect->empty()); + EXPECT_EQ(CallSaxParser(ToString(json)), dom); +} + TEST(Simple, IntegerMinimum) { auto json = formats::json::MakeObject("int3", 1, "int", -10); UEXPECT_THROW_MSG(