Skip to content

Commit d42419e

Browse files
cassavatobifalk
authored andcommitted
fable: Add CustomDeserializer schema type
1 parent 4e35da4 commit d42419e

File tree

5 files changed

+253
-1
lines changed

5 files changed

+253
-1
lines changed

fable/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ if(BuildTests)
5050
# find src -type f -name "*_test.cpp"
5151
src/fable/environment_test.cpp
5252
src/fable/schema/const_test.cpp
53+
src/fable/schema/custom_test.cpp
5354
src/fable/schema/enum_test.cpp
5455
src/fable/schema/factory_test.cpp
5556
src/fable/schema/factory_advanced_test.cpp

fable/include/fable/schema/array.hpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class Array : public Base<Array<T, P>> {
104104
j["minItems"] = min_items_;
105105
}
106106
if (max_items_ != std::numeric_limits<size_t>::max()) {
107-
j["maxitems"] = max_items_;
107+
j["maxItems"] = max_items_;
108108
}
109109
this->augment_schema(j);
110110
return j;

fable/include/fable/schema/custom.hpp

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2021 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/custom.hpp
20+
* \see fable/schema/custom_test.cpp
21+
* \see fable/schema/variant.hpp
22+
*/
23+
24+
#pragma once
25+
#ifndef FABLE_SCHEMA_CUSTOM_HPP_
26+
#define FABLE_SCHEMA_CUSTOM_HPP_
27+
28+
#include <functional> // for function<>
29+
30+
#include <fable/schema/interface.hpp> // for Interface, Box
31+
32+
namespace fable {
33+
namespace schema {
34+
35+
/**
36+
* The CustomDeserializer allows the user to replace the deserialization of a
37+
* Schema with a custom function.
38+
*
39+
* This is especially useful for schema types such as Variants.
40+
*
41+
* Note that if you use this, you may have to define to_json yourself.
42+
*/
43+
class CustomDeserializer : public schema::Interface {
44+
public: // Constructors
45+
CustomDeserializer(Box&& s) : impl_(std::move(s).reset_pointer().get()) {}
46+
CustomDeserializer(Box&& s, std::function<void(CustomDeserializer*, const Conf&)> f)
47+
: impl_(std::move(s).reset_pointer().get()), from_conf_fn_(f) {}
48+
49+
public: // Special
50+
operator Box() const { return Box{this->clone()}; }
51+
52+
void set_from_conf(std::function<void(CustomDeserializer*, const Conf&)> f) { from_conf_fn_ = f; }
53+
54+
CustomDeserializer with_from_conf(std::function<void(CustomDeserializer*, const Conf&)> f) && {
55+
set_from_conf(f);
56+
return std::move(*this);
57+
}
58+
59+
public: // Overrides
60+
Interface* clone() const override { return new CustomDeserializer(*this); }
61+
62+
using Interface::to_json;
63+
JsonType type() const override { return impl_->type(); }
64+
std::string type_string() const override { return impl_->type_string(); }
65+
bool is_required() const override { return impl_->is_required(); }
66+
const std::string& description() const override { return impl_->description(); }
67+
void set_description(const std::string& s) override { return impl_->set_description(s); }
68+
Json usage() const override { return impl_->usage(); }
69+
Json json_schema() const override { return impl_->json_schema(); };
70+
void validate(const Conf& c) const override { impl_->validate(c); }
71+
void to_json(Json& j) const override { impl_->to_json(j); }
72+
73+
void from_conf(const Conf& c) override {
74+
if (!from_conf_fn_) {
75+
throw Error("no deserializer configured");
76+
}
77+
from_conf_fn_(this, c);
78+
}
79+
80+
void reset_ptr() override {
81+
impl_->reset_ptr();
82+
from_conf_fn_ = [](CustomDeserializer*, const Conf&) {
83+
throw Error("cannot deserialize after reset_ptr is called");
84+
};
85+
}
86+
87+
friend void to_json(Json& j, const CustomDeserializer& b) { b.impl_->to_json(j); }
88+
89+
private:
90+
std::shared_ptr<schema::Interface> impl_{nullptr};
91+
std::function<void(CustomDeserializer*, const Conf&)> from_conf_fn_{};
92+
};
93+
94+
} // namespace schema
95+
} // namespace fable
96+
97+
#endif // FABLE_SCHEMA_CUSTOM_HPP_

fable/include/fable/schema/interface.hpp

