diff options
author | David Robillard <d@drobilla.net> | 2018-06-16 10:26:47 -0400 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2019-01-05 17:12:38 +0100 |
commit | 0f943c203ae9653efabb8168b82d2e56898c5fac (patch) | |
tree | ce32ff3f75525edfe02d08395350a44e0a93be75 | |
parent | 6650e22960f4dcd7d66dc560aae0347dc3272e1d (diff) | |
download | serd-0f943c203ae9653efabb8168b82d2e56898c5fac.tar.gz serd-0f943c203ae9653efabb8168b82d2e56898c5fac.tar.bz2 serd-0f943c203ae9653efabb8168b82d2e56898c5fac.zip |
WIP: Add C++ bindings
-rw-r--r-- | doc/reference.doxygen.in | 5 | ||||
-rw-r--r-- | serd/serd.hpp | 1111 | ||||
-rw-r--r-- | tests/serd_cxx_test.cpp | 381 | ||||
-rw-r--r-- | wscript | 21 |
4 files changed, 1512 insertions, 6 deletions
diff --git a/doc/reference.doxygen.in b/doc/reference.doxygen.in index 2a02e35e..e54553d3 100644 --- a/doc/reference.doxygen.in +++ b/doc/reference.doxygen.in @@ -780,7 +780,8 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = @SERD_SRCDIR@/serd/serd.h +INPUT = @SERD_SRCDIR@/serd/serd.h \ + @SERD_SRCDIR@/serd/serd.hpp # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses @@ -847,7 +848,7 @@ EXCLUDE_PATTERNS = # Note that the wildcards are matched against the file with absolute path, so to # exclude all test directories use the pattern */test/* -EXCLUDE_SYMBOLS = +EXCLUDE_SYMBOLS = detail # The EXAMPLE_PATH tag can be used to specify one or more files or directories # that contain example code fragments that are included (see the \include diff --git a/serd/serd.hpp b/serd/serd.hpp new file mode 100644 index 00000000..e7632212 --- /dev/null +++ b/serd/serd.hpp @@ -0,0 +1,1111 @@ +/* + Copyright 2018 David Robillard <http://drobilla.net> + + 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. +*/ + +/// @file serd.hpp C++ API for Serd, a lightweight RDF syntax library + +#ifndef SERD_SERD_HPP +#define SERD_SERD_HPP + +#include <serd/serd.h> + +#include <cassert> +#include <memory> +#include <ostream> +#include <type_traits> +#include <utility> + +/** + @defgroup serdxx Serdxx + C++ bindings for Serd, a lightweight RDF syntax library. + @{ +*/ + +namespace serd { +namespace detail { + +template <typename T> +using FreeFunc = void (*)(T*); + +template <typename T> +using CopyFunc = T* (*)(const T*); + +template <typename T> +void +no_free(T*) +{ +} + +template <typename T> +struct Deleter +{ + Deleter(FreeFunc<T> free_func) : _free_func(free_func) {} + + void operator()(T* ptr) + { + if (_free_func) { + _free_func(ptr); + } + } + + FreeFunc<T> _free_func; +}; + +/// C++ wrapper for a C object +template <typename T, void FreeFunc(T*)> +class Wrapper +{ +public: + using CType = T; + + Wrapper(T* ptr) : _ptr(ptr, Deleter<T>(FreeFunc)) {} + + Wrapper(const T* ptr) : _ptr(const_cast<T*>(ptr), nullptr) {} + + Wrapper(Wrapper&& wrapper) = default; + Wrapper& operator=(Wrapper&& wrapper) = default; + + Wrapper(const Wrapper&) = delete; + Wrapper& operator=(const Wrapper&) = delete; + + bool operator==(const Wrapper&) = delete; + bool operator!=(const Wrapper&) = delete; + + operator const T*() const { return _ptr.get(); } + + inline T* c_obj() { return _ptr.get(); } + inline const T* c_obj() const { return _ptr.get(); } + +protected: + Wrapper(std::nullptr_t) : _ptr(nullptr, nullptr) {} + + std::unique_ptr<T, Deleter<T>> _ptr; +}; + +template <typename T, T* CopyFunc(const T*), void FreeFunc(T*)> +class Copyable : public Wrapper<T, FreeFunc> +{ +public: + Copyable(T* ptr) : Wrapper<T, FreeFunc>(ptr) {} + + Copyable(const T* ptr) : Wrapper<T, FreeFunc>(ptr) {} + + Copyable(const Copyable& wrapper) + : Wrapper<T, FreeFunc>(CopyFunc(wrapper.c_obj())) + { + } + + Copyable(Copyable&&) = default; + Copyable& operator=(Copyable&&) = default; + + Copyable& operator=(const Copyable& wrapper) + { + this->_ptr = std::unique_ptr<T, Deleter<T>>(CopyFunc(wrapper.c_obj()), + FreeFunc); + return *this; + } + +protected: + Copyable(std::nullptr_t) : Wrapper<T, FreeFunc>(nullptr) {} +}; + +static inline size_t +stream_sink(const void* buf, size_t size, size_t nmemb, void* stream) +{ + try { + std::ostream* const s = static_cast<std::ostream*>(stream); + s->write(static_cast<const char*>(buf), std::streamsize(size * nmemb)); + if (s->good()) { + return nmemb; + } + } catch (...) { + } + return 0; +} + +/// Type-safe bit flags +template <typename Flag> +class Flags +{ +public: + static_assert(std::is_enum<Flag>::value, ""); + + using FlagUnderlyingType = typename std::underlying_type<Flag>::type; + using Value = typename std::make_unsigned<FlagUnderlyingType>::type; + + Flags() : _value(0) {} + Flags(const Flag f) : _value(static_cast<Value>(f)) {} + explicit Flags(const Value value) : _value{value} {} + + Flags operator|(const Flag rhs) const + { + return Flags{_value | static_cast<Value>(rhs)}; + } + + Flags operator|(const Flags rhs) const + { + return Flags{_value | rhs._value}; + } + + operator Value() const { return _value; } + +private: + Value _value{}; +}; + +} // namespace detail + +template <typename Flag> +inline typename std::enable_if<std::is_enum<Flag>::value, + detail::Flags<Flag>>::type +operator|(const Flag lhs, const Flag rhs) +{ + return detail::Flags<Flag>{lhs} | rhs; +} + +/// Return status code +enum class Status { + success = SERD_SUCCESS, ///< No error + failure = SERD_FAILURE, ///< Non-fatal failure + err_unknown = SERD_ERR_UNKNOWN, ///< Unknown error + err_bad_syntax = SERD_ERR_BAD_SYNTAX, ///< Invalid syntax + err_bad_arg = SERD_ERR_BAD_ARG, ///< Invalid argument + err_bad_iter = SERD_ERR_BAD_ITER, ///< Use of invalidated iterator + err_not_found = SERD_ERR_NOT_FOUND, ///< Not found + err_id_clash = SERD_ERR_ID_CLASH, ///< Clashing blank node IDs + err_bad_curie = SERD_ERR_BAD_CURIE, ///< Invalid CURIE + err_internal = SERD_ERR_INTERNAL, ///< Unexpected internal error + err_overflow = SERD_ERR_OVERFLOW, ///< Stack overflow + err_invalid = SERD_ERR_INVALID ///< Invalid data +}; + +/// RDF syntax type +enum class Syntax { + Turtle = SERD_TURTLE, ///< Terse RDF Triple Language + NTriples = SERD_NTRIPLES, ///< Line-based RDF triples + NQuads = SERD_NQUADS, ///< Line-based RDF quads (NTriples with graphs) + TriG = SERD_TRIG ///< Terse RDF quads (Turtle with graphs) +}; + +/// Flags indicating inline abbreviation information for a statement +enum class StatementFlag { + empty_S = SERD_EMPTY_S, ///< Empty blank node subject + anon_S = SERD_ANON_S, ///< Start of anonymous subject + anon_O = SERD_ANON_O, ///< Start of anonymous object + list_S = SERD_LIST_S, ///< Start of list subject + list_O = SERD_LIST_O ///< Start of list object +}; + +using StatementFlags = detail::Flags<StatementFlag>; + +/// Flags that control style for a model serialisation +enum class SerialisationFlag { + no_inline_objects = + SERD_NO_INLINE_OBJECTS, ///< Do not inline objects where possible +}; + +using SerialisationFlags = detail::Flags<SerialisationFlag>; + +/** + Type of a syntactic RDF node + + This is more precise than the type of an abstract RDF node. An abstract + node is either a resource, literal, or blank. In syntax there are two ways + to refer to a resource (by URI or CURIE) and two ways to refer to a blank + (by ID or anonymously). Anonymous (inline) blank nodes are expressed using + SerdStatementFlags rather than this type. +*/ +enum class NodeType { + /** + The type of a nonexistent node + + This type is useful as a sentinel, but is never emitted by the reader. + */ + nothing = SERD_NOTHING, + + /** + Literal value + + A literal optionally has either a language, or a datatype (not both). + */ + literal = SERD_LITERAL, + + /** + URI (absolute or relative) + + Value is an unquoted URI string, which is either a relative reference + with respect to the current base URI (e.g. "foo/bar"), or an absolute + URI (e.g. "http://example.org/foo"). + @see <a href="http://tools.ietf.org/html/rfc3986">RFC3986</a>. + */ + URI = SERD_URI, + + /** + CURIE, a shortened URI + + Value is an unquoted CURIE string relative to the current environment, + e.g. "rdf:type". + @see <a href="http://www.w3.org/TR/curie">CURIE Syntax 1.0</a> + */ + CURIE = SERD_CURIE, + + /** + A blank node + + Value is a blank node ID, e.g. "id3", which is meaningful only within + this serialisation. + @see <a href="http://www.w3.org/TeamSubmission/turtle#nodeID">Turtle + <tt>nodeID</tt></a> + */ + blank = SERD_BLANK +}; + +/// Flags indicating certain string properties relevant to serialisation +enum class NodeFlag { + has_newline = SERD_HAS_NEWLINE, ///< Contains line breaks + has_quote = SERD_HAS_QUOTE, ///< Contains quotes + has_datatype = SERD_HAS_DATATYPE, ///< Literal node has datatype + has_language = SERD_HAS_LANGUAGE ///< Literal node has language +}; + +using NodeFlags = detail::Flags<NodeFlag>; + +/// Field in a statement +enum class Field { + subject = SERD_SUBJECT, ///< Subject + predicate = SERD_PREDICATE, ///< Predicate ("key") + object = SERD_OBJECT, ///< Object ("value") + graph = SERD_GRAPH ///< Graph ("context") +}; + +/** + Syntax style options + + The style of the writer output can be controlled by ORing together + values from this enumeration. Note that some options are only supported + for some syntaxes (e.g. NTriples does not support abbreviation and is + always ASCII). +*/ +enum class WriterFlag { + ascii = SERD_STYLE_ASCII, ///< Escape all non-ASCII characters +}; + +using WriterFlags = detail::Flags<WriterFlag>; + +/// Indexing option +enum class ModelFlag { + index_SPO = SERD_INDEX_SPO, ///< Subject, Predicate, Object + index_SOP = SERD_INDEX_SOP, ///< Subject, Object, Predicate + index_OPS = SERD_INDEX_OPS, ///< Object, Predicate, Subject + index_OSP = SERD_INDEX_OSP, ///< Object, Subject, Predicate + index_PSO = SERD_INDEX_PSO, ///< Predicate, Subject, Object + index_POS = SERD_INDEX_POS, ///< Predicate, Object, Subject + index_graphs = SERD_INDEX_GRAPHS ///< Support multiple graphs in model +}; + +using ModelFlags = detail::Flags<ModelFlag>; + +/** + 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. +*/ +template <typename T> +class Optional +{ +public: + using CType = typename T::CType; + + Optional() : _value() {} + Optional(std::nullptr_t) : _value() {} + Optional(const T& value) : _value(value) {} + Optional(T&& value) : _value(std::move(value)) {} + + Optional(const Optional&) = default; + Optional(Optional&&) = default; + Optional& operator=(const Optional&) = default; + Optional& operator=(Optional&&) = default; + + void reset() { *this = nullptr; } + + const T& operator*() const + { + assert(_value.c_obj()); + return _value; + } + + T& operator*() + { + assert(_value.c_obj()); + return _value; + } + + const T* operator->() const + { + assert(_value.c_obj()); + return &_value; + } + + T* operator->() + { + assert(_value.c_obj()); + return &_value; + } + + bool operator==(const Optional& optional) + { + return (!*this && !optional) || + (*this && optional && _value == optional._value); + } + + bool operator!=(const Optional& optional) { return !operator==(optional); } + + explicit operator bool() const { return _value.c_obj(); } + + bool operator!() const { return !_value.c_obj(); } + + inline CType* ptr() { return _value.c_obj(); } + inline const CType* ptr() const { return _value.c_obj(); } + +private: + T _value; +}; + +/** + A temporary reference to a string + + This is used as a function parameter type to allow passing either + `std::string` or `const char*` without requiring multiple wrapper + definitions or the copying overhead of `std::string`. +*/ +struct StringRef +{ + StringRef(const char* str) : _ptr(str) {} + StringRef(const std::string& str) : _ptr(str.c_str()) {} + + StringRef(const StringRef&) = delete; + StringRef& operator=(const StringRef&) = delete; + + operator const char*() const { return _ptr; } + +private: + const char* const _ptr; +}; + +/** + @name String Utilities + @{ +*/ + +inline const char* +strerror(const Status status) +{ + return serd_strerror(static_cast<SerdStatus>(status)); +} + +/** + @} + @name URI + @{ +*/ + +inline std::string +file_uri_parse(const StringRef& uri, std::string* hostname = nullptr) +{ + char* c_hostname = nullptr; + char* c_path = serd_file_uri_parse(uri, &c_hostname); + if (hostname && c_hostname) { + *hostname = c_hostname; + } + const std::string path(c_path); + free(c_hostname); + free(c_path); + return path; +} + +/** + @} + @name Node + @{ +*/ + +class Node : public detail::Copyable<SerdNode, serd_node_copy, serd_node_free> +{ +public: + explicit Node(SerdNode* ptr) : Copyable(ptr) {} + explicit Node(const SerdNode* ptr) : Copyable(ptr) {} + + NodeType type() const { return NodeType(serd_node_get_type(c_obj())); } + const char* c_str() const { return serd_node_get_string(c_obj()); } + std::string str() const { return c_str(); } + size_t size() const { return serd_node_get_length(c_obj()); } + size_t length() const { return serd_node_get_length(c_obj()); } + NodeFlags flags() const { return NodeFlags(serd_node_get_flags(c_obj())); } + + Optional<Node> datatype() const + { + return Node(serd_node_get_datatype(c_obj())); + } + + Optional<Node> language() const + { + return Node(serd_node_get_language(c_obj())); + } + + Node resolve(const Node& base) const + { + return Node(serd_node_resolve(c_obj(), base.c_obj())); + } + + bool operator==(const Node& node) const + { + return serd_node_equals(c_obj(), node.c_obj()); + } + + bool operator!=(const Node& node) const + { + return !serd_node_equals(c_obj(), node.c_obj()); + } + + bool operator<(const Node& node) const + { + return serd_node_compare(c_obj(), node.c_obj()) < 0; + } + + operator std::string() const + { + return std::string(serd_node_get_string(c_obj()), + serd_node_get_length(c_obj())); + } + +private: + friend class Optional<Node>; + Node() : Copyable(nullptr) {} +}; + +inline std::ostream& +operator<<(std::ostream& os, const Node& node) +{ + return os << node.c_str(); +} + +inline Node +make_string(const StringRef& str) +{ + return Node(serd_new_string(str)); +} + +inline Node +make_plain_literal(const StringRef& str, const StringRef& lang) +{ + return Node(serd_new_plain_literal(str, lang)); +} + +inline Node +make_typed_literal(const StringRef& str, const Node& datatype) +{ + return Node(serd_new_typed_literal(str, datatype.c_obj())); +} + +inline Node +make_blank(const StringRef& str) +{ + return Node(serd_new_blank(str)); +} + +inline Node +make_curie(const StringRef& str) +{ + return Node(serd_new_curie(str)); +} + +inline Node +make_uri(const StringRef& str) +{ + return Node(serd_new_uri(str)); +} + +inline Node +make_resolved_uri(const StringRef& str, const Node& base) +{ + return Node(serd_new_resolved_uri(str, base.c_obj())); +} + +inline Node +make_file_uri(const StringRef& path) +{ + return Node(serd_new_file_uri(path, nullptr)); +} + +inline Node +make_file_uri(const StringRef& path, const StringRef& hostname) +{ + return Node(serd_new_file_uri(path, hostname)); +} + +inline Node +make_relative_uri(const StringRef& str, + const Node& base, + const Optional<Node>& root = {}) +{ + return Node(serd_new_relative_uri(str, base.c_obj(), root.ptr())); +} + +inline Node +make_decimal(double d, + unsigned frac_digits, + const Optional<Node>& datatype = {}) +{ + return Node(serd_new_decimal(d, frac_digits, datatype.ptr())); +} + +inline Node +make_integer(int64_t i, const Optional<Node>& datatype = {}) +{ + return Node(serd_new_integer(i, datatype.ptr())); +} + +inline Node +make_blob(const void* buf, + size_t size, + bool wrap_lines, + const Optional<Node>& datatype = {}) +{ + return Node(serd_new_blob(buf, size, wrap_lines, datatype.ptr())); +} + +/** + @} + @name World + @{ +*/ + +class World : public detail::Wrapper<SerdWorld, serd_world_free> +{ +public: + World() : Wrapper(serd_world_new()) {} + + explicit World(SerdWorld* ptr) : Wrapper(ptr) {} + + Node get_blank() { return Node(serd_world_get_blank(c_obj())); } + + void set_message_sink(SerdMessageSink msg_sink, void* handle) + { + serd_world_set_message_sink(c_obj(), msg_sink, handle); + } +}; + +/** + @} + @name Statement + @{ +*/ + +class Statement : public detail::Wrapper<SerdStatement, detail::no_free> +{ +public: + Statement(const SerdStatement* statement) : Wrapper(statement) {} + + Node node(Field field) + { + return Node(serd_statement_get_node(c_obj(), + static_cast<SerdField>(field))); + } + + Node subject() const { return Node(serd_statement_get_subject(c_obj())); } + + Node predicate() const + { + return Node(serd_statement_get_predicate(c_obj())); + } + + Node object() const { return Node(serd_statement_get_object(c_obj())); } + + Node graph() const { return Node(serd_statement_get_graph(c_obj())); } +}; + +/** + @} + @name Sink + @{ +*/ + +class Sink +{ +public: + Sink() + : _sink(serd_sink_new(this, NULL), + detail::Deleter<SerdSink>(serd_sink_free)) + , _is_user(true) + { + serd_sink_set_base_func(_sink.get(), s_base); + serd_sink_set_prefix_func(_sink.get(), s_prefix); + serd_sink_set_statement_func(_sink.get(), s_statement); + serd_sink_set_end_func(_sink.get(), s_end); + } + + explicit Sink(const SerdSink* ptr) + : _sink(const_cast<SerdSink*>(ptr), detail::Deleter<SerdSink>(nullptr)) + , _is_user(false) + { + } + + virtual ~Sink() = default; + + virtual Status base(const Node& uri) + { + return _is_user ? Status::success + : Status(serd_sink_set_base(_sink.get(), uri.c_obj())); + } + + virtual Status prefix(const Node& name, const Node& uri) + { + return _is_user ? Status::success + : Status(serd_sink_set_prefix( + _sink.get(), name.c_obj(), uri.c_obj())); + } + + virtual Status statement(StatementFlags flags, const Statement& statement) + { + return _is_user ? Status::success + : Status(serd_sink_write_statement( + _sink.get(), flags, statement.c_obj())); + } + + virtual Status write(StatementFlags flags, + const Node& subject, + const Node& predicate, + const Node& object, + const Optional<Node>& graph = {}) + { + return _is_user ? Status::success + : Status(serd_sink_write(_sink.get(), + flags, + subject.c_obj(), + predicate.c_obj(), + object.c_obj(), + graph.ptr())); + } + + virtual Status end(const Node& node) + { + return _is_user ? Status::success + : Status(serd_sink_end(_sink.get(), node.c_obj())); + } + + const SerdSink* c_sink() const { return _sink.get(); } + SerdSink* c_sink() { return _sink.get(); } + +private: + static SerdStatus s_base(void* handle, const SerdNode* uri) + { + Sink* const sink = static_cast<Sink*>(handle); + return static_cast<SerdStatus>(sink->base(Node(uri))); + } + + static SerdStatus + s_prefix(void* handle, const SerdNode* name, const SerdNode* uri) + { + Sink* const sink = static_cast<Sink*>(handle); + return static_cast<SerdStatus>(sink->prefix(Node(name), Node(uri))); + } + + static SerdStatus s_statement(void* handle, + SerdStatementFlags flags, + const SerdStatement* statement) + { + Sink* const sink = static_cast<Sink*>(handle); + return static_cast<SerdStatus>( + sink->statement(StatementFlags(flags), statement)); + } + + static SerdStatus s_end(void* handle, const SerdNode* node) + { + Sink* const sink = static_cast<Sink*>(handle); + return static_cast<SerdStatus>(sink->end(Node(node))); + } + + std::unique_ptr<SerdSink, detail::Deleter<SerdSink>> _sink; + bool _is_user; +}; + +/** + @} + @name Environment + @{ +*/ + +class Env : public detail::Copyable<SerdEnv, serd_env_copy, serd_env_free> +{ +public: + explicit Env(const Optional<Node>& base = {}) + : Copyable(serd_env_new(base.ptr())) + { + } + + /// Return the base URI + Node base_uri() const { return Node(serd_env_get_base_uri(c_obj())); } + + /// Set the base URI + Status set_base_uri(const Node& uri) + { + return Status(serd_env_set_base_uri(c_obj(), uri.c_obj())); + } + + /// Set a namespace prefix + Status set_prefix(const Node& name, const Node& uri) + { + return Status(serd_env_set_prefix(c_obj(), name.c_obj(), uri.c_obj())); + } + + /// Set a namespace prefix + Status set_prefix(const StringRef& name, const StringRef& uri) + { + return Status(serd_env_set_prefix_from_strings(c_obj(), name, uri)); + } + + /// Qualify `uri` into a CURIE if possible + Optional<Node> qualify(const Node& uri) const + { + return Node(serd_env_qualify(c_obj(), uri.c_obj())); + } + + /// Expand `node` into an absolute URI if possible + Optional<Node> expand(const Node& node) const + { + return Node(serd_env_expand(c_obj(), node.c_obj())); + } + + /// Send all prefixes to `sink` + void send_prefixes(Sink& sink) const + { + serd_env_send_prefixes(c_obj(), sink.c_sink()); + } + + bool operator==(const Env& env) + { + return serd_env_equals(c_obj(), env.c_obj()); + } + + bool operator!=(const Env& env) { return !operator==(env); } +}; + +/** + @} + @name Reader + @{ +*/ + +class Reader : public detail::Wrapper<SerdReader, serd_reader_free> +{ +public: + Reader(World& world, Syntax syntax, const Sink& sink, size_t stack_size) + : Wrapper(serd_reader_new(world.c_obj(), + SerdSyntax(syntax), + sink.c_sink(), + stack_size)) + { + } + + void set_strict(bool strict) { serd_reader_set_strict(c_obj(), strict); } + + void add_blank_prefix(const StringRef& prefix) + { + serd_reader_add_blank_prefix(c_obj(), prefix); + } + + SerdStatus start_file(const StringRef& uri, bool bulk = true) + { + return serd_reader_start_file(c_obj(), uri, bulk); + } + + SerdStatus start_stream(SerdReadFunc read_func, + SerdStreamErrorFunc error_func, + void* stream, + const Node& name, + size_t page_size) + { + return serd_reader_start_stream(c_obj(), + read_func, + error_func, + stream, + name.c_obj(), + page_size); + } + + SerdStatus + start_string(const StringRef& utf8, const Optional<Node>& name = {}) + { + return serd_reader_start_string(c_obj(), utf8, name.ptr()); + } + + SerdStatus read_chunk() { return serd_reader_read_chunk(c_obj()); } + + SerdStatus read_document() { return serd_reader_read_document(c_obj()); } + + SerdStatus finish() { return serd_reader_finish(c_obj()); } +}; + +/** + @} + @name Writer + @{ +*/ + +class ByteSink : public detail::Wrapper<SerdByteSink, serd_byte_sink_free> +{ +public: + ByteSink(SerdWriteFunc sink, void* stream, size_t block_size) + : Wrapper(serd_byte_sink_new(sink, stream, block_size)) + { + } + + ByteSink(std::ostream& stream) + : Wrapper(serd_byte_sink_new(detail::stream_sink, &stream, 1)) + { + } +}; + +class Writer : public detail::Wrapper<SerdWriter, serd_writer_free>, public Sink +{ +public: + Writer(World& world, + const Syntax syntax, + const WriterFlags writer_flags, + Env& env, + ByteSink& sink) + : Wrapper(serd_writer_new( + world.c_obj(), + SerdSyntax(syntax), + writer_flags, + env.c_obj(), + reinterpret_cast<SerdWriteFunc>(serd_byte_sink_write), + sink.c_obj())) + , Sink(serd_writer_get_sink(c_obj())) + { + } + + Writer(World& world, + const Syntax syntax, + const WriterFlags writer_flags, + Env& env, + SerdWriteFunc write_func, + void* stream) + : Wrapper(serd_writer_new(world.c_obj(), + SerdSyntax(syntax), + writer_flags, + env.c_obj(), + write_func, + stream)) + , Sink(serd_writer_get_sink(c_obj())) + { + } + + Writer(World& world, + const Syntax syntax, + const WriterFlags writer_flags, + Env& env, + std::ostream& stream) + : Wrapper(serd_writer_new(world.c_obj(), + SerdSyntax(syntax), + writer_flags, + env.c_obj(), + detail::stream_sink, + &stream)) + , Sink(serd_writer_get_sink(c_obj())) + { + } + + Status set_root_uri(const Node& uri) + { + return Status(serd_writer_set_root_uri(c_obj(), uri.c_obj())); + } + + SerdStatus finish() { return serd_writer_finish(c_obj()); } +}; + +/** + @} +*/ + +class Iter : public detail::Copyable<SerdIter, serd_iter_copy, serd_iter_free> +{ +public: + Iter(SerdIter* i) : Copyable(i) {} + Iter(const SerdIter* i) : Copyable(i) {} + + Iter& operator++() + { + serd_iter_next(c_obj()); + return *this; + } + + Statement operator*() const { return Statement(serd_iter_get(c_obj())); } + + bool operator==(const Iter& i) const + { + return serd_iter_equals(c_obj(), i.c_obj()); + } + + bool operator!=(const Iter& i) const + { + return !serd_iter_equals(c_obj(), i.c_obj()); + } +}; + +class Range + : public detail::Copyable<SerdRange, serd_range_copy, serd_range_free> +{ +public: + Range(SerdRange* r) : Copyable(r) {} + Range(const SerdRange* r) : Copyable(r) {} + + Iter begin() const { return Iter(serd_range_begin(c_obj())); } + Iter end() const { return Iter(serd_range_end(c_obj())); } + + bool operator==(const Range& i) const + { + return serd_range_equals(c_obj(), i.c_obj()); + } + + bool operator!=(const Range& i) const + { + return !serd_range_equals(c_obj(), i.c_obj()); + } +}; + +class Model + : public detail::Copyable<SerdModel, serd_model_copy, serd_model_free> +{ +public: + using value_type = Statement; + using iterator = Iter; + using const_iterator = Iter; + + Model(World& world, ModelFlags flags) + : Copyable(serd_model_new(world.c_obj(), flags)) + { + } + + Model(World& world, ModelFlag flag) + : Copyable(serd_model_new(world.c_obj(), ModelFlags(flag))) + { + } + + size_t size() const { return serd_model_size(c_obj()); } + + bool empty() const { return serd_model_empty(c_obj()); } + + void insert(const Statement& s) { serd_model_insert(c_obj(), s.c_obj()); } + + void insert(const Node& s, + const Node& p, + const Node& o, + const Optional<Node>& g = {}) + { + serd_model_add(c_obj(), s.c_obj(), p.c_obj(), o.c_obj(), g.ptr()); + } + + Iter find(const Optional<Node>& s, + const Optional<Node>& p, + const Optional<Node>& o, + const Optional<Node>& g = {}) const + { + return Iter( + serd_model_find(c_obj(), s.ptr(), p.ptr(), o.ptr(), g.ptr())); + } + + Range range(const Optional<Node>& s, + const Optional<Node>& p, + const Optional<Node>& o, + const Optional<Node>& g = {}) const + { + return Range( + serd_model_range(c_obj(), s.ptr(), p.ptr(), o.ptr(), g.ptr())); + } + + Optional<Node> get(const Optional<Node>& s, + const Optional<Node>& p, + const Optional<Node>& o, + const Optional<Node>& g = {}) const + { + return Node( + serd_model_get(c_obj(), s.ptr(), p.ptr(), o.ptr(), g.ptr())); + } + + Optional<Statement> get_statement(const Optional<Node>& s, + const Optional<Node>& p, + const Optional<Node>& o, + const Optional<Node>& g = {}) const + { + return Statement(serd_model_get_statement( + c_obj(), s.ptr(), p.ptr(), o.ptr(), g.ptr())); + } + + bool ask(const Optional<Node>& s, + const Optional<Node>& p, + const Optional<Node>& o, + const Optional<Node>& g = {}) const + { + return serd_model_ask(c_obj(), s.ptr(), p.ptr(), o.ptr(), g.ptr()); + } + + size_t count(const Optional<Node>& s, + const Optional<Node>& p, + const Optional<Node>& o, + const Optional<Node>& g = {}) const + { + return serd_model_count(c_obj(), s.ptr(), p.ptr(), o.ptr(), g.ptr()); + } + + bool operator==(const Model& model) const + { + return serd_model_equals(c_obj(), model.c_obj()); + } + + bool operator!=(const Model& model) const + { + return !serd_model_equals(c_obj(), model.c_obj()); + } + + Range all() const { return Range(serd_model_all(c_obj())); } + + iterator begin() const { return iterator(serd_model_begin(c_obj())); } + + iterator end() const { return iterator(serd_model_end(c_obj())); } +}; + +class Inserter : public detail::Wrapper<SerdInserter, serd_inserter_free>, + public Sink +{ +public: + Inserter(Model& model, Env& env, const Optional<Node>& default_graph = {}) + : Wrapper(serd_inserter_new(model.c_obj(), + env.c_obj(), + default_graph.ptr())) + , Sink(serd_inserter_get_sink(c_obj())) + { + } +}; + +} // namespace serd + +/** + @} +*/ + +#endif /* SERD_SERD_HPP */ diff --git a/tests/serd_cxx_test.cpp b/tests/serd_cxx_test.cpp new file mode 100644 index 00000000..2196df56 --- /dev/null +++ b/tests/serd_cxx_test.cpp @@ -0,0 +1,381 @@ +/* + Copyright 2018 David Robillard <http://drobilla.net> + + 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 <cassert> +#include <iostream> +#include <sstream> +#include <type_traits> +#include <utility> +#include <vector> + +template <typename T> +static int +test_move_only(T&& obj) +{ + static_assert(!std::is_copy_constructible<T>::value, ""); + static_assert(!std::is_copy_assignable<T>::value, ""); + + const auto* const ptr = obj.c_obj(); + + // Move construct + T moved{std::move(obj)}; + assert(moved.c_obj() == ptr); + assert(!obj.c_obj()); + + // Move assign + obj = std::move(moved); + assert(obj.c_obj() == ptr); + assert(!moved.c_obj()); + + return 0; +} + +template <typename T> +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); + + 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); + + return 0; +} + +static int +test_operators() +{ + int st = 0; + + serd::World world; + + serd::Model model(world, serd::ModelFlag::index_SPO); + model.insert(serd::make_uri("http://example.org/s"), + serd::make_uri("http://example.org/p"), + serd::make_uri("http://example.org/o")); + + serd::Sink sink; + serd::Env env; + + std::ostringstream stream; + serd::ByteSink byte_sink{stream}; + + st |= test_copy_move( + serd::Optional<serd::Node>{serd::make_string("hello")}); + st |= test_move_only(serd::World{}); + st |= test_move_only(*model.begin()); + st |= test_copy_move(serd::Env{}); + st |= test_move_only(serd::Reader{world, serd::Syntax::Turtle, sink, 4096}); + st |= test_move_only(serd::ByteSink{stream}); + 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{}); + + return st; +} + +static int +test_optional() +{ + test_copy_move(serd::Optional<serd::Node>(serd::make_string("node"))); + + const serd::Node node = serd::make_string("node"); + const serd::Node other = serd::make_string("other"); + + // Truthiness + assert(!serd::Optional<serd::Node>()); + assert(!serd::Optional<serd::Node>(nullptr)); + assert(serd::Optional<serd::Node>(node)); + + // Comparison and general sanity + serd::Optional<serd::Node> optional{node}; + assert(optional); + assert(optional == node); + assert(optional != other); + assert(*optional == node); + assert(optional->str() == "node"); + assert(optional.ptr() != node.c_obj()); // non-const, must be a copy + + // Reset + optional.reset(); + assert(!optional); + assert(!optional.ptr()); + + // Copying and moving + serd::Node nonconst = serd::make_string("nonconst"); + const SerdNode* c_node = nonconst.c_obj(); + + optional = nonconst; + serd::Optional<serd::Node> copied{optional}; + assert(copied == nonconst); + assert(copied.ptr() != c_node); + + optional = std::move(nonconst); + serd::Optional<serd::Node> moved{std::move(optional)}; + assert(moved.ptr() == c_node); + assert(!optional); + + serd::Optional<serd::Node> copy_assigned; + copy_assigned = optional; + assert(copy_assigned == optional); + assert(copy_assigned.ptr() != c_node); + + serd::Optional<serd::Node> move_assigned; + move_assigned = std::move(moved); + assert(move_assigned.ptr() == c_node); + assert(!optional); + + return 0; +} + +static int +test_node(const serd::Node& node) +{ + test_copy_move(node); + + if (node.datatype()) { + return test_node(*node.datatype()); + } else if (node.language()) { + return test_node(*node.language()); + } + return 0; +} + +static int +test_nodes() +{ + using namespace serd; + + const auto type = make_uri("http://example.org/Type"); + const auto base = make_uri("http://example.org/"); + const auto root = make_uri("http://example.org/"); + + assert(!test_node(make_string("hello"))); + assert(!test_node(make_plain_literal("hello", "en"))); + assert(!test_node(make_typed_literal("hello", type))); + assert(!test_node(make_blank("blank"))); + assert(!test_node(make_curie("eg:curie"))); + assert(!test_node(make_uri("http://example.org/thing"))); + assert(!test_node(make_resolved_uri("thing", base))); + assert(!test_node(make_file_uri("/foo/bar", "host"))); + assert(!test_node(make_file_uri("/foo/bar"))); + assert(!test_node(make_file_uri("/foo/bar", "host"))); + assert(!test_node(make_file_uri("/foo/bar"))); + assert(!test_node(make_relative_uri("http://example.org/a", base))); + assert(!test_node(make_relative_uri("http://example.org/a", base, root))); + assert(!test_node(make_decimal(1.2, 7))); + assert(!test_node(make_decimal(3.4, 7, type))); + assert(!test_node(make_integer(56))); + assert(!test_node(make_integer(78, type))); + assert(!test_node(make_blob("blob", 4, true))); + assert(!test_node(make_blob("blob", 4, true, type))); + + return 0; +} + +static int +test_reader() +{ + struct Sink : public serd::Sink + { + serd::Status statement(serd::StatementFlags, + const serd::Statement& statement) override + { + ++n_statements; + stream << statement.subject() << " " << statement.predicate() << " " + << statement.object() << std::endl; + return serd::Status::success; + } + + size_t n_statements{}; + std::stringstream stream{}; + }; + + Sink sink; + serd::World world; + serd::Reader reader(world, serd::Syntax::Turtle, sink, 4096); + + reader.start_string("@prefix eg: <http://example.org> ." + "eg:s eg:p eg:o1 , eg:o2 ."); + reader.read_document(); + + assert(sink.n_statements == 2); + assert(sink.stream.str() == "eg:s eg:p eg:o1\neg:s eg:p eg:o2\n"); + + return 0; +} + +static int +test_writer() +{ + serd::World world; + serd::Env env; + std::ostringstream stream; + serd::ByteSink sink(stream); + serd::Writer writer(world, serd::Syntax::Turtle, {}, env, stream); + + writer.base(serd::make_uri("http://drobilla.net/base/")); + writer.set_root_uri(serd::make_uri("http://drobilla.net/")); + writer.prefix(serd::make_string("eg"), + serd::make_uri("http://example.org/")); + writer.write({}, + serd::make_uri("http://drobilla.net/base/s"), + serd::make_uri("http://example.org/p"), + serd::make_uri("http://drobilla.net/o")); + + writer.finish(); + + assert(stream.str() == "@base <http://drobilla.net/base/> .\n" + "@prefix eg: <http://example.org/> .\n" + "\n" + "<s>\n" + "\teg:p <../o> .\n"); + + return 0; +} + +static int +test_env() +{ + serd::Env env; + + const auto base = serd::make_uri("http://drobilla.net/"); + env.set_base_uri(base); + assert(env.base_uri() == base); + + env.set_prefix(serd::make_string("eg"), + serd::make_uri("http://example.org/")); + + assert(env.qualify(serd::make_uri("http://example.org/foo")) == + serd::make_curie("eg:foo")); + + assert(env.expand(serd::make_uri("foo")) == + serd::make_uri("http://drobilla.net/foo")); + + serd::Env copied{env}; + assert(copied.qualify(serd::make_uri("http://example.org/foo")) == + serd::make_curie("eg:foo")); + + assert(copied.expand(serd::make_uri("foo")) == + serd::make_uri("http://drobilla.net/foo")); + + serd::Env assigned; + assigned = env; + auto curie = env.qualify(serd::make_uri("http://example.org/foo")); + + assert(assigned.qualify(serd::make_uri("http://example.org/foo")) == + serd::make_curie("eg:foo")); + + assert(assigned.expand(serd::make_uri("foo")) == + serd::make_uri("http://drobilla.net/foo")); + + return 0; +} + +static int +test_model() +{ + serd::World world; + serd::Model model(world, + serd::ModelFlag::index_SPO | serd::ModelFlag::index_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"); + + model.insert(s, p, o1); + model.insert(s, p, o2); + + assert(!model.empty()); + assert(model.size() == 2); + assert(model.ask(s, p, o1)); + assert(model.ask(s, p, o1)); + 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.range({}, {}, o1)) { + 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.range({}, {}, o2)) { + assert(statement.subject() == s); + assert(statement.predicate() == p); + assert(statement.object() == o2); + ++o2_count; + } + assert(o2_count == 1); + + serd::Model copy(model); + assert(copy == model); + + copy.insert(s, p, s); + assert(copy != model); + + return 0; +} + +int +main() +{ + int st = 0; + + st |= test_operators(); + st |= test_optional(); + st |= test_nodes(); + st |= test_env(); + st |= test_reader(); + st |= test_writer(); + st |= test_model(); + + return st; +} @@ -21,6 +21,7 @@ out = 'build' # Build directory def options(ctx): ctx.load('compiler_c') + ctx.load('compiler_cxx') autowaf.set_options(ctx, test=True) opt = ctx.get_option_group('Configuration options') autowaf.add_flags( @@ -39,8 +40,10 @@ def options(ctx): def configure(conf): autowaf.display_header('Serd Configuration') conf.load('compiler_c', cache=True) + conf.load('compiler_cxx', cache=True) conf.load('autowaf', cache=True) autowaf.set_c_lang(conf, 'c99') + autowaf.set_cxx_lang(conf, 'c++11') conf.env.update({ 'BUILD_UTILS': not Options.options.no_utils, @@ -127,7 +130,7 @@ lib_source = ['src/base64.c', def build(bld): # C Headers includedir = '${INCLUDEDIR}/serd-%s/serd' % SERD_MAJOR_VERSION - bld.install_files(includedir, bld.path.ant_glob('serd/*.h')) + bld.install_files(includedir, bld.path.ant_glob('serd/*.h*')) # Pkgconfig file autowaf.build_pc(bld, 'SERD', SERD_VERSION, SERD_MAJOR_VERSION, [], @@ -167,6 +170,7 @@ def build(bld): if bld.env.BUILD_TESTS: test_args = {'includes': ['.', './src'], 'cflags': [''] if bld.env.NO_COVERAGE else ['--coverage'], + 'cxxflags': [''] if bld.env.NO_COVERAGE else ['--coverage', '-fno-inline'], 'linkflags': [''] if bld.env.NO_COVERAGE else ['--coverage'], 'lib': lib_args['lib'], 'install_path': ''} @@ -193,6 +197,13 @@ def build(bld): defines = defines, **test_args) + bld(features = 'cxx cxxprogram', + source = 'tests/serd_cxx_test.cpp', + use = 'libserd_profiled', + target = 'serd_cxx_test', + defines = defines, + **test_args) + # Utilities if bld.env.BUILD_UTILS: for i in ['serdi', 'serd_validate']: @@ -535,14 +546,16 @@ def test(ctx): srcdir = ctx.path.abspath() os.environ['PATH'] = '.' + os.pathsep + os.getenv('PATH') - autowaf.pre_test(ctx, APPNAME) + dirs = ['.', 'src', 'tests'] + autowaf.pre_test(ctx, APPNAME, dirs=dirs) autowaf.run_tests(ctx, APPNAME, ['serd_test', + 'serd_cxx_test', 'read_chunk_test', 'nodes_test', 'overflow_test', 'model_test'], - name='Unit') + dirs=dirs, name='Unit') def test_syntax_io(in_name, expected_name, lang): in_path = 'tests/good/%s' % in_name @@ -608,7 +621,7 @@ def test(ctx): for opts in ['', '-m']: run_test_suites(ctx, opts) - autowaf.post_test(ctx, APPNAME) + autowaf.post_test(ctx, APPNAME, dirs=dirs) def posts(ctx): path = str(ctx.path.abspath()) |