aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.ucl
diff options
context:
space:
mode:
Diffstat (limited to 'nihil.ucl')
-rw-r--r--nihil.ucl/CMakeLists.txt1
-rw-r--r--nihil.ucl/emit.ccm4
-rw-r--r--nihil.ucl/map.ccm258
-rw-r--r--nihil.ucl/nihil.ucl.ccm1
-rw-r--r--nihil.ucl/object.ccm13
-rw-r--r--nihil.ucl/parser.ccm7
-rw-r--r--nihil.ucl/tests/CMakeLists.txt1
-rw-r--r--nihil.ucl/tests/array.cc14
-rw-r--r--nihil.ucl/tests/boolean.cc11
-rw-r--r--nihil.ucl/tests/integer.cc10
-rw-r--r--nihil.ucl/tests/map.cc132
-rw-r--r--nihil.ucl/tests/parse.cc44
-rw-r--r--nihil.ucl/tests/real.cc10
-rw-r--r--nihil.ucl/tests/string.cc10
-rw-r--r--nihil.ucl/type.ccm6
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;
}