Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions chaotic/chaotic/back/cpp/templates/type.cpp.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,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() %}
Expand Down Expand Up @@ -408,6 +425,8 @@

{{- _generate_string_parser_definition(type) -}}

{{- _generate_array_default_definitions(type) -}}

{% if generate_serializer %}
{{- _generate_serializer_definition(type) -}}
{% if generate_stream_writer %}
Expand Down
4 changes: 4 additions & 0 deletions chaotic/chaotic/back/cpp/templates/type.hpp.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }},
Expand Down
1 change: 1 addition & 0 deletions chaotic/chaotic/back/cpp/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
22 changes: 20 additions & 2 deletions chaotic/chaotic/back/cpp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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._resolved_schema(), CppArray) and self._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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -811,7 +824,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}>'
Expand Down Expand Up @@ -996,6 +1013,7 @@ class CppArray(CppType):
items: CppType
container: str
validators: CppArrayValidator
default: list[Any] | None = None

KNOWN_X_PROPERTIES = [
'x-usrv-cpp-type',
Expand Down
1 change: 1 addition & 0 deletions chaotic/chaotic/front/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions chaotic/include/userver/chaotic/object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ struct Defaulted {
using Descriptor = DescriptorType;

static constexpr const T& DefaultValue = Default;

static constexpr const T& GetDefaultValue() { return Default; }
};

template <typename DescriptorType, auto DefaultGetter>
struct DefaultedBy {
using Value = TypeOfDescriptor<DescriptorType>;
using Descriptor = DescriptorType;

static const Value& GetDefaultValue() { return DefaultGetter(); }
};

template <typename T>
Expand All @@ -37,6 +47,9 @@ struct IsDefaulted : std::false_type {};
template <typename DescriptorType, typename T, const T& Default>
struct IsDefaulted<Defaulted<DescriptorType, T, Default>> : std::true_type {};

template <typename DescriptorType, auto DefaultGetter>
struct IsDefaulted<DefaultedBy<DescriptorType, DefaultGetter>> : std::true_type {};

template <
typename StructType,
typename ModeDescriptor,
Expand Down
14 changes: 10 additions & 4 deletions chaotic/include/userver/chaotic/sax_parser/object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ struct FieldModeSubscriber<Defaulted<DescriptorType, T, Default>> {
using Type = formats::json::parser::SubscriberSink<TypeOfDescriptor<DescriptorType>>;
};

template <typename DescriptorType, auto DefaultGetter>
struct FieldModeSubscriber<DefaultedBy<DescriptorType, DefaultGetter>> {
using Type = formats::json::parser::SubscriberSink<TypeOfDescriptor<DescriptorType>>;
};

template <typename Field>
using FieldSubscriber = typename FieldModeSubscriber<typename Field::ModeDescriptorType>::Type;

Expand Down Expand Up @@ -225,14 +230,15 @@ class FieldSubparser {
return;
}

const auto& default_value = ModeDescriptorType::GetDefaultValue();
using TargetType = std::decay_t<decltype(object_.*FieldType::kField)>;
using DefaultType = std::decay_t<decltype(ModeDescriptorType::DefaultValue)>;
using DefaultType = std::decay_t<decltype(default_value)>;
if constexpr (std::is_same_v<TargetType, DefaultType>) {
object_.*FieldType::kField = ModeDescriptorType::DefaultValue;
object_.*FieldType::kField = default_value;
} else if constexpr (std::is_arithmetic_v<TargetType> && std::is_arithmetic_v<DefaultType>) {
object_.*FieldType::kField = TargetType{ModeDescriptorType::DefaultValue};
object_.*FieldType::kField = TargetType{default_value};
} else {
object_.*FieldType::kField = chaotic::ConvertTo<TargetType>(ModeDescriptorType::DefaultValue);
object_.*FieldType::kField = chaotic::ConvertTo<TargetType>(default_value);
}
}
}
Expand Down
62 changes: 62 additions & 0 deletions chaotic/integration_tests/schemas/object_single_field.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,68 @@ 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
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:
Expand Down
50 changes: 50 additions & 0 deletions chaotic/integration_tests/tests/render/object_single_field.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,56 @@ 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<std::string>{"a\"b", "c\\d"}));
EXPECT_EQ(defaults.nested, (std::vector<std::vector<int>>{{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<ns::ObjectWithDefaultArrays>();
const auto sax = CallSaxParser<ns::ObjectWithDefaultArrays>("{}");
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<ns::ObjectWithDefaultArrays>();
const auto sax = CallSaxParser<ns::ObjectWithDefaultArrays>(ToString(json));
EXPECT_EQ(dom, sax);
EXPECT_EQ(dom.numbers, (std::vector{9}));
EXPECT_EQ(dom.empty, (std::vector<std::string>{"present"}));
ASSERT_EQ(dom.records.size(), 1);
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<ns::ObjectWithDefaultArrayRefs>(), defaults);
EXPECT_EQ(CallSaxParser<ns::ObjectWithDefaultArrayRefs>("{}"), defaults);

const auto json = formats::json::MakeObject(
"numbers", formats::json::MakeArray(9), "indirect", formats::json::MakeArray()
);
const auto dom = json.As<ns::ObjectWithDefaultArrayRefs>();
EXPECT_EQ(dom.numbers, (std::vector{9}));
EXPECT_TRUE(dom.indirect->empty());
EXPECT_EQ(CallSaxParser<ns::ObjectWithDefaultArrayRefs>(ToString(json)), dom);
}

TEST(Simple, IntegerMinimum) {
auto json = formats::json::MakeObject("int3", 1, "int", -10);
UEXPECT_THROW_MSG(
Expand Down
11 changes: 11 additions & 0 deletions chaotic/tests/front/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 7 additions & 1 deletion scripts/docs/en/userver/chaotic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading