Skip to content

Commit 86a10ab

Browse files
committed
feat chaotic: implement uniqueItems
commit_hash:f9f4faa6221765f3a1d3b9f5e89a40ac6192f855
1 parent 5bcfaae commit 86a10ab

9 files changed

Lines changed: 157 additions & 0 deletions

File tree

chaotic/chaotic/back/cpp/translator.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,12 @@ def _gen_array(
568568
# TODO: name?
569569
items = self._generate_type(name.add_suffix('A'), schema.items)
570570

571+
if schema.uniqueItems and not isinstance(schema.items, (types.Integer, types.String, types.Boolean)):
572+
self._raise(
573+
schema,
574+
'uniqueItems is only supported for integer, string, and boolean item types',
575+
)
576+
571577
user_cpp_type = self._extract_user_cpp_type(schema)
572578
container = self._extract_container(schema)
573579

@@ -585,6 +591,7 @@ def _gen_array(
585591
validators=cpp_types.CppArrayValidator(
586592
minItems=schema.minItems,
587593
maxItems=schema.maxItems,
594+
uniqueItems=schema.uniqueItems,
588595
),
589596
)
590597

chaotic/chaotic/back/cpp/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@ def need_operator_eq(self) -> bool:
844844
class CppArrayValidator:
845845
minItems: int | None = None
846846
maxItems: int | None = None
847+
uniqueItems: bool = False
847848

848849
def is_none(self) -> bool:
849850
return self == CppArrayValidator()
@@ -876,6 +877,8 @@ def parser_type(self, ns: str, name: str) -> str:
876877
validators += f', USERVER_NAMESPACE::chaotic::MinItems<{self.validators.minItems}>'
877878
if self.validators.maxItems is not None:
878879
validators += f', USERVER_NAMESPACE::chaotic::MaxItems<{self.validators.maxItems}>'
880+
if self.validators.uniqueItems:
881+
validators += ', USERVER_NAMESPACE::chaotic::UniqueItems'
879882

880883
parser_type = (
881884
'USERVER_NAMESPACE::chaotic::Array'

chaotic/chaotic/front/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ class Array(Schema):
247247
nullable: bool = False
248248
minItems: int | None = None
249249
maxItems: int | None = None
250+
uniqueItems: bool = False
250251
deprecated: bool = False
251252

252253
@classmethod

chaotic/include/userver/chaotic/validators.hpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
#pragma once
22

33
#include <cstdint>
4+
#include <functional>
45
#include <string>
6+
#include <unordered_set>
57

68
#include <userver/chaotic/exception.hpp>
9+
#include <userver/utils/meta.hpp>
710
#include <userver/utils/text_light.hpp>
811

912
#include <fmt/format.h>
@@ -80,6 +83,48 @@ struct MaxItems final {
8083
}
8184
};
8285

86+
namespace impl {
87+
88+
template <typename T>
89+
struct RefHash {
90+
std::size_t operator()(std::reference_wrapper<const T> ref) const { return std::hash<T>{}(ref.get()); }
91+
};
92+
93+
template <typename T>
94+
struct RefEqual {
95+
bool operator()(std::reference_wrapper<const T> a, std::reference_wrapper<const T> b) const {
96+
return a.get() == b.get();
97+
}
98+
};
99+
100+
template <typename T>
101+
auto MakeReferenceSet(std::size_t reserve_size) {
102+
using Ref = std::reference_wrapper<const T>;
103+
std::unordered_set<Ref, RefHash<T>, RefEqual<T>> seen;
104+
seen.reserve(reserve_size);
105+
return seen;
106+
}
107+
108+
} // namespace impl
109+
110+
struct UniqueItems final {
111+
template <typename T, typename ErrorReporter>
112+
static void Validate(const T& value, ErrorReporter report_error) {
113+
using Item = typename T::value_type;
114+
static_assert(
115+
meta::IsStdHashable<Item>,
116+
"UniqueItems requires a hashable item type (integer, string, or boolean)"
117+
);
118+
auto seen = impl::MakeReferenceSet<Item>(value.size());
119+
for (const auto& item : value) {
120+
if (!seen.insert(std::cref(item)).second) {
121+
report_error(fmt::format("Duplicate items are not allowed ({})", item));
122+
return;
123+
}
124+
}
125+
}
126+
};
127+
83128
template <std::int64_t Value>
84129
struct MinLength final {
85130
template <typename ErrorReporter>

chaotic/integration_tests/schemas/int_minmax.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ definitions:
1717
$ref: '#/definitions/StringMinMaxLength'
1818
zoo:
1919
$ref: '#/definitions/ArrayMinMaxItems'
20+
qux:
21+
$ref: '#/definitions/ArrayUniqueItems'
2022
StringMinMaxLength:
2123
type: string
2224
minLength: 2
@@ -27,6 +29,11 @@ definitions:
2729
type: integer
2830
minItems: 2
2931
maxItems: 5
32+
ArrayUniqueItems:
33+
type: array
34+
items:
35+
type: integer
36+
uniqueItems: true
3037
IntegerMinMaxExclusiveLegacy:
3138
type: integer
3239
minimum: 1

chaotic/integration_tests/tests/render/minmax.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,21 @@ TEST(MinMax, Array) {
9797
);
9898
}
9999

100+
TEST(MinMax, ArrayUniqueItems) {
101+
auto json = formats::json::MakeObject("qux", formats::json::MakeArray(1, 2, 1));
102+
UEXPECT_THROW_MSG(
103+
json.As<ns::IntegerObject>(),
104+
chaotic::Error<formats::json::Value>,
105+
"Error at path 'qux': Duplicate items are not allowed"
106+
);
107+
UEXPECT_THROW_MSG(
108+
FromJsonString(ToString(json), formats::parse::To<ns::IntegerObject>()),
109+
formats::json::parser::ParseError,
110+
"Parse error at pos 13, path 'qux': Error at path 'qux': Duplicate items are not allowed"
111+
);
112+
113+
json = formats::json::MakeObject("qux", formats::json::MakeArray(1, 2, 3));
114+
EXPECT_NO_THROW(json.As<ns::IntegerObject>());
115+
}
116+
100117
USERVER_NAMESPACE_END

chaotic/tests/back/cpp/test_tr_array.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
3+
from chaotic.back.cpp import translator as cpp_translator
14
from chaotic.back.cpp import type_name
25
from chaotic.back.cpp import types as cpp_types
36
from chaotic.front import types as front_types
@@ -21,6 +24,71 @@ def test_array_int(simple_gen, cpp_primitive_type):
2124
}
2225

