Skip to content

Commit 70d5760

Browse files
committed
fable: Extend String schema with enum_of method
This allows input string to explicitely match a specific list of valid strings, and is useful when we want to have a list of valid values, but not define an enum for it. In addition, String is now fully checked with a unit test.
1 parent 1a97427 commit 70d5760

File tree

4 files changed

+187
-3
lines changed

4 files changed

+187
-3
lines changed

fable/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ if(BUILD_TESTING)
7777
src/fable/schema/ignore_test.cpp
7878
src/fable/schema/number_test.cpp
7979
src/fable/schema/optional_test.cpp
80+
src/fable/schema/string_test.cpp
8081
src/fable/schema/struct_test.cpp
8182
src/fable/schema_test.cpp
8283
)

fable/include/fable/schema/string.hpp

+31
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
/**
1919
* \file fable/schema/string.hpp
2020
* \see fable/schema/string.cpp
21+
* \see fable/schema/string_test.cpp
2122
* \see fable/schema.hpp
2223
* \see fable/schema_test.cpp
2324
*/
@@ -29,6 +30,7 @@
2930
#include <limits> // for numeric_limits<>
3031
#include <string> // for string
3132
#include <utility> // for move
33+
#include <vector> // for vector<>
3234

3335
#include <fable/schema/interface.hpp> // for Base<>
3436

