From 8b1ad067d9450b4c9c4384b6bf2859c70a6b0cce Mon Sep 17 00:00:00 2001 From: David Robillard Date: Sat, 16 Jun 2018 10:26:47 -0400 Subject: [WIP] Add C++ bindings --- .includes.imp | 1 + bindings/cpp/include/.clang-tidy | 18 + bindings/cpp/include/serd/Flags.hpp | 101 + bindings/cpp/include/serd/Optional.hpp | 164 ++ bindings/cpp/include/serd/StringView.hpp | 246 ++ bindings/cpp/include/serd/detail/Copyable.hpp | 236 ++ .../cpp/include/serd/detail/DynamicWrapper.hpp | 74 + bindings/cpp/include/serd/detail/StaticWrapper.hpp | 97 + bindings/cpp/include/serd/detail/Wrapper.hpp | 99 + bindings/cpp/include/serd/serd.hpp | 2501 ++++++++++++++++++++ bindings/cpp/meson.build | 77 + bindings/cpp/test/.clang-tidy | 30 + bindings/cpp/test/test_serd_hpp.cpp | 823 +++++++ doc/conf.py.in | 12 +- doc/cpp/.clang-tidy | 25 + doc/cpp/Doxyfile.in | 45 + doc/cpp/api/meson.build | 8 + doc/cpp/cpp_facilities.rst | 53 + doc/cpp/index.rst | 16 + doc/cpp/meson.build | 57 + doc/cpp/model.rst | 266 +++ doc/cpp/nodes.rst | 38 + doc/cpp/overview.cpp | 370 +++ doc/cpp/overview.rst | 83 + doc/cpp/reading_and_writing.rst | 147 ++ doc/cpp/statements.rst | 124 + doc/cpp/stream_processing.rst | 48 + doc/cpp/using_serd.rst | 14 + doc/cpp/world.rst | 45 + doc/cpp/xml/meson.build | 19 + doc/meson.build | 4 + meson.build | 10 + meson_options.txt | 3 + 33 files changed, 5853 insertions(+), 1 deletion(-) create mode 100644 bindings/cpp/include/.clang-tidy create mode 100644 bindings/cpp/include/serd/Flags.hpp create mode 100644 bindings/cpp/include/serd/Optional.hpp create mode 100644 bindings/cpp/include/serd/StringView.hpp create mode 100644 bindings/cpp/include/serd/detail/Copyable.hpp create mode 100644 bindings/cpp/include/serd/detail/DynamicWrapper.hpp create mode 100644 bindings/cpp/include/serd/detail/StaticWrapper.hpp create mode 100644 bindings/cpp/include/serd/detail/Wrapper.hpp create mode 100644 bindings/cpp/include/serd/serd.hpp create mode 100644 bindings/cpp/meson.build create mode 100644 bindings/cpp/test/.clang-tidy create mode 100644 bindings/cpp/test/test_serd_hpp.cpp create mode 100644 doc/cpp/.clang-tidy create mode 100644 doc/cpp/Doxyfile.in create mode 100644 doc/cpp/api/meson.build create mode 100644 doc/cpp/cpp_facilities.rst create mode 100644 doc/cpp/index.rst create mode 100644 doc/cpp/meson.build create mode 100644 doc/cpp/model.rst create mode 100644 doc/cpp/nodes.rst create mode 100644 doc/cpp/overview.cpp create mode 100644 doc/cpp/overview.rst create mode 100644 doc/cpp/reading_and_writing.rst create mode 100644 doc/cpp/statements.rst create mode 100644 doc/cpp/stream_processing.rst create mode 100644 doc/cpp/using_serd.rst create mode 100644 doc/cpp/world.rst create mode 100644 doc/cpp/xml/meson.build diff --git a/.includes.imp b/.includes.imp index ec06835e..843a9b7c 100644 --- a/.includes.imp +++ b/.includes.imp @@ -11,4 +11,5 @@ { "symbol": [ "uintptr_t", "private", "", "public" ] }, { "include": [ "", "private", "", "public", ] }, + { "symbol": [ "std::ostringstream", "private", "", "public" ] } ] diff --git a/bindings/cpp/include/.clang-tidy b/bindings/cpp/include/.clang-tidy new file mode 100644 index 00000000..1b176f19 --- /dev/null +++ b/bindings/cpp/include/.clang-tidy @@ -0,0 +1,18 @@ +Checks: > + *, + -*-named-parameter, + -*-non-private-member-variables-in-classes, + -*-uppercase-literal-suffix, + -altera-struct-pack-align, + -cert-dcl50-cpp, + -clang-diagnostic-unused-macros, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -fuchsia-default-arguments-declarations, + -fuchsia-multiple-inheritance, + -fuchsia-overloaded-operator, + -llvmlibc-*, + -modernize-use-trailing-return-type, + -readability-implicit-bool-conversion, +WarningsAsErrors: '*' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/bindings/cpp/include/serd/Flags.hpp b/bindings/cpp/include/serd/Flags.hpp new file mode 100644 index 00000000..e87bb41d --- /dev/null +++ b/bindings/cpp/include/serd/Flags.hpp @@ -0,0 +1,101 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_FLAGS_HPP +#define SERD_FLAGS_HPP + +#include + +namespace serd { + +/** + @defgroup serdpp_flags Flags + @ingroup serdpp + @{ +*/ + +/** + Type-safe bit flags + + This is a minimal interface for type-safe bit flags, which only allows + values from the corresponding flags enum to be set. It functions like a + normal unsigned integer bit field, but attempting to get or set a flag with + the incorrect type will fail to compile. + + @tparam Flag Strong enumeration type for flag values. +*/ +template +class Flags +{ +public: + static_assert(std::is_enum::value, ""); + + using Value = std::make_unsigned_t>; + + /// Construct with no flags set + constexpr Flags() noexcept = default; + + /// Construct from a raw bit field value + constexpr explicit Flags(const Value value) noexcept + : _value{value} + {} + + /// Construct from a single flag + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + constexpr Flags(const Flag f) noexcept + : _value{static_cast(f)} + {} + + /// Set a flag + constexpr Flags operator|(const Flag rhs) const noexcept + { + return Flags{_value | static_cast(rhs)}; + } + + /// Set all the flags from another set of flags + constexpr Flags operator|(const Flags rhs) const noexcept + { + return Flags{_value | rhs._value}; + } + + /// Return true if only the given flag is set + constexpr bool operator==(const Flag rhs) const noexcept + { + return _value == static_cast(rhs); + } + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + constexpr operator Value() const noexcept { return _value; } + +private: + Value _value{}; +}; + +/// Make a new Flags by combining two individual flags +template +inline constexpr Flags +operator|(const Flag lhs, const Flag rhs) noexcept +{ + return Flags{lhs} | rhs; +} + +/** + @} +*/ + +} // namespace serd + +#endif // SERD_FLAGS_HPP diff --git a/bindings/cpp/include/serd/Optional.hpp b/bindings/cpp/include/serd/Optional.hpp new file mode 100644 index 00000000..ee6a7fc2 --- /dev/null +++ b/bindings/cpp/include/serd/Optional.hpp @@ -0,0 +1,164 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_OPTIONAL_HPP +#define SERD_OPTIONAL_HPP + +#include +#include +#include + +namespace serd { + +/** + @defgroup serdpp_optional Optional + @ingroup serdpp + @{ +*/ + +struct ConstructNullOptional {}; + +/// Special tag for constructing an unset Optional +struct Nullopt { + enum class Construct { internal }; + + explicit constexpr Nullopt(Construct) {} +}; + +/** + A simple optional wrapper around a wrapped type with a pointer-like API + + This works like a typical optional type, but only works with Wrapper types, + and exploits the fact that these are interally just pointers to avoid adding + space overhead for an "is_set" flag, like a generic optional class would. + + Types must explicitly opt-in to being optional by providing a constructor + that takes a single ContructNullOptional argument. This constructor should + only be used by the Optional implementation, which guarantees that such an + object will not be used except by calling its cobj() method. +*/ +template +class Optional +{ +public: + /// The type of the underlying C object + using CType = typename T::CType; + + /// Constructs an empty optional + Optional() = default; + + /// Constructs an empty optional + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Optional(Nullopt) {} + + /// Constructs an optional that contains the given value + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Optional(T value) + : _value{std::move(value)} + {} + + /// Constructs an optional that contains a converted value + template< + typename U, + typename = typename std::enable_if::value>::type> + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Optional(U&& value) + : _value{std::forward(value)} + {} + + /// Destroys any contained value + void reset() { _value = T{nullptr}; } + + /// Accesses the contained value + const T& operator*() const + { + assert(_value.cobj()); + return _value; + } + + /// Accesses the contained value + T& operator*() + { + assert(_value.cobj()); + return _value; + } + + /// Accesses the contained value + const T* operator->() const + { + assert(_value.cobj()); + return &_value; + } + + /// Accesses the contained value + T* operator->() + { + assert(_value.cobj()); + return &_value; + } + + /// Tests if optional objects are equal + bool operator==(const Optional& optional) + { + return (!*this && !optional) || + (*this && optional && _value == optional._value); + } + + /// Tests if optional objects are not equal + bool operator!=(const Optional& optional) { return !operator==(optional); } + + /// Returns true if this optional contains a value + explicit operator bool() const { return _value.cobj(); } + + /// Returns true if this optional does not contain a value + bool operator!() const { return !_value.cobj(); } + + /// Return a pointer to the underlying C object, or null + CType* cobj() { return _value.cobj(); } + + /// Return a pointer to the underlying C object, or null + const CType* cobj() const { return _value.cobj(); } + +private: + T _value{nullptr}; +}; + +/// Creates an optional object from `value` +template +constexpr Optional> +make_optional(T&& value) +{ + return Optional{std::forward(value)}; +} + +/// Creates an optional object with a value constructed in-place from `args` +template +constexpr Optional +make_optional(Args&&... args) +{ + return Optional{std::forward(args)...}; +} + +/// Constant that represents an empty optional +static constexpr Nullopt nullopt{Nullopt::Construct::internal}; + +/** + @} +*/ + +} // namespace serd + +#endif // SERD_OPTIONAL_HPP diff --git a/bindings/cpp/include/serd/StringView.hpp b/bindings/cpp/include/serd/StringView.hpp new file mode 100644 index 00000000..f62f0827 --- /dev/null +++ b/bindings/cpp/include/serd/StringView.hpp @@ -0,0 +1,246 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_STRINGVIEW_HPP +#define SERD_STRINGVIEW_HPP + +#include "serd/serd.h" + +#include +#include +#include +#include +#include +#include + +namespace serd { + +/** + @defgroup serdpp_string_view String View + @ingroup serdpp + @{ +*/ + +/** + Immutable slice of a string. + + This is a minimal implementation that is compatible with std::string_view + and std::string for most basic use cases. This could be replaced with + std::string_view once C++17 support can be relied on. +*/ +class StringView +{ +public: + using char_type = char; + using size_type = size_t; + using traits_type = std::char_traits; + using value_type = char; + using pointer = value_type*; + using const_pointer = const value_type*; + using reference = value_type&; + using const_reference = const value_type&; + using iterator = const char*; + using const_iterator = const char*; + + static constexpr size_type npos = size_t(-1); + + constexpr StringView() noexcept = default; + + constexpr StringView(const char* const str, const size_t len) noexcept + : _str{str} + , _len{len} + {} + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + StringView(const char* const str) noexcept + : _str{str} + , _len{str ? strlen(str) : 0} + {} + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + StringView(const std::string& str) noexcept + : _str{str.c_str()} + , _len{str.length()} + {} + + constexpr size_t size() const { return _len; } + constexpr size_t length() const { return _len; } + constexpr bool empty() const { return _len == 0; } + constexpr const char* c_str() const { return _str; } + constexpr const char* data() const { return _str; } + constexpr const char& front() const { return _str[0]; } + constexpr const char& back() const { return _str[_len - 1]; } + + constexpr const_iterator begin() const { return _str; } + constexpr const_iterator end() const { return _str + _len; } + constexpr const_iterator cbegin() const { return begin(); } + constexpr const_iterator cend() const { return end(); } + + constexpr const char& operator[](const size_t pos) const { return _str[pos]; } + + const char& at(const size_t pos) const + { + if (pos >= size()) { + throw std::out_of_range("serd::StringView::at pos"); + } + + return _str[pos]; + } + + StringView substr(const size_t pos) const + { + if (pos > size()) { + throw std::out_of_range("serd::StringView::substr pos"); + } + + return StringView{data() + pos, size() - pos}; + } + + StringView substr(const size_t pos, const size_t n) const + { + if (pos > size()) { + throw std::out_of_range("serd::StringView::substr pos"); + } + + return StringView{data() + pos, std::min(size() - pos, n)}; + } + + int compare(StringView rhs) const noexcept + { + if (!data() && !rhs.data()) { + return 0; + } + + if (!data()) { + return -1; + } + + if (!rhs.data()) { + return 1; + } + + const size_type len = std::min(size(), rhs.size()); + const int cmp = strncmp(data(), rhs.data(), len); + + if (cmp) { + return cmp; + } + + if (size() == rhs.size()) { + return 0; + } + + if (size() < rhs.size()) { + return -1; + } + + return 1; + } + + template> + std::basic_string str() const + { + return std::basic_string(data(), size(), Alloc{}); + } + + template> + std::basic_string str(const Alloc& alloc) const + { + return std::basic_string(data(), size(), alloc); + } + + explicit operator std::string() const { return str(); } + + explicit operator const char*() const { return _str; } + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + operator SerdStringView() const { return SerdStringView{_str, _len}; } + +private: + const char* const _str{}; + const size_t _len{}; +}; + +inline bool +operator==(const StringView& lhs, const StringView& rhs) +{ + return !lhs.compare(rhs); +} + +inline bool +operator==(const StringView& lhs, const std::string& rhs) +{ + return lhs.length() == rhs.length() && + !strncmp(lhs.c_str(), rhs.c_str(), lhs.length()); +} + +inline bool +operator==(const StringView& lhs, const char* rhs) +{ + return !strncmp(lhs.c_str(), rhs, lhs.length()); +} + +inline bool +operator!=(const StringView& lhs, const StringView& rhs) +{ + return lhs.compare(rhs); +} + +inline bool +operator!=(const StringView& lhs, const std::string& rhs) +{ + return lhs.length() != rhs.length() || + !!strncmp(lhs.c_str(), rhs.c_str(), lhs.length()); +} + +inline bool +operator!=(const StringView& lhs, const char* rhs) +{ + return !!strncmp(lhs.c_str(), rhs, lhs.length()); +} + +inline bool +operator<(const StringView& lhs, const StringView& rhs) +{ + return lhs.compare(rhs) < 0; +} + +inline bool +operator<(const StringView& lhs, const std::string& rhs) +{ + return lhs.c_str() < StringView(rhs); +} + +inline bool +operator<(const StringView& lhs, const char* rhs) +{ + return strncmp(lhs.c_str(), rhs, lhs.length()) < 0; +} + +inline std::ostream& +operator<<(std::ostream& os, const StringView& str) +{ + os.write(str.data(), std::streamsize(str.size())); + return os; +} + +/** + @} +*/ + +} // namespace serd + +#endif // SERD_STRINGVIEW_HPP diff --git a/bindings/cpp/include/serd/detail/Copyable.hpp b/bindings/cpp/include/serd/detail/Copyable.hpp new file mode 100644 index 00000000..7b1604d7 --- /dev/null +++ b/bindings/cpp/include/serd/detail/Copyable.hpp @@ -0,0 +1,236 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_DETAIL_COPYABLE_HPP +#define SERD_DETAIL_COPYABLE_HPP + +// IWYU pragma: no_include "serd/serd.h" + +#include "serd/detail/DynamicWrapper.hpp" +#include "serd/detail/StaticWrapper.hpp" +#include "serd/detail/Wrapper.hpp" + +#include +#include +#include +#include + +namespace serd { +namespace detail { + +/** + @addtogroup serdpp_detail + @{ +*/ + +/// Copy function for a C object +template +using CopyFunc = T* (*)(const T*); + +template* Copy(SerdAllocator*, const T*)> +typename std::enable_if_t::value, T>* +copy(const T* ptr) +{ + return ptr; // Making a view (const reference), do not copy +} + +template* Copy(SerdAllocator*, const T*)> +typename std::enable_if_t::value, T>* +copy(const T* ptr) +{ + return ptr ? Copy(nullptr, ptr) : nullptr; // Making a mutable wrapper, copy +} + +/** + Generic wrapper for a "basic" copyable object. + + This wraps objects with simple ownership semantics where a const pointer is + never owned, and a mutable pointer is owned. This has no space overhead + compared to a raw pointer since the ownership is encoded in the type. +*/ +template* Copy(SerdAllocator*, const T*), + bool Equals(const T*, const T*), + void Free(Mutable*)> +class StaticCopyable : public StaticWrapper +{ +public: + using Deleter = StaticDeleter; + using Base = StaticWrapper; + + explicit StaticCopyable(T* ptr) + : Base{ptr} + {} + + StaticCopyable(const StaticCopyable& wrapper) + : Base(copy(wrapper.cobj())) + {} + + template*)> + explicit StaticCopyable(const StaticCopyable& wrapper) + : Base(copy(wrapper.cobj())) + {} + + StaticCopyable(StaticCopyable&&) noexcept = default; + ~StaticCopyable() noexcept = default; + + StaticCopyable& operator=(StaticCopyable&&) noexcept = default; + + StaticCopyable& operator=(const StaticCopyable& wrapper) + { + if (&wrapper != this) { + this->_ptr = std::unique_ptr(copy(wrapper.cobj())); + } + return *this; + } + + template + bool operator==(const StaticCopyable& wrapper) const + { + return Equals(this->cobj(), wrapper.cobj()); + } + + template + bool operator!=(const StaticCopyable& wrapper) const + { + return !operator==(wrapper); + } +}; + +/** + Generic wrapper for a "basic" copyable object. + + This wraps objects with simple ownership semantics where a const pointer is + never owned, and a mutable pointer is owned. This has no space overhead + compared to a raw pointer since the ownership is encoded in the type. +*/ +template* Copy(SerdAllocator*, const T*), + bool Equals(const T*, const T*), + void Free(SerdAllocator*, Mutable*)> +class StaticAllocatedCopyable : public StaticAllocatedWrapper +{ +public: + using Deleter = StaticAllocatedDeleter; + using Base = StaticAllocatedWrapper; + + explicit StaticAllocatedCopyable(T* ptr) + : Base{ptr} + {} + + StaticAllocatedCopyable(const StaticAllocatedCopyable& wrapper) + : Base(copy(wrapper.cobj())) + {} + + template*)> + explicit StaticAllocatedCopyable( + const StaticAllocatedCopyable& wrapper) + : Base(copy(wrapper.cobj())) + {} + + StaticAllocatedCopyable(StaticAllocatedCopyable&&) noexcept = default; + ~StaticAllocatedCopyable() noexcept = default; + + StaticAllocatedCopyable& operator=(StaticAllocatedCopyable&&) noexcept = + default; + + StaticAllocatedCopyable& operator=(const StaticAllocatedCopyable& wrapper) + { + if (&wrapper != this) { + this->_ptr = std::unique_ptr(copy(wrapper.cobj())); + } + return *this; + } + + template + bool operator==( + const StaticAllocatedCopyable& wrapper) const + { + return Equals(this->cobj(), wrapper.cobj()); + } + + template + bool operator!=( + const StaticAllocatedCopyable& wrapper) const + { + return !operator==(wrapper); + } +}; + +/** + Wrapper for a "dynamic" copyable C object. + + This wraps objects that require dynamic tracking of the ownership. +*/ +template* Copy(SerdAllocator*, const T*), + bool Equals(const T*, const T*), + void Free(Mutable*)> +class DynamicCopyable : public Wrapper> +{ +public: + using Deleter = DynamicDeleter; + using Base = Wrapper; + + explicit DynamicCopyable(std::unique_ptr ptr) + : Base{std::move(ptr)} + {} + + DynamicCopyable(const DynamicCopyable& wrapper) + : Base{Copy(nullptr /* FIXME */, wrapper.cobj()), Ownership::owned} + {} + + DynamicCopyable(DynamicCopyable&&) noexcept = default; + DynamicCopyable& operator=(DynamicCopyable&&) noexcept = default; + + ~DynamicCopyable() noexcept = default; + + DynamicCopyable& operator=(const DynamicCopyable& wrapper) + { + if (&wrapper != this) { + this->_ptr = std::unique_ptr( + Copy(nullptr /* FIXME */, wrapper.cobj()), Ownership::owned); + } + + return *this; + } + + template + bool operator==(const DynamicCopyable& wrapper) const + { + return Equals(this->cobj(), wrapper.cobj()); + } + + template + bool operator!=(const DynamicCopyable& wrapper) const + { + return !operator==(wrapper); + } + +protected: + explicit DynamicCopyable(std::nullptr_t) + : Base(nullptr) + {} +}; + +/** + @} +*/ + +} // namespace detail +} // namespace serd + +#endif // SERD_DETAIL_COPYABLE_HPP diff --git a/bindings/cpp/include/serd/detail/DynamicWrapper.hpp b/bindings/cpp/include/serd/detail/DynamicWrapper.hpp new file mode 100644 index 00000000..6b3c898a --- /dev/null +++ b/bindings/cpp/include/serd/detail/DynamicWrapper.hpp @@ -0,0 +1,74 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_DETAIL_DYNAMICWRAPPER_HPP +#define SERD_DETAIL_DYNAMICWRAPPER_HPP + +// IWYU pragma: no_include "serd/serd.h" + +#include "serd/detail/Wrapper.hpp" + +#include + +namespace serd { +namespace detail { + +/** + Ownership for `DynamicDeleter`. + + @ingroup serdpp_detail +*/ +enum class Ownership { + owned, ///< This pointer owns the data and must delete it + view, ///< This pointer is just a view and must not delete the data +}; + +/** + Deleter for a C object that can handle dynamic ownership. + + Unlike StaticDeleter, this can be used to handle non-owned references to + mutable objects, at the cost of an extra word for tracking the ownership + (since constness in the type can't convey this information). + + @ingroup serdpp_detail +*/ +template*)> +struct DynamicDeleter { + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + DynamicDeleter(const Ownership ownership) + : _ownership{ownership} + {} + + template::value>> + void operator()(Mutable* const ptr) + { + if (_ownership == Ownership::owned) { + Free(ptr); + } + } + + template::value>> + void operator()(const T*) + {} + +private: + Ownership _ownership; +}; + +} // namespace detail +} // namespace serd + +#endif // SERD_DETAIL_DYNAMICWRAPPER_HPP diff --git a/bindings/cpp/include/serd/detail/StaticWrapper.hpp b/bindings/cpp/include/serd/detail/StaticWrapper.hpp new file mode 100644 index 00000000..9ca82118 --- /dev/null +++ b/bindings/cpp/include/serd/detail/StaticWrapper.hpp @@ -0,0 +1,97 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_DETAIL_STATICWRAPPER_HPP +#define SERD_DETAIL_STATICWRAPPER_HPP + +// IWYU pragma: no_include "serd/serd.h" + +#include "serd/detail/Wrapper.hpp" + +#include + +namespace serd { +namespace detail { + +/** + Simple overhead-free deleter for a C object. + + Can be used with const or mutable pointers, but only mutable pointers will + be freed. In other words, mutability implies ownership, and this can not + handle unowned mutable pointers. + + @ingroup serdpp_detail +*/ +template*)> +struct StaticDeleter { + template::value>> + void operator()(Mutable* const ptr) + { + Free(ptr); + } + + template::value>> + void operator()(const T*) + {} +}; + +/// Simple overhead-free wrapper for a C object that can free itself +template*)> +class StaticWrapper : public Wrapper> +{ +public: + explicit StaticWrapper(T* const ptr) + : Wrapper>{ptr} + {} +}; + +/** + Simple overhead-free deleter for a C object. + + Can be used with const or mutable pointers, but only mutable pointers will + be freed. In other words, mutability implies ownership, and this can not + handle unowned mutable pointers. + + @ingroup serdpp_detail +*/ +template*)> +struct StaticAllocatedDeleter { + template::value>> + void operator()(Mutable* const ptr) + { + Free(nullptr, ptr); + } + + template::value>> + void operator()(const T*) + {} +}; + +/// Simple overhead-free wrapper for a C object that uses an allocator +template*)> +class StaticAllocatedWrapper + : public Wrapper> +{ +public: + explicit StaticAllocatedWrapper(T* const ptr) + : Wrapper>{ptr} + {} +}; + +} // namespace detail +} // namespace serd + +#endif // SERD_DETAIL_STATICWRAPPER_HPP diff --git a/bindings/cpp/include/serd/detail/Wrapper.hpp b/bindings/cpp/include/serd/detail/Wrapper.hpp new file mode 100644 index 00000000..39426775 --- /dev/null +++ b/bindings/cpp/include/serd/detail/Wrapper.hpp @@ -0,0 +1,99 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_DETAIL_WRAPPER_HPP +#define SERD_DETAIL_WRAPPER_HPP + +// IWYU pragma: no_include "serd/serd.h" + +#include +#include +#include +#include + +namespace serd { + +/// Utility template for a mutable type which removes const if necessary +template +class Optional; + +/// Detail namespace +namespace detail { + +/** + @defgroup serdpp_detail Serd C++ API details + Internal C++ wrapper details that should not be used directly by clients. + @ingroup serdpp + @{ +*/ + +/// Utility template for a mutable type which removes const if necessary +template +using Mutable = typename std::remove_const_t; + +/// Generic C++ wrapper for a C object +template +class Wrapper +{ +public: + using CType = T; + + explicit Wrapper(T* ptr) + : _ptr{ptr, Deleter{}} + {} + + Wrapper(T* ptr, Deleter deleter) + : _ptr{ptr, std::move(deleter)} + {} + + explicit Wrapper(std::unique_ptr ptr) + : _ptr{std::move(ptr)} + {} + + Wrapper(Wrapper&&) noexcept = default; + Wrapper& operator=(Wrapper&&) noexcept = default; + + Wrapper(const Wrapper&) = delete; + Wrapper& operator=(const Wrapper&) = delete; + + ~Wrapper() = default; + + /// Return a pointer to the underlying C object + T* cobj() { return _ptr.get(); } + + /// Return a pointer to the underlying C object + const T* cobj() const { return _ptr.get(); } + +protected: + friend class Optional; + + explicit Wrapper(std::nullptr_t) + : _ptr{nullptr} + {} + + void reset() { _ptr.reset(); } + + std::unique_ptr _ptr; +}; + +/** + @} +*/ + +} // namespace detail +} // namespace serd + +#endif // SERD_DETAIL_WRAPPER_HPP diff --git a/bindings/cpp/include/serd/serd.hpp b/bindings/cpp/include/serd/serd.hpp new file mode 100644 index 00000000..28e1621f --- /dev/null +++ b/bindings/cpp/include/serd/serd.hpp @@ -0,0 +1,2501 @@ +/* + Copyright 2019-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#ifndef SERD_SERD_HPP +#define SERD_SERD_HPP + +#include "serd/Flags.hpp" // IWYU pragma: export +#include "serd/Optional.hpp" // IWYU pragma: export +#include "serd/StringView.hpp" // IWYU pragma: export +#include "serd/detail/Copyable.hpp" // IWYU pragma: export +#include "serd/detail/DynamicWrapper.hpp" // IWYU pragma: export +#include "serd/detail/StaticWrapper.hpp" // IWYU pragma: export +#include "serd/detail/Wrapper.hpp" // IWYU pragma: export + +#include "serd/serd.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace serd { + +/** + @defgroup serdpp Serd C++ API + @{ +*/ + +/** + @defgroup serdpp_status Status Codes + @{ +*/ + +/// @copydoc SerdStatus +enum class Status { + success = SERD_SUCCESS, ///< @copydoc SERD_SUCCESS + failure = SERD_FAILURE, ///< @copydoc SERD_FAILURE + unknown_error = SERD_UNKNOWN_ERROR, ///< @copydoc SERD_UNKNOWN_ERROR + no_data = SERD_NO_DATA, ///< @copydoc SERD_NO_DATA + overflow = SERD_OVERFLOW, ///< @copydoc SERD_OVERFLOW + + bad_alloc = SERD_BAD_ALLOC, ///< @copydoc SERD_BAD_ALLOC + bad_arg = SERD_BAD_ARG, ///< @copydoc SERD_BAD_ARG + bad_call = SERD_BAD_CALL, ///< @copydoc SERD_BAD_CALL + bad_curie = SERD_BAD_CURIE, ///< @copydoc SERD_BAD_CURIE + bad_cursor = SERD_BAD_CURSOR, ///< @copydoc SERD_BAD_CURSOR + bad_event = SERD_BAD_EVENT, ///< @copydoc SERD_BAD_EVENT + bad_index = SERD_BAD_INDEX, ///< @copydoc SERD_BAD_INDEX + bad_label = SERD_BAD_LABEL, ///< @copydoc SERD_BAD_LABEL + bad_literal = SERD_BAD_LITERAL, ///< @copydoc SERD_BAD_LITERAL + bad_pattern = SERD_BAD_PATTERN, ///< @copydoc SERD_BAD_PATTERN + bad_read = SERD_BAD_READ, ///< @copydoc SERD_BAD_READ + bad_stack = SERD_BAD_STACK, ///< @copydoc SERD_BAD_STACK + bad_syntax = SERD_BAD_SYNTAX, ///< @copydoc SERD_BAD_SYNTAX + bad_text = SERD_BAD_TEXT, ///< @copydoc SERD_BAD_TEXT + bad_uri = SERD_BAD_URI, ///< @copydoc SERD_BAD_URI + bad_write = SERD_BAD_WRITE, ///< @copydoc SERD_BAD_WRITE + bad_data = SERD_BAD_DATA, ///< @copydoc SERD_BAD_DATA +}; + +/// @copydoc serd_strerror +inline const char* +strerror(const Status status) +{ + return serd_strerror(static_cast(status)); +} + +/** + @} + @defgroup serdpp_string String Utilities + @{ +*/ + +/** + Encode `size` bytes of `buf` into `str`, which must be large enough. + + @param buf Input binary data (vector-like container of bytes). + @return A base64 encoded representation of the data in `buf`. +*/ +template +inline std::string +base64_encode(const Container& buf) +{ +#if 0 + const size_t length{serd_base64_encoded_length(buf.size(), wrap_lines)}; + std::string str(length + 1, '\0'); + + serd_base64_encode(&str.at(0), buf.data(), buf.size(), wrap_lines); + return str; +#endif + + (void)buf; + return ""; // FIXME +} + +// FIXME +#if 0 +/** + Decode a base64 string. + + Container must be a vector-like container of bytes. + + @param str Base64 string to decode. + @return The decoded data represented in `str`. +*/ +template> +inline Container +base64_decode(StringView str) +{ +# if 0 + size_t size{serd_base64_decoded_size(str.length())}; + Container buf(size, 0); + + serd_base64_decode(&buf.at(0), &size, str.c_str(), str.length()); + buf.resize(size); + return buf; +# endif + (void)str; + return {}; // FIXME +} +#endif + +// TODO: serd_canonical_path + +// TODO: grouping? +static inline size_t +stream_write(const void* buf, size_t size, size_t nmemb, void* sink) noexcept +{ + (void)size; + assert(size == 1); + + std::ostream& os = *static_cast(sink); + + try { + os.write(static_cast(buf), std::streamsize(nmemb)); + return os.good() ? nmemb : 0u; + } catch (...) { + } + return 0; +} + +/** + @} + @defgroup serdpp_syntax Syntax Utilities + @{ +*/ + +/// @copydoc SerdSyntax +enum class Syntax { + empty = SERD_SYNTAX_EMPTY, ///< @copydoc SERD_SYNTAX_EMPTY + Turtle = SERD_TURTLE, ///< @copydoc SERD_TURTLE + NTriples = SERD_NTRIPLES, ///< @copydoc SERD_NTRIPLES + NQuads = SERD_NQUADS, ///< @copydoc SERD_NQUADS + TriG = SERD_TRIG ///< @copydoc SERD_TRIG +}; + +/// @copydoc serd_syntax_by_name +inline Syntax +syntax_by_name(StringView name) +{ + return Syntax(serd_syntax_by_name(name.c_str())); +} + +/// @copydoc serd_guess_syntax +inline Syntax +guess_syntax(StringView filename) +{ + return Syntax(serd_guess_syntax(filename.c_str())); +} + +/** + Return whether a syntax can represent multiple graphs. + + @return True for @ref Syntax::NQuads and @ref Syntax::TriG, false otherwise. +*/ +inline bool +syntax_has_graphs(const Syntax syntax) +{ + return serd_syntax_has_graphs(static_cast(syntax)); +} + +/** + @} + @defgroup serdpp_data Data + @{ + @defgroup serdpp_uri URI + @{ +*/ + +/** + Get the unescaped path and hostname from a file URI. + + The returned path and `*hostname` must be freed with serd_free(). + + @param uri A file URI. + @param hostname If non-null, set to the hostname, if present. + @return A filesystem path. +*/ +inline std::string +parse_file_uri(StringView uri, std::string* hostname = nullptr) +{ + char* c_hostname = nullptr; + char* c_path = serd_parse_file_uri(nullptr, uri.data(), &c_hostname); + if (hostname && c_hostname) { + *hostname = c_hostname; + } + + std::string path{c_path}; + serd_free(nullptr, c_hostname); + serd_free(nullptr, c_path); + return path; +} + +/// @copydoc serd_uri_string_has_scheme +inline bool +uri_string_has_scheme(StringView string) +{ + return serd_uri_string_has_scheme(string.c_str()); +} + +/** + A parsed URI. + + This directly refers to slices in other strings, it does not own any memory + itself. Thus, URIs can be parsed and/or resolved against a base URI + in-place without allocating memory. +*/ +class URI +{ +public: + /** + Component of a URI. + + Note that there is a distinction between a component being non-present + and present but empty. For example, "file:///path" has an empty + authority, while "file:/path" has no authority. A non-present component + has its `data()` pointer set to null, while an empty component has a + data pointer, but length zero. + */ + using Component = StringView; + + /// Construct a URI by parsing a URI string + explicit URI(StringView str) + : _uri{serd_parse_uri(str.data())} + {} + + /// Construct a URI from a C URI view + explicit URI(const SerdURIView& uri) + : _uri{uri} + {} + + /// Return the scheme of this URI + Component scheme() const { return make_component(_uri.scheme); } + + /// Return the authority of this URI + Component authority() const { return make_component(_uri.authority); } + + /// Return the path prefix of this URI, which is set if it has been resolved + Component path_prefix() const { return make_component(_uri.path_prefix); } + + /// Return the path (suffix) of this URI + Component path() const { return make_component(_uri.path); } + + /// Return the query + Component query() const { return make_component(_uri.query); } + + /// Return the fragment of this URI + Component fragment() const { return make_component(_uri.fragment); } + + /// Return this URI resolved against `base` + URI resolve(const URI& base) const + { + return URI{serd_resolve_uri(_uri, base._uri)}; + } + + /// Return URI as a string + std::string string() const + { + std::ostringstream ss; + + serd_write_uri(_uri, stream_write, &ss); + return ss.str(); + } + + /// Return this URI as a string relative to `base` + std::string relative_string(const URI& base) const + { + std::ostringstream ss; + + const SerdURIView rel = serd_relative_uri(_uri, base._uri); + + serd_write_uri(rel, stream_write, &ss); + return ss.str(); + } + + /** + Return this URI as a string relative to `base` but constrained to `root`. + + The returned URI string is relative iff this URI is a child of both `base` + and `root`. The `root` must be a prefix of `base` and can be used keep + up-references ("../") within a certain namespace. + */ + std::string relative_string(const URI& base, const URI& root) const + { + if (serd_uri_is_within(_uri, root._uri)) { + return relative_string(base); + } + + return string(); + } + + /// Return a pointer to the underlying C object + const SerdURIView* cobj() const { return &_uri; } + +private: + static Component make_component(const SerdStringView slice) + { + return slice.buf ? Component{slice.buf, slice.len} : Component{}; + } + + SerdURIView _uri; +}; + +inline std::ostream& +operator<<(std::ostream& os, const URI& uri) +{ + serd_write_uri(*uri.cobj(), stream_write, &os); + return os; +} + +/** + @} + @defgroup serdpp_node Node + @{ +*/ + +/// @copydoc SerdNodeType +enum class NodeType { + literal = SERD_LITERAL, ///< @copydoc SERD_LITERAL + URI = SERD_URI, ///< @copydoc SERD_URI + blank = SERD_BLANK, ///< @copydoc SERD_BLANK + variable = SERD_VARIABLE, ///< @copydoc SERD_VARIABLE +}; + +/// @copydoc SerdNodeFlag +enum class NodeFlag { + is_long = SERD_IS_LONG, ///< @copydoc SERD_IS_LONG + has_datatype = SERD_HAS_DATATYPE, ///< @copydoc SERD_HAS_DATATYPE + has_language = SERD_HAS_LANGUAGE, ///< @copydoc SERD_HAS_LANGUAGE +}; + +/// Bitwise OR of NodeFlag values +using NodeFlags = Flags; + +template +using NodeHandle = detail::StaticAllocatedCopyable; + +// template +// class NodeWrapper; + +/// A view of an immutable node +// using NodeView = NodeWrapper; +class NodeView; + +/// Common base class for any wrapped node +template +class NodeWrapper : public NodeHandle +{ +public: + template + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + NodeWrapper(const NodeWrapper& node) + : NodeHandle{node} + {} + + /// @copydoc serd_node_type + NodeType type() const { return NodeType(serd_node_type(this->cobj())); } + + /// @copydoc serd_node_string + const char* c_str() const { return serd_node_string(this->cobj()); } + + /// @copydoc serd_node_string + StringView str() const { return StringView{c_str(), length()}; } + + /// @copydoc serd_node_length + size_t size() const { return serd_node_length(this->cobj()); } + + /// @copydoc serd_node_length + size_t length() const { return serd_node_length(this->cobj()); } + + /// @copydoc serd_node_datatype + Optional datatype() const; + + /// @copydoc serd_node_language + Optional language() const; + + /// @copydoc serd_node_string_view + StringView string_view() const { return StringView{c_str(), length()}; } + + /// @copydoc serd_node_uri_view + SerdURIView uri_view() const { return serd_node_uri_view(this->cobj()); } + + /// Returns a newly allocated copy of the node's string + explicit operator std::string() const + { + return std::string{c_str(), length()}; + } + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + explicit operator StringView() const { return StringView(c_str(), length()); } + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + explicit operator SerdStringView() const + { + return SerdStringView{c_str(), length()}; + } + + /// Return a pointer to the first character in the node's string + const char* begin() const { return c_str(); } + + /// Return a pointer to the null terminator at the end of the node's string + const char* end() const { return c_str() + length(); } + + /// Return true if the node's string is empty + bool empty() const { return length() == 0; } + +protected: + explicit NodeWrapper(CObj* const ptr) + : NodeHandle{ptr} + {} +}; + +/// A non-owning constant view of some other node +class NodeView : public NodeWrapper +{ +public: + /// Create a view of a C node pointer + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + NodeView(const SerdNode* const ptr) + : NodeWrapper{ptr} + {} + + /// Create a view of some other node + template + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + NodeView(const NodeWrapper& node) + : NodeWrapper{node} + {} +}; + +/// @copydoc serd_node_datatype +template +inline Optional +NodeWrapper::datatype() const +{ + return NodeView{serd_node_datatype(this->cobj())}; +} + +/// @copydoc serd_node_language +template +inline Optional +NodeWrapper::language() const +{ + return NodeView{serd_node_language(this->cobj())}; +} + +/** + Compare two nodes. + + Nodes are ordered first by type, then by string value, then by language or + datatype, if present. +*/ +inline bool +operator<(const NodeView& lhs, const NodeView& rhs) +{ + return serd_node_compare(lhs.cobj(), rhs.cobj()) < 0; +} + +/// An RDF node +class Node : public NodeWrapper +{ +public: + /// Create a node by taking ownership of a C node + explicit Node(SerdNode* const node) + : NodeWrapper{node} + {} + + /// Create a node by copying another node + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Node(const NodeView& node) + : NodeWrapper{node} + {} + + explicit Node(const SerdValue& value) + : NodeWrapper{serd_new_value(nullptr, value)} + {} + + /// Create an xsd:boolean node from a ``bool`` + explicit Node(const bool b) + : NodeWrapper{serd_new_value(nullptr, serd_bool(b))} + {} + + /// Create an xsd:double node from a ``double`` + explicit Node(const double d) + : NodeWrapper{serd_new_value(nullptr, serd_double(d))} + {} + + /// Create an xsd:float node from a ``float`` + explicit Node(const float f) + : NodeWrapper{serd_new_value(nullptr, serd_float(f))} + {} + + /// Create an xsd:long node from a ``int64_t`` + explicit Node(const int64_t i) + : NodeWrapper{serd_new_value(nullptr, serd_long(i))} + {} + + /// Create an xsd:int node from a ``int32_t`` + explicit Node(const int32_t i) + : NodeWrapper{serd_new_value(nullptr, serd_int(i))} + {} + + /// Create an xsd:short node from a ``int16_t`` + explicit Node(const int16_t i) + : NodeWrapper{serd_new_value(nullptr, serd_short(i))} + {} + + /// Create an xsd:byte node from a ``int8_t`` + explicit Node(const int8_t i) + : NodeWrapper{serd_new_value(nullptr, serd_byte(i))} + {} + + /// Create an xsd:unsignedLong node from a ``int64_t`` + explicit Node(const uint64_t i) + : NodeWrapper{serd_new_value(nullptr, serd_ulong(i))} + {} + + /// Create an xsd:unsignedInt node from a ``int32_t`` + explicit Node(const uint32_t i) + : NodeWrapper{serd_new_value(nullptr, serd_uint(i))} + {} + + /// Create an xsd:unsignedShort node from a ``int16_t`` + explicit Node(const uint16_t i) + : NodeWrapper{serd_new_value(nullptr, serd_ushort(i))} + {} + + /// Create an xsd:unsignedByte node from a ``int8_t`` + explicit Node(const uint8_t i) + : NodeWrapper{serd_new_value(nullptr, serd_ubyte(i))} + {} + + Node(const Node& node) = default; + Node& operator=(const Node& node) = default; + + Node(Node&& node) = default; + Node& operator=(Node&& node) = default; + + ~Node() = default; + +private: + friend class Optional; + friend class Caret; + + explicit Node(std::nullptr_t) + : NodeWrapper{nullptr} + {} +}; + +inline std::ostream& +operator<<(std::ostream& os, const NodeView& node) +{ + return os << node.c_str(); +} + +/// Create a new simple "token" node +inline Node +make_token(const NodeType type, StringView str) +{ + return Node{serd_new_token(nullptr, static_cast(type), str)}; +} + +/// Create a new plain literal node with no language from `str` +inline Node +make_string(StringView str) +{ + return Node{serd_new_string(nullptr, str)}; +} + +/// @copydoc serd_new_uri +inline Node +make_uri(StringView uri) +{ + return Node{serd_new_uri(nullptr, uri)}; +} + +/// @copydoc serd_new_parsed_uri +inline Node +make_uri(SerdURIView uri) +{ + return Node{serd_new_parsed_uri(nullptr, uri)}; +} + +/// @copydoc serd_new_parsed_uri +inline Node +make_uri(URI uri) +{ + return Node{serd_new_parsed_uri(nullptr, *uri.cobj())}; +} + +/// Create a new file URI node from a local filesystem path +inline Node +make_file_uri(StringView path) +{ + return Node{serd_new_file_uri(nullptr, path, SERD_EMPTY_STRING())}; +} + +/// Create a new file URI node from a filesystem path on some host +inline Node +make_file_uri(StringView path, StringView hostname) +{ + return Node{serd_new_file_uri(nullptr, path, hostname)}; +} + +/// @copydoc serd_new_literal +inline Node +make_literal(StringView string, NodeFlags flags, StringView meta) +{ + return Node{ + serd_new_literal(nullptr, string, static_cast(flags), meta)}; +} + +/// Create a new blank node from a local name +inline Node +make_blank(StringView str) +{ + return Node{serd_new_token(nullptr, SERD_BLANK, str)}; +} + +/// Create a new plain literal with an optional language tag +inline Node +make_plain_literal(StringView str, StringView lang) +{ + return Node{serd_new_literal(nullptr, str, SERD_HAS_LANGUAGE, lang)}; +} + +/// Create a new typed literal node from `str` +inline Node +make_typed_literal(StringView str, const StringView datatype) +{ + return Node{serd_new_literal(nullptr, str, SERD_HAS_DATATYPE, datatype)}; +} + +template +inline SerdValue +value(const T value); + +template<> +inline SerdValue +value(const bool value) +{ + return serd_bool(value); +} + +template<> +inline SerdValue +value(const double value) +{ + return serd_double(value); +} + +template<> +inline SerdValue +value(const float value) +{ + return serd_float(value); +} + +template<> +inline SerdValue +value(const int64_t value) +{ + return serd_long(value); +} + +template<> +inline SerdValue +value(const int32_t value) +{ + return serd_int(value); +} + +template<> +inline SerdValue +value(const int16_t value) +{ + return serd_short(value); +} + +template<> +inline SerdValue +value(const int8_t value) +{ + return serd_byte(value); +} + +template<> +inline SerdValue +value(const uint64_t value) +{ + return serd_ulong(value); +} + +template<> +inline SerdValue +value(const uint32_t value) +{ + return serd_uint(value); +} + +template<> +inline SerdValue +value(const uint16_t value) +{ + return serd_ushort(value); +} + +template<> +inline SerdValue +value(const uint8_t value) +{ + return serd_ubyte(value); +} + +template +inline Node +make(const T v) +{ + return Node{value(v)}; +} + +/// @copydoc serd_new_decimal +inline Node +make_decimal(double d) +{ + return Node{serd_new_decimal(nullptr, d)}; +} + +/// @copydoc serd_new_integer +inline Node +make_integer(int64_t i) +{ + return Node{serd_new_integer(nullptr, i)}; +} + +/** + Create a new canonical xsd:base64Binary literal. + + This function can be used to make a node out of arbitrary binary data, which + can be decoded using base64_decode(). + + @param buf Raw binary data to encode in node. + @param size Size of `buf` in bytes. +*/ +inline Node +make_base64(const void* buf, size_t size) +{ + return Node{serd_new_base64(nullptr, buf, size)}; +} + +/// Prototype for Node get() templates +template +inline T +get(NodeView node); + +/// @copydoc serd_get_boolean +template<> +inline bool +get(NodeView node) +{ + return serd_get_value_as(node.cobj(), SERD_BOOL, true).data.as_bool; +} + +/// @copydoc serd_get_double +template<> +inline double +get(NodeView node) +{ + return serd_get_value_as(node.cobj(), SERD_DOUBLE, true).data.as_double; +} + +/// @copydoc serd_get_float +template<> +inline float +get(NodeView node) +{ + return serd_get_value_as(node.cobj(), SERD_FLOAT, true).data.as_float; +} + +/// @copydoc serd_get_long +template<> +inline int64_t +get(NodeView node) +{ + return serd_get_value_as(node.cobj(), SERD_LONG, true).data.as_long; +} + +/// @copydoc serd_get_ulong +template<> +inline uint64_t +get(NodeView node) +{ + return serd_get_value_as(node.cobj(), SERD_ULONG, true).data.as_ulong; +} + +/** + @} + @defgroup serdpp_nodes Nodes + @{ +*/ + +// TODO + +/** + @} + @defgroup serdpp_caret Caret + @{ +*/ + +/// Caret handle +template +using CaretHandle = detail::StaticAllocatedCopyable; + +/// Caret wrapper +template +class CaretWrapper : public CaretHandle +{ +public: + explicit CaretWrapper(CObj* caret) + : CaretHandle{caret} + {} + + template + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CaretWrapper(const CaretWrapper& caret) + : CaretHandle{caret.cobj()} + {} + + /// @copydoc serd_caret_name + NodeView name() const { return NodeView(serd_caret_name(this->cobj())); } + + /// @copydoc serd_caret_line + unsigned line() const { return serd_caret_line(this->cobj()); } + + /// @copydoc serd_caret_column + unsigned column() const { return serd_caret_column(this->cobj()); } +}; + +/// Caret view +using CaretView = CaretWrapper; + +/// Extra data managed by mutable (user created) Caret +struct CaretData { + Node name_node; +}; + +/// @copydoc SerdCaret +class Caret + : private CaretData + , public CaretWrapper +{ +public: + /** + Create a new caret. + + @param name The name of the document or stream (usually a file URI) + @param line The line number in the document (1-based) + @param col The column number in the document (1-based) + */ + Caret(const NodeView& name, const unsigned line, const unsigned col) + : CaretData{Node{name}} + , CaretWrapper{serd_caret_new(nullptr, name_node.cobj(), line, col)} + {} + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Caret(const CaretView& caret) + : Caret(caret.name(), caret.line(), caret.column()) + {} + +private: + friend class Optional; + friend class Statement; + + explicit Caret(std::nullptr_t) + : CaretData{Node{nullptr}} + , CaretWrapper{nullptr} + {} +}; + +/** + @} + @defgroup serdpp_statement Statement + @{ +*/ + +/// @copydoc SerdField +enum class Field { + subject = SERD_SUBJECT, ///< @copydoc SERD_SUBJECT + predicate = SERD_PREDICATE, ///< @copydoc SERD_PREDICATE + object = SERD_OBJECT, ///< @copydoc SERD_OBJECT + graph = SERD_GRAPH ///< @copydoc SERD_GRAPH +}; + +template +using StatementHandle = detail::StaticAllocatedCopyable; + +template +class StatementWrapper; + +/// View of a constant statement +using StatementView = StatementWrapper; + +/// Extra data managed by mutable (user created) Statement +struct StatementData { + Node _subject; + Node _predicate; + Node _object; + Optional _graph; + Optional _caret; +}; + +/// Statement wrapper +template +class StatementWrapper : public StatementHandle +{ +public: + explicit StatementWrapper(CObj* statement) + : StatementHandle{statement} + {} + + template + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + StatementWrapper(const StatementWrapper& statement) + : StatementHandle{statement} + {} + + /// @copydoc serd_statement_node + NodeView node(Field field) const + { + return NodeView{ + serd_statement_node(this->cobj(), static_cast(field))}; + } + + /// @copydoc serd_statement_subject + NodeView subject() const + { + return NodeView{serd_statement_subject(this->cobj())}; + } + + /// @copydoc serd_statement_predicate + NodeView predicate() const + { + return NodeView{serd_statement_predicate(this->cobj())}; + } + + /// @copydoc serd_statement_object + NodeView object() const + { + return NodeView{serd_statement_object(this->cobj())}; + } + + /// @copydoc serd_statement_graph + Optional graph() const + { + return NodeView{serd_statement_graph(this->cobj())}; + } + + /// @copydoc serd_statement_caret + Optional caret() const + { + return CaretView{serd_statement_caret(this->cobj())}; + } + + /// @copydoc serd_statement_matches + bool matches(Optional subject, + Optional predicate, + Optional object, + Optional graph = {}) const + { + return serd_statement_matches(this->cobj(), + subject.cobj(), + predicate.cobj(), + object.cobj(), + graph.cobj()); + } + +private: + template + friend class CursorWrapper; + + StatementWrapper() + : StatementHandle{nullptr} + {} +}; + +/// @copydoc SerdStatement +class Statement + : public StatementData + , public StatementWrapper +{ +public: + Statement(const NodeView& s, + const NodeView& p, + const NodeView& o, + const NodeView& g, + Optional caret = {}) + : StatementData{s, p, o, g, caret ? *caret : Optional{}} + , StatementWrapper{serd_statement_new(nullptr, + _subject.cobj(), + _predicate.cobj(), + _object.cobj(), + _graph.cobj(), + _caret.cobj())} + {} + + Statement(const NodeView& s, + const NodeView& p, + const NodeView& o, + Optional caret = {}) + : StatementData{s, p, o, {}, caret ? *caret : Optional{}} + , StatementWrapper{serd_statement_new(nullptr, + _subject.cobj(), + _predicate.cobj(), + _object.cobj(), + nullptr, + _caret.cobj())} + {} + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + Statement(const StatementView& statement) + : StatementData{statement.subject(), + statement.predicate(), + statement.object(), + statement.graph() ? *statement.graph() : Optional{}, + statement.caret() ? *statement.caret() : Optional{}} + , StatementWrapper{statement} + {} +}; + +/** + @} + @} + @defgroup serdpp_world World + @{ +*/ + +/// @copydoc SerdLogLevel +enum class LogLevel { + emergency = SERD_LOG_LEVEL_EMERGENCY, ///< @copydoc SERD_LOG_LEVEL_EMERGENCY + alert = SERD_LOG_LEVEL_ALERT, ///< @copydoc SERD_LOG_LEVEL_ALERT + critical = SERD_LOG_LEVEL_CRITICAL, ///< @copydoc SERD_LOG_LEVEL_CRITICAL + error = SERD_LOG_LEVEL_ERROR, ///< @copydoc SERD_LOG_LEVEL_ERROR + warning = SERD_LOG_LEVEL_WARNING, ///< @copydoc SERD_LOG_LEVEL_WARNING + notice = SERD_LOG_LEVEL_NOTICE, ///< @copydoc SERD_LOG_LEVEL_NOTICE + info = SERD_LOG_LEVEL_INFO, ///< @copydoc SERD_LOG_LEVEL_INFO + debug = SERD_LOG_LEVEL_DEBUG ///< @copydoc SERD_LOG_LEVEL_DEBUG +}; + +/// Extended fields for a log message +using LogFields = std::map; + +/// User-provided callback function for handling a log message +using LogFunc = std::function; + +/// @copydoc SerdWorld +class World : public detail::StaticWrapper +{ +public: + World() + : StaticWrapper{serd_world_new(nullptr)} + {} + + NodeView get_blank() { return NodeView(serd_world_get_blank(cobj())); } + + void set_message_func(LogFunc log_func) + { + _log_func = std::move(log_func); + serd_set_log_func(cobj(), s_log_func, this); + } + + SERD_LOG_FUNC(4, 5) + Status log(const LogLevel level, + const LogFields& fields, + const char* const fmt, + ...) + { + va_list args; + va_start(args, fmt); + + std::vector c_fields(fields.size()); + size_t index = 0; + for (const auto& f : fields) { + c_fields[index].key = f.first.c_str(); + c_fields[index].value = f.second.c_str(); + ++index; + } + + const SerdStatus st = serd_vxlogf(cobj(), + static_cast(level), + fields.size(), + c_fields.data(), + fmt, + args); + + va_end(args); + return static_cast(st); + } + +private: + SERD_LOG_FUNC(1, 0) + static std::string format(const char* fmt, va_list args) noexcept + { + va_list args_copy; + va_copy(args_copy, args); + + const auto n_bytes = + static_cast(vsnprintf(nullptr, 0, fmt, args_copy)); + + va_end(args_copy); + +#if __cplusplus >= 201703L + std::string result(n_bytes, '\0'); + vsnprintf(result.data(), n_bytes + 1u, fmt, args); +#else + std::vector str(n_bytes + 1u, '\0'); + vsnprintf(str.data(), n_bytes + 1u, fmt, args); + std::string result(str.data(), size_t(n_bytes)); +#endif + return result; + } + + static SerdStatus s_log_func(void* handle, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const SerdStringView message) noexcept + { + const auto* const self = static_cast(handle); + try { + LogFields cpp_fields; + for (size_t i = 0; i < n_fields; ++i) { + cpp_fields.emplace(fields[i].key, fields[i].value); + } + + return static_cast( + self->_log_func(static_cast(level), + cpp_fields, + std::string(message.buf, message.len))); + } catch (...) { + return SERD_UNKNOWN_ERROR; + } + } + + LogFunc _log_func{}; +}; + +/** + @} + @defgroup serdpp_streaming Data Streaming + @{ +*/ + +/** + @defgroup serdpp_event Events + @{ +*/ + +// TODO + +/// @copydoc SerdStatementFlag +enum class StatementFlag { + empty_S = SERD_EMPTY_S, ///< @copydoc SERD_EMPTY_S + anon_S = SERD_ANON_S, ///< @copydoc SERD_ANON_S + anon_O = SERD_ANON_O, ///< @copydoc SERD_ANON_O + list_S = SERD_LIST_S, ///< @copydoc SERD_LIST_S + list_O = SERD_LIST_O, ///< @copydoc SERD_LIST_O + terse_S = SERD_TERSE_S, ///< @copydoc SERD_TERSE_S + terse_O = SERD_TERSE_O ///< @copydoc SERD_TERSE_O +}; + +/// Bitwise OR of StatementFlag values +using StatementFlags = Flags; + +/// @copydoc SerdEventType +enum class EventType { + base = SERD_BASE, ///< @copydoc SERD_BASE + prefix = SERD_PREFIX, ///< @copydoc SERD_PREFIX + statement = SERD_STATEMENT, ///< @copydoc SERD_STATEMENT + end = SERD_END ///< @copydoc SERD_END +}; + +struct BaseEvent { + NodeView uri; ///< Base URI +}; + +struct PrefixEvent { + NodeView name; ///< Prefix name + NodeView uri; ///< Namespace URI +}; + +struct StatementEvent { + StatementFlags flags; ///< Flags for pretty-printing + StatementView statement; ///< Statement +}; + +struct EndEvent { + NodeView node; ///< Anonymous node that is finished +}; + +class Event +{ +public: + explicit Event(const SerdEvent* const e) + : _event{*e} + {} + + EventType type() const { return static_cast(_event.type); } + + BaseEvent base() const + { + assert(_event.type == SERD_BASE); + return {NodeView{_event.base.uri}}; + } + + PrefixEvent prefix() const + { + assert(_event.type == SERD_PREFIX); + return {NodeView{_event.prefix.name}, NodeView{_event.prefix.uri}}; + } + + StatementEvent statement() const + { + assert(_event.type == SERD_STATEMENT); + return {StatementFlags{_event.statement.flags}, + StatementView{_event.statement.statement}}; + } + + EndEvent end() const + { + assert(_event.type == SERD_END); + return {NodeView{_event.end.node}}; + } + +private: + SerdEvent _event; + + // union { + // BaseEvent base; + // PrefixEvent prefix; + // StatementEvent statement; + // EndEvent end; + // } event; +}; + +/** + @} + @defgroup serdpp_sink Sink + @{ +*/ + +// FIXME: Document +using BaseFunc = std::function; +using PrefixFunc = std::function; +using StatementFunc = std::function; +using EndFunc = std::function; + +/// Common base class for any wrapped sink +template +class SinkWrapper : public detail::StaticWrapper +{ +public: + /// @copydoc serd_sink_write_base + Status base(const NodeView& uri) const + { + return Status(serd_sink_write_base(this->cobj(), uri.cobj())); + } + + /// @copydoc serd_sink_write_prefix + Status prefix(NodeView name, const NodeView& uri) const + { + return Status( + serd_sink_write_prefix(this->cobj(), name.cobj(), uri.cobj())); + } + + /// @copydoc serd_sink_write_statement + Status statement(StatementFlags flags, StatementView statement) const + { + return Status( + serd_sink_write_statement(this->cobj(), flags, statement.cobj())); + } + + /// @copydoc serd_sink_write + Status write(StatementFlags flags, + const NodeView& subject, + const NodeView& predicate, + const NodeView& object, + Optional graph = {}) const + { + return Status(serd_sink_write(this->cobj(), + flags, + subject.cobj(), + predicate.cobj(), + object.cobj(), + graph.cobj())); + } + + /// @copydoc serd_sink_write_end + Status end(const NodeView& node) const + { + return Status(serd_sink_write_end(this->cobj(), node.cobj())); + } + +protected: + explicit SinkWrapper(CSink* const ptr) + : detail::StaticWrapper{ptr} + {} +}; + +/// A non-owning constant view of some other sink +class SinkView final : public SinkWrapper +{ +public: + /// Create a view of a C sink + explicit SinkView(const SerdSink* const ptr) + : SinkWrapper{ptr} + {} + + /// Create a view of some other sink + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + SinkView(const SinkWrapper& sink) + : SinkWrapper{sink.cobj()} + {} +}; + +/// @copydoc SerdSink +class Sink final : public SinkWrapper +{ +public: + explicit Sink(const World& world) + : SinkWrapper{serd_sink_new(world.cobj(), this, s_event, nullptr)} + {} + + explicit Sink(SerdSink* const ptr) + : SinkWrapper{ptr} + {} + + /// Set a function to be called when the base URI changes + void set_base_func(BaseFunc base_func) { _base_func = std::move(base_func); } + + /// Set a function to be called when a namespace prefix changes + void set_prefix_func(PrefixFunc prefix_func) + { + _prefix_func = std::move(prefix_func); + } + + /// Set a function to be called for every statement + void set_statement_func(StatementFunc statement_func) + { + _statement_func = std::move(statement_func); + } + + /// Set a function to be called at the end of an anonymous node + void set_end_func(EndFunc end_func) { _end_func = std::move(end_func); } + +private: + static SerdStatus s_base(void* handle, const SerdNode* uri) noexcept + { + const auto* const sink = static_cast(handle); + return sink->_base_func ? SerdStatus(sink->_base_func(NodeView(uri))) + : SERD_SUCCESS; + } + + static SerdStatus s_prefix(void* handle, + const SerdNode* name, + const SerdNode* uri) noexcept + { + const auto* const sink = static_cast(handle); + return sink->_prefix_func + ? SerdStatus(sink->_prefix_func(NodeView(name), NodeView(uri))) + : SERD_SUCCESS; + } + + static SerdStatus s_statement(void* handle, + SerdStatementFlags flags, + const SerdStatement* statement) noexcept + { + const auto* const sink = static_cast(handle); + return sink->_statement_func + ? SerdStatus(sink->_statement_func(StatementFlags(flags), + StatementView(statement))) + : SERD_SUCCESS; + } + + static SerdStatus s_end(void* handle, const SerdNode* node) noexcept + { + const auto* const sink = static_cast(handle); + return sink->_end_func ? SerdStatus(sink->_end_func(NodeView(node))) + : SERD_SUCCESS; + } + + static SerdStatus s_event(void* handle, const SerdEvent* event) noexcept + { + const auto* const sink = static_cast(handle); + + switch (event->type) { + case SERD_BASE: + return sink->_base_func + ? SerdStatus(sink->_base_func(NodeView(event->base.uri))) + : SERD_SUCCESS; + case SERD_PREFIX: + return sink->_prefix_func + ? SerdStatus(sink->_prefix_func(NodeView(event->prefix.name), + NodeView(event->prefix.uri))) + : SERD_SUCCESS; + case SERD_STATEMENT: + return sink->_statement_func + ? SerdStatus(sink->_statement_func( + StatementFlags(event->statement.flags), + StatementView(event->statement.statement))) + : SERD_SUCCESS; + case SERD_END: + return sink->_end_func + ? SerdStatus(sink->_end_func(NodeView(event->end.node))) + : SERD_SUCCESS; + } + + return SERD_SUCCESS; + } + + BaseFunc _base_func{}; + PrefixFunc _prefix_func{}; + StatementFunc _statement_func{}; + EndFunc _end_func{}; +}; + +/** + @} + @defgroup serdpp_canon Canon + @{ +*/ + +/// @copydoc SerdCanonFlag +enum class CanonFlag { + lax = SERD_CANON_LAX ///< @copydoc SERD_CANON_LAX +}; + +/// @copydoc SerdCanonFlags +using CanonFlags = Flags; + +/// @copydoc serd_canon_new +inline Sink +make_canon(const World& world, SinkView target, const CanonFlags flags) +{ + return Sink{serd_canon_new(world.cobj(), target.cobj(), flags)}; +} + +/** + @} + @defgroup serdpp_filter Filter + @{ +*/ + +/// @copydoc serd_filter_new +inline Sink +make_filter(const World& world, + SinkView target, + Optional subject, + Optional predicate, + Optional object, + Optional graph, + const bool inclusive) +{ + return Sink{serd_filter_new(world.cobj(), + target.cobj(), + subject.cobj(), + predicate.cobj(), + object.cobj(), + graph.cobj(), + inclusive)}; +} + +/** + @} + @} + @defgroup serdpp_env Environment + @{ +*/ + +template +using EnvHandle = + detail::StaticCopyable; + +/// Env wrapper +template +class EnvWrapper : public EnvHandle +{ +public: + /// Return the base URI + NodeView base_uri() const + { + return NodeView(serd_env_base_uri(this->cobj())); + } + + /// Set the base URI + Status set_base_uri(const StringView& uri) + { + return Status(serd_env_set_base_uri(this->cobj(), uri)); + } + + /// Set a namespace prefix + Status set_prefix(StringView name, StringView uri) + { + return Status(serd_env_set_prefix(this->cobj(), name, uri)); + } + + /// Expand `node` into an absolute URI if possible + Optional expand(const NodeView& node) const + { + return Node{serd_env_expand_node(this->cobj(), node.cobj())}; + } + + /// Send all prefixes to `sink` + void write_prefixes(SinkView sink) const + { + serd_env_write_prefixes(this->cobj(), sink.cobj()); + } + +protected: + explicit EnvWrapper(std::unique_ptr ptr) + : EnvHandle{std::move(ptr)} + {} + + explicit EnvWrapper(CObj* const ptr) + : EnvHandle{ptr} + {} +}; + +/// EnvView +using EnvView = EnvWrapper; + +/// @copydoc SerdEnv +class Env : public EnvWrapper +{ +public: + explicit Env(World& world) + : EnvWrapper{serd_env_new(world.cobj(), SERD_EMPTY_STRING())} + {} + + explicit Env(World& world, const NodeView& base) + : EnvWrapper{serd_env_new(world.cobj(), base.string_view())} + {} +}; + +/** + @} + @defgroup serdpp_syntax_io Reading and Writing + @{ + @defgroup serdpp_byte_source Byte Source + @{ +*/ + +/// @copydoc SerdInputStream +class InputStream : public SerdInputStream +{ +public: + explicit InputStream(SerdInputStream stream) + : SerdInputStream{stream} + {} + + InputStream(const InputStream&) = delete; + InputStream& operator=(const InputStream&) = delete; + + InputStream(InputStream&&) = default; + InputStream& operator=(InputStream&&) = default; + + ~InputStream() { serd_close_input(this); } +}; + +static inline size_t +istream_read(void* const buf, + const size_t size, + const size_t nmemb, + void* const stream) noexcept +{ + std::istream& is = *static_cast(stream); + const size_t len = size * nmemb; + + try { + is.read(static_cast(buf), static_cast(len)); + } catch (...) { + return 0u; + } + + return is.fail() ? 0u : len; +} + +static inline int +istream_error(void* const stream) +{ + std::istream& is = *static_cast(stream); + + return !is.good(); +} + +inline InputStream +open_input_stream(std::istream& is) +{ + return InputStream{ + serd_open_input_stream(istream_read, istream_error, nullptr, &is)}; +} + +// InputStream +// open_input_string(StringView string) +// { +// return InputStream{ +// serd_open_input_string(stream(istream_read, istream_error, nullptr, +// &is)}; +// } + +/** + @} + @defgroup serdpp_reader Reader + @{ +*/ + +/// @copydoc SerdReaderFlag +enum class ReaderFlag { + lax = SERD_READ_LAX, ///< @copydoc SERD_READ_LAX + variables = SERD_READ_VARIABLES, ///< @copydoc SERD_READ_VARIABLES + relative = SERD_READ_RELATIVE, ///< @copydoc SERD_READ_RELATIVE + global = SERD_READ_GLOBAL, ///< @copydoc SERD_READ_GLOBAL +}; + +/// @copydoc SerdReaderFlags +using ReaderFlags = Flags; + +/// @copydoc SerdReader +class Reader : public detail::StaticWrapper +{ +public: + Reader(World& world, + const Syntax syntax, + const ReaderFlags flags, + Env& env, + SinkView sink, + size_t stack_size) + : StaticWrapper{serd_reader_new(world.cobj(), + SerdSyntax(syntax), + flags, + env.cobj(), + sink.cobj(), + stack_size)} + {} + + Status start(SerdInputStream& in, + const NodeView& input_name, + const size_t block_size) + { + return Status( + serd_reader_start(cobj(), &in, input_name.cobj(), block_size)); + } + + /// @copydoc serd_reader_read_chunk + Status read_chunk() { return Status(serd_reader_read_chunk(cobj())); } + + /// @copydoc serd_reader_read_document + Status read_document() { return Status(serd_reader_read_document(cobj())); } + + /// @copydoc serd_reader_finish + Status finish() { return Status(serd_reader_finish(cobj())); } + +private: + static inline size_t s_stream_read(void* buf, + size_t size, + size_t nmemb, + void* stream) noexcept + { + assert(size == 1); + (void)size; + + try { + auto* const s = static_cast(stream); + s->read(static_cast(buf), std::streamsize(nmemb)); + if (s->good()) { + return nmemb; + } + } catch (...) { + } + + return 0; + } + + static inline int s_stream_error(void* stream) noexcept + { + try { + auto* const s = static_cast(stream); + return (!(s->good())); + } catch (...) { + } + + return 1; + } +}; + +/** + @} + @defgroup serdpp_byte_sink Byte Sink + @{ +*/ + +/** + Sink function for string output. + + Similar semantics to `SerdWriteFunc` (and in turn `fwrite`), but takes char* + for convenience and may set errno for more informative error reporting than + supported by `SerdStreamErrorFunc`. + + @return Number of elements (bytes) written, which is short on error. +*/ +using WriteFunc = std::function; + +/// @copydoc SerdOutputStream +class OutputStream : public SerdOutputStream +{ +public: + explicit OutputStream(SerdOutputStream stream) + : SerdOutputStream{stream} + {} + + OutputStream(const OutputStream&) = delete; + OutputStream& operator=(const OutputStream&) = delete; + + OutputStream(OutputStream&&) = default; + OutputStream& operator=(OutputStream&&) = default; + + ~OutputStream() { close(); } + + /// @copydoc serd_close_output + Status close() { return static_cast(serd_close_output(this)); } +}; + +static inline size_t +ostream_write(const void* const buf, + const size_t size, + const size_t nmemb, + void* const stream) noexcept +{ + std::ostream& os = *static_cast(stream); + const size_t len = size * nmemb; + + try { + os.write(static_cast(buf), std::streamsize(len)); + } catch (...) { + return 0u; + } + + return os.fail() ? 0u : len; +} + +inline OutputStream +open_output_stream(std::ostream& os) +{ + return OutputStream{ + serd_open_output_stream(ostream_write, nullptr, nullptr, &os)}; +} + +inline OutputStream +open_output_file(const StringView path) +{ + return OutputStream{serd_open_output_file(path.c_str())}; +} + +// /// @copydoc SerdOutputStream +// class OutputStream : public SerdOutputStream +// { +// public: +// /// Create a byte sink that writes to a function in blocks +// OutputStream(WriteFunc write_func, SerdStreamCloseFunc close_func) +// : StaticWrapper{serd_byte_sink_new_function(s_write, +// nullptr, +// this, +// block_size)} +// , _write_func{std::move(write_func)} +// {} + +// OutputStream(StringView filename, const size_t block_size) +// : StaticWrapper{serd_byte_sink_new_filename(filename.c_str(), +// block_size)} +// {} + +// /// Create a byte sink that writes to a function one byte at a time +// explicit OutputStream(WriteFunc write_func) +// : OutputStream{std::move(write_func), 1} +// {} + +// /// Create a byte sink from a standard output stream +// explicit OutputStream(std::ostream& stream) +// : StaticWrapper{serd_byte_sink_new_function(s_write, nullptr, this, 1)} +// , _write_func{[&](const char* str, size_t len) { +// stream.write(str, std::streamsize(len)); +// return stream.good() ? len : size_t(0); +// }} +// {} + +// Status close() { return static_cast(serd_byte_sink_close(cobj())); +// } + +// private: +// static inline size_t s_write(const void* buf, +// size_t size, +// size_t nmemb, +// void* sink) noexcept +// { +// assert(size == 1); +// (void)size; + +// auto* self = static_cast(sink); + +// try { +// return self->_write_func(static_cast(buf), nmemb); +// } catch (...) { +// } + +// return 0; +// } + +// WriteFunc _write_func; +// }; + +/** + @} + @defgroup serdpp_writer Writer + @{ +*/ + +/// @copydoc SerdWriterFlag +enum class WriterFlag { + ascii = SERD_WRITE_ASCII, ///< @copydoc SERD_WRITE_ASCII + expanded = SERD_WRITE_EXPANDED, ///< @copydoc SERD_WRITE_EXPANDED + verbatim = SERD_WRITE_VERBATIM, ///< @copydoc SERD_WRITE_VERBATIM + terse = SERD_WRITE_TERSE, ///< @copydoc SERD_WRITE_TERSE + lax = SERD_WRITE_LAX, ///< @copydoc SERD_WRITE_LAX + rdf_type = SERD_WRITE_RDF_TYPE ///< @copydoc SERD_WRITE_RDF_TYPE +}; + +/// @copydoc SerdWriterFlags +using WriterFlags = Flags; + +/// @copydoc SerdWriter +class Writer : public detail::StaticWrapper +{ +public: + /** + Create a writer that writes syntax to the given byte sink. + + @param world The world that this writer is a part of. + + @param syntax Syntax to write. + + @param flags Flags to control writer behaviour. + + @param env Environment used for expansion and abbreviation. The writer + uses a reference to this, so the environment must outlive the writer. + + @param out Stream where output is written. The writer uses a reference to + this, so the stream must outlive the writer. + + @param block_size Number of bytes to write to the output stream at once. + */ + Writer(World& world, + const Syntax syntax, + const WriterFlags flags, + Env& env, + OutputStream& out, + const size_t block_size = 1u) + : StaticWrapper{serd_writer_new(world.cobj(), + SerdSyntax(syntax), + flags, + env.cobj(), + &out, + block_size)} + {} + + /// Return a sink that can be used to write data + SinkView sink() { return SinkView{serd_writer_sink(cobj())}; } + + /// @copydoc serd_writer_set_root_uri + Status set_root_uri(const StringView uri) + { + return Status(serd_writer_set_root_uri(cobj(), uri)); + } + + /// @copydoc serd_writer_finish + Status finish() { return Status(serd_writer_finish(cobj())); } +}; + +/** + @} + @} + @defgroup serdpp_storage Storage + @{ +*/ + +/** + @defgroup serdpp_iterator Iterator + @{ +*/ + +/// Empty class for end sentinels to provide an iterator-like interface +class EndCursor +{}; + +template +using CursorHandle = detail:: + DynamicCopyable; + +template +class CursorWrapper : public CursorHandle +{ +public: + CursorWrapper(CursorWrapper&&) noexcept = default; + CursorWrapper(const CursorWrapper&) = default; + + CursorWrapper& operator=(CursorWrapper&&) noexcept = default; + CursorWrapper& operator=(const CursorWrapper&) = default; + + ~CursorWrapper() = default; + + const StatementView& operator*() const + { + _statement = StatementView{serd_cursor_get(this->cobj())}; + return _statement; + } + + const StatementView* operator->() const + { + _statement = StatementView{serd_cursor_get(this->cobj())}; + return &_statement; + } + + CursorWrapper& operator++() + { + serd_cursor_advance(this->cobj()); + return *this; + } + +protected: + CursorWrapper(CObj* ptr, detail::Ownership ownership) + : CursorHandle{{ptr, ownership}} + {} + + mutable StatementView _statement{}; +}; + +/// Cursor view +class CursorView : public CursorWrapper +{ +public: + // explicit CursorView(const SerdCursor* const ptr) + // : CursorWrapper{ptr, detail::Ownership::view} + // {} + + // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions) + CursorView(const CursorWrapper& iter) + : CursorWrapper{iter.cobj(), detail::Ownership::view} + {} +}; + +// using CursorView = CursorWrapper; + +/// @copydoc SerdCursor +class Cursor : public CursorWrapper +{ +public: + explicit Cursor(CursorView iter) + : CursorWrapper{serd_cursor_copy(nullptr, iter.cobj()), + detail::Ownership::owned} + {} + + Cursor(SerdCursor* const ptr, const detail::Ownership ownership) + : CursorWrapper{ptr, ownership} + {} + + Cursor(Cursor&&) = default; + Cursor(const Cursor&) = default; + + Cursor& operator=(Cursor&&) = default; + Cursor& operator=(const Cursor&) = default; + + ~Cursor() = default; + + // Cursor& operator++() + // { + // serd_cursor_advance(this->cobj()); + // return *this; + // } +}; + +/** + @} + @defgroup serdpp_range Range + @{ +*/ + +/// @copydoc SerdDescribeFlag +enum class DescribeFlag { + no_type_first = SERD_NO_TYPE_FIRST ///< @copydoc SERD_NO_TYPE_FIRST +}; + +/// Bitwise OR of DescribeFlag values +using DescribeFlags = Flags; + +/** + @} + @defgroup serdpp_model Model + @{ +*/ + +/// @copydoc SerdModelFlag +enum class ModelFlag { + store_graphs = SERD_STORE_GRAPHS, ///< @copydoc SERD_STORE_GRAPHS + store_carets = SERD_STORE_CARETS ///< @copydoc SERD_STORE_CARETS +}; + +/// Bitwise OR of ModelFlag values +using ModelFlags = Flags; + +/// @copydoc SerdStatementOrder +enum class StatementOrder { + SPO, ///< @copydoc SERD_ORDER_SPO + SOP, ///< @copydoc SERD_ORDER_SOP + OPS, ///< @copydoc SERD_ORDER_OPS + OSP, ///< @copydoc SERD_ORDER_OSP + PSO, ///< @copydoc SERD_ORDER_PSO + POS, ///< @copydoc SERD_ORDER_POS + GSPO, ///< @copydoc SERD_ORDER_GSPO + GSOP, ///< @copydoc SERD_ORDER_GSOP + GOPS, ///< @copydoc SERD_ORDER_GOPS + GOSP, ///< @copydoc SERD_ORDER_GOSP + GPSO, ///< @copydoc SERD_ORDER_GPSO + GPOS ///< @copydoc SERD_ORDER_GPOS +}; + +using ModelHandle = detail::StaticCopyable; + +/// @copydoc SerdModel +class Model : public ModelHandle +{ +public: + using value_type = Statement; + using iterator = Cursor; + using const_iterator = Cursor; + + Model(World& world, + const StatementOrder default_order, + const ModelFlags flags) + : ModelHandle{serd_model_new(world.cobj(), + static_cast(default_order), + flags)} + {} + + /// @copydoc serd_model_size + size_t size() const { return serd_model_size(cobj()); } + + /// @copydoc serd_model_empty + bool empty() const { return serd_model_empty(cobj()); } + + /// @copydoc serd_model_add_index + Status add_index(const StatementOrder order) + { + return static_cast( + serd_model_add_index(cobj(), static_cast(order))); + } + + /// @copydoc serd_model_drop_index + Status drop_index(const StatementOrder order) + { + return static_cast( + serd_model_drop_index(cobj(), static_cast(order))); + } + + /// @copydoc serd_model_insert + Status insert(StatementView s) + { + return static_cast(serd_model_insert(cobj(), s.cobj())); + } + + /// @copydoc serd_model_add + Status insert(const NodeView& s, + const NodeView& p, + const NodeView& o, + Optional g = {}) + { + return static_cast( + serd_model_add(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj())); + } + + /// @copydoc serd_model_insert_statements + Status insert_statements(Cursor&& range) + { + return static_cast( + serd_model_insert_statements(cobj(), range.cobj())); + } + + /** + Remove a statement from a model via an iterator. + + Calling this function invalidates all iterators on `model` except `iter`. + + @param iter Iterator to the element to erase. + + @returns An iterator to the statement following the erased statement, + or the end iterator if the statement was the last or an error occurred. + */ + Cursor erase(Cursor iter) + { + if (!serd_model_erase(cobj(), iter.cobj())) { + return iter; + } + + return iter; + } + + Cursor erase(const CursorView& iter) + { + Cursor iter_copy{iter}; + + if (!serd_model_erase(cobj(), iter_copy.cobj())) { + return iter_copy; + } + + return iter_copy; + } + + /** + Remove a range from a model. + + Calling this function invalidates all iterators on `model` except `iter`. + + @param range Range to erase. + */ + Status erase_statements(Cursor range) + { + return static_cast( + serd_model_erase_statements(cobj(), range.cobj())); + } + + class Range + { + public: + Range(Cursor begin, Cursor end) + : _begin(std::move(begin)) + , _end(std::move(end)) + {} + + const Cursor& begin() const { return _begin; } + Cursor& begin() { return _begin; } + const Cursor& end() const { return _end; } + + private: + Cursor _begin; + Cursor _end; + }; + + /// @copydoc serd_model_find + Range find(Optional s, + Optional p, + Optional o, + Optional g = {}) const + { + return Range{ + Cursor{serd_model_find(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj()), + detail::Ownership::owned}, + end()}; + } + + /// @copydoc serd_model_get + Optional get(Optional s, + Optional p, + Optional o, + Optional g = {}) const + { + return NodeView( + serd_model_get(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj())); + } + + /// @copydoc serd_model_get_statement + Optional get_statement(Optional s, + Optional p, + Optional o, + Optional g = {}) const + { + return StatementView( + serd_model_get_statement(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj())); + } + + /// @copydoc serd_model_ask + bool ask(Optional s, + Optional p, + Optional o, + Optional g = {}) const + { + return serd_model_ask(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj()); + } + + /// @copydoc serd_model_count + size_t count(Optional s, + Optional p, + Optional o, + Optional g = {}) const + { + return serd_model_count(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj()); + } + + /// @copydoc serd_model_begin_ordered + Cursor begin_ordered(StatementOrder order) const + { + return Cursor( + serd_model_begin_ordered(cobj(), static_cast(order)), + detail::Ownership::owned); + } + + /// @copydoc serd_model_begin + iterator begin() const + { + return iterator(serd_model_begin(cobj()), detail::Ownership::owned); + } + + /// @copydoc serd_model_end + iterator end() const + { + // TODO: cache? + return iterator(serd_cursor_copy(nullptr, serd_model_end(cobj())), + detail::Ownership::owned); + // return iterator(serd_model_end(cobj()), detail::Ownership::view); + } + +private: + friend class Optional; + + explicit Model(std::nullptr_t) + : StaticCopyable{nullptr} + {} +}; + +/** + @} + @defgroup serdpp_inserter Inserter + @{ +*/ + +/** + Create an inserter that inserts statements into a model. + + @param model The model to insert received statements into. +*/ +inline Sink +make_inserter(Model& model) +{ + return Sink{serd_inserter_new(model.cobj(), nullptr)}; +} + +/** + Create an inserter that inserts statements into a specific graph in a model. + + @param model The model to insert received statements into. + + @param default_graph The default graph to set for any statements that have + no graph. This allows, for example, loading a Turtle document into an + isolated graph in the model. +*/ +inline Sink +make_inserter(Model& model, NodeView default_graph) +{ + return Sink{serd_inserter_new(model.cobj(), default_graph.cobj())}; +} + +/** + @} + @defgroup serdpp_validator Validator + @{ +*/ + +/// @copydoc SerdValidatorCheck +enum class ValidatorCheck : uint64_t { + /// @copydoc SERD_CHECK_NOTHING + nothing, + + /// @copydoc SERD_CHECK_ALL_VALUES_FROM + all_values_from = SERD_CHECK_ALL_VALUES_FROM, + + /// @copydoc SERD_CHECK_ANY_URI + any_uri = SERD_CHECK_ANY_URI, + + /// @copydoc SERD_CHECK_CARDINALITY_EQUAL + cardinality_equal = SERD_CHECK_CARDINALITY_EQUAL, + + /// @copydoc SERD_CHECK_CARDINALITY_MAX + cardinality_max = SERD_CHECK_CARDINALITY_MAX, + + /// @copydoc SERD_CHECK_CARDINALITY_MIN + cardinality_min = SERD_CHECK_CARDINALITY_MIN, + + /// @copydoc SERD_CHECK_CLASS_CYCLE + class_cycle = SERD_CHECK_CLASS_CYCLE, + + /// @copydoc SERD_CHECK_CLASS_LABEL + class_label = SERD_CHECK_CLASS_LABEL, + + /// @copydoc SERD_CHECK_DATATYPE_PROPERTY + datatype_property = SERD_CHECK_DATATYPE_PROPERTY, + + /// @copydoc SERD_CHECK_DATATYPE_TYPE + datatype_type = SERD_CHECK_DATATYPE_TYPE, + + /// @copydoc SERD_CHECK_DEPRECATED_CLASS + deprecated_class = SERD_CHECK_DEPRECATED_CLASS, + + /// @copydoc SERD_CHECK_DEPRECATED_PROPERTY + deprecated_property = SERD_CHECK_DEPRECATED_PROPERTY, + + /// @copydoc SERD_CHECK_FUNCTIONAL_PROPERTY + functional_property = SERD_CHECK_FUNCTIONAL_PROPERTY, + + /// @copydoc SERD_CHECK_INSTANCE_LITERAL + instance_literal = SERD_CHECK_INSTANCE_LITERAL, + + /// @copydoc SERD_CHECK_INSTANCE_TYPE + instance_type = SERD_CHECK_INSTANCE_TYPE, + + /// @copydoc SERD_CHECK_INVERSE_FUNCTIONAL_PROPERTY + inverse_functional_property = SERD_CHECK_INVERSE_FUNCTIONAL_PROPERTY, + + /// @copydoc SERD_CHECK_LITERAL_INSTANCE + literal_instance = SERD_CHECK_LITERAL_INSTANCE, + + /// @copydoc SERD_CHECK_LITERAL_MAX_EXCLUSIVE + literal_max_exclusive = SERD_CHECK_LITERAL_MAX_EXCLUSIVE, + + /// @copydoc SERD_CHECK_LITERAL_MAX_INCLUSIVE + literal_max_inclusive = SERD_CHECK_LITERAL_MAX_INCLUSIVE, + + /// @copydoc SERD_CHECK_LITERAL_MIN_EXCLUSIVE + literal_min_exclusive = SERD_CHECK_LITERAL_MIN_EXCLUSIVE, + + /// @copydoc SERD_CHECK_LITERAL_MIN_INCLUSIVE + literal_min_inclusive = SERD_CHECK_LITERAL_MIN_INCLUSIVE, + + /// @copydoc SERD_CHECK_LITERAL_PATTERN + literal_pattern = SERD_CHECK_LITERAL_PATTERN, + + /// @copydoc SERD_CHECK_LITERAL_RESTRICTION + literal_restriction = SERD_CHECK_LITERAL_RESTRICTION, + + /// @copydoc SERD_CHECK_LITERAL_VALUE + literal_value = SERD_CHECK_LITERAL_VALUE, + + /// @copydoc SERD_CHECK_OBJECT_PROPERTY + object_property = SERD_CHECK_OBJECT_PROPERTY, + + /// @copydoc SERD_CHECK_PLAIN_LITERAL_DATATYPE + plain_literal_datatype = SERD_CHECK_PLAIN_LITERAL_DATATYPE, + + /// @copydoc SERD_CHECK_PREDICATE_TYPE + predicate_type = SERD_CHECK_PREDICATE_TYPE, + + /// @copydoc SERD_CHECK_PROPERTY_CYCLE + property_cycle = SERD_CHECK_PROPERTY_CYCLE, + + /// @copydoc SERD_CHECK_PROPERTY_DOMAIN + property_domain = SERD_CHECK_PROPERTY_DOMAIN, + + /// @copydoc SERD_CHECK_PROPERTY_LABEL + property_label = SERD_CHECK_PROPERTY_LABEL, + + /// @copydoc SERD_CHECK_PROPERTY_RANGE + property_range = SERD_CHECK_PROPERTY_RANGE, + + /// @copydoc SERD_CHECK_SOME_VALUES_FROM + some_values_from = SERD_CHECK_SOME_VALUES_FROM, +}; + +/// @copydoc SerdValidator +class Validator + : public detail::StaticWrapper +{ +public: + /// Create a new validator with no checks enabled + explicit Validator(World& world) + : StaticWrapper{serd_validator_new(world.cobj())} + {} + + /// @copydoc serd_validator_enable_checks + Status enable_checks(StringView pattern) + { + return static_cast( + serd_validator_enable_checks(cobj(), pattern.c_str())); + } + + /// @copydoc serd_validator_disable_checks + Status disable_checks(StringView pattern) + { + return static_cast( + serd_validator_disable_checks(cobj(), pattern.c_str())); + } + + /** + Validate all statements in a specific graph in a model. + + This performs validation based on the XSD, RDF, RDFS, and OWL + vocabularies. All necessary data, including those vocabularies and any + property/class definitions that use them, are assumed to be in the model. + + Validation errors are reported to the world's error sink. + + @param model The model to validate. + + @return @ref Status::success if no errors are found, or @ref + Status::bad_data if validation checks failed. + */ + Status validate(const Model& model, NodeView graph, EnvView env) + { + return static_cast( + serd_validate(cobj(), model.cobj(), graph.cobj(), env.cobj())); + } + + /** + Validate all statements in a specific graph in a model. + + This performs the same validation as the other overload with a graph, but + does not take an env. URIs in the log output will not be abbreviated. + */ + Status validate(const Model& model, NodeView graph) + { + return static_cast( + serd_validate(cobj(), model.cobj(), graph.cobj(), nullptr)); + } + + /** + Validate all statements in a model. + + This overload validates all statements in any graph in the model. + */ + Status validate(const Model& model) + { + return static_cast( + serd_validate(cobj(), model.cobj(), nullptr, nullptr)); + } +}; + +/** + @} + @} + @} +*/ + +} // namespace serd + +#endif // SERD_SERD_HPP diff --git a/bindings/cpp/meson.build b/bindings/cpp/meson.build new file mode 100644 index 00000000..200bb13e --- /dev/null +++ b/bindings/cpp/meson.build @@ -0,0 +1,77 @@ +versioned_cpp_name = 'serdpp' + version_suffix + +# Set ultra strict warnings for developers, if requested +cpp_suppressions = [] +if get_option('strict') + if cpp.get_id() == 'clang' + cpp_suppressions += [ + '-Wno-documentation-unknown-command', + '-Wno-format-nonliteral', + '-Wno-nullability-extension', + '-Wno-padded', + ] + elif cpp.get_id() == 'gcc' + cpp_suppressions += [ + '-Wno-abi-tag', + '-Wno-float-equal', + '-Wno-multiple-inheritance', + '-Wno-padded', + '-Wno-switch-default', + ] + elif cpp.get_id() == 'msvc' + cpp_suppressions += [ + '/wd4355', # 'this' used in base member initializer list + '/wd4571', # structured exceptions are no longer caught + '/wd4623', # default constructor implicitly deleted + '/wd4625', # copy constructor implicitly deleted + '/wd4626', # assignment operator implicitly deleted + '/wd4710', # function not inlined + '/wd4868', # may not enforce left-to-right evaluation order + '/wd5026', # move constructor implicitly deleted + '/wd5027', # move assignment operator implicitly deleted + ] + endif +endif + +exess_cpp_args = cpp.get_supported_arguments(cpp_suppressions) + +cpp_headers = [ + 'include/serd/Flags.hpp', + 'include/serd/Optional.hpp', + 'include/serd/StringView.hpp', + 'include/serd/serd.hpp', +] + +cpp_detail_headers = [ + 'include/serd/detail/Copyable.hpp', + 'include/serd/detail/Wrapper.hpp', +] + +cpp_header_files = files(cpp_headers) +cpp_detail_header_files = files(cpp_detail_headers) + +serdpp_dep = declare_dependency( + include_directories: include_directories(['include']), + link_with: libserd) + +pkg.generate( + description: 'C++ bindings for serd', + filebase: versioned_cpp_name, + name: 'Serdpp', + subdirs: [versioned_cpp_name], + version: meson.project_version()) + +# Install headers to a versioned include directory +install_headers(cpp_header_files, subdir: versioned_cpp_name / 'serd') +install_headers(cpp_detail_header_files, + subdir: versioned_cpp_name / 'serd/detail') + +cpp_test_args = prog_args + cpp.get_supported_arguments(['-Wno-float-equal']) + +test('serd.hpp', + executable('test_serd_hpp', + 'test/test_serd_hpp.cpp', + include_directories: include_directories(['include']), + cpp_args: exess_cpp_args + cpp_test_args, + dependencies: [serd_dep, serdpp_dep]), + suite: ['bindings', 'cpp']) diff --git a/bindings/cpp/test/.clang-tidy b/bindings/cpp/test/.clang-tidy new file mode 100644 index 00000000..e71d5f54 --- /dev/null +++ b/bindings/cpp/test/.clang-tidy @@ -0,0 +1,30 @@ +Checks: > + *, + -*-magic-numbers, + -*-non-private-member-variables-in-classes, + -*-uppercase-literal-suffix, + -altera-struct-pack-align, + -cert-dcl50-cpp, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-vararg, + -fuchsia-default-arguments-calls, + -fuchsia-default-arguments-declarations, + -fuchsia-multiple-inheritance, + -fuchsia-overloaded-operator, + -google-readability-todo, + -google-runtime-references, + -hicpp-named-parameter, + -hicpp-no-array-decay, + -hicpp-signed-bitwise, + -hicpp-vararg, + -llvmlibc-*, + -misc-no-recursion, + -modernize-use-trailing-return-type, + -performance-unnecessary-value-param, + -readability-function-cognitive-complexity, + -readability-implicit-bool-conversion, + -readability-named-parameter, +WarningsAsErrors: '*' +HeaderFilterRegex: '*.hpp' +FormatStyle: file diff --git a/bindings/cpp/test/test_serd_hpp.cpp b/bindings/cpp/test/test_serd_hpp.cpp new file mode 100644 index 00000000..3f919b31 --- /dev/null +++ b/bindings/cpp/test/test_serd_hpp.cpp @@ -0,0 +1,823 @@ +/* + Copyright 2018-2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#undef NDEBUG + +#include "serd/serd.hpp" + +#include +#include +#include +#include +#include +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#include + +template +static int +test_move_only(T&& obj) +{ + static_assert(!std::is_copy_constructible::value, ""); + static_assert(!std::is_copy_assignable::value, ""); + + const auto* const ptr = obj.cobj(); + + // Move construct + T moved{std::forward(obj)}; + assert(moved.cobj() == ptr); + assert(!obj.cobj()); // NOLINT + + // Move assign + obj = std::move(moved); + assert(obj.cobj() == ptr); + assert(!moved.cobj()); // NOLINT + + return 0; +} + +template +static int +test_copy_move(const T& obj) +{ + T copy{obj}; + assert(copy == obj); + + T moved{std::move(copy)}; + assert(moved == obj); + assert(copy != obj); // NOLINT + + T copy_assigned{obj}; + copy_assigned = obj; + assert(copy_assigned == obj); + + T move_assigned{obj}; + move_assigned = std::move(copy_assigned); + assert(move_assigned == obj); + assert(copy_assigned != obj); // NOLINT + + return 0; +} + +static int +test_operators() +{ + int st = 0; + + serd::World world; + + serd::Model model( + world, serd::StatementOrder::SPO, serd::ModelFlag::store_carets); + + model.insert(serd::Statement{serd::make_uri("http://example.org/s"), + serd::make_uri("http://example.org/p"), + serd::make_uri("http://example.org/o"), + serd::Caret{serd::make_uri("test.ttl"), 1, 1}}); + + serd::Sink sink{world}; + serd::Env env{world}; + + std::ostringstream stream; + + // st |= test_move_only(serd::World{}); + st |= test_copy_move(serd::Statement{*model.begin()}); + st |= + test_copy_move(serd::Caret{serd::make_uri("http://example.org/doc"), 1, 2}); + st |= test_copy_move(model.begin()->caret()); + st |= test_copy_move(serd::Env{world}); + st |= test_move_only( + serd::Reader{world, serd::Syntax::Turtle, {}, env, sink, 4096}); + st |= test_copy_move(model.begin()); + // st |= test_copy_move(model.all()); + // Sink + st |= test_copy_move(model); + // st |= test_move_only(serd::Inserter{model, env}); + // st |= test_move_only(serd::Sink{}); + + st |= test_copy_move(serd::Env{world}); + + return st; +} + +template +static int +test_optional(const Value& value, const Value& other) +{ + test_copy_move(value); + + // Truthiness + assert(!serd::Optional()); + // assert(!serd::Optional(nullptr)); + assert(serd::Optional(value)); + + // Comparison and general sanity + serd::Optional optional{value}; + assert(optional); + assert(optional == value); + assert(optional != other); + assert(*optional == value); + assert(optional.cobj() != value.cobj()); // non-const, must be a copy + + // Reset + optional.reset(); + assert(!optional); + assert(!optional.cobj()); + + // Copying and moving + Value nonconst = value; + const auto* c_ptr = nonconst.cobj(); + + optional = nonconst; + serd::Optional copied{optional}; + assert(copied == nonconst); + assert(copied.cobj() != c_ptr); + + optional = std::move(nonconst); + serd::Optional moved{std::move(optional)}; + assert(moved.cobj() == c_ptr); + assert(!optional); // NOLINT + + serd::Optional copy_assigned; + copy_assigned = optional; + assert(copy_assigned == optional); + assert(copy_assigned.cobj() != c_ptr); + + serd::Optional move_assigned; + move_assigned = std::move(moved); + assert(move_assigned.cobj() == c_ptr); + assert(!optional); + + serd::Optional nullopt_assigned; + nullopt_assigned = {}; + assert(!nullopt_assigned.cobj()); + + return 0; +} + +static int +test_optional() +{ + test_optional(serd::make_string("value"), serd::make_string("other")); + + { + serd::World world; + + serd::Model value{world, serd::StatementOrder::SPO, {}}; + value.insert(serd::make_uri("http://example.org/s1"), + serd::make_uri("http://example.org/p1"), + serd::make_uri("http://example.org/o1")); + + serd::Model other(world, serd::StatementOrder::SPO, {}); + value.insert(serd::make_uri("http://example.org/s2"), + serd::make_uri("http://example.org/p2"), + serd::make_uri("http://example.org/o2")); + + test_optional(value, other); + } + + return 0; +} + +static int +test_node(const serd::Node& node) +{ + test_copy_move(node); + + if (node.datatype()) { + return test_node(*node.datatype()); + } + + if (node.language()) { + return test_node(*node.language()); + } + + return 0; +} + +static int +test_string() +{ + assert(!strcmp(serd::strerror(serd::Status::unknown_error), "Unknown error")); + + return 0; +} + +static int +test_stringview() +{ + const serd::StringView hello{"hello"}; + + assert(hello.front() == 'h'); + assert(hello.back() == 'o'); + + assert(*hello.begin() == 'h'); + assert(*hello.end() == '\0'); + assert(*(hello.end() - 1) == 'o'); + assert(*hello.cbegin() == 'h'); + assert(*hello.cend() == '\0'); + assert(*(hello.cend() - 1) == 'o'); + + assert(hello[0] == 'h'); + assert(hello[1] == 'e'); + assert(hello.at(0) == 'h'); + assert(hello.at(1) == 'e'); + assert(hello.substr(2) == "llo"); + + assert(hello.str() == "hello"); + assert(std::string(hello) == "hello"); + assert(!strcmp(static_cast(hello), "hello")); + + std::stringstream ss; + ss << hello; + assert(ss.str() == "hello"); + + bool threw = false; + try { + hello.at(6); + } catch (const std::out_of_range&) { + threw = true; + } + assert(threw); + + try { + hello.substr(6); + } catch (const std::out_of_range&) { + threw = true; + } + assert(threw); + + assert(serd::StringView{} == serd::StringView{}); // NOLINT + assert(hello == "hello"); + assert(hello == std::string{"hello"}); + assert(hello == serd::StringView{"hello"}); + + assert(hello != "world"); + assert(hello != std::string{"world"}); + assert(hello != serd::StringView{"world"}); + + assert(serd::StringView{"a"}.compare(serd::StringView{"ab"}) < 0); + assert(serd::StringView{"ab"}.compare(serd::StringView{"a"}) > 0); + assert(serd::StringView{"ab"}.compare(serd::StringView{"ab"}) == 0); + + assert(hello < serd::StringView{"world"}); + assert(hello < std::string{"world"}); + assert(hello < "world"); + + assert(!(hello < serd::StringView{"apple"})); + assert(!(hello < std::string{"apple"})); + assert(!(hello < "apple")); + + return 0; +} + +// FIXME +#if 0 +static int +test_base64() +{ + const std::vector data{1, 1, 2, 3, 5}; + + const auto encoded = serd::base64_encode(data); + const auto decoded = serd::base64_decode(encoded); + + assert(decoded == data); + + return 0; +} +#endif + +static int +test_syntax() +{ + assert(serd::syntax_by_name("Turtle") == serd::Syntax::Turtle); + assert(serd::guess_syntax("foo.trig") == serd::Syntax::TriG); + assert(!serd::syntax_has_graphs(serd::Syntax::NTriples)); + return 0; +} + +static int +test_nodes() +{ + const auto type = serd::make_uri("http://example.org/Type"); + const auto base = serd::make_uri("http://example.org/"); + const auto root = serd::make_uri("http://example.org/"); + + assert(base.type() == serd::NodeType::URI); + assert(base.str() == "http://example.org/"); + assert(base.size() == strlen("http://example.org/")); + assert(base == root); + assert(base < type); + assert(!base.empty()); + assert(std::count(base.begin(), base.end(), '/') == 3); + + const auto relative = serd::make_uri("rel/uri"); + // const auto resolved = relative.resolve(base); + // assert(static_cast(resolved) == "http://example.org/rel/uri"); + // assert(static_cast(resolved) == + // "http://example.org/rel/uri"); + + const auto string = serd::make_string("hello\n\"world\""); + + const auto number = serd::make_integer(42); + assert(number.datatype() == + serd::make_uri("http://www.w3.org/2001/XMLSchema#integer")); + + const auto tagged = serd::make_plain_literal("hallo", "de"); + assert(tagged.language() == serd::make_string("de")); + + assert(!test_node(serd::make_string("hello"))); + assert(!test_node(serd::make_plain_literal("hello", "en"))); + assert(!test_node(serd::make_typed_literal("hello", serd::StringView(type)))); + assert(!test_node(serd::make_blank("blank"))); + assert(!test_node(serd::make_uri("http://example.org/thing"))); + assert(!test_node(serd::make_file_uri("/foo/bar", "host"))); + assert(!test_node(serd::make_file_uri("/foo/bar"))); + assert(!test_node(serd::make_file_uri("/foo/bar", "host"))); + assert(!test_node(serd::make_file_uri("/foo/bar"))); + assert(!test_node(serd::make_decimal(1.2))); + assert(!test_node(serd::make_decimal(3.4))); + assert(!test_node(serd::make_integer(56))); + assert(!test_node(serd::make_base64("blob", 4))); + + assert(serd::get(serd::make(true)) == true); + assert(serd::get(serd::make(false)) == false); + assert(serd::get(serd::make(1.5)) == 1.5); + assert(serd::get(serd::make(-2.5)) == -2.5); + assert(serd::get(serd::make(1.2f)) == 1.2f); + assert(serd::get(serd::make(-2.5f)) == -2.5f); + assert(serd::get(serd::make(12)) == 12); + assert(serd::get(serd::make(-34)) == -34); + + return 0; +} + +static int +test_uri() +{ + const auto uri = serd::make_uri("file:/path"); + const auto no_authority = serd::URI(serd::StringView(uri)); + assert(no_authority.scheme() == "file"); + assert(!no_authority.authority().data()); + assert(no_authority.path() == "/path"); + + const auto empty_authority = serd::URI("file:///path"); + assert(empty_authority.scheme() == "file"); + assert(empty_authority.authority().data()); + assert(empty_authority.authority().empty()); + assert(empty_authority.path() == "/path"); + + const auto base = serd::URI("http://example.org/base/"); + assert(base.scheme() == "http"); + assert(base.authority() == "example.org"); + assert(!base.path_prefix().data()); + assert(base.path() == "/base/"); + assert(!base.query().data()); + assert(!base.fragment().data()); + + const auto rel = serd::URI("relative/path?query#fragment"); + assert(!rel.scheme().data()); + assert(!rel.authority().data()); + assert(!rel.path_prefix().data()); + assert(rel.path() == "relative/path"); + assert(rel.query() == "query"); + assert(rel.fragment() == "#fragment"); + + const auto resolved = rel.resolve(base); + assert(resolved.scheme() == "http"); + assert(resolved.authority() == "example.org"); + assert(resolved.path_prefix() == "/base/"); + assert(resolved.path() == "relative/path"); + assert(resolved.query() == "query"); + assert(resolved.fragment() == "#fragment"); + + assert(resolved.string() == + "http://example.org/base/relative/path?query#fragment"); + std::cerr << resolved.relative_string(base) << std::endl; + assert(resolved.relative_string(base) == "relative/path?query#fragment"); + + const auto domain = serd::URI("http://example.org/"); + assert(domain.relative_string(resolved) == "../../"); + assert(domain.relative_string(resolved, base) == domain.string()); + + auto local_file_uri = serd::parse_file_uri("file:///foo/%20bar"); + assert(local_file_uri == "/foo/ bar"); + + auto hostname = std::string(); + auto host_file_uri = serd::parse_file_uri("file://host/foo", &hostname); + assert(hostname == "host"); + assert(host_file_uri == "/foo"); + + assert(serd::uri_string_has_scheme("http://example.org/")); + assert(!serd::uri_string_has_scheme("foo/bar")); + + std::ostringstream ss; + ss << resolved; + assert(ss.str() == "http://example.org/base/relative/path?query#fragment"); + + return 0; +} + +static int +test_reader() +{ + serd::World world; + serd::Optional base_uri; + serd::Optional ns_name; + serd::Optional ns_uri; + serd::Optional ended_node; + size_t n_statements{}; + std::stringstream stream{}; + serd::Sink sink{world}; + + sink.set_base_func([&](serd::NodeView uri) { + base_uri = uri; + return serd::Status::success; + }); + + sink.set_prefix_func([&](serd::NodeView name, serd::NodeView uri) { + ns_name = name; + ns_uri = uri; + return serd::Status::success; + }); + + sink.set_statement_func( + [&](const serd::StatementFlags, const serd::Statement& statement) { + ++n_statements; + stream << statement.subject() << " " << statement.predicate() << " " + << statement.object() << std::endl; + return serd::Status::success; + }); + + sink.set_end_func([&](serd::NodeView node) { + ended_node = node; + return serd::Status::success; + }); + + // FIXME +#if 0 + serd::World world; + serd::Env env; + serd::Reader reader( + world, serd::Syntax::Turtle, serd::ReaderFlag::global, env, sink, 4096); + + const std::string input("@base ." + "@prefix eg: ." + "eg:s eg:p [ eg:p2 eg:o2 ] ."); + + // Read from string + serd::InputStream string_source = serd::open_string(input); + reader.start(string_source); + reader.read_document(); + + assert(n_statements == 2); + assert(stream.str() == "http://example.org/s http://example.org/p b1\n" + "b1 http://example.org/p2 http://example.org/o2\n"); + + assert(base_uri == serd::make_uri("http://example.org/base")); + assert(ns_name == serd::make_string("eg")); + assert(ns_uri == serd::make_uri("http://example.org/")); + + // Read from C++ stream + std::stringstream ss("eg:s eg:p eg:o3 , _:blank ."); + serd::ByteSource byte_source(ss); + stream.str(""); + reader.start(byte_source); + assert(reader.read_chunk() == serd::Status::success); + assert(reader.read_chunk() != serd::Status::success); + + assert(n_statements == 4); + assert(stream.str() == + "http://example.org/s http://example.org/p http://example.org/o3\n" + "http://example.org/s http://example.org/p blank\n"); + + assert(reader.finish() == serd::Status::success); +#endif + + return 0; +} + +static serd::Status +write_test_doc(serd::Writer& writer) +{ + const auto& sink = writer.sink(); + + const auto blank = serd::make_blank("b1"); + sink.base(serd::make_uri("http://drobilla.net/base/")); + sink.prefix(serd::make_string("eg"), serd::make_uri("http://example.org/")); + sink.write(serd::StatementFlag::anon_O, + serd::make_uri("http://drobilla.net/base/s"), + serd::make_uri("http://example.org/p"), + blank); + sink.statement({}, + serd::Statement(blank, + serd::make_uri("http://example.org/p2"), + serd::make_uri("http://drobilla.net/o"))); + sink.end(blank); + + return writer.finish(); +} + +static const char* const writer_test_doc = + "@base .\n" + "@prefix eg: .\n" + "\n" + "\n" + "\t [\n" + "\t\t \n" + "\t] .\n"; + +static int +test_writer_ostream() +{ + serd::World world; + serd::Env env{world}; + + { + std::ostringstream stream; + serd::OutputStream out{serd::open_output_stream(stream)}; + serd::Writer writer(world, serd::Syntax::Turtle, {}, env, out); + + write_test_doc(writer); + assert(stream.str() == writer_test_doc); + } + + { + std::ofstream bad_file("/does/not/exist"); + bad_file.clear(); + bad_file.exceptions(std::ofstream::badbit); + + serd::OutputStream bad_file_out{serd::open_output_stream(bad_file)}; + serd::Writer writer(world, serd::Syntax::Turtle, {}, env, bad_file_out); + + const serd::Status st = + writer.sink().base(serd::make_uri("http://drobilla.net/base/")); + + assert(st == serd::Status::bad_write); + } + + return 0; +} + +static int +test_writer_string_sink() +{ +// FIXME +#if 0 + serd::World world; + serd::Env env; + std::string output; + + serd::ByteSink byte_sink{[&output](const char* str, size_t len) { + output += str; + return len; + }}; + + serd::Writer writer(world, serd::Syntax::Turtle, {}, env, byte_sink); + + write_test_doc(writer); + assert(output == writer_test_doc); +#endif + + return 0; +} + +static int +test_env() +{ + serd::World world; + serd::Env env{world, serd::make_uri("http://example.org/")}; + assert(env.base_uri() == serd::make_uri("http://example.org/")); + + env = serd::Env{world}; + + const auto base = serd::make_uri("http://drobilla.net/"); + env.set_base_uri(serd::StringView(base)); + assert(env.base_uri() == base); + + env.set_prefix("eg", "http://drobilla.net/"); + env.set_prefix("eg", "http://example.org/"); + + assert(env.expand(serd::make_uri("foo")) == + serd::make_uri("http://drobilla.net/foo")); + + serd::Env copied{env}; + assert(copied.cobj() != env.cobj()); + assert(copied.expand(serd::make_uri("foo")) == + serd::make_uri("http://drobilla.net/foo")); + + serd::Env assigned{world}; + assigned = env; + assert(assigned.cobj() != env.cobj()); + assert(assigned.expand(serd::make_uri("foo")) == + serd::make_uri("http://drobilla.net/foo")); + + serd::Sink sink{world}; + serd::Optional ns_name; + serd::Optional ns_uri; + + sink.set_prefix_func([&](serd::NodeView name, serd::NodeView uri) { + ns_name = name; + ns_uri = uri; + return serd::Status::success; + }); + + env.write_prefixes(sink); + assert(ns_name == serd::make_string("eg")); + assert(ns_uri == serd::make_uri("http://example.org/")); + + return 0; +} + +static int +test_statement() +{ + const auto s = serd::make_uri("http://example.org/s"); + const auto p = serd::make_uri("http://example.org/p"); + const auto o = serd::make_uri("http://example.org/o"); + const auto g = serd::make_uri("http://example.org/g"); + const auto cur = serd::Caret{serd::make_string("test"), 42, 53}; + + const auto t_statement = serd::Statement{s, p, o}; + + assert(t_statement.subject() == s); + assert(t_statement.predicate() == p); + assert(t_statement.object() == o); + assert(!t_statement.graph()); + assert(!t_statement.caret()); + + const auto q_statement = serd::Statement{s, p, o, g, cur}; + assert(q_statement.subject() == s); + assert(q_statement.predicate() == p); + assert(q_statement.object() == o); + assert(q_statement.graph() == g); + assert(q_statement.caret() == cur); + + assert(q_statement.node(serd::Field::subject) == s); + assert(q_statement.node(serd::Field::predicate) == p); + assert(q_statement.node(serd::Field::object) == o); + assert(q_statement.node(serd::Field::graph) == g); + + return 0; +} + +static int +test_model() +{ + serd::World world; + serd::Model model(world, serd::StatementOrder::SPO, {}); + + model.add_index(serd::StatementOrder::OPS); + + assert(model.empty()); + + const auto s = serd::make_uri("http://example.org/s"); + const auto p = serd::make_uri("http://example.org/p"); + const auto o1 = serd::make_uri("http://example.org/o1"); + const auto o2 = serd::make_uri("http://example.org/o2"); + + serd::NodeView b = world.get_blank(); + // auto r = b.resolve(s); + + model.insert(s, p, o1); + model.insert(serd::Statement{s, p, o2}); + + assert(!model.empty()); + assert(model.size() == 2); + assert(model.ask(s, p, o1)); + assert(model.count(s, p, o1) == 1); + assert(!model.ask(s, p, s)); + + size_t total_count = 0; + for (const auto& statement : model) { + assert(statement.subject() == s); + assert(statement.predicate() == p); + assert(statement.object() == o1 || statement.object() == o2); + ++total_count; + } + assert(total_count == 2); + + size_t o1_count = 0; + for (const auto& statement : model.find({}, {}, o1)) { + assert(statement.cobj()); + assert(statement.subject() == s); + assert(statement.predicate() == p); + assert(statement.object() == o1); + ++o1_count; + } + assert(o1_count == 1); + + size_t o2_count = 0; + for (const auto& statement : model.find({}, {}, o2)) { + assert(statement.subject() == s); + assert(statement.predicate() == p); + assert(statement.object() == o2); + ++o2_count; + } + assert(o2_count == 1); + + assert(model.get({}, p, o1) == s); + + const auto statement = model.get_statement(s, p, {}); + assert(statement); + assert(statement->subject() == s); + assert(statement->predicate() == p); + assert(statement->object() == o1); + + const auto range = model.find(s, p, {}); + assert(range.begin()->subject() == s); + assert(range.begin()->predicate() == p); + assert(range.begin()->object() == o1); + + serd::Model copy(model); + assert(copy == model); + + copy.insert(s, p, s); + assert(copy != model); + + return 0; +} + +static int +test_log() +{ + serd::World world; + bool called = false; + world.set_message_func([&called](const serd::LogLevel level, + const serd::LogFields& fields, + const std::string& msg) { + assert(fields.at("TEST_EXTRA") == "extra field"); + assert(level == serd::LogLevel::error); + assert(msg == "bad argument to something: 42\n"); + called = true; + return serd::Status::success; + }); + + const auto success = world.log(serd::LogLevel::error, + {{"TEST_EXTRA", "extra field"}}, + "bad argument to %s: %d\n", + "something", + 42); + + assert(called); + assert(success == serd::Status::success); + + world.set_message_func([](const serd::LogLevel, + const serd::LogFields&, + const std::string&) -> serd::Status { + throw std::runtime_error("error"); + }); + + const auto failure = world.log(serd::LogLevel::error, {}, "failure"); + assert(failure == serd::Status::unknown_error); + + return 0; +} + +int +main() +{ + using TestFunc = int (*)(); + + constexpr std::array tests{{test_operators, + test_optional, + test_nodes, + test_string, + test_stringview, + // test_base64, + test_syntax, + test_uri, + test_env, + test_reader, + test_writer_ostream, + test_writer_string_sink, + test_statement, + test_model, + test_log}}; + + int failed = 0; + for (const auto& test : tests) { + failed += test(); + } + + std::cerr << "Failed " << failed << " tests" << std::endl; + + return failed; +} diff --git a/doc/conf.py.in b/doc/conf.py.in index cfb67183..ca9b69f9 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -48,7 +48,17 @@ _opaque = [ "va_list", ] -nitpick_ignore = list(map(lambda x: ("c:identifier", x), _opaque)) +_c_nitpick_ignore = map(lambda x: ("c:identifier", x), _opaque) +_cpp_nitpick_ignore = map(lambda x: ("cpp:identifier", x), _opaque) +nitpick_ignore = ( + list(_c_nitpick_ignore) + + list(_cpp_nitpick_ignore) + + [ + ("cpp:identifier", "detail"), + ("cpp:identifier", "serd"), + ("cpp:identifier", "serd::detail"), + ] +) # HTML output diff --git a/doc/cpp/.clang-tidy b/doc/cpp/.clang-tidy new file mode 100644 index 00000000..df126af1 --- /dev/null +++ b/doc/cpp/.clang-tidy @@ -0,0 +1,25 @@ +Checks: > + *, + -*-magic-numbers, + -*-named-parameter, + -*-non-private-member-variables-in-classes, + -*-uppercase-literal-suffix, + -altera-struct-pack-align, + -clang-analyzer-deadcode.DeadStores, + -clang-analyzer-nullability.NullablePassedToNonnull, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-vararg, + -fuchsia-default-argument*, + -fuchsia-multiple-inheritance, + -fuchsia-overloaded-operator, + -google-runtime-references, + -hicpp-no-array-decay, + -hicpp-signed-bitwise, + -hicpp-vararg, + -llvmlibc-*, + -modernize-use-trailing-return-type, + -readability-implicit-bool-conversion, +WarningsAsErrors: '*' +HeaderFilterRegex: '.*' +FormatStyle: file diff --git a/doc/cpp/Doxyfile.in b/doc/cpp/Doxyfile.in new file mode 100644 index 00000000..de0a8766 --- /dev/null +++ b/doc/cpp/Doxyfile.in @@ -0,0 +1,45 @@ +PROJECT_NAME = Serd +PROJECT_BRIEF = "A lightweight library for RDF storage and serialisation" + +QUIET = YES +WARN_AS_ERROR = NO +WARN_IF_UNDOCUMENTED = YES +WARN_NO_PARAMDOC = NO + +CASE_SENSE_NAMES = YES +EXTRACT_ALL = NO +EXTRACT_LOCAL_CLASSES = YES +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = YES +HIDE_FRIEND_COMPOUNDS = YES +HIDE_IN_BODY_DOCS = YES +HIDE_UNDOC_CLASSES = YES +HIDE_UNDOC_MEMBERS = YES +INHERIT_DOCS = NO +INLINE_INHERITED_MEMB = NO +REFERENCES_LINK_SOURCE = NO + +AUTOLINK_SUPPORT = NO +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +JAVADOC_AUTOBRIEF = YES +SHOW_FILES = NO +XML_PROGRAMLISTING = NO + +MACRO_EXPANSION = YES +PREDEFINED = SERD_ALLOCATED \ + SERD_API \ + SERD_CONST_FUNC= \ + SERD_DEPRECATED_BY(x)= \ + SERD_DISABLE_DEPRECATED \ + SERD_NONNULL= \ + SERD_NULLABLE= \ + SERD_PURE_FUNC= + +RECURSIVE = YES +STRIP_FROM_PATH = @SERD_SRCDIR@ +INPUT = @SERD_SRCDIR@/include \ + @SERD_SRCDIR@/bindings/cpp/include + +OUTPUT_DIRECTORY = @DOX_OUTPUT@ diff --git a/doc/cpp/api/meson.build b/doc/cpp/api/meson.build new file mode 100644 index 00000000..33171d5f --- /dev/null +++ b/doc/cpp/api/meson.build @@ -0,0 +1,8 @@ +cpp_serd_rst = custom_target( + 'Serd C++ API ReST Documentation', + command: [dox_to_sphinx, + '-l', 'cpp', + '-f', '@INPUT0@', + meson.current_build_dir()], + input: [cpp_index_xml] + cpp_rst_files, + output: 'serd.rst') diff --git a/doc/cpp/cpp_facilities.rst b/doc/cpp/cpp_facilities.rst new file mode 100644 index 00000000..53b8704d --- /dev/null +++ b/doc/cpp/cpp_facilities.rst @@ -0,0 +1,53 @@ +C++ Facilities +============== + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +String Views +------------ + +For performance reasons, +most functions that take a string take a :type:`StringView`. +This allows many types of string to be passed as an argument, +and redundant string measurement to be avoided. + +:type:`StringView` works similarly to ``std::string_view`` (and will likely be removed when C++17 support is more widespread). +A :type:`StringView` parameter will accept a string literal, +dynamic C string, +or a ``std::string`` as an argument. +Note, however, that the constructor that takes only a ``const char*`` calls ``strlen`` to measure the string, +so care should be taken to avoid this in performance-critical code if the string length is already known. + +Optionals +--------- + +Several places in the C API take or return a pointer that may be null. +This is wrapped more safely in the C++ API as an :class:`Optional`. + +From a user perspective, :class:`Optional` works similarly to ``std::optional``, +with pointer-like access operators and explicit boolean conversion enabling code like: + +.. code-block:: cpp + + if (optional_value) { + use_value(*optional_value); + } + +or: + +.. code-block:: cpp + + if (optional_object) { + optional_object->do_something(); + } + +The :class:`Optional` implementation is serd-specific, +and takes advantage of the fact that the contained object is really just a "fancy pointer". +This means that null can be used to represent an unset value, +avoiding the space overhead of more general types like ``std::optional``. + +A pointer to the underlying C object can be retrieved with the :func:`~Optional::cobj` method, +which will return null if the optional is unset. + diff --git a/doc/cpp/index.rst b/doc/cpp/index.rst new file mode 100644 index 00000000..32ad97c9 --- /dev/null +++ b/doc/cpp/index.rst @@ -0,0 +1,16 @@ +#### +Serd +#### + +.. include:: summary.rst + +This is the documentation for its C++ bindings, +a thin header-only wrapper that provides more convenience and safety, +with minimal overhead compared to using the C API directly. + +.. toctree:: + + using_serd + api/serdpp + api/serdpp_detail + api/serd diff --git a/doc/cpp/meson.build b/doc/cpp/meson.build new file mode 100644 index 00000000..4f0d7b4f --- /dev/null +++ b/doc/cpp/meson.build @@ -0,0 +1,57 @@ +config = configuration_data() +config.set('SERD_VERSION', meson.project_version()) + +conf_py = configure_file(configuration: config, + input: files('../conf.py.in'), + output: 'conf.py') + +configure_file(copy: true, + input: files('../summary.rst'), + output: 'summary.rst') + +configure_file(copy: true, input: files('overview.cpp'), output: 'overview.cpp') + +executable('overview', files('overview.cpp'), + dependencies: [serd_dep, serdpp_dep]) + +cpp_rst_files = files( + 'cpp_facilities.rst', + 'index.rst', + 'model.rst', + 'nodes.rst', + 'overview.rst', + 'reading_and_writing.rst', + 'statements.rst', + 'stream_processing.rst', + 'using_serd.rst', + 'world.rst', +) + +foreach f : cpp_rst_files + configure_file(copy: true, input: f, output: '@PLAINNAME@') +endforeach + +subdir('xml') +subdir('api') + +docs = custom_target( + 'singlehtml C++ documentation for serd', + command: [sphinx_build, '-M', 'singlehtml', + meson.current_build_dir(), meson.current_build_dir(), + '-E', '-q', '-t', 'singlehtml'], + input: [cpp_rst_files, cpp_serd_rst, cpp_index_xml], + output: 'singlehtml', + build_by_default: true, + install: true, + install_dir: docdir / 'serd-0') + +docs = custom_target( + 'html C++ documentation for serd', + command: [sphinx_build, '-M', 'html', + meson.current_build_dir(), meson.current_build_dir(), + '-E', '-q', '-t', 'html'], + input: [cpp_rst_files, cpp_serd_rst, cpp_index_xml], + output: 'html', + build_by_default: true, + install: true, + install_dir: docdir / 'serd-0') diff --git a/doc/cpp/model.rst b/doc/cpp/model.rst new file mode 100644 index 00000000..22e47160 --- /dev/null +++ b/doc/cpp/model.rst @@ -0,0 +1,266 @@ +Model +===== + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +A :struct:`Model` is an indexed set of statements. +A model can be used to store any set of data, +from a few statements (for example, a protocol message), +to an entire document, +to a database with millions of statements. + +Constructing a model requires a world, +and :type:`flags ` which can be used to configure the model: + +.. literalinclude:: overview.cpp + :start-after: begin model-new + :end-before: end model-new + :dedent: 2 + +Combinations of flags can be used to enable different indices, +or the storage of graphs and cursors. +For example, to be able to quickly search by predicate, +and store a cursor for each statement, +the flag :enumerator:`ModelFlag::store_carets` and a :enumerator:`StatementOrder::PSO` index can be added like so: + +.. literalinclude:: overview.cpp + :start-after: begin fancy-model-new + :end-before: end fancy-model-new + :dedent: 2 + +Model Operations +---------------- + +Models are value-like and can be copied and compared for equality: + +.. literalinclude:: overview.cpp + :start-after: begin model-copy + :end-before: end model-copy + :dedent: 2 + +The number of statements in a model can be accessed with the :func:`~Model::size` and :func:`~Model::empty` methods: + +.. literalinclude:: overview.cpp + :start-after: begin model-size + :end-before: end model-size + :dedent: 2 + +Destroying a model invalidates all nodes and statements within that model, +so care should be taken to ensure that no dangling pointers are created. + +Adding Statements +----------------- + +Statements can be added to the model by passing the nodes of the statement to :func:`~Model::insert`: + +.. literalinclude:: overview.cpp + :start-after: begin model-add + :end-before: end model-add + :dedent: 2 + +Alternatively, if you already have a statement (for example from another model), +the overload that takes a :type:`StatementView` can be used instead. +For example, the first statement in one model could be added to another like so: + +.. literalinclude:: overview.cpp + :start-after: begin model-insert + :end-before: end model-insert + :dedent: 2 + +An entire range of statements can be inserted at once by passing a range. +For example, all statements in one model could be copied into another like so: + +.. literalinclude:: overview.cpp + :start-after: begin model-add-range + :end-before: end model-add-range + :dedent: 2 + +Note that this overload consumes its argument, +so a copy must be made to insert a range without modifying the original. + +Iteration +--------- + +An iterator is a reference to a particular statement in a model. +The :func:`~Model::begin` method returns an iterator to the first statement in the model, +and :func:`~Model::end` returns a sentinel that is one past the last statement in the model: + +.. literalinclude:: overview.cpp + :start-after: begin model-begin-end + :end-before: end model-begin-end + :dedent: 2 + +Iterators can be advanced and compared manually: + +.. literalinclude:: overview.cpp + :start-after: begin iter-next + :end-before: end iter-next + :dedent: 2 + +However, they are more typically used automatically when iterating over a model: + +.. literalinclude:: overview.cpp + :start-after: begin model-iteration + :end-before: end model-iteration + :dedent: 2 + +Ranges +------ + +It is often more convenient to work with ranges of statements, +rather than iterators to individual statements. + +The simplest range, +the range of all statements in the model, +is returned by the :func:`~Model::begin()` method: + +.. literalinclude:: overview.cpp + :start-after: begin model-all + :end-before: end model-all + :dedent: 2 + +Iterating over this range will produce statements in GSPO or SPO order, +which is the natural order for writing nicely abbreviated documents. + +In some more advanced cases, +it is useful to iterate over a model in a specific order, +for example to optimise an algorithm that performs several iterations at once. +A range of all statements with a specific ordering can be had via the :func:`~Model::begin_ordered()` method: + +.. literalinclude:: overview.cpp + :start-after: begin model-ordered + :end-before: end model-ordered + :dedent: 2 + +Ranges work like immutable collections, +and can be iterated over in the typical way: + +.. literalinclude:: overview.cpp + :start-after: begin range-iteration + :end-before: end range-iteration + :dedent: 2 + + +Pattern Matching +---------------- + +There are several model methods that can be used to quickly find statements in the model that match a pattern. +The simplest is :func:`~Model::ask` which checks if there is any matching statement: + +.. literalinclude:: overview.cpp + :start-after: begin model-ask + :end-before: end model-ask + :dedent: 2 + +To access the unknown fields, +an iterator to the matching statement can be found with :func:`~Model::find` instead: + +.. literalinclude:: overview.cpp + :start-after: begin model-find + :end-before: end model-find + :dedent: 2 + +Similar to :func:`~Model::ask`, +:func:`~Model::count` can be used to count the number of matching statements: + +.. literalinclude:: overview.cpp + :start-after: begin model-count + :end-before: end model-count + :dedent: 2 + +To iterate over matching statements, +:func:`~Model::find` can be used, +which returns a cursor that will visit only statements that match the pattern: + +.. literalinclude:: overview.cpp + :start-after: begin model-range + :end-before: end model-range + :dedent: 2 + +Indexing +-------- + +A model can contain several indices that use different orderings to support different kinds of queries. +For good performance, +there should be an index where the least significant fields in the ordering correspond to wildcards in the pattern +(or, in other words, one where the most significant fields in the ordering correspond to nodes given in the pattern). +The table below lists the indices that best support a kind of pattern, +where a "?" represents a wildcard. + ++---------+--------------+ +| Pattern | Good Indices | ++=========+==============+ +| s p o | Any | ++---------+--------------+ +| s p ? | SPO, PSO | ++---------+--------------+ +| s ? o | SOP, OSP | ++---------+--------------+ +| s ? ? | SPO, SOP | ++---------+--------------+ +| ? p o | POS, OPS | ++---------+--------------+ +| ? p ? | POS, PSO | ++---------+--------------+ +| ? ? o | OSP, OPS | ++---------+--------------+ +| ? ? ? | Any | ++---------+--------------+ + +If graphs are enabled, +then statements are indexed both with and without the graph fields, +so queries with and without a graph wildcard will have similar performance. + +Since indices take up space and slow down insertion, +it is best to enable the fewest indices possible that cover the queries that will be performed. +For example, +an applications might enable just SPO and OPS order, +because they always search for specific subjects or objects, +but never for just a predicate without specifying any other field. + +Getting Values +-------------- + +Sometimes you are only interested in a single node, +and it is cumbersome to first search for a statement and then get the node from it. +A more convenient way is to use the :func:`~Model::get` method. +To get a value, specify a pattern where exactly one of the subject, predicate, and object is a wildcard. +If a statement matches, then the node that "fills" the wildcard will be returned: + +.. literalinclude:: overview.cpp + :start-after: begin model-get + :end-before: end model-get + :dedent: 2 + +If multiple statements match the pattern, +then the matching node from an arbitrary statement is returned. +It is an error to specify more than one wildcard, excluding the graph. + +The similar :func:`~Model::get_statement` instead returns the matching statement: + +.. literalinclude:: overview.cpp + :start-after: begin model-get-statement + :end-before: end model-get-statement + :dedent: 2 + +Erasing Statements +------------------ + +Individual statements can be erased with :func:`~Model::erase`, +which takes an iterator: + +.. literalinclude:: overview.cpp + :start-after: begin model-erase + :end-before: end model-erase + :dedent: 2 + +There is also an overload that takes a range and erases all statements in that range: + +.. literalinclude:: overview.cpp + :start-after: begin model-erase-range + :end-before: end model-erase-range + :dedent: 2 + +Erasing statements from a model invalidates all iterators to that model. diff --git a/doc/cpp/nodes.rst b/doc/cpp/nodes.rst new file mode 100644 index 00000000..c59bbd0d --- /dev/null +++ b/doc/cpp/nodes.rst @@ -0,0 +1,38 @@ +Nodes +===== + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +Nodes are the basic building blocks of data. +Nodes are essentially strings, +but also have a :enum:`type `, +and optionally either a datatype or a language. + +In the abstract, a node is either a literal, a URI, or blank. +Serd also has a type for variable nodes, +which are used for some features but not present in RDF data. + +Construction +------------ + +Several convenient construction functions are provided that return nodes: + +- :func:`make_token` +- :func:`make_uri` +- :func:`make_file_uri` +- :func:`make_literal` +- :func:`make_boolean` +- :func:`make_decimal` +- :func:`make_double` +- :func:`make_float` +- :func:`make_integer` +- :func:`make_base64` + +Accessors +--------- + +The datatype or language of a node can be retrieved with :func:`~NodeWrapper::datatype` or :func:`~NodeWrapper::language`, respectively. +Note that only literals can have a datatype or language, +but never both at once. diff --git a/doc/cpp/overview.cpp b/doc/cpp/overview.cpp new file mode 100644 index 00000000..63c4f690 --- /dev/null +++ b/doc/cpp/overview.cpp @@ -0,0 +1,370 @@ +/* + Copyright 2021 David Robillard + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +/* + Example code that is included in the documentation. Code in the + documentation is included from here rather than written inline so that it can + be tested and avoid rotting. The code here doesn't make much sense, but is + written such that it at least compiles and will run without crashing. +*/ + +#include "serd/serd.hpp" + +#include +#include +#include + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-variable" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wunused-variable" +# pragma GCC diagnostic ignored "-Wunused-but-set-variable" +#endif + +using namespace serd; // NOLINT(google-build-using-namespace) + +static void +statements() +{ + // begin statement-new + Statement triple{make_uri("http://example.org/drobilla"), // Subject + make_uri("http://example.org/firstName"), // Predicate + make_string("David")}; // Object + // end statement-new + + // begin statement-new-graph + Statement quad{make_uri("http://example.org/drobilla"), // Subject + make_uri("http://example.org/firstName"), // Predicate + make_string("David"), // Object + make_uri("http://example.org/userData")}; // Graph + // end statement-new-graph + + // begin statement-new-cursor + Node file{make_uri("file:///tmp/userdata.ttl")}; + Statement triple2{make_uri("http://example.org/drobilla"), // Subject + make_uri("http://example.org/firstName"), // Predicate + make_string("David"), // Object + Caret{file, 4, 27}}; // Caret + // end statement-new-cursor + + // begin statement-new-graph-cursor + Statement quad2{make_uri("http://example.org/drobilla"), // Subject + make_uri("http://example.org/firstName"), // Predicate + make_string("David"), // Object + make_uri("http://example.org/userData"), // Graph + Caret{file, 4, 27}}; // Caret + // end statement-new-graph-cursor +} + +static void +statements_accessing_fields() +{ + Node ss{make_uri("http://example.org/s")}; + Node sp{make_uri("http://example.org/p")}; + Node so{make_uri("http://example.org/o")}; + + Statement statement{ss, sp, so}; + + // begin get-subject + NodeView s = statement.node(Field::subject); + // end get-subject + + // begin get-pog + NodeView p = statement.predicate(); + NodeView o = statement.object(); + Optional g = statement.graph(); + // end get-pog + + // begin get-caret + Optional c = statement.caret(); + // end get-caret +} + +static void +statements_comparison() +{ + Node ss{make_uri("http://example.org/s")}; + Node sp{make_uri("http://example.org/p")}; + Node so{make_uri("http://example.org/o")}; + + Statement statement1{ss, sp, so}; + Statement statement2{ss, sp, so}; + + // begin statement-equals + if (statement1 == statement2) { + std::cout << "Match" << std::endl; + } + // end statement-equals + + const Statement& statement = statement1; + + // begin statement-matches + if (statement.matches({}, make_uri("http://example.org/name"), {})) { + std::cout << statement.subject() << " has name " << statement.object() + << std::endl; + } + // end statement-matches +} + +static void +world() +{ + // begin world-new + World world; + // end world-new + + // begin get-blank + Node blank = world.get_blank(); + // end get-blank +} + +static void +model() +{ + World world; + + // begin model-new + Model model{world, StatementOrder::SPO, {}}; + // end model-new + + // begin fancy-model-new + Model fancy_model{world, StatementOrder::SPO, ModelFlag::store_carets}; + fancy_model.add_index(StatementOrder::PSO); + // end fancy-model-new + + // begin model-copy + Model copy{model}; + assert(copy == model); + + copy = fancy_model; + assert(copy == fancy_model); + // end model-copy + + // begin model-size + if (model.empty()) { + std::cout << "Model is empty" << std::endl; + } else if (model.size() > 9000) { + std::cout << "Model has over 9000 statements" << std::endl; + } + // end model-size + + // begin model-add + Node s{make_uri("http://example.org/thing")}; + Node p{make_uri("http://example.org/name")}; + Node o{make_string("Thing")}; + + model.insert(s, p, o); + // end model-add + + Model other_model{model}; + + // begin model-insert + model.insert(*other_model.begin()); + // end model-insert + + // begin model-add-range + model.insert_statements(other_model.begin()); + // end model-add-range + + // begin model-begin-end + Cursor i = model.begin(); + if (i == model.end()) { + std::cout << "Model is empty" << std::endl; + } else { + std::cout << "First statement subject: " << i->subject() << std::endl; + } + // end model-begin-end + + // begin iter-next + if (++i != model.end()) { + std::cout << "Second statement subject: " << i->subject() << std::endl; + } + // end iter-next + + // begin model-iteration + for (const StatementView& statement : model) { + std::cout << "Model statement subject: " << statement.subject() + << std::endl; + } + // end model-iteration + + // begin model-all + Cursor all = model.begin(); + // end model-all + + // begin model-ordered + Cursor ordered = model.begin_ordered(StatementOrder::OPS); + // end model-ordered + + // begin range-iteration + // FIXME + // for (const StatementView& statement : all) { + // std::cout << "Cursor statement subject: " << statement.subject() + // << std::endl; + // } + // end range-iteration + + // begin model-ask + Node rdf_type = make_uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); + + if (model.ask({}, rdf_type, {}, {})) { + std::cout << "Model contains a type statement" << std::endl; + } + // end model-ask + + // Add a statement so that the searching examples below work + Node inst{make_uri("http://example.org/i")}; + Node type{make_uri("http://example.org/T")}; + model.insert(inst, rdf_type, type); + + // begin model-find + Model::Range it = model.find({}, rdf_type, {}); + + NodeView instance = it.begin()->subject(); + // end model-find + + // begin model-count + size_t n = model.count(instance, rdf_type, {}); + std::cout << "Instance has " << n << " types" << std::endl; + // end model-count + + // begin model-range + for (const StatementView& statement : model.find(instance, rdf_type, {})) { + std::cout << "Instance has type " << statement.object() << std::endl; + } + // end model-range + + // begin model-get + Optional t = model.get(instance, rdf_type, {}); + if (t) { + std::cout << "Instance has type " << *t << std::endl; + } + // end model-get + + // begin model-get-statement + Optional ts = model.get_statement(instance, rdf_type, {}); + if (ts) { + std::cout << "Instance " << ts->subject() << " has type " << ts->object() + << std::endl; + } + // end model-get-statement + + // begin model-erase + Model::Range itype = model.find({}, rdf_type, {}); + model.erase(itype.begin()); + // end model-erase + + // begin model-erase-range + // FIXME + // Model::Range all_types = model.find({}, rdf_type, {}); + // model.erase_statements(all_types); + // end model-erase-range +} + +static void +reading_writing() +{ + World world; + + // begin env-new + Node base = make_file_uri("/some/file.ttl"); + + Env env{world, base}; + // end env-new + + // begin env-set-prefix + env.set_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"); + // end env-set-prefix + + // begin byte-sink-new + OutputStream out = serd::open_output_file("/tmp/eg.ttl"); + // end byte-sink-new + + // begin writer-new + Writer writer{world, serd::Syntax::Turtle, {}, env, out}; + // end writer-new + + // begin reader-new + Reader reader{world, Syntax::Turtle, {}, env, writer.sink(), 4096}; + // end reader-new + + // begin read-document + Status st = reader.read_document(); + if (st != Status::success) { + std::cout << "Error reading document: " << strerror(st) << std::endl; + } + // end read-document + + // begin byte-sink-close + // out.close(); + // end byte-sink-close + + // begin inserter-new + Model model{world, StatementOrder::SPO, {}}; + SinkWrapper inserter = make_inserter(model); + // end inserter-new + + // begin model-reader-new + Reader model_reader{world, Syntax::Turtle, {}, env, inserter, 4096}; + + st = model_reader.read_document(); + if (st != Status::success) { + std::cout << "Error loading model: " << strerror(st) << std::endl; + } + // end model-reader-new + + // begin write-range + // FIXME + // model.all().write(writer.sink(), {}); + // end write-range + + // begin canon-new + SinkWrapper canon = make_canon(world, inserter, {}); + // end canon-new + + Node rdf_type = make_uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"); + + // begin filter-new + SinkWrapper filter = make_filter(world, + inserter, // Target + {}, // Subject + rdf_type, // Predicate + {}, // Object + {}, // Graph + true); // Inclusive + // end filter-new +} + +int +main() +{ + statements(); + statements_accessing_fields(); + statements_comparison(); + world(); + model(); + reading_writing(); + + return 0; +} + +#if defined(__clang__) +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/doc/cpp/overview.rst b/doc/cpp/overview.rst new file mode 100644 index 00000000..763620e8 --- /dev/null +++ b/doc/cpp/overview.rst @@ -0,0 +1,83 @@ +######## +Overview +######## + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +The serd C++ API is declared in ``serd.hpp``: + +.. code-block:: cpp + + #include + +An application using serd first creates a :doc:`api/serd_world`, +which represents an instance of serd and is used to manage "global" facilities like logging. + +The rest of the API declares objects that can be used together in different ways. +They can be broadly placed into four categories: + +Data + A :doc:`api/serd_node` is the basic building block of data, + 3 or 4 nodes together make a :doc:`api/serd_statement`. + All data is expressed in this form. + +Streams + Objects stream data to each other via :doc:`api/serd_sink`, + which is an abstract interface that receives :doc:`api/serd_event`. + An event is essentially a statement, + but there are a few additional event types that reflect context changes and support pretty-printing. + + Some objects both act as a sink and send data to another sink, + which allow them to be inserted in a data `pipeline` to process the data as it streams through. + For example, + a :doc:`api/serd_canon` converts literals to canonical form, + and a :doc:`api/serd_filter` filters statements that match (or do not match) some pattern. + + The syntactic context at a particular point is represented by an :doc:`api/serd_env`. + This stores the base URI and set of namespace prefixes, + which are used to expand relative and abbreviated URIs. + +Reading and Writing + Reading and writing data is performed using a :doc:`api/serd_reader`, + which reads text and emits data to a sink, + and a :doc:`api/serd_writer`, + which is a sink that writes the incoming data as text. + Both work in a streaming fashion so that large documents can be pretty-printed, + translated, + or otherwise processed quickly using only a small amount of memory. + +Storage + A set of statements can be stored in memory as a :doc:`api/serd_model`. + A model acts as a collection of statements, + and provides most of the interface expected for a standard C++ collection. + There are also several query methods which search for statements quickly, + provided an appropriate index is enabled. + + Data can be loaded into a model via an :doc:`api/serd_inserter`, + which is a sink that inserts incoming statements into a model. + +The sink interface acts as a generic connection which can be used to build custom data processing pipelines. +For example, +a simple pipeline to read a document, filter out some statements, and write the result to a new file, +would look something like: + +.. image:: ../_static/writer_pipeline.svg + +Here, event streams are shown as a dashed line, and a solid line represents explicit use of an object. +In other words, dashed lines represent connections via the abstract :doc:`api/serd_sink` interface. +In this case both reader and writer are using the same environment, +so the output document will have the same abbreviations as the input. +It is also possible to use different environments, +for example to set additional namespace prefixes to further abbreviate the document. + +Similarly, a document could be loaded into a model with canonical literals using a pipeline like: + +.. image:: ../_static/model_pipeline.svg + +Many other useful pipelines can be built from the objects included in serd, +and applications can implement custom sinks if those are not sufficient. + +The remainder of this overview gives a bottom-up introduction to the API, +with links to the complete reference where further detail can be found. diff --git a/doc/cpp/reading_and_writing.rst b/doc/cpp/reading_and_writing.rst new file mode 100644 index 00000000..893e6f7b --- /dev/null +++ b/doc/cpp/reading_and_writing.rst @@ -0,0 +1,147 @@ +Reading and Writing +=================== + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +Reading and writing documents in a textual syntax is handled by the :struct:`Reader` and :struct:`Writer`, respectively. +Serd is designed around a concept of event streams, +so the reader or writer can be at the beginning or end of a "pipeline" of stream processors. +This allows large documents to be processed quickly in an "online" fashion, +while requiring only a small constant amount of memory. +If you are familiar with XML, +this is roughly analogous to SAX. + +A common setup is to simply connect a reader directly to a writer. +This can be used for things like pretty-printing, +or converting a document from one syntax to another. +This can be done by passing the sink returned by the writer's :func:`~Writer::sink` method to the :class:`~Reader` constructor. + +First though, +an environment needs to be set up in order to write a document. +This defines the base URI and any namespace prefixes, +which are used to resolve any relative URIs or prefixed names by the reader, +and to abbreviate the output by the writer. +In most cases, the base URI should simply be the URI of the file being written. +For example: + +.. literalinclude:: overview.cpp + :start-after: begin env-new + :end-before: end env-new + :dedent: 2 + +Namespace prefixes can also be defined for any vocabularies used: + +.. literalinclude:: overview.cpp + :start-after: begin env-set-prefix + :end-before: end env-set-prefix + :dedent: 2 + +The reader will set any additional prefixes from the document as they are encountered. + +We now have an environment set up for the contents of our document, +but still need to specify where to write it. +This is done by creating an :struct:`OutputStream`, +which is a generic interface that can be set up to write to a file, +a buffer in memory, +or a custom function that can be used to write output anywhere. +In this case, we will write to the file we set up as the base URI: + +.. literalinclude:: overview.cpp + :start-after: begin byte-sink-new + :end-before: end byte-sink-new + :dedent: 2 + +The second argument is the page size in bytes, +so I/O will be performed in chunks for better performance. +The value used here, 4096, is a typical filesystem block size that should perform well on most machines. + +With an environment and byte sink ready, +the writer can now be created: + +.. literalinclude:: overview.cpp + :start-after: begin writer-new + :end-before: end writer-new + :dedent: 2 + +Output is written by feeding statements and other events to the sink returned by the writer's :func:`~Writer::sink` method. +:struct:`Sink` is the generic interface for anything that can consume data streams. +Many objects provide the same interface to do various things with the data, +but in this case we will send data directly to the writer: + +.. literalinclude:: overview.cpp + :start-after: begin reader-new + :end-before: end reader-new + :dedent: 2 + +The third argument of the reader constructor takes a bitwise ``OR`` of :enum:`ReaderFlag` flags that can be used to configure the reader. +In this case no flags are given, +but for example, +passing ``ReaderFlag::lax | ReaderFlag::relative`` would enable lax mode and preserve relative URIs in the input. + +Now that we have a reader that is set up to directly push its output to a writer, +we can finally process the document: + +.. literalinclude:: overview.cpp + :start-after: begin read-document + :end-before: end read-document + :dedent: 2 + +Alternatively, one "chunk" of input can be read at a time with :func:`~Reader::read_chunk`. +A "chunk" is generally one top-level description of a resource, +including any anonymous blank nodes in its description, +but this depends on the syntax and the structure of the document being read. + +The reader pushes events to its sink as input is read, +so in this scenario the data should now have been re-written by the writer +(assuming no error occurred). +To finish and ensure that a complete document has been read and written, +:func:`~Reader::finish` can be called followed by :func:`~Writer::finish`. +However these will be automatically called on destruction if necessary, +so if the reader and writer are no longer required they can simply be destroyed. + +Finally, closing the byte sink will flush and close the output file, +so it is ready to be read again later. +Similar to the reader and writer, +this can be done explicitly by calling its :func:`~OutputStream::close` method, +or implicitly by destroying the byte sink if it is no longer needed: + +.. literalinclude:: overview.cpp + :start-after: begin byte-sink-close + :end-before: end byte-sink-close + :dedent: 2 + +Reading into a Model +-------------------- + +A document can be loaded into a model by setting up a reader that pushes data to a model `inserter` rather than a writer: + +.. literalinclude:: overview.cpp + :start-after: begin inserter-new + :end-before: end inserter-new + :dedent: 2 + +The process of reading the document is the same as above, +only the sink is different: + +.. literalinclude:: overview.cpp + :start-after: begin model-reader-new + :end-before: end model-reader-new + :dedent: 2 + +.. + Writing a Model + --------------- + + A model, or parts of a model, can be written by writing the desired range using its :func:`Range::write` method: + + .. literalinclude:: overview.cpp + :start-after: begin write-range + :end-before: end write-range + :dedent: 2 + + By default, + this writes the range in chunks suited to pretty-printing with anonymous blank nodes (like "[ ... ]" in Turtle or TriG). + The flag :enumerator:`SerialisationFlag::no_inline_objects` can be given to instead write the range in a simple SPO order, + which can be useful in other situations because it is faster and emits statements in strictly increasing order. diff --git a/doc/cpp/statements.rst b/doc/cpp/statements.rst new file mode 100644 index 00000000..a77c8050 --- /dev/null +++ b/doc/cpp/statements.rst @@ -0,0 +1,124 @@ +Statements +========== + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +A :struct:`Statement` is a tuple of either 3 or 4 nodes: +the `subject`, `predicate`, `object`, and optional `graph`. +Statements declare that a subject has some property. +The predicate identifies the property, +and the object is its value on the subject. + +A statement can be thought of as a very simple machine-readable sentence. +The subject and object are as in natural language, +and the predicate is something like a verb, but more general. +For example, we could make a statement in English about your intrepid author: + + drobilla has the first name "David" + +We can break this statement into 3 pieces like so: + +.. list-table:: + :header-rows: 1 + + * - Subject + - Predicate + - Object + * - drobilla + - has the first name + - "David" + +To make a :class:`Statement` out of this, we need to define some URIs. +In RDF, the subject and predicate must be *resources* with an identifier +(for example, neither can be a string). +Conventionally, predicate names do not start with "has" or similar words, +since that would be redundant in this context. +So, we assume that ``http://example.org/drobilla`` is the URI for drobilla, +and that ``http://example.org/firstName`` has been defined somewhere to be +a property with the appropriate meaning, +and can make an equivalent :class:`Statement`: + +.. literalinclude:: overview.cpp + :start-after: begin statement-new + :end-before: end statement-new + :dedent: 2 + +Statements also have an additional field, the graph, +which is used to group statements together. +For example, this can be used to store the document where statements originated, +or to keep schema data separate from application data. +A statement with a graph can be constructed by passing the graph as the fourth parameter: + +.. literalinclude:: overview.cpp + :start-after: begin statement-new-graph + :end-before: end statement-new-graph + :dedent: 2 + +Finally, a :class:`Caret` may also be passed which records a position in the file that the statement was loaded from. +This is typically used for printing useful error messages. +The cursor is considered metadata and not part of the statement itself, +for example, +it is not considered in equality comparison. +Typically, the cursor will be automatically set by a reader, +but a statement with a cursor can be constructed manually by passing the cursor as the last parameter: + +.. literalinclude:: overview.cpp + :start-after: begin statement-new-cursor + :end-before: end statement-new-cursor + :dedent: 2 + +.. literalinclude:: overview.cpp + :start-after: begin statement-new-graph-cursor + :end-before: end statement-new-graph-cursor + :dedent: 2 + + +Accessing Fields +---------------- + +Statement fields can be accessed with the :func:`~StatementWrapper::node` method, for example: + +.. literalinclude:: overview.cpp + :start-after: begin get-subject + :end-before: end get-subject + :dedent: 2 + +Alternatively, an accessor function is provided for each field: + +.. literalinclude:: overview.cpp + :start-after: begin get-pog + :end-before: end get-pog + :dedent: 2 + +Every statement has a subject, predicate, and object, +but the graph is optional. +The caret is also optional, +and can be accessed with the :func:`~StatementWrapper::caret` method: + +.. literalinclude:: overview.cpp + :start-after: begin get-caret + :end-before: end get-caret + :dedent: 2 + +Comparison +---------- + +Two statements can be compared with the equals operator: + +.. literalinclude:: overview.cpp + :start-after: begin statement-equals + :end-before: end statement-equals + :dedent: 2 + +Statements are equal if all four corresponding pairs of nodes are equal. +The cursor is considered metadata, and is ignored for comparison. + +It is also possible to match statements against a pattern with the :func:`~StatementWrapper::matches` method, +where empty parameters act as wildcards: + +.. literalinclude:: overview.cpp + :start-after: begin statement-matches + :end-before: end statement-matches + :dedent: 2 diff --git a/doc/cpp/stream_processing.rst b/doc/cpp/stream_processing.rst new file mode 100644 index 00000000..39265287 --- /dev/null +++ b/doc/cpp/stream_processing.rst @@ -0,0 +1,48 @@ +Stream Processing +================= + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +The above examples show how a document can be either written to a file or loaded into a model, +simply by changing the sink that the data is written to. +There are also sinks that filter or transform the data before passing it on to another sink, +which can be used to build more advanced pipelines with several processing stages. + +Canonical Literals +------------------ + +A `canon` is a stream processor that converts literals with supported XSD datatypes into canonical form. +For example, this will rewrite an xsd:decimal literal like ".10" as "0.1". +A canon can be constructed by passing the "target" sink that the transformed statements should be written to, +for example: + +.. literalinclude:: overview.cpp + :start-after: begin canon-new + :end-before: end canon-new + :dedent: 2 + +The last argument is a bitwise ``OR`` of :enum:`CanonFlag` flags. +For example, :enumerator:`CanonFlag::lax` will tolerate and pass through invalid literals, +which can be useful for cleaning up questionabe data as much as possible without losing any information. + +Filtering Statements +-------------------- + +A `filter` is a stream processor that filters statements based on a pattern. +It can be configured in either inclusive or exclusive mode, +which passes through only statements that match or don't match the pattern, +respectively. +A filter can be constructed by passing the target sink, +the statement pattern as individual nodes, +and an inclusive flag. +For example, all statements with predicate ``rdf:type`` could be filtered out when loading a model: + +.. literalinclude:: overview.cpp + :start-after: begin filter-new + :end-before: end filter-new + :dedent: 2 + +If ``false`` is passed for the last parameter instead, +then the filter operates in exclusive mode and will instead insert only statements with predicate ``rdf:type``. diff --git a/doc/cpp/using_serd.rst b/doc/cpp/using_serd.rst new file mode 100644 index 00000000..ee9112ef --- /dev/null +++ b/doc/cpp/using_serd.rst @@ -0,0 +1,14 @@ +########## +Using Serd +########## + +.. toctree:: + + overview + cpp_facilities + nodes + statements + world + model + reading_and_writing + stream_processing diff --git a/doc/cpp/world.rst b/doc/cpp/world.rst new file mode 100644 index 00000000..d6736485 --- /dev/null +++ b/doc/cpp/world.rst @@ -0,0 +1,45 @@ +World +===== + +.. default-domain:: cpp +.. highlight:: cpp +.. namespace:: serd + +So far, we have only used nodes and statements, +which are simple independent objects. +Higher-level facilities in Serd require a :struct:`World`, +which represents the global library state. + +A program typically uses just one world, +which can be constructed with no arguments: + +.. literalinclude:: overview.cpp + :start-after: begin world-new + :end-before: end world-new + :dedent: 2 + +All "global" library state is handled explicitly via the world. +Serd does not contain any static mutable data, +allowing it to be used concurrently in several parts of a program, +for example in plugins. + +If multiple worlds *are* used in a single program, +they must never be mixed: +objects "inside" one world can not be used with objects inside another. + +Note that the world is not a database, +it only manages a small amount of library state for things like configuration and logging. + +Generating Blanks +----------------- + +Blank nodes, or simply "blanks", +are used for resources that do not have URIs. +Unlike URIs, they are not global identifiers, +and only have meaning within their local context (for example, a document). +The world provides a method for automatically generating unique blank identifiers: + +.. literalinclude:: overview.cpp + :start-after: begin get-blank + :end-before: end get-blank + :dedent: 2 diff --git a/doc/cpp/xml/meson.build b/doc/cpp/xml/meson.build new file mode 100644 index 00000000..afc187d0 --- /dev/null +++ b/doc/cpp/xml/meson.build @@ -0,0 +1,19 @@ +doxygen = find_program('doxygen') + +cpp_doxygen_input = [] +foreach h : cpp_headers + cpp_doxygen_input += ['..' / h] +endforeach + +config = configuration_data() +config.set('SERD_SRCDIR', serd_src_root) +config.set('DOX_OUTPUT', meson.current_build_dir() / '..') + +cpp_doxyfile = configure_file(configuration: config, + input: files('../Doxyfile.in'), + output: 'Doxyfile') + +cpp_index_xml = custom_target('serd-cpp-index.xml', + command: [doxygen, '@INPUT0@'], + input: [cpp_doxyfile] + cpp_header_files, + output: 'index.xml') diff --git a/doc/meson.build b/doc/meson.build index 870ff308..546997e7 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -9,6 +9,10 @@ build_docs = doxygen.found() and sphinx_build.found() if build_docs subdir('_static') subdir('c') + + if is_variable('cpp') + subdir('cpp') + endif endif if not meson.is_subproject() and meson.version().version_compare('>=0.53.0') diff --git a/meson.build b/meson.build index 352c1734..db138663 100644 --- a/meson.build +++ b/meson.build @@ -6,6 +6,7 @@ project('serd', ['c'], 'b_ndebug=if-release', 'buildtype=release', 'c_std=c99', + 'cpp_std=c++14', 'default_library=shared', 'warning_level=2', ]) @@ -21,6 +22,10 @@ versioned_name = 'serd' + version_suffix pkg = import('pkgconfig') cc = meson.get_compiler('c') +if add_languages('cpp', required: get_option('bindings_cpp')) + cpp = meson.get_compiler('cpp') +endif + # Set ultra strict warnings for developers, if requested c_warnings = [] c_suppressions = [] @@ -251,6 +256,11 @@ endif # Install header to a versioned include directory install_headers(c_headers, subdir: versioned_name / 'serd') +# C++ bindings +if is_variable('cpp') + subdir('bindings/cpp') +endif + if not get_option('docs').disabled() subdir('doc') endif diff --git a/meson_options.txt b/meson_options.txt index 4562d9de..c95dc74e 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,6 @@ +option('bindings_cpp', type: 'feature', value: 'disabled', yield: true, + description: 'Build C++ bindings') + option('docs', type: 'feature', value: 'auto', yield: true, description: 'Build documentation') -- cgit v1.2.1