2326

27+
@pytest.mark.parametrize(
28+
'item_schema,expected_cpp_type,expected_validators',
29+
[
30+
(
31+
{'type': 'integer'},
32+
'int',
33+
cpp_types.CppPrimitiveValidator(prefix='typeA'),
34+
),
35+
(
36+
{'type': 'string'},
37+
'std::string',
38+
cpp_types.CppPrimitiveValidator(prefix='typeA'),
39+
),
40+
(
41+
{'type': 'boolean'},
42+
'bool',
43+
cpp_types.CppPrimitiveValidator(),
44+
),
45+
],
46+
)
47+
def test_array_unique_items_valid_types(
48+
simple_gen,
49+
cpp_primitive_type,
50+
item_schema,
51+
expected_cpp_type,
52+
expected_validators,
53+
):
54+
types = simple_gen({
55+
'type': 'array',
56+
'uniqueItems': True,
57+
'items': item_schema,
58+
})
59+
assert types == {
60+
'::type': cpp_types.CppArray(
61+
raw_cpp_type=type_name.TypeName('::type'),
62+
user_cpp_type=None,
63+
json_schema=front_types.Schema(),
64+
nullable=False,
65+
items=cpp_primitive_type(
66+
validators=expected_validators,
67+
raw_cpp_type_str=expected_cpp_type,
68+
),
69+
container='std::vector',
70+
validators=cpp_types.CppArrayValidator(uniqueItems=True),
71+
),
72+
}
73+
74+
75+
@pytest.mark.parametrize(
76+
'item_schema',
77+
[
78+
{'type': 'number'},
79+
{'type': 'array', 'items': {'type': 'integer'}},
80+
],
81+
)
82+
def test_array_unique_items_invalid_type(simple_gen, item_schema):
83+
with pytest.raises(cpp_translator.TranslatorError) as exc:
84+
simple_gen({
85+
'type': 'array',
86+
'uniqueItems': True,
87+
'items': item_schema,
88+
})
89+
assert exc.value.msg == ('uniqueItems is only supported for integer, string, and boolean item types')
90+
91+
2492
def test_array_array_with_validators(simple_gen, cpp_primitive_type):
2593
types = simple_gen({
2694
'type': 'array',

chaotic/tests/front/test_array.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ def test_full_array(simple_parse):
2323
})
2424

2525

26+
def test_array_unique_items(simple_parse):
27+
simple_parse({
28+
'type': 'array',
29+
'uniqueItems': True,
30+
'items': {'type': 'integer'},
31+
})
32+
33+
2634
def test_int_array_array_array(simple_parse):
2735
simple_parse({
2836
'type': 'array',

scripts/docs/en/userver/chaotic.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ Array type is mapped to different C++ types depending on `x-usrv-cpp-container`
283283
Array supports the following validators:
284284
* `minItems`
285285
* `maxItems`
286+
* `uniqueItems`
286287
287288
Example:
288289

0 commit comments

Comments
 (0)