@@ -213,6 +215,34 @@ class String : public Base<String> {
213215
*/
214216
void set_environment(Environment* env);
215217

218+
/**
219+
* Return the set of valid values for the string.
220+
*
221+
* \return valid values
222+
*/
223+
const std::vector<std::string>& enum_of() const;
224+
225+
/**
226+
* Set the valid values for the string.
227+
*
228+
* \note If a pattern is set, the string will need to validate
229+
* against the pattern in addition to this list.
230+
*
231+
* \param init list of valid values
232+
* \return *this, for chaining
233+
*/
234+
String enum_of(std::vector<std::string>&& init);
235+
236+
/**
237+
* Set the valid values for the string.
238+
*
239+
* \note If a pattern is set, the string will need to validate
240+
* against the pattern in addition to this list.
241+
*
242+
* \param init list of valid values
243+
*/
244+
void set_enum_of(std::vector<std::string>&& init);
245+
216246
public: // Overrides
217247
Json json_schema() const override;
218248
void validate(const Conf& c) const override;
@@ -228,6 +258,7 @@ class String : public Base<String> {
228258
size_t min_length_{0};
229259
size_t max_length_{std::numeric_limits<size_t>::max()};
230260
std::string pattern_{};
261+
std::vector<std::string> enum_{};
231262
Environment* env_{nullptr};
232263
Type* ptr_{nullptr};
233264
};

fable/src/fable/schema/string.cpp

+19-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222

2323
#include <fable/schema/string.hpp>
2424

25-
#include <limits> // for numeric_limits
26-
#include <regex> // for regex, regex_match
27-
#include <string> // for string
25+
#include <algorithm> // for find
26+
#include <limits> // for numeric_limits
27+
#include <regex> // for regex, regex_match
28+
#include <string> // for string
2829

2930
#include <fable/environment.hpp> // for interpolate_vars
3031

@@ -76,13 +77,23 @@ String String::environment(Environment* env) && {
7677
return std::move(*this);
7778
}
7879

80+
const std::vector<std::string>& String::enum_of() const { return enum_; }
81+
void String::set_enum_of(std::vector<std::string>&& init) { enum_ = std::move(init); }
82+
String String::enum_of(std::vector<std::string>&& init) {
83+
enum_ = std::move(init);
84+
return std::move(*this);
85+
}
86+
7987
Json String::json_schema() const {
8088
Json j{
8189
{"type", "string"},
8290
};
8391
if (!pattern_.empty()) {
8492
j["pattern"] = pattern_;
8593
}
94+
if (!enum_.empty()) {
95+
j["enum"] = enum_;
96+
}
8697
if (min_length_ != 0) {
8798
j["minLength"] = min_length_;
8899
}
@@ -109,6 +120,11 @@ void String::validate(const Conf& c) const {
109120
if (!pattern_.empty() && !std::regex_match(src, std::regex(pattern_))) {
110121
this->throw_error(c, "expect string to match regex '{}': {}", pattern_, src);
111122
}
123+
if (!enum_.empty()) {
124+
if (std::find(enum_.begin(), enum_.end(), src) == enum_.end()) {
125+
this->throw_error(c, "expect string to match one of {}, got {}", Json{enum_}.dump(), src);
126+
}
127+
}
112128
} // namespace schema
113129

114130
void String::reset_ptr() { ptr_ = nullptr; }
+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2023 Robert Bosch GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
/**
19+
* \file fable/schema/string_test.cpp
20+
* \see fable/schema/string.hpp
21+
*/
22+
23+
#include <gtest/gtest.h>
24+
25+
#include <fable/confable.hpp> // for Confable
26+
#include <fable/environment.hpp> // for Environment
27+
#include <fable/schema/string.hpp> // for String, ...
28+
#include <fable/utility/gtest.hpp> // for assert_to_json, ...
29+
30+
#define TO_CONF(x) fable::Conf{fable::Json(x)}
31+
32+
TEST(fable_schema_string, plain) {
33+
std::string t;
34+
auto s = fable::schema::make_schema(&t, "plain");
35+
36+
fable::assert_to_json(s, TO_CONF(""));
37+
fable::assert_from_eq_to(s, TO_CONF("hello string"));
38+
fable::assert_schema_eq(s, R"({
39+
"type": "string",
40+
"description": "plain"
41+
})");
42+
}
43+
44+
TEST(fable_schema_string, not_empty) {
45+
std::string t;
46+
auto s = fable::schema::make_schema(&t, "not empty").not_empty();
47+
48+
fable::assert_invalidate(s, TO_CONF(""));
49+
fable::assert_from_eq_to(s, TO_CONF("not empty"));
50+
fable::assert_schema_eq(s, R"({
51+
"type": "string",
52+
"description": "not empty",
53+
"minLength": 1
54+
})");
55+
}
56+
57+
TEST(fable_schema_string, min_max_length) {
58+
std::string t;
59+
auto s = fable::schema::make_schema(&t, "min 4, max 8").min_length(4).max_length(8);
60+
61+
for (const auto& x : {"", "a", "asdfasdfx"}) {
62+
fable::assert_invalidate(s, TO_CONF(x));
63+
}
64+
for (const auto& x : {"asdf", "xxxxxx", "abcdefgh"}) {
65+
fable::assert_from_eq_to(s, TO_CONF(x));
66+
}
67+
fable::assert_schema_eq(s, R"({
68+
"type": "string",
69+
"description": "min 4, max 8",
70+
"minLength": 4,
71+
"maxLength": 8
72+
})");
73+
}
74+
75+
TEST(fable_schema_string, pattern) {
76+
std::string t;
77+
auto s = fable::schema::make_schema(&t, "c_identifier").c_identifier();
78+
79+
for (const auto& x : {"0_", "0", "not identifier", "", "a-b"}) {
80+
fable::assert_invalidate(s, TO_CONF(x));
81+
}
82+
for (const auto& x : {"asdf", "_8", "_", "c_identier", "somethingElse"}) {
83+
fable::assert_from_eq_to(s, TO_CONF(x));
84+
}
85+
fable::assert_schema_eq(s, R"({
86+
"type": "string",
87+
"description": "c_identifier",
88+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
89+
})");
90+
}
91+
92+
TEST(fable_schema_string, interpolate) {
93+
std::string t;
94+
fable::Environment env{
95+
{"TEST", "true"},
96+
{"NAME", "world"}
97+
};
98+
auto s = fable::schema::make_schema(&t, "interpolate").interpolate(true).environment(&env);
99+
100+
EXPECT_ANY_THROW(assert_from_conf(s, TO_CONF("${}")));
101+
EXPECT_ANY_THROW(assert_from_conf(s, TO_CONF("${__UNLIKELY}")));
102+
103+
fable::assert_from_conf(s, TO_CONF("${__UNLIKELY-default}"));
104+
fable::assert_to_json(s, fable::Json("default"));
105+
ASSERT_EQ(t, "default");
106+
107+
fable::assert_from_conf(s, TO_CONF("${TEST-false}"));
108+
fable::assert_to_json(s, fable::Json("true"));
109+
ASSERT_EQ(t, "true");
110+
111+
fable::assert_from_conf(s, TO_CONF("hello ${NAME}"));
112+
fable::assert_to_json(s, fable::Json("hello world"));
113+
ASSERT_EQ(t, "hello world");
114+
115+
fable::assert_schema_eq(s, R"({
116+
"type": "string",
117+
"description": "interpolate"
118+
})");
119+
}
120+
121+
TEST(fable_schema_string, enum) {
122+
std::string t;
123+
auto s = fable::schema::make_schema(&t, "enum").enum_of({"true", "false"});
124+
125+
for (const auto& x : {"", "asdf", "False"}) {
126+
fable::assert_invalidate(s, TO_CONF(x));
127+
}
128+
for (const auto& x : {"true", "false"}) {
129+
fable::assert_from_eq_to(s, TO_CONF(x));
130+
}
131+
fable::assert_schema_eq(s, R"({
132+
"type": "string",
133+
"description": "enum",
134+
"enum": [ "true", "false" ]
135+
})");
136+
}

0 commit comments

Comments
 (0)