$darkmode
array.hpp
Go to the documentation of this file.
1 /*
2  * Copyright 2022 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  */
24 #pragma once
25 
26 #include <limits> // for numeric_limits<>
27 #include <memory> // for shared_ptr<>
28 #include <string> // for string, stoul
29 #include <type_traits> // for enable_if_t<>, is_convertible<>
30 #include <utility> // for move
31 #include <vector> // for vector<>
32 
33 #include <fable/json.hpp> // for Json
34 #include <fable/schema/interface.hpp> // for Base<>, Box
35 
36 namespace fable::schema {
37 
38 template <typename T, size_t N, typename P>
39 class Array : public Base<Array<T, N, P>> {
40  public: // Types and Constructors
41  using Type = std::array<T, N>;
42  using PrototypeSchema = P;
43 
44  Array(Type* ptr, std::string desc)
45  : Array<T, N, P>(ptr, make_prototype<T>(), std::move(desc)) {}
46 
47  Array(Type* ptr, PrototypeSchema prototype)
48  : Base<Array<T, N, P>>(), prototype_(std::move(prototype)), ptr_(ptr) {}
49 
50  Array(Type* ptr, PrototypeSchema prototype, std::string desc)
51  : Base<Array<T, N, P>>(std::move(desc)), prototype_(std::move(prototype)), ptr_(ptr) {}
52 
53  public: // Specials
59  [[nodiscard]] bool require_all() const { return option_require_all_; }
60 
64  void set_require_all(bool value) {
65  option_require_all_ = value;
66  this->type_ = value ? JsonType::array : JsonType::null;
67  }
68 
72  Array<T, N, P> require_all(bool value) && {
73  this->set_require_all(value);
74  return std::move(*this);
75  }
76 
77  public: // Overrides
78  [[nodiscard]] std::string type_string() const override { return "array of " + prototype_.type_string(); }
79 
80  [[nodiscard]] Json json_schema() const override {
81  Json j;
82  if (option_require_all_) {
83  j = this->json_schema_array();
84  } else {
85  j = Json::object({
86  {"oneOf", Json::array({
87  this->json_schema_array(),
88  this->json_schema_object(),
89  })},
90  });
91  }
92 
93  this->augment_schema(j);
94  return j;
95  }
96 
97  bool validate(const Conf& c, std::optional<SchemaError>& err) const override {
98  if (option_require_all_) {
99  // Only support array input that sets all indices.
100  if (!this->validate_type(c, err)) {
101  return false;
102  }
103  if (!this->validate_array(c, err)) {
104  return false;
105  }
106  return true;
107  }
108 
109  // If not require-all, then we can also support object notation.
110  switch (c->type()) {
111  case JsonType::array:
112  return this->validate_array(c, err);
113  case JsonType::object:
114  return this->validate_object(c, err);
115  default:
116  return this->set_wrong_type(err, c);
117  }
118  }
119 
120  using Interface::to_json;
121  void to_json(Json& j) const override {
122  assert(ptr_ != nullptr);
123  j = serialize(*ptr_);
124  }
125 
126  void from_conf(const Conf& c) override {
127  assert(ptr_ != nullptr);
128  assert(c->type() == JsonType::array || c->type() == JsonType::object);
129 
130  this->deserialize_into(c, *ptr_);
131  }
132 
133  [[nodiscard]] Json serialize(const Type& xs) const {
134  Json j = Json::array();
135  serialize_into(j, xs);
136  return j;
137  }
138 
142  void serialize_into(Json& j, const Type& v) const {
143  assert(j.type() == JsonType::array);
144  for (const auto& x : v) {
145  j.emplace_back(prototype_.serialize(x));
146  }
147  }
148 
155  [[nodiscard]] Type deserialize(const Conf& c) const {
156  Type array;
157  this->deserialize_into(c, array);
158  return array;
159  }
160 
161  void deserialize_into(const Conf& c, Type& v) const {
162  if (c->type() == JsonType::array) {
163  this->deserialize_from_array(v, c);
164  } else if (c->type() == JsonType::object) {
165  this->deserialize_from_object(v, c);
166  } else {
167  throw this->wrong_type(c);
168  }
169  }
170 
171  void reset_ptr() override { ptr_ = nullptr; }
172 
173  private:
174  [[nodiscard]] Json json_schema_array() const {
175  return Json::object({
176  {"type", "array"},
177  {"items", prototype_.json_schema()},
178  {"minItems", N},
179  {"maxItems", N},
180  });
181  }
182 
183  [[nodiscard]] Json json_schema_object() const {
184  return Json::object({
185  {"type", "object"},
186  {"additionalProperties", false},
187  {"patternProperties",
188  {
189  {"^[0-9]+$",
190  {
191  {"type", prototype_.json_schema()},
192  }},
193  }},
194  });
195  }
196 
207  bool validate_array(const Conf& c, std::optional<SchemaError>& err) const {
208  assert(c->type() == JsonType::array);
209  if (c->size() != N) {
210  return this->set_error(err, c, "require exactly {} items in array, got {}", N, c->size());
211  }
212  for (const auto& x : c.to_array()) {
213  if (!prototype_.validate(x, err)) {
214  return false;
215  }
216  }
217  return true;
218  }
219 
234  bool validate_object(const Conf& c, std::optional<SchemaError>& err) const {
235  assert(c->type() == JsonType::object);
236  for (const auto& kv : c->items()) {
237  const auto& key = kv.key();
238  try {
239  std::ignore = this->parse_index(key);
240  } catch (std::exception& e) {
241  return this->set_error(err, c, e.what());
242  }
243  if (prototype_.validate(c.at(key), err)) {
244  return false;
245  }
246  }
247  return true;
248  }
249 
262  [[nodiscard]] size_t parse_index(const std::string& s) const {
263  // We'd like to just be able to use std::stoul, but unfortunately
264  // the standard library seems to think strings like "234x" are ok.
265  if (s.empty()) {
266  throw std::invalid_argument("invalid index key in object, require integer, got ''");
267  }
268  if (s.size() > 1 && s[0] == '0') {
269  throw std::invalid_argument(fmt::format("invalid index key in object, require base-10 value, got '{}'", s));
270  }
271  for (char ch : s) {
272  if (ch < '0' || ch > '9') {
273  throw std::invalid_argument(fmt::format("invalid index key in object, require integer, got '{}'", s));
274  }
275  }
276  size_t idx = std::stoul(s);
277  if (idx >= N) {
278  throw std::invalid_argument(fmt::format("out-of-range index key in object, require < {}, got '{}'", N, s));
279  }
280  return idx;
281  }
282 
283  [[nodiscard]] size_t parse_index(const Conf& c, const std::string& s) const {
284  try {
285  return parse_index(s);
286  } catch (std::exception& e) {
287  throw this->error(c, e.what());
288  }
289  }
290 
291  [[nodiscard]] SchemaError wrong_type(const Conf& c) const {
292  std::string got = to_string(c->type());
293  return this->error(c, "property must have type array or object, got {}", got);
294  }
295 
296  void deserialize_from_object(Type& array, const Conf& c) const {
297  for (const auto& kv : c->items()) {
298  const auto& key = kv.key();
299  size_t idx = this->parse_index(c, key);
300  prototype_.deserialize_into(c.at(key), array[idx]);
301  }
302  }
303 
304  void deserialize_from_array(Type& array, const Conf& c) const {
305  auto src = c.to_array();
306  size_t n = src.size();
307  assert(n == N);
308  for (size_t i = 0; i < n; i++) {
309  array[i] = prototype_.deserialize(src[i]);
310  }
311  }
312 
313  private:
314  bool option_require_all_{false};
315  PrototypeSchema prototype_{};
316  Type* ptr_{nullptr};
317 };
318 
319 template <typename T, typename P, size_t N>
320 Array<T, N, P> make_schema(std::array<T, N>* ptr, P prototype, std::string desc) {
321  return Array<T, N, P>(ptr, std::move(prototype), std::move(desc));
322 }
323 
324 template <typename T, size_t N>
325 Array<T, N, decltype(make_prototype<T>())> make_schema(std::array<T, N>* ptr, std::string desc) {
326  return Array<T, N, decltype(make_prototype<T>())>(ptr, std::move(desc));
327 }
328 
329 } // namespace fable::schema
Definition: conf.hpp:76
Definition: array.hpp:39
void reset_ptr() override
Definition: array.hpp:171
Type deserialize(const Conf &c) const
Definition: array.hpp:155
void from_conf(const Conf &c) override
Definition: array.hpp:126
bool validate(const Conf &c, std::optional< SchemaError > &err) const override
Definition: array.hpp:97
Json json_schema() const override
Definition: array.hpp:80
void to_json(Json &j) const override
Definition: array.hpp:121
bool require_all() const
Definition: array.hpp:59
void serialize_into(Json &j, const Type &v) const
Definition: array.hpp:142
Array< T, N, P > require_all(bool value) &&
Definition: array.hpp:72
void set_require_all(bool value)
Definition: array.hpp:64
std::string type_string() const override
Definition: array.hpp:78
Definition: interface.hpp:398
bool validate_type(const Conf &c, std::optional< SchemaError > &err) const
Definition: interface.hpp:459
virtual Json to_json() const
Definition: interface.hpp:254
nlohmann::json Json
Definition: fable_fwd.hpp:35