add Schema

This commit is contained in:
falsycat 2023-07-30 12:03:42 +09:00
parent 96d3269dee
commit 3ba04554c9
4 changed files with 633 additions and 0 deletions

View File

@ -12,6 +12,7 @@ target_sources(nf7_iface
common/container.hh
common/future.hh
common/observer.hh
common/schema.hh
common/task.hh
common/task_context.hh
common/value.hh
@ -33,6 +34,7 @@ target_sources(nf7_iface_test
common/future_test.cc
common/observer_test.hh
common/observer_test.cc
common/schema_test.cc
common/task_test.cc
common/value_test.cc
)

267
iface/common/schema.hh Normal file
View File

@ -0,0 +1,267 @@
// No copyright
#pragma once
#include <algorithm>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
#include "iface/common/exception.hh"
#include "iface/common/value.hh"
namespace nf7 {
class Schema;
class Schema {
public:
class Walker {
public:
using Key = std::variant<std::monostate, uint64_t, std::string>;
enum Msg {
kTypeIncompatible,
kNumericOverflow,
kNumericUnderflow,
kObjectUnknownItem,
kObjectMissingItem,
kCustomInfo,
kCustomWarn,
kCustomError,
};
Walker() = default;
Walker(const Walker&) = delete;
Walker(Walker&&) = delete;
Walker& operator=(const Walker&) = delete;
Walker& operator=(Walker&&) = delete;
virtual void Enter(const Key&) noexcept { }
virtual void Leave() noexcept { }
virtual void Push(const Schema&, const Value&) noexcept { }
virtual void Pop(bool) noexcept { }
virtual void AddMsg(Msg, std::string_view = "") noexcept { }
};
class ObjectItem final {
public:
enum Flag : uint8_t {
kRequired = 1 << 0,
};
using Flags = uint8_t;
ObjectItem(std::string_view name,
const std::shared_ptr<const Schema>& schema,
Flags flags = 0) noexcept
: name_(name),
schema_(schema),
flags_(flags) { }
ObjectItem(std::string_view name,
Schema&& schema,
Flags flags = 0)
try : ObjectItem(name,
std::make_shared<Schema>(std::move(schema)),
flags) {
} catch (const std::bad_alloc&) {
throw Exception {"memory shortage"};
}
ObjectItem(const ObjectItem&) = default;
ObjectItem(ObjectItem&&) = default;
ObjectItem& operator=(const ObjectItem&) = default;
ObjectItem& operator=(ObjectItem&&) = default;
const std::string& name() const noexcept { return name_; }
const Schema& schema() const noexcept { return *schema_; }
Flags flags() const noexcept { return flags_; }
private:
std::string name_;
std::shared_ptr<const Schema> schema_;
Flags flags_;
};
public:
class Constraint {
public:
using Matcher = std::function<bool(Walker&, const Value&)>;
explicit Constraint(Matcher&& matcher = {}) noexcept
: matcher_(std::move(matcher)) {
}
Constraint(const Constraint&) = delete;
Constraint(Constraint&&) = default;
Constraint& operator=(const Constraint&) = delete;
Constraint& operator=(Constraint&&) = default;
virtual bool Match(Walker& walker, const Value& v) const noexcept {
return !matcher_ || matcher_(walker, v);
}
private:
Matcher matcher_;
};
class Null : public Constraint {
public:
using Constraint::Constraint;
bool Match(Walker& walker, const Value& v) const noexcept override {
bool accept = true;
if (!v.is<Value::Null>()) {
walker.AddMsg(Walker::kTypeIncompatible);
accept = false;
}
return Constraint::Match(walker, v) && accept;
}
};
template <typename N>
class Numeric : public Constraint {
public:
explicit Numeric(N min, N max, Matcher&& matcher = {}) noexcept
: Constraint(std::move(matcher)), min_(min), max_(max) {
}
bool Match(Walker& walker, const Value& v) const noexcept override {
bool accept = true;
const auto n = v.asIf<N>();
if (!n) {
walker.AddMsg(Walker::kTypeIncompatible);
accept = false;
}
if (n && *n < min_) {
walker.AddMsg(Walker::kNumericUnderflow);
accept = false;
}
if (n && *n > max_) {
walker.AddMsg(Walker::kNumericOverflow);
accept = false;
}
return Constraint::Match(walker, v) && accept;
}
private:
N min_, max_;
};
using Integer = Numeric<Value::Integer>;
using Real = Numeric<Value::Real>;
class Object : public Constraint {
public:
enum Flag : uint8_t {
kExclusive = 1 << 0,
};
using Flags = uint8_t;
explicit Object(std::vector<ObjectItem>&& items,
Flags flags = 0,
Matcher&& matcher = {}) noexcept
: Constraint(std::move(matcher)),
items_(std::move(items)),
flags_(flags) { }
bool Match(Walker& walker, const Value& v) const noexcept override {
bool accept = true;
const auto obj = v.asIf<Value::Object>();
if (!obj) {
walker.AddMsg(Walker::kTypeIncompatible);
accept = false;
} else {
for (const auto& item : items_) {
const auto itr = std::find_if(
obj->begin(), obj->end(),
[&](const auto& pair) { return pair.first == item.name(); });
if (itr != obj->end()) {
walker.Enter(Walker::Key {item.name()});
item.schema().Match(walker, itr->second);
walker.Leave();
} else if (item.flags() & ObjectItem::kRequired) {
accept = false;
walker.Enter(Walker::Key {item.name()});
walker.AddMsg(Walker::kObjectMissingItem);
walker.Leave();
}
}
for (const auto& pair : *obj) {
const auto itr = std::find_if(
items_.begin(), items_.end(),
[&](const auto& item) { return pair.first == item.name(); });
if (itr == items_.end()) {
walker.Enter(Walker::Key {pair.first});
walker.AddMsg(Walker::kObjectUnknownItem);
walker.Leave();
if (flags_ & kExclusive) {
accept = false;
}
}
}
}
return Constraint::Match(walker, v) && accept;
}
private:
std::vector<ObjectItem> items_;
Flags flags_;
};
using ConstraintVariant = std::variant<Null, Integer, Real, Object>;
public:
static Schema MakeNull(Constraint::Matcher&& matcher = {}) noexcept {
return Schema {Null {std::move(matcher)}};
}
static Schema MakeInteger(
Value::Integer min = std::numeric_limits<Value::Integer>::min(),
Value::Integer max = std::numeric_limits<Value::Integer>::max(),
Constraint::Matcher&& matcher = {}) noexcept {
return Schema {Integer {min, max, std::move(matcher)}};
}
static Schema MakeReal(
Value::Real min = -std::numeric_limits<Value::Real>::infinity(),
Value::Real max = std::numeric_limits<Value::Real>::infinity(),
Constraint::Matcher&& matcher = {}) noexcept {
return Schema {Real {min, max, std::move(matcher)}};
}
static Schema MakeObject(
std::vector<ObjectItem>&& items,
Object::Flags flags = 0,
Constraint::Matcher&& matcher = {}) noexcept {
return Schema {Object { std::move(items), flags, std::move(matcher) }};
}
public:
Schema(const Schema&) = delete;
Schema(Schema&&) = default;
Schema& operator=(const Schema&) = delete;
Schema& operator=(Schema&&) = default;
bool Match(Walker& walker, const Value& v) const noexcept {
walker.Push(*this, v);
const auto accept =
std::visit([&](auto& c){ return c.Match(walker, v); }, constraint_);
walker.Pop(accept);
return accept;
}
private:
explicit Schema(ConstraintVariant&& v) noexcept
: constraint_(std::move(v)) { }
private:
ConstraintVariant constraint_;
};
} // namespace nf7

338
iface/common/schema_test.cc Normal file
View File

@ -0,0 +1,338 @@
// No copyright
#include "iface/common/schema.hh"
#include "iface/common/schema_test.hh"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <functional>
#include <string>
#include <type_traits>
#include "iface/common/value.hh"
inline void TestSchema(
const nf7::Value& value,
const nf7::Schema& schema,
bool accept,
auto setup) {
nf7::test::SchemaWalkerMock walker;
::testing::Sequence seq;
EXPECT_CALL(walker, Push(::testing::Ref(schema), ::testing::Ref(value)))
.InSequence(seq);
if constexpr (std::is_invocable_v<
decltype(setup),
nf7::test::SchemaWalkerMock&>) {
setup(walker);
} else if constexpr (std::is_invocable_v<
decltype(setup),
nf7::test::SchemaWalkerMock&,
::testing::Sequence&>) {
setup(walker, seq);
}
EXPECT_CALL(walker, Pop(accept))
.InSequence(seq);
schema.Match(walker, value);
}
inline void TestSchema(
const nf7::Value& value,
const nf7::Schema& schema,
bool accept) {
TestSchema(value, schema, accept, [](auto&){});
}
#define EXPECT_MSG_(m, msg) EXPECT_CALL((m), AddMsg(msg, ::testing::_))
TEST(Schema, CustomMatcher) {
TestSchema(nf7::Value::Null {},
nf7::Schema::MakeNull([](auto&, auto&) { return false; }),
false);
}
TEST(Schema, NullAccept) {
TestSchema(nf7::Value::Null {}, nf7::Schema::MakeNull(), true);
}
TEST(Schema, NullRejectByTypeIncompatible) {
TestSchema(nf7::Value::Integer {}, nf7::Schema::MakeNull(), false,
[](auto& m) {
EXPECT_MSG_(m, nf7::Schema::Walker::kTypeIncompatible);
});
}
TEST(Schema, IntegerAccept) {
TestSchema(nf7::Value::Integer {}, nf7::Schema::MakeInteger(), true);
}
TEST(Schema, IntegerAcceptWithRange) {
TestSchema(nf7::Value::Integer {0}, nf7::Schema::MakeInteger(0, 100), true);
TestSchema(nf7::Value::Integer {100}, nf7::Schema::MakeInteger(0, 100), true);
}
TEST(Schema, IntegerRejectByTypeIncompatible) {
TestSchema(nf7::Value::Real {}, nf7::Schema::MakeInteger(), false,
[](auto& m) {
EXPECT_MSG_(m, nf7::Schema::Walker::kTypeIncompatible);
});
}
TEST(Schema, IntegerRejectByUnderflow) {
TestSchema(nf7::Value::Integer {-1}, nf7::Schema::MakeInteger(0, 100), false,
[](auto& m) {
EXPECT_MSG_(m, nf7::Schema::Walker::kNumericUnderflow);
});
}
TEST(Schema, IntegerRejectByOverflow) {
TestSchema(nf7::Value::Integer {101}, nf7::Schema::MakeInteger(0, 100), false,
[](auto& m) {
EXPECT_MSG_(m, nf7::Schema::Walker::kNumericOverflow);
});
}
TEST(Schema, RealAccept) {
TestSchema(nf7::Value::Real {}, nf7::Schema::MakeReal(), true);
}
TEST(Schema, RealAcceptWithRange) {
TestSchema(nf7::Value::Real {0}, nf7::Schema::MakeReal(0, 1), true);
TestSchema(nf7::Value::Real {1}, nf7::Schema::MakeReal(0, 1), true);
}
TEST(Schema, RealRejectByTypeIncompatible) {
TestSchema(nf7::Value::Integer {}, nf7::Schema::MakeReal(), false,
[](auto& m) {
EXPECT_MSG_(m, nf7::Schema::Walker::kTypeIncompatible);
});
}
TEST(Schema, RealRejectByUnderflow) {
TestSchema(nf7::Value::Real {-0.1}, nf7::Schema::MakeReal(0, 1), false,
[](auto& m) {
EXPECT_MSG_(m, nf7::Schema::Walker::kNumericUnderflow);
});
}
TEST(Schema, RealRejectByOverflow) {
TestSchema(nf7::Value::Real {1.1}, nf7::Schema::MakeReal(0, 1), false,
[](auto& m) {
EXPECT_MSG_(m, nf7::Schema::Walker::kNumericOverflow);
});
}
TEST(Schema, ObjectAccept) {
TestSchema(
nf7::MakeValue<nf7::Value::Object::Pair>({
{"hello", nf7::Value::Null {}},
{"world", nf7::Value::Null {}},
}),
nf7::Schema::MakeObject({
nf7::Schema::ObjectItem {"hello", nf7::Schema::MakeNull()},
nf7::Schema::ObjectItem {"world", nf7::Schema::MakeNull()},
}),
true,
[](auto& m, auto& seq) {
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"hello"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"world"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
});
}
TEST(Schema, ObjectAcceptExclusive) {
TestSchema(
nf7::MakeValue<nf7::Value::Object::Pair>({
{"hello", nf7::Value::Null {}},
{"world", nf7::Value::Null {}},
}),
nf7::Schema::MakeObject({
nf7::Schema::ObjectItem {"hello", nf7::Schema::MakeNull()},
nf7::Schema::ObjectItem {"world", nf7::Schema::MakeNull()},
}, nf7::Schema::Object::kExclusive),
true,
[](auto& m, auto& seq) {
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"hello"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"world"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
});
}
TEST(Schema, ObjectAcceptMissingItem) {
::testing::Sequence seq_unknown;
TestSchema(
nf7::MakeValue<nf7::Value::Object::Pair>({
{"hello", nf7::Value::Null {}},
}),
nf7::Schema::MakeObject({
nf7::Schema::ObjectItem {"hello", nf7::Schema::MakeNull()},
nf7::Schema::ObjectItem {"world", nf7::Schema::MakeNull()},
}),
true,
[&](auto& m, auto& seq) {
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"hello"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
});
}
TEST(Schema, ObjectAcceptUnknownItem) {
::testing::Sequence seq_unknown;
TestSchema(
nf7::MakeValue<nf7::Value::Object::Pair>({
{"hello", nf7::Value::Null {}},
{"world", nf7::Value::Null {}},
{"bye", nf7::Value::Null {}},
}),
nf7::Schema::MakeObject({
nf7::Schema::ObjectItem {"hello", nf7::Schema::MakeNull()},
nf7::Schema::ObjectItem {"world", nf7::Schema::MakeNull()},
}),
true,
[&](auto& m, auto& seq) {
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"hello"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"world"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"bye"}))
.InSequence(seq_unknown);
EXPECT_MSG_(m, nf7::Schema::Walker::kObjectUnknownItem)
.InSequence(seq_unknown);
EXPECT_CALL(m, Leave())
.InSequence(seq_unknown);
});
}
TEST(Schema, ObjectAcceptNested) {
TestSchema(
nf7::MakeValue<nf7::Value::Object::Pair>({
{"hello", nf7::MakeValue<nf7::Value::Object::Pair>({
{"world", nf7::Value::Null {}},
})},
}),
nf7::Schema::MakeObject({
{"hello", nf7::Schema::MakeObject({
{"world", nf7::Schema::MakeNull()},
})},
}),
true,
[&](auto& m, auto& seq) {
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"hello"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"world"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
});
}
TEST(Schema, ObjectRejectMissingItem) {
::testing::Sequence seq_unknown;
TestSchema(
nf7::MakeValue<nf7::Value::Object::Pair>({
{"hello", nf7::Value::Null {}},
}),
nf7::Schema::MakeObject({
{"hello", nf7::Schema::MakeNull()},
{"world", nf7::Schema::MakeNull(), nf7::Schema::ObjectItem::kRequired},
}),
false,
[&](auto& m, auto& seq) {
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"hello"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"world"}))
.InSequence(seq);
EXPECT_MSG_(m, nf7::Schema::Walker::kObjectMissingItem)
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
});
}
TEST(Schema, ObjectRejectUnknownItem) {
::testing::Sequence seq_unknown;
TestSchema(
nf7::MakeValue<nf7::Value::Object::Pair>({
{"hello", nf7::Value::Null {}},
{"world", nf7::Value::Null {}},
{"bye", nf7::Value::Null {}},
}),
nf7::Schema::MakeObject({
{"hello", nf7::Schema::MakeNull()},
{"world", nf7::Schema::MakeNull()},
}, nf7::Schema::Object::kExclusive),
false,
[&](auto& m, auto& seq) {
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"hello"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"world"}))
.InSequence(seq);
EXPECT_CALL(m, Push)
.InSequence(seq);
EXPECT_CALL(m, Pop(true))
.InSequence(seq);
EXPECT_CALL(m, Leave())
.InSequence(seq);
EXPECT_CALL(m, Enter(nf7::Schema::Walker::Key {"bye"}))
.InSequence(seq_unknown);
EXPECT_MSG_(m, nf7::Schema::Walker::kObjectUnknownItem)
.InSequence(seq_unknown);
EXPECT_CALL(m, Leave())
.InSequence(seq_unknown);
});
}

View File

@ -0,0 +1,26 @@
// No copyright
#pragma once
#include "iface/common/schema.hh"
#include <gmock/gmock.h>
#include <string_view>
namespace nf7::test {
class SchemaWalkerMock : public Schema::Walker {
public:
SchemaWalkerMock() = default;
MOCK_METHOD(void, Enter, (const Key&), (noexcept, override));
MOCK_METHOD(void, Leave, (), (noexcept, override));
MOCK_METHOD(void, Push, (const Schema&, const Value&), (noexcept, override));
MOCK_METHOD(void, Pop, (bool), (noexcept, override));
MOCK_METHOD(void, AddMsg, (Msg, std::string_view), (noexcept, override));
};
} // namespace nf7::test