diff options
Diffstat (limited to 'nihil.ucl')
| -rw-r--r-- | nihil.ucl/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | nihil.ucl/emit.ccm | 4 | ||||
| -rw-r--r-- | nihil.ucl/map.ccm | 258 | ||||
| -rw-r--r-- | nihil.ucl/nihil.ucl.ccm | 1 | ||||
| -rw-r--r-- | nihil.ucl/object.ccm | 13 | ||||
| -rw-r--r-- | nihil.ucl/parser.ccm | 7 | ||||
| -rw-r--r-- | nihil.ucl/tests/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | nihil.ucl/tests/array.cc | 14 | ||||
| -rw-r--r-- | nihil.ucl/tests/boolean.cc | 11 | ||||
| -rw-r--r-- | nihil.ucl/tests/integer.cc | 10 | ||||
| -rw-r--r-- | nihil.ucl/tests/map.cc | 132 | ||||
| -rw-r--r-- | nihil.ucl/tests/parse.cc | 44 | ||||
| -rw-r--r-- | nihil.ucl/tests/real.cc | 10 | ||||
| -rw-r--r-- | nihil.ucl/tests/string.cc | 10 | ||||
| -rw-r--r-- | nihil.ucl/type.ccm | 6 |
15 files changed, 444 insertions, 78 deletions
diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt index 9c6b3f3..cb051a3 100644 --- a/nihil.ucl/CMakeLists.txt +++ b/nihil.ucl/CMakeLists.txt @@ -17,6 +17,7 @@ target_sources(nihil.ucl PUBLIC array.ccm boolean.ccm integer.ccm + map.ccm real.ccm string.ccm ) diff --git a/nihil.ucl/emit.ccm b/nihil.ucl/emit.ccm index a451f88..c4aa79b 100644 --- a/nihil.ucl/emit.ccm +++ b/nihil.ucl/emit.ccm @@ -156,8 +156,8 @@ export auto operator<<(std::ostream &stream, object const &o) /* * Specialisation of std::formatter<> for object. */ -template<> -struct std::formatter<nihil::ucl::object, char> +template<std::derived_from<nihil::ucl::object> T> +struct std::formatter<T, char> { nihil::ucl::emitter emitter = nihil::ucl::emitter::json; diff --git a/nihil.ucl/map.ccm b/nihil.ucl/map.ccm new file mode 100644 index 0000000..b486787 --- /dev/null +++ b/nihil.ucl/map.ccm @@ -0,0 +1,258 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cassert> +#include <cstdint> +#include <cstdlib> +#include <memory> +#include <optional> +#include <string> + +#include <ucl.h> + +export module nihil.ucl:map; + +import :object; + +namespace nihil::ucl { + +// Exception thrown when map::operator[] does not find the key. +export struct key_not_found : error { + key_not_found(std::string_view key) + : error("key '{}' not found in map", key) + , _key(key) + {} + + auto key(this key_not_found const &self) -> std::string_view + { + return self._key; + } + +private: + std::string _key; +}; + +export template<datatype T> +struct map; + +template<datatype T> +struct map_iterator { + using difference_type = std::ptrdiff_t; + using value_type = std::pair<std::string_view, T>; + using reference = value_type &; + using const_reference = value_type const &; + using pointer = value_type *; + using const_pointer = value_type const *; + + struct sentinel{}; + + auto operator==(this map_iterator const &self, sentinel) -> bool + { + return (self._state->cur == nullptr); + } + + auto operator++(this map_iterator &self) -> map_iterator & + { + self._state->next(); + return self; + } + + auto operator++(this map_iterator &self, int) -> map_iterator & + { + self._state->next(); + return self; + } + + auto operator*(this map_iterator const &self) -> value_type { + auto obj = T(ref, self._state->cur); + return {obj.key(), std::move(obj)}; + } + +private: + friend struct map<T>; + + map_iterator(::ucl_object_t const *obj) + { + _state = std::make_shared<state>(obj); + ++(*this); + } + + struct state { + state(::ucl_object_t const *obj) + { + if ((iter = ::ucl_object_iterate_new(obj)) == nullptr) + throw error("failed to create UCL iterator"); + } + + state(state const &) = delete; + auto operator=(this state &, state const &) -> state& = delete; + + ~state() { + if (iter != nullptr) + ::ucl_object_iterate_free(iter); + } + + auto next() -> void { + cur = ::ucl_object_iterate_safe(iter, true); + } + + ucl_object_iter_t iter = nullptr; + ucl_object_t const *cur = nullptr; + }; + + std::shared_ptr<state> _state; +}; + +export template<datatype T = object> +struct map final : object { + inline static constexpr object_type ucl_type = object_type::object; + + using value_type = std::pair<std::string_view, T>; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using iterator = map_iterator<T>; + + // Create an empty map + map() : object(noref, ::ucl_object_typed_new(UCL_OBJECT)) + { + if (_object == nullptr) + throw error("failed to create UCL object"); + } + + // Create a new map from a UCL object. + map(ref_t, ::ucl_object_t const *uobj) + : object(nihil::ucl::ref, uobj) + { + if (type() != ucl_type) + throw type_mismatch(ucl_type, type()); + } + + map(noref_t, ::ucl_object_t *uobj) + : object(noref, uobj) + { + if (type() != ucl_type) + throw type_mismatch(ucl_type, type()); + } + + /* + * Create a map from an iterator pair. + */ + template<std::input_iterator Iterator> + requires(std::convertible_to<std::iter_value_t<Iterator>, value_type>) + map(Iterator first, Iterator last) + : map() + { + if (_object == nullptr) + throw error("failed to create UCL object"); + + // This is exception safe, because if we throw here the + // base class destructor will free the map. + while (first != last) { + insert(*first); + ++first; + } + } + + /* + * Create a map from a range. + */ + template<std::ranges::range Range> + requires(std::convertible_to<std::ranges::range_value_t<Range>, + value_type>) + map(std::from_range_t, Range &&range) + : map(std::ranges::begin(range), + std::ranges::end(range)) + { + } + + /* + * Create a map from an initializer_list. + */ + map(std::initializer_list<value_type> const &list) + : map(std::ranges::begin(list), std::ranges::end(list)) + { + } + + /* + * Map iterator access. + */ + + auto begin(this map const &self) -> iterator + { + return {self.get_ucl_object()}; + } + + auto end(this map const &) -> iterator::sentinel + { + return {}; + } + + /* + * Reserve space for future insertions. + */ + auto reserve(this map &self, size_type nelems) -> void + { + ::ucl_object_reserve(self.get_ucl_object(), nelems); + } + + /* + * Add an element to the map. + */ + auto insert(this map &self, value_type const &v) -> void + { + auto uobj = ::ucl_object_ref(v.second.get_ucl_object()); + + ::ucl_object_insert_key(self.get_ucl_object(), uobj, + v.first.data(), v.first.size(), true); + } + + /* + * Access a map element by key. + */ + auto find(this map const &self, std::string_view key) + -> std::optional<T> + { + auto const *obj = ::ucl_object_lookup_len( + self.get_ucl_object(), + key.data(), key.size()); + if (obj == nullptr) + return {}; + + return {T(nihil::ucl::ref, obj)}; + } + + /* + * Remove an object from the map. + */ + auto remove(this map &self, std::string_view key) -> bool + { + return ::ucl_object_delete_keyl(self.get_ucl_object(), + key.data(), key.size()); + } + + /* + * Remove an object from the map and return it. + */ + auto pop(this map &self, std::string_view key) + -> std::optional<T> + { + auto *uobj = ::ucl_object_pop_keyl(self.get_ucl_object(), + key.data(), key.size()); + if (uobj) + return T(noref, uobj); + return {}; + } + + auto operator[] (this map const &self, std::string_view key) -> T + { + auto obj = self.find(key); + if (obj) + return *obj; + throw key_not_found(key); + } +}; + +} // namespace nihil::ucl diff --git a/nihil.ucl/nihil.ucl.ccm b/nihil.ucl/nihil.ucl.ccm index 909fe18..66e2c2b 100644 --- a/nihil.ucl/nihil.ucl.ccm +++ b/nihil.ucl/nihil.ucl.ccm @@ -15,5 +15,6 @@ export import :type; export import :array; export import :boolean; export import :integer; +export import :map; export import :real; export import :string; diff --git a/nihil.ucl/object.ccm b/nihil.ucl/object.ccm index 72abc00..0814125 100644 --- a/nihil.ucl/object.ccm +++ b/nihil.ucl/object.ccm @@ -133,19 +133,6 @@ export struct object { return {dptr, dlen}; } - // Return a sub-object of this one. - auto lookup(this object const &self, std::string_view key) - -> std::optional<object> - { - auto const *obj = ::ucl_object_lookup_any( - self.get_ucl_object(), - key.data(), key.size()); - if (obj == nullptr) - return {}; - - return {object(nihil::ucl::ref, obj)}; - } - protected: // The object we're wrapping. ::ucl_object_t *_object = nullptr; diff --git a/nihil.ucl/parser.ccm b/nihil.ucl/parser.ccm index 968e906..8e715d0 100644 --- a/nihil.ucl/parser.ccm +++ b/nihil.ucl/parser.ccm @@ -17,6 +17,7 @@ export module nihil.ucl:parser; import nihil; import :error; import :object; +import :map; namespace nihil::ucl { @@ -138,7 +139,7 @@ export struct parser { } // Return the top object of this parser. - auto top(this parser &self) -> object + auto top(this parser &self) -> map<object> { if (self._parser == nullptr) throw error("attempt to call top() on an empty parser"); @@ -161,8 +162,8 @@ private: std::vector<std::unique_ptr<macro_handler>> _macros; }; -// Utility function to parse something and return the object. -export auto parse(std::ranges::range auto &&data) -> object { +// Utility function to parse something and return the top-level object. +export auto parse(std::ranges::range auto &&data) -> map<object> { auto p = parser(); p.add(std::forward<decltype(data)>(data)); return p.top(); diff --git a/nihil.ucl/tests/CMakeLists.txt b/nihil.ucl/tests/CMakeLists.txt index 2c4ec5d..93559a7 100644 --- a/nihil.ucl/tests/CMakeLists.txt +++ b/nihil.ucl/tests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(nihil.ucl.test array.cc boolean.cc integer.cc + map.cc real.cc string.cc ) diff --git a/nihil.ucl/tests/array.cc b/nihil.ucl/tests/array.cc index ce86058..023b3bf 100644 --- a/nihil.ucl/tests/array.cc +++ b/nihil.ucl/tests/array.cc @@ -97,14 +97,8 @@ TEST_CASE("ucl: array: parse", "[ucl]") using namespace std::literals; using namespace nihil::ucl; - auto input = "value = [1, 42, 666]"sv; - auto obj = parse(input); - auto v = obj.lookup("value"); - - REQUIRE(v); - REQUIRE(v->key() == "value"); - - auto arr = object_cast<array<integer>>(*v); + auto obj = parse("value = [1, 42, 666]"sv); + auto arr = object_cast<array<integer>>(obj["value"]); REQUIRE(arr.size() == 3); REQUIRE(arr[0] == 1); REQUIRE(arr[1] == 42); @@ -170,9 +164,7 @@ TEST_CASE("ucl: array: heterogeneous elements", "[ucl]") using namespace nihil::ucl; auto obj = parse("array [ 42, true, \"test\" ];"); - auto v = obj.lookup("array"); - REQUIRE(v); - auto arr = object_cast<array<>>(*v); + auto arr = object_cast<array<>>(obj["array"]); REQUIRE(arr.size() == 3); diff --git a/nihil.ucl/tests/boolean.cc b/nihil.ucl/tests/boolean.cc index b0b3b58..ed5e1d7 100644 --- a/nihil.ucl/tests/boolean.cc +++ b/nihil.ucl/tests/boolean.cc @@ -46,13 +46,10 @@ TEST_CASE("ucl: boolean: parse", "[ucl]") { using namespace std::literals; - auto input = "value = true"sv; - auto obj = nihil::ucl::parse(input); - - auto v = obj.lookup("value"); - REQUIRE(v); - REQUIRE(v->key() == "value"); - REQUIRE(object_cast<nihil::ucl::boolean>(*v).value() == true); + auto obj = nihil::ucl::parse("value = true"sv); + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE(object_cast<nihil::ucl::boolean>(v) == true); } TEST_CASE("ucl: boolean: emit", "[ucl]") diff --git a/nihil.ucl/tests/integer.cc b/nihil.ucl/tests/integer.cc index c7db851..ad513ca 100644 --- a/nihil.ucl/tests/integer.cc +++ b/nihil.ucl/tests/integer.cc @@ -46,12 +46,10 @@ TEST_CASE("ucl: parse: integer", "[ucl]") { using namespace std::literals; - auto input = "value = 42"sv; - auto obj = nihil::ucl::parse(input); - auto v = obj.lookup("value"); - REQUIRE(v); - REQUIRE(v->key() == "value"); - REQUIRE(object_cast<nihil::ucl::integer>(*v).value() == 42); + auto obj = nihil::ucl::parse("value = 42"sv); + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE(object_cast<nihil::ucl::integer>(v) == 42); } TEST_CASE("ucl: integer: emit", "[ucl]") diff --git a/nihil.ucl/tests/map.cc b/nihil.ucl/tests/map.cc new file mode 100644 index 0000000..d106c79 --- /dev/null +++ b/nihil.ucl/tests/map.cc @@ -0,0 +1,132 @@ +/* + * This source code is released into the public domain. + */ + +#include <catch2/catch_test_macros.hpp> + +import nihil.ucl; + +TEST_CASE("ucl: map: default construct", "[ucl]") +{ + auto map = nihil::ucl::map<>(); + REQUIRE(str(map.type()) == "object"); +} + +TEST_CASE("ucl: map: construct from initializer_list", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + REQUIRE(str(map.type()) == "object"); + REQUIRE(map["1"] == 1); + REQUIRE(map["42"] == 42); +} + +TEST_CASE("ucl: map: insert", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto m = map<integer>(); + + m.insert({"test1"sv, integer(42)}); + m.insert({"test2"sv, integer(666)}); + + REQUIRE(m["test1"] == 42); + REQUIRE(m["test2"] == 666); +} + +TEST_CASE("ucl: map: find", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + auto obj = map.find("42"); + REQUIRE(obj != std::nullopt); + REQUIRE(*obj == 42); + + obj = map.find("43"); + REQUIRE(obj == std::nullopt); +} + +TEST_CASE("ucl: map: iterate", "[ucl]") +{ + using namespace nihil::ucl; + using namespace std::literals; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + auto i = 0u; + + for (auto [key, value] : map) { + if (key == "1") + REQUIRE(value == 1); + else if (key == "42") + REQUIRE(value == 42); + else + REQUIRE(false); + ++i; + } + + REQUIRE(i == 2); +} + +TEST_CASE("ucl: map: operator[] throws key_not_found", "[ucl]") +{ + auto map = nihil::ucl::map<nihil::ucl::integer>(); + REQUIRE_THROWS_AS(map["nonesuch"], nihil::ucl::key_not_found); +} + +TEST_CASE("ucl: map: remove", "[uc]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + REQUIRE(map.find("42") != std::nullopt); + REQUIRE(map.remove("42") == true); + REQUIRE(map.find("42") == std::nullopt); + REQUIRE(map["1"] == 1); + + REQUIRE(map.remove("42") == false); +} + +TEST_CASE("ucl: map: pop", "[uc]") +{ + using namespace std::literals; + using namespace nihil::ucl; + + auto map = nihil::ucl::map<integer>{ + {"1"sv, integer(1)}, + {"42"sv, integer(42)}, + }; + + REQUIRE(map.find("42") != std::nullopt); + + auto obj = map.pop("42"); + REQUIRE(obj != std::nullopt); + REQUIRE(*obj == 42); + + REQUIRE(map.find("42") == std::nullopt); + REQUIRE(map["1"] == 1); + + obj = map.pop("42"); + REQUIRE(obj == std::nullopt); +} diff --git a/nihil.ucl/tests/parse.cc b/nihil.ucl/tests/parse.cc index 3a4f061..3cf5742 100644 --- a/nihil.ucl/tests/parse.cc +++ b/nihil.ucl/tests/parse.cc @@ -13,38 +13,38 @@ import nihil.ucl; TEST_CASE("ucl parse: iterate array", "[ucl]") { using namespace std::literals; + using namespace nihil::ucl; - auto input = "value = [1, 42, 666];"sv; - auto obj = nihil::ucl::parse(input); + auto obj = parse("value = [1, 42, 666];"sv); - auto array = obj.lookup("value"); - REQUIRE(array); - REQUIRE(array->key() == "value"); + auto arr = obj["value"]; + REQUIRE(arr.key() == "value"); + + auto vec = std::vector(std::from_range, + object_cast<array<integer>>(arr)); - auto vec = std::vector<nihil::ucl::object>(); - std::ranges::copy(*array, std::back_inserter(vec)); REQUIRE(vec.size() == 3); - REQUIRE(object_cast<nihil::ucl::integer>(vec[0]).value() == 1); - REQUIRE(object_cast<nihil::ucl::integer>(vec[1]).value() == 42); - REQUIRE(object_cast<nihil::ucl::integer>(vec[2]).value() == 666); + REQUIRE(vec[0] == 1); + REQUIRE(vec[1] == 42); + REQUIRE(vec[2] == 666); } TEST_CASE("ucl parse: iterate hash", "[ucl]") { using namespace std::literals; + using namespace nihil::ucl; auto input = "int = 42; bool = true; str = \"test\";"sv; - auto obj = nihil::ucl::parse(input); - - for (auto &&value : obj) { - if (value.key() == "int") - REQUIRE(object_cast<nihil::ucl::integer>(value).value() - == 42); - else if (value.key() == "bool") - REQUIRE(object_cast<nihil::ucl::boolean>(value).value() - == true); - else if (value.key() == "str") - REQUIRE(object_cast<nihil::ucl::string>(value).value() - == "test"); + auto obj = parse(input); + + for (auto &&[key, value] : obj) { + REQUIRE(key == value.key()); + + if (key == "int") + REQUIRE(object_cast<integer>(value) == 42); + else if (key == "bool") + REQUIRE(object_cast<boolean>(value) == true); + else if (key == "str") + REQUIRE(object_cast<string>(value) == "test"); } } diff --git a/nihil.ucl/tests/real.cc b/nihil.ucl/tests/real.cc index d97e767..4bd9b3f 100644 --- a/nihil.ucl/tests/real.cc +++ b/nihil.ucl/tests/real.cc @@ -48,12 +48,10 @@ TEST_CASE("ucl: real: parse", "[ucl]") { using namespace std::literals; - auto input = "value = 42.1"sv; - auto obj = nihil::ucl::parse(input); - auto v = obj.lookup("value"); - REQUIRE(v); - REQUIRE(v->key() == "value"); - REQUIRE_THAT(object_cast<nihil::ucl::real>(*v).value(), + auto obj = nihil::ucl::parse("value = 42.1"sv); + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE_THAT(object_cast<nihil::ucl::real>(v).value(), Catch::Matchers::WithinRel(42.1)); } diff --git a/nihil.ucl/tests/string.cc b/nihil.ucl/tests/string.cc index b702b51..19052cd 100644 --- a/nihil.ucl/tests/string.cc +++ b/nihil.ucl/tests/string.cc @@ -108,12 +108,10 @@ TEST_CASE("ucl: string: parse", "[ucl]") { using namespace std::literals; - auto input = "value = \"str\""sv; - auto obj = nihil::ucl::parse(input); - auto v = obj.lookup("value"); - REQUIRE(v); - REQUIRE(v->key() == "value"); - REQUIRE(object_cast<nihil::ucl::string>(*v).value() == "str"); + auto obj = nihil::ucl::parse("value = \"str\""sv); + auto v = obj["value"]; + REQUIRE(v.key() == "value"); + REQUIRE(object_cast<nihil::ucl::string>(v) == "str"); } TEST_CASE("ucl: string: emit", "[ucl]") diff --git a/nihil.ucl/type.ccm b/nihil.ucl/type.ccm index 2aef1a6..5050e7a 100644 --- a/nihil.ucl/type.ccm +++ b/nihil.ucl/type.ccm @@ -76,12 +76,14 @@ export struct type_mismatch : error { {} // The type we expected. - auto expected_type(this type_mismatch const &self) -> object_type { + auto expected_type(this type_mismatch const &self) -> object_type + { return self._expected_type; } // The type we got. - auto actual_type(this type_mismatch const &self) -> object_type { + auto actual_type(this type_mismatch const &self) -> object_type + { return self._actual_type; } |
