From f41970666675f873d7c1075efd192f22df8d17fe Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Sun, 22 Jun 2025 14:46:53 +0100 Subject: add nihil.ucl (incomplete) --- nihil.ucl/CMakeLists.txt | 19 ++- nihil.ucl/array.ccm | 342 +++++++++++++++++++++++++++++++++++++++++ nihil.ucl/boolean.ccm | 77 ++++++++++ nihil.ucl/emit.ccm | 141 +++++++++++++++++ nihil.ucl/error.ccm | 26 ++++ nihil.ucl/integer.ccm | 77 ++++++++++ nihil.ucl/nihil.ucl.ccm | 9 +- nihil.ucl/object.ccm | 279 +++++++++++++++++++++++++++++++++ nihil.ucl/parser.ccm | 170 ++++++++++++++++++++ nihil.ucl/real.ccm | 76 +++++++++ nihil.ucl/string.ccm | 78 ++++++++++ nihil.ucl/tests/CMakeLists.txt | 23 +++ nihil.ucl/tests/array.cc | 109 +++++++++++++ nihil.ucl/tests/boolean.cc | 50 ++++++ nihil.ucl/tests/emit.cc | 8 + nihil.ucl/tests/integer.cc | 50 ++++++ nihil.ucl/tests/object.cc | 22 +++ nihil.ucl/tests/parse.cc | 50 ++++++ nihil.ucl/tests/real.cc | 53 +++++++ nihil.ucl/tests/string.cc | 38 +++++ 20 files changed, 1695 insertions(+), 2 deletions(-) create mode 100644 nihil.ucl/array.ccm create mode 100644 nihil.ucl/boolean.ccm create mode 100644 nihil.ucl/emit.ccm create mode 100644 nihil.ucl/error.ccm create mode 100644 nihil.ucl/integer.ccm create mode 100644 nihil.ucl/object.ccm create mode 100644 nihil.ucl/parser.ccm create mode 100644 nihil.ucl/real.ccm create mode 100644 nihil.ucl/string.ccm create mode 100644 nihil.ucl/tests/CMakeLists.txt create mode 100644 nihil.ucl/tests/array.cc create mode 100644 nihil.ucl/tests/boolean.cc create mode 100644 nihil.ucl/tests/emit.cc create mode 100644 nihil.ucl/tests/integer.cc create mode 100644 nihil.ucl/tests/object.cc create mode 100644 nihil.ucl/tests/parse.cc create mode 100644 nihil.ucl/tests/real.cc create mode 100644 nihil.ucl/tests/string.cc (limited to 'nihil.ucl') diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt index 59021aa..f01fb25 100644 --- a/nihil.ucl/CMakeLists.txt +++ b/nihil.ucl/CMakeLists.txt @@ -3,11 +3,28 @@ pkg_check_modules(LIBUCL REQUIRED libucl) add_library(nihil.ucl STATIC) +target_link_libraries(nihil.ucl PUBLIC nihil) target_sources(nihil.ucl PUBLIC FILE_SET modules TYPE CXX_MODULES FILES - nihil.ucl.ccm) + nihil.ucl.ccm + emit.ccm + error.ccm + object.ccm + parser.ccm + + array.ccm + boolean.ccm + integer.ccm + real.ccm + string.ccm +) + target_compile_options(nihil.ucl PUBLIC ${LIBUCL_CFLAGS_OTHER}) target_include_directories(nihil.ucl PUBLIC ${LIBUCL_INCLUDE_DIRS}) target_link_libraries(nihil.ucl PUBLIC ${LIBUCL_LIBRARIES}) target_link_directories(nihil.ucl PUBLIC ${LIBUCL_LIBRARY_DIRS}) +if(NIHIL_TESTS) + add_subdirectory(tests) + enable_testing() +endif() diff --git a/nihil.ucl/array.ccm b/nihil.ucl/array.ccm new file mode 100644 index 0000000..26cd9b9 --- /dev/null +++ b/nihil.ucl/array.ccm @@ -0,0 +1,342 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +#include + +export module nihil.ucl:array; + +import :object; + +namespace nihil::ucl { + +export template +struct array; + +template +struct array_iterator { + using difference_type = std::ptrdiff_t; + using value_type = T; + using reference = T&; + using pointer = T*; + + array_iterator() = default; + + auto operator* (this array_iterator const &self) -> T + { + auto uobj = ::ucl_array_find_index(self._array, self._idx); + if (uobj == nullptr) + throw error("failed to fetch UCL array index"); + + return T(::ucl_object_ref(uobj)); + } + + auto operator[] (this array_iterator const &self, + difference_type idx) + -> T + { + return *(self + idx); + } + + auto operator++ (this array_iterator &self) -> array_iterator& + { + ++self._idx; + return self; + } + + auto operator++ (this array_iterator &self, int) -> array_iterator + { + auto copy = self; + ++self; + return copy; + } + + auto operator-- (this array_iterator &self) -> array_iterator& + { + if (self._idx == 0) + throw std::out_of_range("attempt to iterate before " + "start of UCL array"); + --self._idx; + return self; + } + + auto operator-- (this array_iterator &self, int) -> array_iterator + { + auto copy = self; + --self; + return copy; + } + + auto operator== (this array_iterator const &lhs, + array_iterator const &rhs) + -> bool + { + return lhs._idx == rhs._idx; + } + + auto operator<=> (this array_iterator const &lhs, + array_iterator const &rhs) + { + return lhs._idx <=> rhs._idx; + } + + auto operator+= (this array_iterator &lhs, + difference_type rhs) + -> array_iterator & + { + lhs._idx += rhs; + return lhs; + } + + auto operator-= (this array_iterator &lhs, + difference_type rhs) + -> array_iterator & + { + lhs._idx -= rhs; + return lhs; + } + + auto operator- (this array_iterator const &lhs, + array_iterator const &rhs) + -> difference_type + { + return lhs._idx - rhs._idx; + } + +private: + friend struct array; + + ::ucl_object_t const *_array{}; + std::size_t _idx{}; + + array_iterator(::ucl_object_t const *array, std::size_t idx) + : _array(array) + , _idx(idx) + {} +}; + +export template +auto operator+(array_iterator const &lhs, + typename array_iterator::difference_type rhs) + -> array_iterator +{ + auto copy = lhs; + copy += rhs; + return copy; +} + +export template +auto operator+(typename array_iterator::difference_type lhs, + array_iterator const &rhs) + -> array_iterator +{ + return rhs - lhs; +} + +export template +auto operator-(array_iterator const &lhs, + typename array_iterator::difference_type rhs) + -> array_iterator +{ + auto copy = lhs; + copy -= rhs; + return copy; +} + +static_assert(std::random_access_iterator>); + +export template +struct array final : object { + inline static constexpr object_type ucl_type = object_type::array; + + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + array() : object(::ucl_object_typed_new(UCL_ARRAY)) + { + if (_object == nullptr) + throw error("failed to create UCL object"); + } + + explicit array(::ucl_object_t *uobj) : object(uobj) + { + assert(type() == object_type::array); + } + + /* + * Create an array from an iterator pair. + */ + template + requires(std::convertible_to, T>) + array(Iterator first, Iterator last) + : object(::ucl_object_typed_new(UCL_ARRAY)) + { + 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 array. + while (first != last) { + push_back(*first); + ++first; + } + } + + /* + * Create an array from a range. + */ + template + requires(std::convertible_to, T>) + array(std::from_range_t, Range &&range) + : array(std::ranges::begin(range), + std::ranges::end(range)) + { + } + + /* + * Create an array from an initializer_list. + */ + array(std::initializer_list const &list) + : array(std::ranges::begin(list), + std::ranges::end(list)) + { + } + + /* + * Array iterator access. + */ + + auto begin(this array const &self) -> array_iterator + { + return {self.get_ucl_object(), 0}; + } + + auto end(this array const &self) -> array_iterator + { + return {self.get_ucl_object(), self.size()}; + } + + /* + * Return the size of this array. + */ + auto size(this array const &self) -> size_type + { + return ::ucl_array_size(self.get_ucl_object()); + } + + /* + * Test if this array is empty. + */ + auto empty(this array const &self) -> bool + { + return self.size() == 0; + } + + /* + * Reserve space for future insertions. + */ + auto reserve(this array &self, size_type nelems) -> void + { + ::ucl_object_reserve(self.get_ucl_object(), nelems); + } + + /* + * Append an element to the array. + */ + auto push_back(this array &self, value_type &&v) -> void + { + // There's no real benefit to moving the object here, but + // move it anyway to preserve the expected semantics. + auto copy = std::move(v); + self.push_back(copy); + } + + auto push_back(this array &self, value_type const &v) -> void + { + auto uobj = ::ucl_object_ref(v.get_ucl_object()); + ::ucl_array_append(self.get_ucl_object(), uobj); + } + + /* + * Prepend an element to the array. + */ + auto push_front(this array &self, value_type &&v) -> void + { + // There's no real benefit to moving the object here, but + // move it anyway to preserve the expected semantics. + auto copy = std::move(v); + self.push_front(copy); + } + + auto push_front(this array &self, value_type const &v) -> void + { + auto uobj = ::ucl_object_ref(v.get_ucl_object()); + ::ucl_array_prepend(self.get_ucl_object(), uobj); + } + + /* + * Access an array element by index. + */ + auto at(this array const &self, size_type idx) -> T + { + if (idx >= self.size()) + throw std::out_of_range("UCL array index out of range"); + + auto uobj = ::ucl_array_find_index(self.get_ucl_object(), idx); + if (uobj == nullptr) + throw error("failed to fetch UCL array index"); + + return T(::ucl_object_ref(uobj)); + } + + auto operator[] (this array const &self, size_type idx) -> T + { + return self.at(idx); + } + + /* + * Return the first element. + */ + auto front(this array const &self) -> T + { + return self.at(0); + } + + /* + * Return the last element. + */ + auto back(this array const &self) -> T + { + if (self.empty()) + throw std::out_of_range("attempt to access back() on " + "empty UCL array"); + return self.at(self.size() - 1); + } +}; + +/* + * Comparison operators. + */ + +export template +auto operator==(array const &a, array const &b) -> bool +{ + if (a.size() != b.size()) + return false; + + for (typename array::size_type i = 0; i < a.size(); ++i) + if (a.at(i) != b.at(i)) + return false; + + return true; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/boolean.ccm b/nihil.ucl/boolean.ccm new file mode 100644 index 0000000..db6c864 --- /dev/null +++ b/nihil.ucl/boolean.ccm @@ -0,0 +1,77 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +#include + +export module nihil.ucl:boolean; + +import :object; + +namespace nihil::ucl { + +export struct boolean final : object { + using value_type = bool; + + inline static constexpr object_type ucl_type = object_type::boolean; + + boolean(value_type value) + : object(::ucl_object_frombool(value)) + { + if (_object == nullptr) + throw error("failed to create UCL object"); + } + + explicit boolean(::ucl_object_t *uobj) : object(uobj) + { + assert(type() == object_type::boolean); + } + + auto value(this boolean const &self) -> value_type + { + auto v = value_type{}; + auto const *uobj = self.get_ucl_object(); + + if (::ucl_object_toboolean_safe(uobj, &v)) + return v; + + std::abort(); + } +}; + +/* + * Comparison operators. + */ + +export auto operator== (boolean const &a, boolean const &b) + -> bool +{ + return a.value() == b.value(); +} + +export auto operator<=> (boolean const &a, boolean const &b) + -> std::strong_ordering +{ + return a.value() <=> b.value(); +} + +export auto operator== (boolean const &a, boolean::value_type b) + -> bool +{ + return a.value() == b; +} + +export auto operator<=> (boolean const &a, boolean::value_type b) + -> std::strong_ordering +{ + return a.value() <=> b; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/emit.ccm b/nihil.ucl/emit.ccm new file mode 100644 index 0000000..8fdf616 --- /dev/null +++ b/nihil.ucl/emit.ccm @@ -0,0 +1,141 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include +#include + +#include + +export module nihil.ucl:emit; + +import :object; + +namespace nihil::ucl { + +export enum struct emitter { + configuration = UCL_EMIT_CONFIG, + compact_json = UCL_EMIT_JSON_COMPACT, + json = UCL_EMIT_JSON, + yaml = UCL_EMIT_YAML, +}; + +/* + * Wrap ucl_emitter_functions for a particular output iterator type. + * + * We can't throw exceptions here since we're called from C code. The emit + * functions return an integer value, but it's not really clear what this is + * for (for example, returning errors?) and the C API seems to mostly ignore + * it. So, we just eat errors and keep going. + */ +template Iterator> +struct emit_wrapper { + emit_wrapper(Iterator iterator_) + : iterator(std::move(iterator_)) + {} + + static auto append_character(unsigned char c, std::size_t nchars, + void *ud) + noexcept -> int + try { + auto *self = static_cast(ud); + + while (nchars--) + *self->iterator++ = static_cast(c); + + return 0; + } catch (...) { + return 0; + } + + static auto append_len(unsigned char const *str, std::size_t len, + void *ud) + noexcept -> int + try { + auto *self = static_cast(ud); + + for (auto c : std::span(str, len)) + *self->iterator++ = static_cast(c); + + return 0; + } catch (...) { + return 0; + } + + static auto append_int(std::int64_t value, void *ud) + noexcept -> int + try { + auto constexpr bufsize = + std::numeric_limits::digits10; + auto buf = std::array(); + + auto *self = static_cast(ud); + auto result = std::to_chars(buf.data(), buf.data() + buf.size(), + value, 10); + + if (result.ec == std::errc()) + for (auto c : std::span(buf.data(), result.ptr)) + *self->iterator++ = c; + + return 0; + } catch (...) { + return 0; + } + + static auto append_double(double value, void *ud) + noexcept -> int + try { + auto constexpr bufsize = + std::numeric_limits::digits10; + auto buf = std::array(); + + auto *self = static_cast(ud); + auto result = std::to_chars(buf.data(), buf.data() + buf.size(), + value); + + if (result.ec == std::errc()) + for (auto c : std::span(buf.data(), result.ptr)) + *self->iterator++ = c; + + return 0; + } catch (...) { + return 0; + } + + auto get_functions(this emit_wrapper &self) -> ucl_emitter_functions + { + auto ret = ucl_emitter_functions{}; + + ret.ucl_emitter_append_character = &emit_wrapper::append_character; + ret.ucl_emitter_append_len = &emit_wrapper::append_len; + ret.ucl_emitter_append_int = &emit_wrapper::append_int; + ret.ucl_emitter_append_double = &emit_wrapper::append_double; + ret.ud = &self; + + return ret; + } + +private: + Iterator iterator{}; +}; + +export auto emit(object const &object, emitter format, + std::output_iterator auto &&it) + -> void +{ + auto ucl_format = static_cast(format); + auto wrapper = emit_wrapper(it); + auto functions = wrapper.get_functions(); + + ::ucl_object_emit_full(object.get_ucl_object(), ucl_format, + &functions, nullptr); +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/error.ccm b/nihil.ucl/error.ccm new file mode 100644 index 0000000..4eda774 --- /dev/null +++ b/nihil.ucl/error.ccm @@ -0,0 +1,26 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +export module nihil.ucl:error; + +import nihil; + +namespace nihil::ucl { + +/* + * Exception thrown when an issue occurs with UCL. + */ +export struct error : generic_error { + template + error(std::format_string fmt, Args &&...args) + : generic_error(fmt, std::forward(args)...) + {} +}; + +} // namespace nihil::ucl diff --git a/nihil.ucl/integer.ccm b/nihil.ucl/integer.ccm new file mode 100644 index 0000000..d3009a1 --- /dev/null +++ b/nihil.ucl/integer.ccm @@ -0,0 +1,77 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +#include + +export module nihil.ucl:integer; + +import :object; + +namespace nihil::ucl { + +export struct integer final : object { + using value_type = std::int64_t; + + inline static constexpr object_type ucl_type = object_type::integer; + + integer(value_type value) + : object(::ucl_object_fromint(value)) + { + if (_object == nullptr) + throw error("failed to create UCL object"); + } + + explicit integer(::ucl_object_t *uobj) : object(uobj) + { + assert(type() == object_type::integer); + } + + auto value(this integer const &self) -> value_type + { + auto v = value_type{}; + auto const *uobj = self.get_ucl_object(); + + if (::ucl_object_toint_safe(uobj, &v)) + return v; + + std::abort(); + } +}; + +/* + * Comparison operators. + */ + +export auto operator== (integer const &a, integer const &b) + -> bool +{ + return a.value() == b.value(); +} + +export auto operator<=> (integer const &a, integer const &b) + -> std::strong_ordering +{ + return a.value() <=> b.value(); +} + +export auto operator== (integer const &a, integer::value_type b) + -> bool +{ + return a.value() == b; +} + +export auto operator<=> (integer const &a, integer::value_type b) + -> std::strong_ordering +{ + return a.value() <=> b; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/nihil.ucl.ccm b/nihil.ucl/nihil.ucl.ccm index 23151e0..d0924d8 100644 --- a/nihil.ucl/nihil.ucl.ccm +++ b/nihil.ucl/nihil.ucl.ccm @@ -5,5 +5,12 @@ module; export module nihil.ucl; +export import :emit; +export import :object; +export import :parser; -// TODO: Implement. +export import :array; +export import :boolean; +export import :integer; +export import :real; +export import :string; diff --git a/nihil.ucl/object.ccm b/nihil.ucl/object.ccm new file mode 100644 index 0000000..0b8c95f --- /dev/null +++ b/nihil.ucl/object.ccm @@ -0,0 +1,279 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * A UCL object. The object is immutable and internally refcounted, so it + * may be copied as needed. + * + */ + +#include +#include +#include +#include +#include +#include + +#include + +export module nihil.ucl:object; + +import nihil; +import :error; + +namespace nihil::ucl { + +export struct parser; + +/*********************************************************************** + * The basic object type. + */ + +enum struct object_type { + object = UCL_OBJECT, + array = UCL_ARRAY, + integer = UCL_INT, + real = UCL_FLOAT, + string = UCL_STRING, + boolean = UCL_BOOLEAN, + time = UCL_TIME, + userdata = UCL_USERDATA, + null = UCL_NULL, +}; + +template +concept datatype = requires(T o) { + { T::ucl_type } -> std::convertible_to; +}; + +export struct object { + inline static constexpr object_type ucl_type = object_type::object; + + // Free our object on destruction. + virtual ~object() { + if (_object) + ::ucl_object_unref(_object); + } + + // Movable. + object(object &&other) noexcept + : _object(std::exchange(other._object, nullptr)) + {} + + auto operator=(this object &self, object &&other) noexcept + -> object & + { + if (&self != &other) + self._object = std::exchange(other._object, nullptr); + return self; + } + + // Copyable. + object(object const &other) noexcept + : _object(nullptr) + { + *this = other; + } + + auto operator=(this object &self, object const &other) + -> object & + { + if (&self != &other) { + if (self._object != nullptr) + ::ucl_object_unref(self._object); + + if (other._object != nullptr) { + self._object = ::ucl_object_copy(other._object); + if (self._object == nullptr) + throw error("failed to copy UCL object"); + } else { + self._object = nullptr; + } + } + + return self; + } + + // Return the type of this object. + auto type(this object const &self) -> object_type + { + switch (ucl_object_type(self.get_ucl_object())) { + case UCL_OBJECT: + return object_type::object; + case UCL_ARRAY: + return object_type::array; + case UCL_INT: + return object_type::integer; + case UCL_FLOAT: + return object_type::real; + case UCL_STRING: + return object_type::string; + case UCL_BOOLEAN: + return object_type::boolean; + case UCL_TIME: + return object_type::time; + case UCL_USERDATA: + return object_type::userdata; + case UCL_NULL: + return object_type::null; + default: + std::abort(); + } + } + + // Return the underlying object. + auto get_ucl_object(this object &self) -> ::ucl_object_t * + { + return self._object; + } + + auto get_ucl_object(this object const &self) -> ::ucl_object_t const * + { + return self._object; + } + + // Return the key of this object. + auto key(this object const &self) -> std::string_view + { + auto dlen = std::size_t{}; + auto const *dptr = ::ucl_object_keyl(self._object, &dlen); + return {dptr, dlen}; + } + + // Return a sub-object of this one. + auto lookup(this object const &self, std::string_view key) + -> std::optional + { + auto const *obj = ::ucl_object_lookup_any( + self._object, key.data(), key.size()); + if (obj == nullptr) + return {}; + + return {object(::ucl_object_ref(obj))}; + } + +protected: + // Create an object from an existing ucl_object_t. We assume the + // object has already been referenced. + object(::ucl_object_t *object) : _object(object) {} + + // The object we're wrapping. + ::ucl_object_t *_object = nullptr; + +private: + + friend struct parser; + friend struct iterator; +}; + +/*********************************************************************** + * Object iteration. + */ + +export struct iterator { + using difference_type = std::ptrdiff_t; + using value_type = object; + using reference = value_type &; + using const_reference = value_type const &; + using pointer = value_type *; + using const_pointer = value_type const *; + + struct sentinel{}; + + explicit iterator(object const &obj) + { + _state = std::make_shared(obj); + ++(*this); + } + + auto operator==(this iterator const &self, sentinel) -> bool + { + return (self._state->cur == nullptr); + } + + auto operator++(this iterator &self) -> iterator & + { + self._state->next(); + return self; + } + + auto operator++(this iterator &self, int) -> iterator & + { + self._state->next(); + return self; + } + + auto operator*(this iterator const &self) -> object { + auto *ptr = ::ucl_object_ref(self._state->cur); + return object(ptr); + } + +private: + struct state { + state(object const &obj) + { + auto const *uobj = obj.get_ucl_object(); + if ((iter = ::ucl_object_iterate_new(uobj)) == 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; +}; + +static_assert(std::input_iterator); + +export auto begin(object const &o) -> iterator +{ + return iterator(o); +} + +export auto end(object const &) -> iterator::sentinel +{ + return {}; +} + +/*********************************************************************** + * Value access by object_cast. + */ + +// Exception thrown when object_cast fails. +export struct bad_object_cast : error { + bad_object_cast() + : error("bad object_cast<>: object does not match target type") + {} +}; + +//export template +////auto object_cast(object const &from) -> To = delete; + +export template +auto object_cast(object const &from) -> To +{ + if (from.type() != To::ucl_type) + throw bad_object_cast(); + + auto const *uobj = from.get_ucl_object(); + auto *refptr = ::ucl_object_ref(uobj); + return To(refptr); +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/parser.ccm b/nihil.ucl/parser.ccm new file mode 100644 index 0000000..17ed79c --- /dev/null +++ b/nihil.ucl/parser.ccm @@ -0,0 +1,170 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +#include + +export module nihil.ucl:parser; + +import nihil; +import :error; +import :object; + +namespace nihil::ucl { + +/* + * Exception thrown when an issue occurs parsing UCL. + */ +export struct parse_error : error { + template + parse_error(std::format_string fmt, Args &&...args) + : error(fmt, std::forward(args)...) + {} +}; + +// UCL parser flags. +export inline constexpr int parser_key_lower = UCL_PARSER_KEY_LOWERCASE; +export inline constexpr int parser_zerocopy = UCL_PARSER_ZEROCOPY; +export inline constexpr int parser_no_time = UCL_PARSER_NO_TIME; + +export struct parser; + +// A macro handler. This proxies the C API callback to the C++ API. +using macro_callback_t = bool (std::string_view); + +struct macro_handler { + std::function callback; + + // Handle a callback from the C API. + static auto handle(unsigned char const *data, std::size_t len, void *ud) + -> bool + { + auto handler = static_cast(ud); + auto string = std::string_view( + reinterpret_cast(data), + len); + return handler->callback(string); + } +}; + +/* + * A UCL parser. This wraps the C ucl_parser API. + */ +export struct parser { + + // Create a new parser with the given flags. + parser(int flags) { + if ((_parser = ::ucl_parser_new(flags)) != nullptr) + return; + + throw error("failed to create UCL parser"); + } + + // Create a new parser with the default flags. + parser() : parser(0) {} + + // Destroy our parser when we're destroyed. + ~parser() + { + if (_parser) + ::ucl_parser_free(_parser); + } + + // Add a parser macro. Unlike ucl_parser_register_macro, this doesn't + // take a userdata parameter; it's assumed the user will use lambda + // capture or similar if needed. + template F> + auto register_macro(this parser &self, + std::string_view name, + F &&func) -> void + requires (std::same_as>) + { + auto handler = std::make_unique( + std::move(func)); + + auto cname = std::string(name); + ::ucl_parser_register_macro(self._parser, cname.c_str(), + ¯o_handler::handle, + handler.get()); + + self._macros.emplace_back(std::move(handler)); + } + + // Add a parser variable. + auto register_value(this parser &self, + std::string_view variable, + std::string_view value) -> void + { + ::ucl_parser_register_variable(self._parser, + std::string(variable).c_str(), + std::string(value).c_str()); + } + + // Add data to the parser. + auto add(this parser &self, + std::ranges::contiguous_range auto &&data) + -> void + // Only bytes (chars) are permitted. + requires(sizeof(std::ranges::range_value_t) == 1) + { + // UCL accepts unsigned chars, but this is quite unhelpful + // when reading from files or strings. + auto dptr = reinterpret_cast( + std::ranges::data(data)); + + auto ret = ::ucl_parser_add_chunk(self._parser, dptr, + std::ranges::size(data)); + if (ret == false) + throw parse_error("{}", + ::ucl_parser_get_error(self._parser)); + } + + auto add(this parser &self, std::ranges::range auto &&data) + -> void + requires (!std::ranges::contiguous_range) + { + auto cdata = std::vector( + std::from_range, + std::forward(data)); + return self.add(std::move(cdata)); + } + + // Return the top object of this parser. + auto top(this parser &self) -> object + { + if (self._parser == nullptr) + throw error("attempt to call top() on an empty parser"); + + auto obj = ::ucl_parser_get_object(self._parser); + if (obj == nullptr) + throw error("attempt to call top() on an empty parser"); + + return {obj}; + } + +private: + // The parser object. Should never be null, unless we've been + // moved-from. + ucl_parser *_parser = nullptr; + + // Functions added by register_macro. We have to store these as + // pointers because we pass the address to libucl. + std::vector> _macros; +}; + +// Utility function to parse something and return the object. +export auto parse(std::ranges::range auto &&data) -> object { + auto p = parser(); + p.add(std::forward(data)); + return p.top(); +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/real.ccm b/nihil.ucl/real.ccm new file mode 100644 index 0000000..4639109 --- /dev/null +++ b/nihil.ucl/real.ccm @@ -0,0 +1,76 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include + +#include + +export module nihil.ucl:real; + +import :object; + +namespace nihil::ucl { + +export struct real final : object { + using value_type = double; + + inline static constexpr object_type ucl_type = object_type::real; + + real(value_type value) + : object(::ucl_object_fromdouble(value)) + { + if (_object == nullptr) + throw error("failed to create UCL object"); + } + + explicit real(::ucl_object_t *uobj) : object(uobj) + { + assert(type() == object_type::real); + } + + auto value(this real const &self) -> value_type + { + auto v = value_type{}; + auto const *uobj = self.get_ucl_object(); + + if (::ucl_object_todouble_safe(uobj, &v)) + return v; + + std::abort(); + } +}; + +/* + * Comparison operators. + */ + +export auto operator== (real const &a, real const &b) + -> bool +{ + return a.value() == b.value(); +} + +export auto operator<=> (real const &a, real const &b) + -> std::partial_ordering +{ + return a.value() <=> b.value(); +} + +export auto operator== (real const &a, real::value_type b) + -> bool +{ + return a.value() == b; +} + +export auto operator<=> (real const &a, real::value_type b) + -> std::partial_ordering +{ + return a.value() <=> b; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/string.ccm b/nihil.ucl/string.ccm new file mode 100644 index 0000000..e41c70f --- /dev/null +++ b/nihil.ucl/string.ccm @@ -0,0 +1,78 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include + +#include + +export module nihil.ucl:string; + +import :object; + +namespace nihil::ucl { + +export struct string final : object { + using value_type = std::string_view; + + inline static constexpr object_type ucl_type = object_type::string; + + string(value_type value) + : object(::ucl_object_fromstring_common( + value.data(), value.size(), UCL_STRING_RAW)) + { + if (_object == nullptr) + throw error("failed to create UCL object"); + } + + explicit string(::ucl_object_t *uobj) : object(uobj) + { + assert(type() == object_type::string); + } + + auto value(this string const &self) -> value_type + { + char const *dptr{}; + std::size_t dlen; + + auto const *uobj = self.get_ucl_object(); + if (::ucl_object_tolstring_safe(uobj, &dptr, &dlen)) + return {dptr, dlen}; + + std::abort(); + } +}; + +/* + * Comparison operators. + */ + +export auto operator== (string const &a, string const &b) + -> bool +{ + return a.value() == b.value(); +} + +export auto operator<=> (string const &a, string const &b) + -> std::strong_ordering +{ + return a.value() <=> b.value(); +} + +export auto operator== (string const &a, string::value_type b) + -> bool +{ + return a.value() == b; +} + +export auto operator<=> (string const &a, string::value_type b) + -> std::strong_ordering +{ + return a.value() <=> b; +} + +} // namespace nihil::ucl diff --git a/nihil.ucl/tests/CMakeLists.txt b/nihil.ucl/tests/CMakeLists.txt new file mode 100644 index 0000000..2c4ec5d --- /dev/null +++ b/nihil.ucl/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +# This source code is released into the public domain. + +add_executable(nihil.ucl.test + emit.cc + parse.cc + + object.cc + array.cc + boolean.cc + integer.cc + real.cc + string.cc +) + +target_link_libraries(nihil.ucl.test PRIVATE + nihil nihil.ucl + Catch2::Catch2WithMain) + +find_package(Catch2 REQUIRED) + +include(CTest) +include(Catch) +catch_discover_tests(nihil.ucl.test) diff --git a/nihil.ucl/tests/array.cc b/nihil.ucl/tests/array.cc new file mode 100644 index 0000000..687b02c --- /dev/null +++ b/nihil.ucl/tests/array.cc @@ -0,0 +1,109 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.ucl; + +TEST_CASE("ucl: array: construct", "[ucl]") +{ + auto arr = nihil::ucl::array(); + REQUIRE(arr.size() == 0); +} + +TEST_CASE("ucl: array: push_back", "[ucl]") +{ + auto arr = nihil::ucl::array(); + REQUIRE(arr.size() == 0); + + arr.push_back(nihil::ucl::integer(1)); + arr.push_back(nihil::ucl::integer(42)); + arr.push_back(nihil::ucl::integer(666)); + + REQUIRE(arr.size() == 3); + REQUIRE(arr[0].value() == 1); + REQUIRE(arr[1].value() == 42); + REQUIRE(arr[2].value() == 666); + + REQUIRE_THROWS_AS(arr[3], std::out_of_range); + + REQUIRE(arr.front() == 1); + REQUIRE(arr.back() == 666); + +} + +TEST_CASE("ucl: array: compare", "[ucl]") +{ + auto arr = nihil::ucl::array(); + arr.push_back(1); + arr.push_back(42); + arr.push_back(666); + + auto arr2 = nihil::ucl::array(); + REQUIRE(arr != arr2); + + arr2.push_back(1); + arr2.push_back(42); + arr2.push_back(666); + REQUIRE(arr == arr2); + + auto arr3 = nihil::ucl::array(); + arr3.push_back(1); + arr3.push_back(1); + arr3.push_back(1); + REQUIRE(arr != arr3); +} + +TEST_CASE("ucl: array: iterator", "[ucl]") +{ + auto arr = nihil::ucl::array{1, 42, 666}; + + auto it = arr.begin(); + REQUIRE(*it == 1); + + ++it; + REQUIRE(*it == 42); + + ++it; + REQUIRE(*it == 666); + + --it; + REQUIRE(*it == 42); +} + +TEST_CASE("ucl: array: parse", "[ucl]") +{ + using namespace std::literals; + + auto input = "value = [1, 42, 666]"sv; + auto obj = nihil::ucl::parse(input); + auto v = obj.lookup("value"); + + REQUIRE(v); + REQUIRE(v->key() == "value"); + + auto arr = object_cast>(*v); + REQUIRE(arr.size() == 3); + REQUIRE(arr[0] == 1); + REQUIRE(arr[1] == 42); + REQUIRE(arr[2] == 666); +} + +TEST_CASE("ucl: array: emit", "[ucl]") +{ + auto ucl = nihil::ucl::parse("array = [1, 42, 666];"); + + auto output = std::string(); + emit(ucl, nihil::ucl::emitter::configuration, + std::back_inserter(output)); + + REQUIRE(output == +"array [\n" +" 1,\n" +" 42,\n" +" 666,\n" +"]\n"); +} diff --git a/nihil.ucl/tests/boolean.cc b/nihil.ucl/tests/boolean.cc new file mode 100644 index 0000000..c023498 --- /dev/null +++ b/nihil.ucl/tests/boolean.cc @@ -0,0 +1,50 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.ucl; + +TEST_CASE("ucl: boolean: construct", "[ucl]") +{ + auto obj = nihil::ucl::boolean(true); + REQUIRE(object_cast(obj).value() == true); +} + +TEST_CASE("ucl: boolean: compare", "[ucl]") +{ + auto b = nihil::ucl::boolean(true); + + REQUIRE(b == nihil::ucl::boolean(true)); + REQUIRE(b == true); + + REQUIRE(b != nihil::ucl::boolean(false)); + REQUIRE(b != false); +} + +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(*v).value() == true); +} + +TEST_CASE("ucl: boolean: emit", "[ucl]") +{ + auto ucl = nihil::ucl::parse("bool = true;"); + + auto output = std::string(); + emit(ucl, nihil::ucl::emitter::configuration, + std::back_inserter(output)); + + REQUIRE(output == "bool = true;\n"); +} diff --git a/nihil.ucl/tests/emit.cc b/nihil.ucl/tests/emit.cc new file mode 100644 index 0000000..13f5914 --- /dev/null +++ b/nihil.ucl/tests/emit.cc @@ -0,0 +1,8 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil; +import nihil.ucl; diff --git a/nihil.ucl/tests/integer.cc b/nihil.ucl/tests/integer.cc new file mode 100644 index 0000000..da119e6 --- /dev/null +++ b/nihil.ucl/tests/integer.cc @@ -0,0 +1,50 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.ucl; + +TEST_CASE("ucl: integer: construct", "[ucl]") +{ + auto obj = nihil::ucl::integer(42); + REQUIRE(object_cast(obj).value() == 42); +} + +TEST_CASE("ucl: integer: compare", "[ucl]") +{ + auto i = nihil::ucl::integer(42); + + REQUIRE(i == nihil::ucl::integer(42)); + REQUIRE(i != nihil::ucl::integer(1)); + REQUIRE((i == 42) == true); + + REQUIRE(i > nihil::ucl::integer(40)); + REQUIRE(i > 40); +} + +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(*v).value() == 42); +} + +TEST_CASE("ucl: integer: emit", "[ucl]") +{ + auto ucl = nihil::ucl::parse("int = 42;"); + + auto output = std::string(); + emit(ucl, nihil::ucl::emitter::configuration, + std::back_inserter(output)); + + REQUIRE(output == "int = 42;\n"); +} diff --git a/nihil.ucl/tests/object.cc b/nihil.ucl/tests/object.cc new file mode 100644 index 0000000..f9cef9f --- /dev/null +++ b/nihil.ucl/tests/object.cc @@ -0,0 +1,22 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.ucl; + +TEST_CASE("ucl object: get_ucl_object", "[ucl]") +{ + auto obj = nihil::ucl::integer(42); + + REQUIRE(obj.get_ucl_object() != nullptr); + static_assert(std::same_as<::ucl_object_t *, + decltype(obj.get_ucl_object())>); + + auto const cobj = obj; + static_assert(std::same_as<::ucl_object_t const *, + decltype(cobj.get_ucl_object())>); +} diff --git a/nihil.ucl/tests/parse.cc b/nihil.ucl/tests/parse.cc new file mode 100644 index 0000000..3a4f061 --- /dev/null +++ b/nihil.ucl/tests/parse.cc @@ -0,0 +1,50 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include +#include + +import nihil; +import nihil.ucl; + +TEST_CASE("ucl parse: iterate array", "[ucl]") +{ + using namespace std::literals; + + auto input = "value = [1, 42, 666];"sv; + auto obj = nihil::ucl::parse(input); + + auto array = obj.lookup("value"); + REQUIRE(array); + REQUIRE(array->key() == "value"); + + auto vec = std::vector(); + std::ranges::copy(*array, std::back_inserter(vec)); + REQUIRE(vec.size() == 3); + REQUIRE(object_cast(vec[0]).value() == 1); + REQUIRE(object_cast(vec[1]).value() == 42); + REQUIRE(object_cast(vec[2]).value() == 666); +} + +TEST_CASE("ucl parse: iterate hash", "[ucl]") +{ + using namespace std::literals; + + 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(value).value() + == 42); + else if (value.key() == "bool") + REQUIRE(object_cast(value).value() + == true); + else if (value.key() == "str") + REQUIRE(object_cast(value).value() + == "test"); + } +} diff --git a/nihil.ucl/tests/real.cc b/nihil.ucl/tests/real.cc new file mode 100644 index 0000000..275684a --- /dev/null +++ b/nihil.ucl/tests/real.cc @@ -0,0 +1,53 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include +#include + +import nihil.ucl; + +TEST_CASE("ucl: real: construct", "[ucl]") +{ + auto obj = nihil::ucl::real(42.1); + REQUIRE_THAT(object_cast(obj).value(), + Catch::Matchers::WithinRel(42.1)); +} + +TEST_CASE("ucl: real: compare", "[ucl]") +{ + auto i = nihil::ucl::real(42); + + REQUIRE(i == nihil::ucl::real(42)); + REQUIRE(i != nihil::ucl::real(1)); + REQUIRE((i == 42) == true); + + REQUIRE(i > nihil::ucl::real(40)); + REQUIRE(i > 40); +} + +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(*v).value(), + Catch::Matchers::WithinRel(42.1)); +} + +TEST_CASE("ucl: real: emit", "[ucl]") +{ + auto ucl = nihil::ucl::parse("real = 42.2"); + + auto output = std::string(); + emit(ucl, nihil::ucl::emitter::configuration, + std::back_inserter(output)); + + REQUIRE(output == "real = 42.2;\n"); +} diff --git a/nihil.ucl/tests/string.cc b/nihil.ucl/tests/string.cc new file mode 100644 index 0000000..4385dbb --- /dev/null +++ b/nihil.ucl/tests/string.cc @@ -0,0 +1,38 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.ucl; + +TEST_CASE("ucl: string: construct", "[ucl]") +{ + auto obj = nihil::ucl::string("testing"); + REQUIRE(object_cast(obj).value() == "testing"); +} + +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(*v).value() == "str"); +} + +TEST_CASE("ucl: string: emit", "[ucl]") +{ + auto ucl = nihil::ucl::parse("str = \"te\\\"st\";"); + + auto output = std::string(); + emit(ucl, nihil::ucl::emitter::configuration, + std::back_inserter(output)); + + REQUIRE(output == "str = \"te\\\"st\";\n"); +} -- cgit v1.2.3