diff options
-rw-r--r-- | doc/reference.doxygen.in | 5 | ||||
-rw-r--r-- | serd/serd.hpp | 1029 | ||||
-rw-r--r-- | tests/serd_cxx_test.cpp | 397 | ||||
-rw-r--r-- | wscript | 25 |
4 files changed, 1448 insertions, 8 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..bb2d48a1 --- /dev/null +++ b/serd/serd.hpp @@ -0,0 +1,1029 @@ +/* + 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 { + +class Node; + +/** + Return status code. +*/ +enum class Status { + Success = SERD_SUCCESS, /**< No error */ + Failure = SERD_FAILURE, /**< Non-fatal failure */ + ErrUnknown = SERD_ERR_UNKNOWN, /**< Unknown error */ + ErrBadSyntax = SERD_ERR_BAD_SYNTAX, /**< Invalid syntax */ + ErrBadArg = SERD_ERR_BAD_ARG, /**< Invalid argument */ + ErrNotFound = SERD_ERR_NOT_FOUND, /**< Not found */ + ErrIdClash = SERD_ERR_ID_CLASH, /**< Clashing blank node IDs */ + ErrBadCurie = SERD_ERR_BAD_CURIE, /**< Invalid CURIE */ + ErrInternal = SERD_ERR_INTERNAL /**< Unexpected internal error */ +}; + +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) {} +}; + +template <typename Traits> +size_t +stream_sink(const void* buf, size_t size, size_t nmemb, void* stream) +{ + using Stream = std::basic_ostream<char, Traits>; + + try { + Stream* const s = static_cast<Stream*>(stream); + s->write(static_cast<const char*>(buf), std::streamsize(size * nmemb)); + if (s->good()) { + return nmemb; + } + } catch (...) { + } + return 0; +} + +} // namespace detail + +/** + 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. + The fact that these are interally just pointers is used 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; +}; + +using Type = SerdType; +using NodeFlags = SerdNodeFlags; +using StyleFlags = SerdStyleFlags; +using IndexOptions = SerdIndexOptions; +using Field = SerdField; +using StatementFlags = SerdStatementFlags; + +class Statement; + +/** + RDF syntax. +*/ +enum class Syntax { + /** + Turtle - Terse RDF Triple Language (UTF-8). + @see <a href="http://www.w3.org/TeamSubmission/turtle/">Turtle</a> + */ + Turtle = SERD_TURTLE, + + /** + NTriples - Line-based RDF triples (ASCII). + @see <a href="http://www.w3.org/TR/rdf-testcases#ntriples">NTriples</a> + */ + NTriples = SERD_NTRIPLES, + + /** + NQuads - Line-based RDF quads (UTF-8). + @see <a href="https://www.w3.org/TR/n-quads/">NQuads</a> + */ + NQuads = SERD_NQUADS, + + /** + TriG - Terse RDF quads (UTF-8). + @see <a href="https://www.w3.org/TR/trig/">Trig</a> + */ + TriG = SERD_TRIG +}; + +/** + 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 Style { + ascii = SERD_STYLE_ASCII, /**< Escape all non-ASCII characters. */ +}; + +inline StyleFlags +operator|(const Style a, const Style b) +{ + return (StyleFlags)(int(a) | int(b)); +} + +inline StyleFlags +operator|(const StyleFlags a, const Style b) +{ + return (StyleFlags)(int(a) | int(b)); +} + +/** + Indexing option. +*/ +enum class IndexOption { + SPO = SERD_SPO, /**< Subject, Predicate, Object */ + SOP = SERD_SOP, /**< Subject, Object, Predicate */ + OPS = SERD_OPS, /**< Object, Predicate, Subject */ + OSP = SERD_OSP, /**< Object, Subject, Predicate */ + PSO = SERD_PSO, /**< Predicate, Subject, Object */ + POS = SERD_POS /**< Predicate, Object, Subject */ +}; + +inline IndexOptions +operator|(const IndexOption a, const IndexOption b) +{ + return (IndexOptions)(int(a) | int(b)); +} + +inline IndexOptions +operator|(const IndexOptions a, const IndexOption b) +{ + return (IndexOptions)(int(a) | int(b)); +} + +/** + @name String Utilities + @{ +*/ + +inline const char* +strerror(const Status status) +{ + return serd_strerror((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) {} + + Type type() const { return 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 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())); + } + + 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())); +} + +// TODO: serd_node_resolve? + +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_error_sink(SerdErrorSink error_sink, void* handle) + { + serd_world_set_error_sink(c_obj(), error_sink, handle); + } + + // operator SerdWorld*() { return c_obj(); } +}; + +/** + @} + @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(), 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), 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 nodes(StatementFlags flags, + const Node& subject, + const Node& predicate, + const Node& object, + const Optional<Node>& graph = {}) + { + return _is_user ? Status::Success + : Status(serd_sink_write_nodes(_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())); + } + + // operator const SerdSink*() const { return _sink.get(); } + // operator SerdSink*() { return _sink.get(); } + + const SerdSink* c_sink() const { return _sink.get(); } + SerdSink* c_sink() { return _sink.get(); } + +private: + static SerdStatus s_base(void* handle, const SerdNode* uri) + { + return (SerdStatus)((Sink*)handle)->base(Node(uri)); + } + + static SerdStatus + s_prefix(void* handle, const SerdNode* name, const SerdNode* uri) + { + return (SerdStatus)((Sink*)handle)->prefix(Node(name), Node(uri)); + } + + static SerdStatus s_statement(void* handle, + SerdStatementFlags flags, + const SerdStatement* statement) + { + return (SerdStatus)((Sink*)handle)->statement(flags, statement); + } + + static SerdStatus s_end(void* handle, const SerdNode* node) + { + return (SerdStatus)((Sink*)handle)->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) + { + 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); } + + // operator SerdEnv*() { return c_obj(); } +}; + +/** + @} + @name Reader + @{ +*/ + +class Reader : public detail::Wrapper<SerdReader, serd_reader_free>, public Sink +{ +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)) + { + } + + template <typename Traits> + ByteSink(std::basic_ostream<char, Traits>& stream) + : Wrapper(serd_byte_sink_new(detail::stream_sink<Traits>, &stream, 1)) + { + } + + // operator SerdByteSink*() { return c_obj(); } +}; + +class Writer : public detail::Wrapper<SerdWriter, serd_writer_free>, public Sink +{ +public: + Writer(World& world, + const Syntax syntax, + const StyleFlags style, + Env& env, + ByteSink& sink) + : Wrapper(serd_writer_new(world.c_obj(), + SerdSyntax(syntax), + style, + env.c_obj(), + (SerdWriteFunc)serd_byte_sink_write, + sink.c_obj())) + , Sink(serd_writer_get_sink(c_obj())) + { + } + + Writer(World& world, + const Syntax syntax, + const StyleFlags style, + Env& env, + SerdWriteFunc write_func, + void* stream) + : Wrapper(serd_writer_new(world.c_obj(), + SerdSyntax(syntax), + style, + env.c_obj(), + write_func, + stream)) + , Sink(serd_writer_get_sink(c_obj())) + { + } + + // FIXME: test + template <typename Traits> + Writer(World& world, + const Syntax syntax, + const StyleFlags style, + Env& env, + std::basic_ostream<char, Traits>& stream) + : Wrapper(serd_writer_new(world.c_obj(), + SerdSyntax(syntax), + style, + env.c_obj(), + detail::stream_sink<Traits>, + &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) + { + return serd_iter_equals(c_obj(), i.c_obj()); + } + bool operator!=(const Iter& i) + { + 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) + { + return serd_range_equals(c_obj(), i.c_obj()); + } + bool operator!=(const Range& i) + { + 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, SerdIndexOptions indices, SerdModelFlags flags) + : Copyable(serd_model_new(world.c_obj(), indices, flags)) + { + } + + Model(World& world, IndexOption indices, SerdModelFlags flags) + : Copyable(serd_model_new(world.c_obj(), + (SerdIndexOptions)indices, + flags)) + { + } + + 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 = {}) + { + 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 = {}) + { + 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 = {}) + { + return Node( + serd_model_get(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()); + } + + 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())); } + + // operator SerdModel*() { return 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..1f5656cc --- /dev/null +++ b/tests/serd_cxx_test.cpp @@ -0,0 +1,397 @@ +/* + 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. +*/ + +#include "test_utils.h" + +#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)}; + CHECK(moved.c_obj() == ptr); + CHECK(!obj.c_obj()); + + // Move assign + obj = std::move(moved); + CHECK(obj.c_obj() == ptr); + CHECK(!moved.c_obj()); + + return 0; +} + +template <typename T> +static int +test_copy(const T& obj) +{ + T copy{obj}; + CHECK(copy == obj); + + T copy_assigned{obj}; + copy_assigned = obj; + CHECK(copy_assigned == obj); + + return 0; +} + +template <typename T> +static int +test_copy_move(const T& obj) +{ + T copy{obj}; + CHECK(copy == obj); + + T moved{std::move(copy)}; + CHECK(moved == obj); + CHECK(copy != obj); + + T copy_assigned{obj}; + copy_assigned = obj; + CHECK(copy_assigned == obj); + + T move_assigned{obj}; + move_assigned = std::move(copy_assigned); + CHECK(move_assigned == obj); + CHECK(copy_assigned != obj); + + return 0; +} + +static int +test_operators() +{ + int st = 0; + + serd::World world; + + serd::Model model(world, serd::IndexOption::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_copy_move(model.all()); + // st |= test_move_only(serd::Sink{}); + // st |= test_move(serd::Statement{}); + + 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 + CHECK(!serd::Optional<serd::Node>()); + CHECK(!serd::Optional<serd::Node>(nullptr)); + CHECK(serd::Optional<serd::Node>(node)); + + // Comparison and general sanity + serd::Optional<serd::Node> optional{node}; + CHECK(optional); + CHECK(optional == node); + CHECK(optional != other); + CHECK(*optional == node); + CHECK(optional->str() == "node"); + CHECK(optional.ptr() != node.c_obj()); // non-const, must be a copy + + // Reset + optional.reset(); + CHECK(!optional); + CHECK(!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}; + CHECK(copied == nonconst); + CHECK(copied.ptr() != c_node); + + optional = std::move(nonconst); + serd::Optional<serd::Node> moved{std::move(optional)}; + CHECK(moved.ptr() == c_node); + CHECK(!optional); + + serd::Optional<serd::Node> copy_assigned; + copy_assigned = optional; + CHECK(copy_assigned == optional); + CHECK(copy_assigned.ptr() != c_node); + + serd::Optional<serd::Node> move_assigned; + move_assigned = std::move(moved); + CHECK(move_assigned.ptr() == c_node); + CHECK(!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/"); + + CHECK(!test_node(make_string("hello"))); + CHECK(!test_node(make_plain_literal("hello", "en"))); + CHECK(!test_node(make_typed_literal("hello", type))); + CHECK(!test_node(make_blank("blank"))); + CHECK(!test_node(make_curie("eg:curie"))); + CHECK(!test_node(make_uri("http://example.org/thing"))); + CHECK(!test_node(make_resolved_uri("thing", base))); + CHECK(!test_node(make_file_uri("/foo/bar", "host"))); + CHECK(!test_node(make_file_uri("/foo/bar"))); + CHECK(!test_node(make_file_uri("/foo/bar", "host"))); + CHECK(!test_node(make_file_uri("/foo/bar"))); + CHECK(!test_node(make_relative_uri("http://example.org/a", base))); + CHECK(!test_node(make_relative_uri("http://example.org/a", base, root))); + CHECK(!test_node(make_decimal(1.2, 7))); + CHECK(!test_node(make_decimal(3.4, 7, type))); + CHECK(!test_node(make_integer(56))); + CHECK(!test_node(make_integer(78, type))); + CHECK(!test_node(make_blob("blob", 4, true))); + CHECK(!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(); + + CHECK(sink.n_statements == 2); + CHECK(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, 0, env, sink); + + 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.nodes(0, + serd::make_uri("http://drobilla.net/base/s"), + serd::make_uri("http://example.org/p"), + serd::make_uri("http://drobilla.net/o")); + + writer.finish(); + + CHECK(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); + CHECK(env.base_uri() == base); + + env.set_prefix(serd::make_string("eg"), + serd::make_uri("http://example.org/")); + + CHECK(env.qualify(serd::make_uri("http://example.org/foo")) == + serd::make_curie("eg:foo")); + + CHECK(env.expand(serd::make_uri("foo")) == + serd::make_uri("http://drobilla.net/foo")); + + serd::Env copied{env}; + CHECK(copied.qualify(serd::make_uri("http://example.org/foo")) == + serd::make_curie("eg:foo")); + + CHECK(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")); + + CHECK(assigned.qualify(serd::make_uri("http://example.org/foo")) == + serd::make_curie("eg:foo")); + + CHECK(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::IndexOption::SPO | serd::IndexOption::OPS, 0); + + CHECK(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); + + CHECK(!model.empty()); + CHECK(model.size() == 2); + CHECK(model.ask(s, p, o1)); + CHECK(model.ask(s, p, o1)); + CHECK(!model.ask(s, p, s)); + + size_t total_count = 0; + for (const auto& statement : model) { + CHECK(statement.subject() == s); + CHECK(statement.predicate() == p); + CHECK(statement.object() == o1 || statement.object() == o2); + ++total_count; + } + CHECK(total_count == 2); + + size_t o1_count = 0; + for (const auto& statement : model.range({}, {}, o1)) { + CHECK(statement.subject() == s); + CHECK(statement.predicate() == p); + CHECK(statement.object() == o1); + ++o1_count; + } + CHECK(o1_count == 1); + + size_t o2_count = 0; + for (const auto& statement : model.range({}, {}, o2)) { + CHECK(statement.subject() == s); + CHECK(statement.predicate() == p); + CHECK(statement.object() == o2); + ++o2_count; + } + CHECK(o2_count == 1); + + serd::Model copy(model); + CHECK(copy == model); + + copy.insert(s, p, s); + CHECK(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, @@ -126,7 +129,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, [], @@ -165,6 +168,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', '-fno-inline-small-functions', '-fno-default-inline'], 'linkflags': [''] if bld.env.NO_COVERAGE else ['--coverage'], 'lib': lib_args['lib'], 'install_path': ''} @@ -191,6 +195,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']: @@ -219,8 +230,8 @@ def build(bld): bld.install_files('${MANDIR}/man1', 'doc/serdi.1') bld.add_post_fun(autowaf.run_ldconfig) - if bld.env.DOCS: - bld.add_post_fun(lambda ctx: autowaf.make_simple_dox(APPNAME)) + # if bld.env.DOCS: + # bld.add_post_fun(lambda ctx: autowaf.make_simple_dox(APPNAME)) def lint(ctx): "checks code for style issues" @@ -531,14 +542,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 @@ -604,7 +617,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()) |