+10
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ class Box : public Interface {
271271
Box(std::shared_ptr<Interface> i) : impl_(std::move(i)) { assert(impl_); } // NOLINT
272272

273273
public: // Special
274+
/**
275+
* Return the underlying Interface.
276+
*/
277+
std::shared_ptr<Interface> get() { return impl_; }
278+
274279
/**
275280
* Return this type as a pointer to T.
276281
*
@@ -317,6 +322,11 @@ class Box : public Interface {
317322
return std::dynamic_pointer_cast<T>(impl_);
318323
}
319324

325+
Box reset_pointer() && {
326+
reset_ptr();
327+
return std::move(*this);
328+
}
329+
320330
public: // Overrides
321331
using Interface::to_json;
322332
Interface* clone() const override { return impl_->clone(); }
+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2021 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/custom_test.cpp
20+
* \see fable/schema/custom.hpp
21+
*/
22+
23+
#include <string> // for string
24+
#include <vector> // for vector<>
25+
26+
#include <gtest/gtest.h> // for TEST
27+
28+
#include <fable/confable.hpp> // for Confable
29+
#include <fable/schema.hpp> // for Schema, Variant, Conf, Json
30+
#include <fable/schema/custom.hpp> // for CustomDeserializer
31+
#include <fable/utility/gtest.hpp> // for assert_from_conf
32+
33+
namespace {
34+
35+
/**
36+
* MyCustomStruct models a command that can take either a string which it
37+
* passes to a shell to run, or an array, which it runs directly.
38+
*
39+
* It needs to make use of CustomDeserializer for this to work.
40+
* We dispense with the error checking that would be required in production
41+
* code, since this is just a unit test.
42+
*/
43+
struct MyCustomStruct : public fable::Confable {
44+
std::string executable;
45+
std::vector<std::string> args;
46+
47+
CONFABLE_SCHEMA(MyCustomStruct) {
48+
// clang-format off
49+
using namespace fable;
50+
using namespace fable::schema; // NOLINT(build/namespaces)
51+
return Struct{
52+
{"command", Variant{
53+
"system command to execute",
54+
// Variant #1: system command to pass to a shell
55+
{
56+
CustomDeserializer(
57+
make_prototype<std::string>(),
58+
[this](CustomDeserializer*, const Conf& c) {
59+
this->executable = "/bin/bash";
60+
this->args = {"-c", c.get<std::string>()};
61+
}
62+
),
63+
// Variant #2: an array with the first element the command to run
64+
// and the rest of the items the arguments to the command.
65+
CustomDeserializer(
66+
make_prototype<std::vector<std::string>>().min_items(1),
67+
[this](CustomDeserializer*, const Conf& c) {
68+
auto args = c.get<std::vector<std::string>>();
69+
this->executable = args[0];
70+
for (size_t i = 1; i < args.size(); ++i) {
71+
this->args.push_back(args[i]);
72+
}
73+
}
74+
),
75+
},
76+
}.require()}
77+
};
78+
// clang-format on
79+
}
80+
81+
void to_json(fable::Json& j) const override {
82+
j = fable::Json{
83+
{"executable", executable},
84+
{"args", args},
85+
};
86+
}
87+
};
88+
89+
} // anonymous namespace
90+
91+
TEST(fable_schema_custom, schema) {
92+
MyCustomStruct tmp;
93+
fable::assert_schema_eq(tmp, R"({
94+
"type": "object",
95+
"properties": {
96+
"command": {
97+
"description": "system command to execute",
98+
"anyOf": [
99+
{
100+
"type": "string"
101+
},
102+
{
103+
"type": "array",
104+
"items": {
105+
"type": "string"
106+
},
107+
"minItems": 1
108+
}
109+
]
110+
}
111+
},
112+
"required": ["command"],
113+
"additionalProperties": false
114+
})");
115+
}
116+
117+
TEST(fable_schema_custom, from_conf) {
118+
MyCustomStruct tmp;
119+
120+
fable::assert_from_conf(tmp, R"({
121+
"command": "echo 'Hello World'"
122+
})");
123+
ASSERT_EQ(tmp.executable, "/bin/bash");
124+
125+
fable::assert_from_conf(tmp, R"({
126+
"command": ["echo", "Hello World!"]
127+
})");
128+
ASSERT_EQ(tmp.executable, "echo");
129+
130+
fable::assert_invalidate(tmp, R"({
131+
"command": []
132+
})");
133+
ASSERT_EQ(tmp.executable, "echo");
134+
}
135+
136+
TEST(fable_schema_custom, to_json) {
137+
MyCustomStruct tmp;
138+
tmp.executable = "echo";
139+
tmp.args = {"Hello World"};
140+
fable::assert_to_json(tmp, R"({
141+
"executable": "echo",
142+
"args": ["Hello World"]
143+
})");
144+
}

0 commit comments

Comments
 (0)