$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 
95 template <typename T, size_t N, typename P>
96 class Array : public Base<Array<T, N, P>> {
97  public: // Types and Constructors
98  using Type = std::array<T, N>;
99  using PrototypeSchema = P;
100 
101  Array(Type* ptr, std::string desc) : Array<T, N, P>(ptr, make_prototype<T>(), std::move(desc)) {}
102 
103  Array(Type* ptr, PrototypeSchema prototype)
104  : Base<Array<T, N, P>>(), prototype_(std::move(prototype)), ptr_(ptr) {}
105 
106  Array(Type* ptr, PrototypeSchema prototype, std::string desc)
107  : Base<Array<T, N, P>>(std::move(desc)), prototype_(std::move(prototype)), ptr_(ptr) {}
108 
109  public: // Specials
116  [[nodiscard]] bool require_all() const { return option_require_all_; }
117 
135  void set_require_all(bool value) {
136  option_require_all_ = value;
137  this->type_ = value ? JsonType::array : JsonType::null;
138  }
139 
145  [[nodiscard]] Array<T, N, P> require_all(bool value) && {
146  this->set_require_all(value);
147  return std::move(*this);
148  }
149 
150  public: // Overrides
151  [[nodiscard]] std::string type_string() const override {
152  return "array of " + prototype_.type_string();
153  }
154 
155  [[nodiscard]] Json json_schema() const override {
156  Json j;
157  if (option_require_all_) {
158  j = this->json_schema_array();
159  } else {
160  j = Json::object({
161  {"oneOf", Json::array({
162  this->json_schema_array(),
163  this->json_schema_object(),
164  })},
165  });
166  }
167 
168  this->augment_schema(j);
169  return j;
170  }
171 
172  bool validate(const Conf& c, std::optional<SchemaError>& err) const override {
173  if (option_require_all_) {
174  // Only support array input that sets all indices.
175  if (!this->validate_type(c, err)) {
176  return false;
177  }
178  if (!this->validate_array(c, err)) {
179  return false;
180  }
181  return true;
182  }
183 
184  // If not require-all, then we can also support object notation.
185  switch (c->type()) {
186  case JsonType::array:
187  return this->validate_array(c, err);
188  case JsonType::object:
189  return this->validate_object(c, err);
190  default:
191  return this->set_wrong_type(err, c);
192  }
193  }
194 
195  using Interface::to_json;
196 
197  void to_json(Json& j) const override {
198  assert(ptr_ != nullptr);
199  j = serialize(*ptr_);
200  }
201 
202  void from_conf(const Conf& c) override {
203  assert(ptr_ != nullptr);
204  assert(c->type() == JsonType::array || c->type() == JsonType::object);
205 
206  this->deserialize_into(c, *ptr_);
207  }
208 
209  [[nodiscard]] Json serialize(const Type& xs) const {
210  Json j = Json::array();
211  serialize_into(j, xs);
212  return j;
213  }
214 
218  void serialize_into(Json& j, const Type& v) const {
219  assert(j.type() == JsonType::array);
220  for (size_t i = 0; i < N; i++) {
221  j.emplace_back(prototype_.serialize(v[i]));
222  }
223  }
224 
231  [[nodiscard]] Type deserialize(const Conf& c) const {
232  Type array;
233  this->deserialize_into(c, array);
234  return array;
235  }
236 
237  void deserialize_into(const Conf& c, Type& v) const {
238  if (c->type() == JsonType::array) {
239  this->deserialize_from_array(v, c);
240  } else if (c->type() == JsonType::object) {
241  this->deserialize_from_object(v, c);
242  } else {
243  throw this->wrong_type(c);
244  }
245  }
246 
247  void reset_ptr() override { ptr_ = nullptr; }
248 
249  private:
250  [[nodiscard]] Json json_schema_array() const {
251  return Json::object({
252  {"type", "array"},
253  {"items", prototype_.json_schema()},
254  {"minItems", N},
255  {"maxItems", N},
256  });
257  }
258 
259  [[nodiscard]] Json json_schema_object() const {
260  return Json::object({
261  {"type", "object"},
262  {"additionalProperties", false},
263  {"patternProperties",
264  {
265  {"^[0-9]+$",
266  {
267  {"type", prototype_.json_schema()},
268  }},
269  }},
270  });
271  }
272 
283  bool validate_array(const Conf& c, std::optional<SchemaError>& err) const {
284  assert(c->type() == JsonType::array);
285  if (c->size() != N) {
286  return this->set_error(err, c, "require exactly {} items in array, got {}", N, c->size());
287  }
288  for (const auto& x : c.to_array()) {
289  if (!prototype_.validate(x, err)) {
290  return false;
291  }
292  }
293  return true;
294  }
295 
317  bool validate_object(const Conf& c, std::optional<SchemaError>& err) const {
318  assert(c->type() == JsonType::object);
319  for (const auto& kv : c->items()) {
320  const auto& key = kv.key();
321  try {
322  std::ignore = this->parse_index(key);
323  } catch (std::exception& e) {
324  return this->set_error(err, c, e.what());
325  }
326  if (prototype_.validate(c.at(key), err)) {
327  return false;
328  }
329  }
330  return true;
331  }
332 
345  [[nodiscard]] size_t parse_index(const std::string& s) const {
346  // We'd like to just be able to use std::stoul, but unfortunately
347  // the standard library seems to think strings like "234x" are ok.
348  if (s.empty()) {
349  throw std::invalid_argument("invalid index key in object, require integer, got ''");
350  }
351  if (s.size() > 1 && s[0] == '0') {
352  throw std::invalid_argument(
353  fmt::format("invalid index key in object, require base-10 value, got '{}'", s));
354  }
355  for (char ch : s) {
356  if (ch < '0' || ch > '9') {
357  throw std::invalid_argument(
358  fmt::format("invalid index key in object, require integer, got '{}'", s));
359  }
360  }
361  size_t idx = std::stoul(s);
362  if (idx >= N) {
363  throw std::invalid_argument(
364  fmt::format("out-of-range index key in object, require < {}, got '{}'", N, s));
365  }
366  return idx;
367  }
368 
369  [[nodiscard]] size_t parse_index(const Conf& c, const std::string& s) const {
370  try {
371  return parse_index(s);
372  } catch (std::exception& e) {
373  throw this->error(c, e.what());
374  }
375  }
376 
377  [[nodiscard]] SchemaError wrong_type(const Conf& c) const {
378  std::string got = to_string(c->type());
379  return this->error(c, "property must have type array or object, got {}", got);
380  }
381 
382  void deserialize_from_object(Type& array, const Conf& c) const {
383  for (const auto& kv : c->items()) {
384  const auto& key = kv.key();
385  size_t idx = this->parse_index(c, key);
386  prototype_.deserialize_into(c.at(key), array[idx]);
387  }
388  }
389 
390  void deserialize_from_array(Type& array, const Conf& c) const {
391  auto src = c.to_array();
392  if (src.size() != N) {
393  throw this->error(c, "require exactly {} items in array, got {}", N, c->size());
394  }
395  for (size_t i = 0; i < N; i++) {
396  array[i] = prototype_.deserialize(src[i]);
397  }
398  }
399 
400  private:
401  bool option_require_all_{false};
402  PrototypeSchema prototype_{};
403  Type* ptr_{nullptr};
404 };
405 
406 template <typename T, typename P, size_t N, typename S>
407 Array<T, N, P> make_schema(std::array<T, N>* ptr, P&& prototype, S&& desc) {
408  return {ptr, std::forward<P>(prototype), std::forward<S>(desc)};
409 }
410 
411 template <typename T, size_t N, typename S>
412 Array<T, N, decltype(make_prototype<T>())> make_schema(std::array<T, N>* ptr, S&& desc) {
413  return {ptr, std::forward<S>(desc)};
414 }
415 
416 } // namespace fable::schema
Definition: conf.hpp:82
Definition: array.hpp:96
void reset_ptr() override
Definition: array.hpp:247
Type deserialize(const Conf &c) const
Definition: array.hpp:231
void from_conf(const Conf &c) override
Definition: array.hpp:202
bool validate(const Conf &c, std::optional< SchemaError > &err) const override
Definition: array.hpp:172
Json json_schema() const override
Definition: array.hpp:155
void to_json(Json &j) const override
Definition: array.hpp:197
bool require_all() const
Definition: array.hpp:116
void serialize_into(Json &j, const Type &v) const
Definition: array.hpp:218
Array< T, N, P > require_all(bool value) &&
Definition: array.hpp:145
void set_require_all(bool value)
Definition: array.hpp:135
std::string type_string() const override
Definition: array.hpp:151
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