aboutsummaryrefslogtreecommitdiffstats
path: root/bindings/cpp
diff options
context:
space:
mode:
Diffstat (limited to 'bindings/cpp')
-rw-r--r--bindings/cpp/.clang-tidy24
-rw-r--r--bindings/cpp/doc/.clang-tidy22
-rw-r--r--bindings/cpp/doc/Doxyfile.in50
-rw-r--r--bindings/cpp/doc/api/meson.build14
-rw-r--r--bindings/cpp/doc/cpp_facilities.rst53
-rw-r--r--bindings/cpp/doc/index.rst14
-rw-r--r--bindings/cpp/doc/meson.build84
-rw-r--r--bindings/cpp/doc/model.rst243
-rw-r--r--bindings/cpp/doc/nodes.rst38
-rw-r--r--bindings/cpp/doc/overview.cpp353
-rw-r--r--bindings/cpp/doc/overview.rst83
-rw-r--r--bindings/cpp/doc/reading_and_writing.rst147
-rw-r--r--bindings/cpp/doc/statements.rst124
-rw-r--r--bindings/cpp/doc/stream_processing.rst48
-rw-r--r--bindings/cpp/doc/using_serd.rst14
-rw-r--r--bindings/cpp/doc/world.rst45
-rw-r--r--bindings/cpp/doc/xml/meson.build26
-rw-r--r--bindings/cpp/include/.clang-tidy15
-rw-r--r--bindings/cpp/include/serd/Flags.hpp88
-rw-r--r--bindings/cpp/include/serd/Optional.hpp151
-rw-r--r--bindings/cpp/include/serd/StringView.hpp233
-rw-r--r--bindings/cpp/include/serd/detail/Copyable.hpp106
-rw-r--r--bindings/cpp/include/serd/detail/Wrapper.hpp138
-rw-r--r--bindings/cpp/include/serd/serd.hpp2332
-rw-r--r--bindings/cpp/meson.build110
-rw-r--r--bindings/cpp/test/.clang-tidy27
-rw-r--r--bindings/cpp/test/test_serd_hpp.cpp794
27 files changed, 5376 insertions, 0 deletions
diff --git a/bindings/cpp/.clang-tidy b/bindings/cpp/.clang-tidy
new file mode 100644
index 00000000..ced5452a
--- /dev/null
+++ b/bindings/cpp/.clang-tidy
@@ -0,0 +1,24 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+Checks: >
+ *,
+ -altera-*,
+ -bugprone-easily-swappable-parameters,
+ -cert-dcl50-cpp,
+ -google-readability-todo,
+ -llvmlibc-*,
+ -modernize-concat-nested-namespaces,
+ -modernize-unary-static-assert,
+ -modernize-use-nodiscard,
+ -modernize-use-trailing-return-type,
+ -readability-identifier-length,
+ -readability-named-parameter,
+CheckOptions:
+ - key: hicpp-uppercase-literal-suffix.NewSuffixes
+ value: 'L;U;UL;ULL'
+ - key: readability-uppercase-literal-suffix.NewSuffixes
+ value: 'L;U;UL;ULL'
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*/serd/.*\.h.*'
+FormatStyle: file
diff --git a/bindings/cpp/doc/.clang-tidy b/bindings/cpp/doc/.clang-tidy
new file mode 100644
index 00000000..9d2aa259
--- /dev/null
+++ b/bindings/cpp/doc/.clang-tidy
@@ -0,0 +1,22 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+Checks: >
+ -*-magic-numbers,
+ -*-named-parameter,
+ -*-non-private-member-variables-in-classes,
+ -*-uppercase-literal-suffix,
+ -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,
+ -readability-implicit-bool-conversion,
+InheritParentConfig: true
diff --git a/bindings/cpp/doc/Doxyfile.in b/bindings/cpp/doc/Doxyfile.in
new file mode 100644
index 00000000..0b888a3b
--- /dev/null
+++ b/bindings/cpp/doc/Doxyfile.in
@@ -0,0 +1,50 @@
+# Copyright 2021-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+PROJECT_NAME = Serd
+PROJECT_BRIEF = "A lightweight library for working with RDF data"
+
+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_API \
+ SERD_CONST_API= \
+ SERD_MALLOC_API= \
+ SERD_PURE_API= \
+ ZIX_ALLOCATED= \
+ ZIX_ALWAYS_INLINE_FUNC= \
+ ZIX_CONST_FUNC= \
+ ZIX_NONNULL= \
+ ZIX_NULLABLE= \
+ ZIX_PURE_FUNC= \
+
+RECURSIVE = YES
+STRIP_FROM_PATH = @SERD_SRCDIR@
+INPUT = @SERD_SRCDIR@/bindings/cpp/include \
+ @SERD_SRCDIR@/include
+
+OUTPUT_DIRECTORY = @DOX_OUTPUT@
diff --git a/bindings/cpp/doc/api/meson.build b/bindings/cpp/doc/api/meson.build
new file mode 100644
index 00000000..7a3ea903
--- /dev/null
+++ b/bindings/cpp/doc/api/meson.build
@@ -0,0 +1,14 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+cpp_serd_rst = custom_target(
+ 'Serd C++ API ReST Documentation',
+ command: [
+ sphinxygen,
+ '-l', 'cpp',
+ '-f', '@INPUT0@',
+ meson.current_build_dir(),
+ ],
+ input: [cpp_index_xml] + cpp_rst_files,
+ output: 'serd.rst',
+)
diff --git a/bindings/cpp/doc/cpp_facilities.rst b/bindings/cpp/doc/cpp_facilities.rst
new file mode 100644
index 00000000..53b8704d
--- /dev/null
+++ b/bindings/cpp/doc/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/bindings/cpp/doc/index.rst b/bindings/cpp/doc/index.rst
new file mode 100644
index 00000000..7ce13766
--- /dev/null
+++ b/bindings/cpp/doc/index.rst
@@ -0,0 +1,14 @@
+####
+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
diff --git a/bindings/cpp/doc/meson.build b/bindings/cpp/doc/meson.build
new file mode 100644
index 00000000..85ee389d
--- /dev/null
+++ b/bindings/cpp/doc/meson.build
@@ -0,0 +1,84 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+docdir = get_option('datadir') / 'doc'
+
+config = configuration_data()
+config.set('SERD_STATIC_PATH', serd_src_root / 'doc' / '_static')
+config.set('SERD_TITLE', get_option('title'))
+config.set('SERD_VERSION', meson.project_version())
+
+conf_py = configure_file(
+ configuration: config,
+ input: files(serd_src_root / 'doc' / 'conf.py.in'),
+ output: 'conf.py',
+)
+
+configure_file(
+ copy: true,
+ input: files('overview.cpp'),
+ output: 'overview.cpp',
+)
+
+executable(
+ 'overview',
+ files('overview.cpp'),
+ cpp_args: cpp_suppressions,
+ 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',
+ build_by_default: true,
+ 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],
+ install: true,
+ install_dir: docdir / 'serd-0',
+ output: 'singlehtml',
+)
+
+docs = custom_target(
+ 'html',
+ build_by_default: true,
+ 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],
+ install: true,
+ install_dir: docdir / 'serd-0',
+ output: 'html',
+)
diff --git a/bindings/cpp/doc/model.rst b/bindings/cpp/doc/model.rst
new file mode 100644
index 00000000..6438a7f0
--- /dev/null
+++ b/bindings/cpp/doc/model.rst
@@ -0,0 +1,243 @@
+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 <ModelFlags>` 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.
+
+Cursor
+------
+
+A cursor is a reference to a statement in a model,
+or the end of the model.
+Cursors work more or less like standard C++ iterators,
+although they are smarter internally to present filtered views of a model.
+The :func:`~Model::begin` method returns a cursor to the first statement in the model,
+and :func:`~Model::end` returns the end sentinel (which must not be dereferenced).
+
+.. 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
+
+For the simple case of iterating over every statement in a model,
+range-based ``for`` syntax can be used to avoid using cursors directly:
+
+.. literalinclude:: overview.cpp
+ :start-after: begin model-iteration
+ :end-before: end model-iteration
+ :dedent: 2
+
+Explicit use of cursors is more useful for more advanced cases.
+For example, the above will scan the statements in the model's default statement order,
+but it is also possible to scan the statements in an arbitrary order
+(provided that the model has an appropriate index):
+
+.. literalinclude:: overview.cpp
+ :start-after: begin model-ordered
+ :end-before: end model-ordered
+ :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/bindings/cpp/doc/nodes.rst b/bindings/cpp/doc/nodes.rst
new file mode 100644
index 00000000..4726d738
--- /dev/null
+++ b/bindings/cpp/doc/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 <NodeType>`,
+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 constructors are provided that build nodes:
+
+- :func:`make_token`
+- :func:`make_uri`
+- :func:`make_file_uri`
+- :func:`make_literal`
+- :func:`make_decimal`
+- :func:`make_integer`
+- :func:`make_base64`
+
+Literal nodes for number types (`bool`, `double`, `int32_t`, and so on) can be constructed with the generic :func:`make` template.
+
+
+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/bindings/cpp/doc/overview.cpp b/bindings/cpp/doc/overview.cpp
new file mode 100644
index 00000000..02f0c80a
--- /dev/null
+++ b/bindings/cpp/doc/overview.cpp
@@ -0,0 +1,353 @@
+// Copyright 2021-2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+/*
+ 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.h"
+#include "serd/serd.hpp"
+
+#include <cassert>
+#include <cstddef>
+#include <iostream>
+
+#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<NodeView> g = statement.graph();
+ // end get-pog
+
+ // begin get-caret
+ Optional<CaretView> 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
+ NodeView 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 other_model{world, StatementOrder::SPO, ModelFlag::store_carets};
+ other_model.add_index(StatementOrder::PSO);
+ // end fancy-model-new
+
+ // begin model-copy
+ Model copy{model};
+ assert(copy == model);
+
+ copy = other_model;
+ assert(copy == other_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
+
+ // begin model-insert
+ other_model.insert(*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 << "Subject: " << statement.subject() << std::endl;
+ }
+ // end model-iteration
+
+ // begin model-all
+ Cursor all = model.begin();
+ // end model-all
+
+ // begin model-ordered
+ for (auto i = other_model.begin_ordered(StatementOrder::PSO);
+ i != model.end();
+ ++i) {
+ std::cout << "Predicate: " << i->predicate() << std::endl;
+ }
+ // end model-ordered
+
+ // 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<NodeView> t = model.get(instance, rdf_type, {});
+ if (t) {
+ std::cout << "Instance has type " << *t << std::endl;
+ }
+ // end model-get
+
+ // begin model-get-statement
+ Optional<StatementView> 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, {}};
+ Sink 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
+ Sink 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
+ Sink 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/bindings/cpp/doc/overview.rst b/bindings/cpp/doc/overview.rst
new file mode 100644
index 00000000..c2b529bd
--- /dev/null
+++ b/bindings/cpp/doc/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 <serd/serd.hpp>
+
+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:: ../../../doc/_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:: ../../../doc/_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/bindings/cpp/doc/reading_and_writing.rst b/bindings/cpp/doc/reading_and_writing.rst
new file mode 100644
index 00000000..893e6f7b
--- /dev/null
+++ b/bindings/cpp/doc/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/bindings/cpp/doc/statements.rst b/bindings/cpp/doc/statements.rst
new file mode 100644
index 00000000..a77c8050
--- /dev/null
+++ b/bindings/cpp/doc/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/bindings/cpp/doc/stream_processing.rst b/bindings/cpp/doc/stream_processing.rst
new file mode 100644
index 00000000..39265287
--- /dev/null
+++ b/bindings/cpp/doc/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/bindings/cpp/doc/using_serd.rst b/bindings/cpp/doc/using_serd.rst
new file mode 100644
index 00000000..ee9112ef
--- /dev/null
+++ b/bindings/cpp/doc/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/bindings/cpp/doc/world.rst b/bindings/cpp/doc/world.rst
new file mode 100644
index 00000000..d6736485
--- /dev/null
+++ b/bindings/cpp/doc/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/bindings/cpp/doc/xml/meson.build b/bindings/cpp/doc/xml/meson.build
new file mode 100644
index 00000000..ff4771b0
--- /dev/null
+++ b/bindings/cpp/doc/xml/meson.build
@@ -0,0 +1,26 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+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/bindings/cpp/include/.clang-tidy b/bindings/cpp/include/.clang-tidy
new file mode 100644
index 00000000..899cb35a
--- /dev/null
+++ b/bindings/cpp/include/.clang-tidy
@@ -0,0 +1,15 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+Checks: >
+ -*-named-parameter,
+ -*-non-private-member-variables-in-classes,
+ -*-uppercase-literal-suffix,
+ -cert-dcl50-cpp,
+ -clang-diagnostic-unused-macros,
+ -cppcoreguidelines-pro-bounds-pointer-arithmetic,
+ -fuchsia-default-arguments-declarations,
+ -fuchsia-multiple-inheritance,
+ -fuchsia-overloaded-operator,
+ -readability-implicit-bool-conversion,
+InheritParentConfig: true
diff --git a/bindings/cpp/include/serd/Flags.hpp b/bindings/cpp/include/serd/Flags.hpp
new file mode 100644
index 00000000..a332ee73
--- /dev/null
+++ b/bindings/cpp/include/serd/Flags.hpp
@@ -0,0 +1,88 @@
+// Copyright 2019-2021 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef SERD_FLAGS_HPP
+#define SERD_FLAGS_HPP
+
+#include <type_traits>
+
+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 Flag>
+class Flags
+{
+public:
+ static_assert(std::is_enum<Flag>::value, "");
+
+ using Value = std::make_unsigned_t<std::underlying_type_t<Flag>>;
+
+ /// 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<Value>(f)}
+ {}
+
+ /// Set a flag
+ constexpr Flags operator|(const Flag rhs) const noexcept
+ {
+ return Flags{_value | static_cast<Value>(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<Value>(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<class Flag>
+inline constexpr Flags<Flag>
+operator|(const Flag lhs, const Flag rhs) noexcept
+{
+ return Flags<Flag>{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..965e5f1a
--- /dev/null
+++ b/bindings/cpp/include/serd/Optional.hpp
@@ -0,0 +1,151 @@
+// Copyright 2019-2021 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef SERD_OPTIONAL_HPP
+#define SERD_OPTIONAL_HPP
+
+#include <cassert>
+#include <type_traits>
+#include <utility>
+
+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 T>
+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<std::is_convertible<U, T>::value>::type>
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ Optional(U&& value)
+ : _value{std::forward<U>(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<class T>
+constexpr Optional<std::decay_t<T>>
+make_optional(T&& value)
+{
+ return Optional<T>{std::forward<T>(value)};
+}
+
+/// Creates an optional object with a value constructed in-place from `args`
+template<class T, class... Args>
+constexpr Optional<T>
+make_optional(Args&&... args)
+{
+ return Optional<T>{std::forward<Args>(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..baf6f225
--- /dev/null
+++ b/bindings/cpp/include/serd/StringView.hpp
@@ -0,0 +1,233 @@
+// Copyright 2019-2021 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef SERD_STRINGVIEW_HPP
+#define SERD_STRINGVIEW_HPP
+
+#include "serd/serd.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstring>
+#include <ostream>
+#include <stdexcept>
+#include <string>
+
+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<char>;
+ 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 = static_cast<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<class Alloc = std::allocator<char>>
+ std::basic_string<char, traits_type, Alloc> str() const
+ {
+ return std::basic_string<char, traits_type, Alloc>(data(), size(), Alloc{});
+ }
+
+ template<class Alloc = std::allocator<char>>
+ std::basic_string<char, traits_type, Alloc> str(const Alloc& alloc) const
+ {
+ return std::basic_string<char, traits_type, Alloc>(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 ZixStringView() const { return ZixStringView{_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(), static_cast<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..5a5b5cde
--- /dev/null
+++ b/bindings/cpp/include/serd/detail/Copyable.hpp
@@ -0,0 +1,106 @@
+// Copyright 2019-2021 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef SERD_DETAIL_COPYABLE_HPP
+#define SERD_DETAIL_COPYABLE_HPP
+
+// IWYU pragma: no_include "serd/serd.h"
+
+#include "serd/detail/Wrapper.hpp"
+
+#include <cstddef>
+#include <memory>
+#include <type_traits>
+#include <utility>
+
+namespace serd {
+namespace detail {
+
+/**
+ @addtogroup serdpp_detail
+ @{
+*/
+
+/// Copy function for an allocator-managed C object
+template<class T>
+using CopyFunc = Mutable<T>* (*)(ZixAllocator*, const T*);
+
+/// Equality comparison function for C objects
+template<class T>
+using EqualsFunc = bool (*)(const T*, const T*);
+
+template<class T, CopyFunc<T> copy_func>
+typename std::enable_if_t<std::is_const<T>::value, T>*
+copy_cobj(const T* ptr)
+{
+ return ptr; // Constant wrapper, do not copy
+}
+
+template<class T, CopyFunc<T> copy_func>
+typename std::enable_if_t<!std::is_const<T>::value, T>*
+copy_cobj(const T* ptr)
+{
+ return ptr ? copy_func(nullptr, ptr) : nullptr; // 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<class T, class Deleter, CopyFunc<T> copy, EqualsFunc<T> equals>
+class Copyable : public Wrapper<T, Deleter>
+{
+public:
+ using Base = Wrapper<T, Deleter>;
+
+ explicit Copyable(T* ptr)
+ : Base{ptr}
+ {}
+
+ Copyable(const Copyable& wrapper)
+ : Base(copy_cobj<T, copy>(wrapper.cobj()))
+ {}
+
+ template<class U, class UDeleter>
+ explicit Copyable(const Copyable<U, UDeleter, copy, equals>& wrapper)
+ : Base(copy_cobj<T, copy>(wrapper.cobj()))
+ {}
+
+ Copyable(Copyable&&) noexcept = default;
+ Copyable& operator=(Copyable&&) noexcept = default;
+
+ ~Copyable() noexcept = default;
+
+ Copyable& operator=(const Copyable& wrapper)
+ {
+ if (&wrapper != this) {
+ this->_ptr =
+ std::unique_ptr<T, Deleter>(copy_cobj<T, copy>(wrapper.cobj()));
+ }
+ return *this;
+ }
+
+ template<class U, class UDeleter, Mutable<T>* UCopy(ZixAllocator*, const T*)>
+ bool operator==(const Copyable<U, UDeleter, UCopy, equals>& wrapper) const
+ {
+ return equals(this->cobj(), wrapper.cobj());
+ }
+
+ template<class U, class UDeleter, Mutable<T>* UCopy(ZixAllocator*, const T*)>
+ bool operator!=(const Copyable<U, UDeleter, UCopy, equals>& wrapper) const
+ {
+ return !operator==(wrapper);
+ }
+};
+
+/**
+ @}
+*/
+
+} // namespace detail
+} // namespace serd
+
+#endif // SERD_DETAIL_COPYABLE_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..887e6643
--- /dev/null
+++ b/bindings/cpp/include/serd/detail/Wrapper.hpp
@@ -0,0 +1,138 @@
+// Copyright 2019-2021 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#ifndef SERD_DETAIL_WRAPPER_HPP
+#define SERD_DETAIL_WRAPPER_HPP
+
+// IWYU pragma: no_include "serd/serd.h"
+
+#include <cstddef>
+#include <memory>
+#include <type_traits>
+#include <utility>
+
+namespace serd {
+
+/// Utility template for a mutable type which removes const if necessary
+template<class T>
+class Optional;
+
+/// Detail namespace
+namespace detail {
+
+/**
+ @defgroup serdpp_detail 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<class T>
+using Mutable = typename std::remove_const_t<T>;
+
+/// Generic C++ wrapper for a C object
+template<class T, class Deleter>
+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<T, Deleter> 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<T>;
+
+ explicit Wrapper(std::nullptr_t)
+ : _ptr{nullptr}
+ {}
+
+ void reset() { _ptr.reset(); }
+
+ std::unique_ptr<T, Deleter> _ptr;
+};
+
+/// Free function for an object that can free itself
+template<class T>
+using StandaloneFreeFunc = void(Mutable<T>*);
+
+/// Free function for an object managed via an allocator
+template<class T>
+using AllocatedFreeFunc = void(ZixAllocator*, Mutable<T>*);
+
+/**
+ 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<class T, StandaloneFreeFunc<T> free>
+struct StandaloneDeleter {
+ template<class = std::enable_if<!std::is_const<T>::value>>
+ void operator()(Mutable<T>* const ptr)
+ {
+ free(ptr);
+ }
+
+ template<class = std::enable_if<std::is_const<T>::value>>
+ void operator()(const T*)
+ {}
+};
+
+/**
+ 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<class T, AllocatedFreeFunc<T> free>
+struct AllocatedDeleter {
+ template<class = std::enable_if<!std::is_const<T>::value>>
+ void operator()(Mutable<T>* const ptr)
+ {
+ free(nullptr, ptr);
+ }
+
+ template<class = std::enable_if<std::is_const<T>::value>>
+ void operator()(const T*)
+ {}
+};
+
+/**
+ @}
+*/
+
+} // 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..3e316a07
--- /dev/null
+++ b/bindings/cpp/include/serd/serd.hpp
@@ -0,0 +1,2332 @@
+// Copyright 2019-2021 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#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/Wrapper.hpp" // IWYU pragma: export
+#include "serd/serd.h"
+
+#include <cassert>
+#include <cstdarg>
+#include <cstdint>
+#include <cstdio>
+#include <functional>
+#include <map>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+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<SerdStatus>(status));
+}
+
+/**
+ @}
+ @defgroup serdpp_string String Utilities
+ @{
+*/
+
+// FIXME: 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<std::ostream*>(sink);
+
+ try {
+ os.write(static_cast<const char*>(buf),
+ static_cast<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 static_cast<Syntax>(serd_syntax_by_name(name.c_str()));
+}
+
+/// @copydoc serd_guess_syntax
+inline Syntax
+guess_syntax(StringView filename)
+{
+ return static_cast<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<SerdSyntax>(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};
+ zix_free(nullptr, c_hostname);
+ zix_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 ZixStringView slice)
+ {
+ return slice.data ? Component{slice.data, slice.length} : 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 SerdValue
+using Value = SerdValue;
+
+template<class T>
+inline SerdValue
+value(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);
+}
+
+/// @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<NodeFlag>;
+
+template<typename CPtr>
+using NodeDeleter = detail::AllocatedDeleter<CPtr, serd_node_free>;
+
+template<typename CPtr>
+using NodeHandle =
+ detail::Copyable<CPtr, NodeDeleter<CPtr>, serd_node_copy, serd_node_equals>;
+
+class NodeView;
+
+/// Common base class for any wrapped node
+template<typename CPtr>
+class NodeWrapper : public NodeHandle<CPtr>
+{
+public:
+ template<class C>
+ explicit NodeWrapper(const NodeWrapper<C>& node)
+ : NodeHandle<CPtr>{node}
+ {}
+
+ /// @copydoc serd_node_type
+ NodeType type() const
+ {
+ return static_cast<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<NodeView> datatype() const;
+
+ /// @copydoc serd_node_language
+ Optional<NodeView> 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()); }
+
+ /// @copydoc serd_node_decoded_size
+ size_t decoded_size() const { return serd_node_decoded_size(this->cobj()); }
+
+ /// @copydoc serd_node_decode
+ SerdWriteResult decode(const size_t buf_size, void* const buf) const
+ {
+ return serd_node_decode(this->cobj(), buf_size, buf);
+ }
+
+ /// 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 ZixStringView() const
+ {
+ return ZixStringView{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(CPtr* const ptr)
+ : NodeHandle<CPtr>{ptr}
+ {}
+};
+
+/// A non-owning constant view of some other node
+class NodeView : public NodeWrapper<const SerdNode>
+{
+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<class C>
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ NodeView(const NodeWrapper<C>& node)
+ : NodeWrapper{node}
+ {}
+};
+
+/// @copydoc serd_node_datatype
+template<typename CPtr>
+inline Optional<NodeView>
+NodeWrapper<CPtr>::datatype() const
+{
+ return NodeView{serd_node_datatype(this->cobj())};
+}
+
+/// @copydoc serd_node_language
+template<typename CPtr>
+inline Optional<NodeView>
+NodeWrapper<CPtr>::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<SerdNode>
+{
+public:
+ /// Create a node by taking ownership of a C node
+ explicit Node(SerdNode* const node)
+ : NodeWrapper<SerdNode>{node}
+ {}
+
+ /// Create a node by copying another node
+ explicit Node(const NodeView& node)
+ : NodeWrapper<SerdNode>{node}
+ {}
+
+ explicit Node(const Value& value)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(value))}
+ {}
+
+ /// Create an xsd:boolean node from a ``bool``
+ explicit Node(const bool b)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_bool(b)))}
+ {}
+
+ /// Create an xsd:double node from a ``double``
+ explicit Node(const double d)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_double(d)))}
+ {}
+
+ /// Create an xsd:float node from a ``float``
+ explicit Node(const float f)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_float(f)))}
+ {}
+
+ /// Create an xsd:long node from a ``int64_t``
+ explicit Node(const int64_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_long(i)))}
+ {}
+
+ /// Create an xsd:int node from a ``int32_t``
+ explicit Node(const int32_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_int(i)))}
+ {}
+
+ /// Create an xsd:short node from a ``int16_t``
+ explicit Node(const int16_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_short(i)))}
+ {}
+
+ /// Create an xsd:byte node from a ``int8_t``
+ explicit Node(const int8_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_byte(i)))}
+ {}
+
+ /// Create an xsd:unsignedLong node from a ``int64_t``
+ explicit Node(const uint64_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_ulong(i)))}
+ {}
+
+ /// Create an xsd:unsignedInt node from a ``int32_t``
+ explicit Node(const uint32_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_uint(i)))}
+ {}
+
+ /// Create an xsd:unsignedShort node from a ``int16_t``
+ explicit Node(const uint16_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(serd_ushort(i)))}
+ {}
+
+ /// Create an xsd:unsignedByte node from a ``int8_t``
+ explicit Node(const uint8_t i)
+ : NodeWrapper{serd_node_new(nullptr, serd_a_primitive(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<Node>;
+ 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_node_new(nullptr, serd_a_token(static_cast<SerdNodeType>(type), str))};
+}
+
+/// Create a new plain literal node with no language from `str`
+inline Node
+make_string(StringView str)
+{
+ return Node{serd_node_new(nullptr, serd_a_string_view(str))};
+}
+
+/// @copydoc serd_a_uri
+inline Node
+make_uri(StringView uri)
+{
+ return Node{serd_node_new(nullptr, serd_a_uri(uri))};
+}
+
+/// @copydoc serd_a_parsed_uri
+inline Node
+make_uri(SerdURIView uri)
+{
+ return Node{serd_node_new(nullptr, serd_a_parsed_uri(uri))};
+}
+
+/// @copydoc serd_a_parsed_uri
+inline Node
+make_uri(URI uri)
+{
+ return Node{serd_node_new(nullptr, serd_a_parsed_uri(*uri.cobj()))};
+}
+
+/// Create a new file URI node from a local filesystem path
+inline Node
+make_file_uri(StringView path)
+{
+ return Node{
+ serd_node_new(nullptr, serd_a_file_uri(path, zix_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_node_new(nullptr, serd_a_file_uri(path, hostname))};
+}
+
+/// @copydoc serd_a_literal
+inline Node
+make_literal(StringView string, NodeFlags flags, StringView meta)
+{
+ return Node{serd_node_new(
+ nullptr, serd_a_literal(string, static_cast<SerdNodeFlags>(flags), meta))};
+}
+
+/// Create a new blank node from a local name
+inline Node
+make_blank(StringView str)
+{
+ return Node{serd_node_new(nullptr, serd_a_blank(str))};
+}
+
+/// Create a new plain literal with an optional language tag
+inline Node
+make_plain_literal(StringView str, StringView lang)
+{
+ return Node{serd_node_new(nullptr, serd_a_plain_literal(str, lang))};
+}
+
+/// Create a new typed literal node from `str`
+inline Node
+make_typed_literal(StringView str, const StringView datatype)
+{
+ return Node{serd_node_new(nullptr, serd_a_typed_literal(str, datatype))};
+}
+
+/**
+ Create a new literal from a number.
+
+ This supports `bool`, `float`, `double`, and both signed and unsigned
+ integers from 8 to 64 bits wide. The returned node will have the
+ corresponding xsd datatype, for example, `uint16_t` will produce an
+ `xsd:unsignedShort` literal.
+*/
+template<class T>
+inline Node
+make(const T v)
+{
+ return Node{value(v)};
+}
+
+/// @copydoc serd_a_decimal
+inline Node
+make_decimal(double d)
+{
+ return Node{serd_node_new(nullptr, serd_a_decimal(d))};
+}
+
+/// @copydoc serd_a_integer
+inline Node
+make_integer(int64_t i)
+{
+ return Node{serd_node_new(nullptr, serd_a_integer(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 Node::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_node_new(nullptr, serd_a_base64(size, buf))};
+}
+
+/// Prototype for Node get() templates
+template<class T>
+inline T
+get(NodeView node);
+
+/// Return the value of `node` coerced to a boolean
+template<>
+inline bool
+get<bool>(NodeView node)
+{
+ return serd_node_value_as(node.cobj(), SERD_BOOL, true).data.as_bool;
+}
+
+/// Return the value of `node` coerced to a double
+template<>
+inline double
+get<double>(NodeView node)
+{
+ return serd_node_value_as(node.cobj(), SERD_DOUBLE, true).data.as_double;
+}
+
+/// Return the value of `node` coerced to a float
+template<>
+inline float
+get<float>(NodeView node)
+{
+ return serd_node_value_as(node.cobj(), SERD_FLOAT, true).data.as_float;
+}
+
+/// Return the value of `node` coerced to a int64_t
+template<>
+inline int64_t
+get<int64_t>(NodeView node)
+{
+ return serd_node_value_as(node.cobj(), SERD_LONG, true).data.as_long;
+}
+
+/// Return the value of `node` coerced to a uint64_t
+template<>
+inline uint64_t
+get<uint64_t>(NodeView node)
+{
+ return serd_node_value_as(node.cobj(), SERD_ULONG, true).data.as_ulong;
+}
+
+/**
+ @}
+ @defgroup serdpp_nodes Nodes
+ @{
+*/
+
+// TODO
+
+/**
+ @}
+ @defgroup serdpp_caret Caret
+ @{
+*/
+
+/// Deleter for a Caret wrapper
+template<typename CPtr>
+using CaretDeleter = detail::AllocatedDeleter<CPtr, serd_caret_free>;
+
+/// Caret handle
+template<typename CPtr>
+using CaretHandle = detail::
+ Copyable<CPtr, CaretDeleter<CPtr>, serd_caret_copy, serd_caret_equals>;
+
+/// Caret wrapper
+template<typename CPtr>
+class CaretWrapper : public CaretHandle<CPtr>
+{
+public:
+ explicit CaretWrapper(CPtr* caret)
+ : CaretHandle<CPtr>{caret}
+ {}
+
+ template<class C>
+ explicit CaretWrapper(const CaretWrapper<C>& caret)
+ : CaretHandle<CPtr>{caret.cobj()}
+ {}
+
+ /// @copydoc serd_caret_document
+ NodeView document() const
+ {
+ return NodeView(serd_caret_document(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()); }
+};
+
+/// A non-owning constant view of a caret
+class CaretView : public CaretWrapper<const SerdCaret>
+{
+public:
+ /// Create a view of a C caret pointer
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ CaretView(const SerdCaret* const ptr)
+ : CaretWrapper{ptr}
+ {}
+
+ /// Create a view of some other caret
+ template<class C>
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ CaretView(const CaretWrapper<C>& caret)
+ : CaretWrapper{caret}
+ {}
+};
+
+/// Extra data managed by mutable (user created) Caret
+struct CaretData {
+ Node name_node;
+};
+
+/// @copydoc SerdCaret
+class Caret
+ : private CaretData
+ , public CaretWrapper<SerdCaret>
+{
+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)}
+ {}
+
+ explicit Caret(const CaretView& caret)
+ : Caret(caret.document(), caret.line(), caret.column())
+ {}
+
+private:
+ friend class Optional<Caret>;
+ 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
+};
+
+/// Deleter for a Statement wrapper
+template<typename CPtr>
+using StatementDeleter = detail::AllocatedDeleter<CPtr, serd_statement_free>;
+
+template<typename CPtr>
+using StatementHandle = detail::Copyable<CPtr,
+ StatementDeleter<CPtr>,
+ serd_statement_copy,
+ serd_statement_equals>;
+
+template<typename CPtr>
+class StatementWrapper;
+
+/// Extra data managed by mutable (user created) Statement
+struct StatementData {
+ Node _subject;
+ Node _predicate;
+ Node _object;
+ Optional<Node> _graph;
+ Optional<Caret> _caret;
+};
+
+/// Statement wrapper
+template<typename CPtr>
+class StatementWrapper : public StatementHandle<CPtr>
+{
+public:
+ explicit StatementWrapper(CPtr* statement)
+ : StatementHandle<CPtr>{statement}
+ {}
+
+ template<class C>
+ explicit StatementWrapper(const StatementWrapper<C>& statement)
+ : StatementHandle<CPtr>{statement}
+ {}
+
+ /// @copydoc serd_statement_node
+ NodeView node(Field field) const
+ {
+ return NodeView{
+ serd_statement_node(this->cobj(), static_cast<SerdField>(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<NodeView> graph() const
+ {
+ return NodeView{serd_statement_graph(this->cobj())};
+ }
+
+ /// @copydoc serd_statement_caret
+ Optional<CaretView> caret() const
+ {
+ return CaretView{serd_statement_caret(this->cobj())};
+ }
+
+ /// @copydoc serd_statement_matches
+ bool matches(Optional<NodeView> subject,
+ Optional<NodeView> predicate,
+ Optional<NodeView> object,
+ Optional<NodeView> graph = {}) const
+ {
+ return serd_statement_matches(this->cobj(),
+ subject.cobj(),
+ predicate.cobj(),
+ object.cobj(),
+ graph.cobj());
+ }
+
+private:
+ friend class Cursor;
+
+ StatementWrapper()
+ : StatementHandle<CPtr>{nullptr}
+ {}
+};
+
+template<typename CPtr>
+class CursorWrapper;
+
+/// A non-owning constant view of a statement
+class StatementView final : public StatementWrapper<const SerdStatement>
+{
+public:
+ /// Create a view of a C statement
+ explicit StatementView(const SerdStatement* const ptr)
+ : StatementWrapper<const SerdStatement>{ptr}
+ {}
+
+ /// Create a view of some other statement
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ StatementView(const StatementWrapper<SerdStatement>& statement)
+ : StatementWrapper<const SerdStatement>{statement.cobj()}
+ {}
+
+ // FIXME
+ // private:
+ // friend class CursorWrapper<const SerdStatement>;
+ // friend class CursorWrapper<SerdStatement>;
+
+ // StatementView(std::nullptr_t)
+ // : StatementWrapper<const SerdStatement>{nullptr}
+ // {}
+};
+
+/// @copydoc SerdStatement
+class Statement
+ : public StatementData
+ , public StatementWrapper<SerdStatement>
+{
+public:
+ Statement(const NodeView& s,
+ const NodeView& p,
+ const NodeView& o,
+ const NodeView& g)
+ : StatementData{Node{s}, Node{p}, Node{o}, Node{g}, {}}
+ , StatementWrapper{serd_statement_new(nullptr,
+ _subject.cobj(),
+ _predicate.cobj(),
+ _object.cobj(),
+ _graph.cobj(),
+ nullptr)}
+ {}
+
+ Statement(const NodeView& s,
+ const NodeView& p,
+ const NodeView& o,
+ const NodeView& g,
+ const CaretView& caret)
+ : StatementData{Node{s}, Node{p}, Node{o}, Node{g}, Caret{caret}}
+ , 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)
+ : StatementData{Node{s}, Node{p}, Node{o}, {}, {}}
+ , StatementWrapper{serd_statement_new(nullptr,
+ _subject.cobj(),
+ _predicate.cobj(),
+ _object.cobj(),
+ nullptr,
+ nullptr)}
+ {}
+
+ Statement(const NodeView& s,
+ const NodeView& p,
+ const NodeView& o,
+ const CaretView& caret)
+ : StatementData{Node{s}, Node{p}, Node{o}, {}, Caret{caret}}
+ , StatementWrapper{serd_statement_new(nullptr,
+ _subject.cobj(),
+ _predicate.cobj(),
+ _object.cobj(),
+ nullptr,
+ _caret.cobj())}
+ {}
+
+ explicit Statement(const StatementView& statement)
+ : StatementData{Node{statement.subject()},
+ Node{statement.predicate()},
+ Node{statement.object()},
+ statement.graph() ? Node{*statement.graph()}
+ : Optional<Node>{},
+ statement.caret() ? Caret{*statement.caret()}
+ : Optional<Caret>{}}
+ , 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<StringView, StringView>;
+
+/// User-provided callback function for handling a log message
+using LogFunc = std::function<Status(LogLevel, LogFields, std::string&&)>;
+
+/// Deleter for a World wrapper
+using WorldDeleter = detail::StandaloneDeleter<SerdWorld, serd_world_free>;
+
+using WorldHandle = detail::Wrapper<SerdWorld, WorldDeleter>;
+
+/// @copydoc SerdWorld
+class World : public WorldHandle
+{
+public:
+ World()
+ : WorldHandle{serd_world_new(nullptr)}
+ {}
+
+ NodeView get_blank()
+ {
+ return static_cast<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<SerdLogField> 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<SerdLogLevel>(level),
+ fields.size(),
+ c_fields.data(),
+ fmt,
+ args);
+
+ va_end(args);
+ return static_cast<Status>(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 = vsnprintf(nullptr, 0, fmt, args_copy);
+
+ va_end(args_copy);
+
+ const auto buffer_size = static_cast<size_t>(n_bytes) + 1U;
+
+#if __cplusplus >= 201703L
+ std::string result(static_cast<size_t>(n_bytes), '\0');
+ if (vsnprintf(result.data(), buffer_size, fmt, args) != n_bytes) {
+ return "";
+ }
+#else
+ std::vector<char> str(buffer_size, '\0');
+ if (vsnprintf(str.data(), buffer_size, fmt, args) != n_bytes) {
+ return "";
+ }
+
+ std::string result(str.data(), static_cast<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 ZixStringView message) noexcept
+ {
+ const auto* const self = static_cast<const World*>(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<SerdStatus>(
+ self->_log_func(static_cast<LogLevel>(level),
+ cpp_fields,
+ std::string(message.data, message.length)));
+ } 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<StatementFlag>;
+
+/// @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<EventType>(_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
+ @{
+*/
+
+/// A function called when the base URI changes
+using BaseFunc = std::function<Status(NodeView)>;
+
+/// A function called when a namespace prefix is defined
+using PrefixFunc = std::function<Status(NodeView name, NodeView uri)>;
+
+/// A function called when a statement is emitted
+using StatementFunc = std::function<Status(StatementFlags, StatementView)>;
+
+/// A function called at the end of anonymous node descriptions
+using EndFunc = std::function<Status(NodeView)>;
+
+/// Deleter for a Sink wrapper
+template<class CPtr>
+using SinkDeleter = detail::StandaloneDeleter<CPtr, serd_sink_free>;
+
+template<class CPtr>
+using SinkHandle = detail::Wrapper<CPtr, SinkDeleter<CPtr>>;
+
+/// Common base class for any wrapped sink
+template<class CPtr>
+class SinkWrapper : public SinkHandle<CPtr>
+{
+public:
+ /// @copydoc serd_sink_write_base
+ Status base(const NodeView& uri) const
+ {
+ return static_cast<Status>(serd_sink_write_base(this->cobj(), uri.cobj()));
+ }
+
+ /// @copydoc serd_sink_write_prefix
+ Status prefix(NodeView name, const NodeView& uri) const
+ {
+ return static_cast<Status>(
+ serd_sink_write_prefix(this->cobj(), name.cobj(), uri.cobj()));
+ }
+
+ /// @copydoc serd_sink_write_statement
+ Status statement(StatementFlags flags, StatementView statement) const
+ {
+ return static_cast<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<NodeView> graph = {}) const
+ {
+ return static_cast<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 static_cast<Status>(serd_sink_write_end(this->cobj(), node.cobj()));
+ }
+
+protected:
+ explicit SinkWrapper(CPtr* const ptr)
+ : SinkHandle<CPtr>{ptr}
+ {}
+};
+
+/// A non-owning constant view of some other sink
+class SinkView final : public SinkWrapper<const SerdSink>
+{
+public:
+ /// Create a view of a C sink
+ explicit SinkView(const SerdSink* const ptr)
+ : SinkWrapper<const SerdSink>{ptr}
+ {}
+
+ /// Create a view of some other sink
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ SinkView(const SinkWrapper<SerdSink>& sink)
+ : SinkWrapper<const SerdSink>{sink.cobj()}
+ {}
+};
+
+/// @copydoc SerdSink
+class Sink final : public SinkWrapper<SerdSink>
+{
+public:
+ explicit Sink(const World& world)
+ : SinkWrapper{serd_sink_new(serd_world_allocator(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<const Sink*>(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<const Sink*>(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<const Sink*>(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<const Sink*>(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<const Sink*>(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<CanonFlag>;
+
+/// @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<NodeView> subject,
+ Optional<NodeView> predicate,
+ Optional<NodeView> object,
+ Optional<NodeView> 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
+ @{
+*/
+
+/// Deleter for an Env wrapper
+template<typename CPtr>
+using EnvDeleter = detail::StandaloneDeleter<CPtr, serd_env_free>;
+
+template<typename CPtr>
+using EnvHandle =
+ detail::Copyable<CPtr, EnvDeleter<CPtr>, serd_env_copy, serd_env_equals>;
+
+/// Env wrapper
+template<typename CPtr>
+class EnvWrapper : public EnvHandle<CPtr>
+{
+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 static_cast<Status>(serd_env_set_base_uri(this->cobj(), uri));
+ }
+
+ /// Set a namespace prefix
+ Status set_prefix(StringView name, StringView uri)
+ {
+ return static_cast<Status>(serd_env_set_prefix(this->cobj(), name, uri));
+ }
+
+ /// Expand `node` into an absolute URI if possible
+ Optional<Node> 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<CPtr> ptr)
+ : EnvHandle<CPtr>{std::move(ptr)}
+ {}
+
+ explicit EnvWrapper(CPtr* const ptr)
+ : EnvHandle<CPtr>{ptr}
+ {}
+};
+
+/// EnvView
+using EnvView = EnvWrapper<const SerdEnv>;
+
+/// @copydoc SerdEnv
+class Env : public EnvWrapper<SerdEnv>
+{
+public:
+ explicit Env(World& world)
+ : EnvWrapper{
+ serd_env_new(serd_world_allocator(world.cobj()), zix_empty_string())}
+ {}
+
+ explicit Env(World& world, const NodeView& base)
+ : EnvWrapper{
+ serd_env_new(serd_world_allocator(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 is)
+ : SerdInputStream{is}
+ {}
+
+ 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<std::istream*>(stream);
+ const size_t len = size * nmemb;
+
+ try {
+ is.read(static_cast<char*>(buf), static_cast<std::streamsize>(len));
+ } catch (...) {
+ return 0U;
+ }
+
+ return is.fail() ? 0U : len;
+}
+
+static inline int
+istream_error(void* const stream)
+{
+ std::istream& is = *static_cast<std::istream*>(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<ReaderFlag>;
+
+/// Deleter for a Reader wrapper
+using ReaderDeleter = detail::StandaloneDeleter<SerdReader, serd_reader_free>;
+
+using ReaderHandle = detail::Wrapper<SerdReader, ReaderDeleter>;
+
+/// @copydoc SerdReader
+class Reader : public ReaderHandle
+{
+public:
+ Reader(World& world,
+ const Syntax syntax,
+ const ReaderFlags flags,
+ Env& env,
+ SinkView sink)
+ : ReaderHandle{serd_reader_new(world.cobj(),
+ static_cast<SerdSyntax>(syntax),
+ flags,
+ env.cobj(),
+ sink.cobj())}
+ {}
+
+ Status start(SerdInputStream& in,
+ const NodeView& input_name,
+ const size_t block_size)
+ {
+ return static_cast<Status>(
+ serd_reader_start(cobj(), &in, input_name.cobj(), block_size));
+ }
+
+ /// @copydoc serd_reader_read_chunk
+ Status read_chunk()
+ {
+ return static_cast<Status>(serd_reader_read_chunk(cobj()));
+ }
+
+ /// @copydoc serd_reader_read_document
+ Status read_document()
+ {
+ return static_cast<Status>(serd_reader_read_document(cobj()));
+ }
+
+ /// @copydoc serd_reader_finish
+ Status finish() { return static_cast<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<std::istream*>(stream);
+ s->read(static_cast<char*>(buf), static_cast<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<std::istream*>(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<size_t(const char*, size_t)>;
+
+/// @copydoc SerdOutputStream
+class OutputStream : public SerdOutputStream
+{
+public:
+ explicit OutputStream(SerdOutputStream os)
+ : SerdOutputStream{os}
+ {}
+
+ 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<Status>(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<std::ostream*>(stream);
+ const size_t len = size * nmemb;
+
+ try {
+ os.write(static_cast<const char*>(buf), static_cast<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())};
+}
+
+/**
+ @}
+ @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
+ longhand = SERD_WRITE_LONGHAND, ///< @copydoc SERD_WRITE_LONGHAND
+ contextual = SERD_WRITE_CONTEXTUAL, ///< @copydoc SERD_WRITE_CONTEXTUAL
+ escapes = SERD_WRITE_ESCAPES, ///< @copydoc SERD_WRITE_ESCAPES
+};
+
+/// @copydoc SerdWriterFlags
+using WriterFlags = Flags<WriterFlag>;
+
+/// Deleter for a Writer wrapper
+using WriterDeleter = detail::StandaloneDeleter<SerdWriter, serd_writer_free>;
+
+using WriterHandle = detail::Wrapper<SerdWriter, WriterDeleter>;
+
+/// @copydoc SerdWriter
+class Writer : public WriterHandle
+{
+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)
+ : WriterHandle{serd_writer_new(world.cobj(),
+ static_cast<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 static_cast<Status>(serd_writer_set_root_uri(cobj(), uri));
+ }
+
+ /// @copydoc serd_writer_finish
+ Status finish() { return static_cast<Status>(serd_writer_finish(cobj())); }
+};
+
+/**
+ @}
+ @}
+ @defgroup serdpp_storage Storage
+ @{
+*/
+
+/**
+ @defgroup serdpp_cursor Cursor
+ @{
+*/
+
+/// Deleter for a CursorHandle
+template<typename CPtr>
+using CursorDeleter = detail::AllocatedDeleter<CPtr, serd_cursor_free>;
+
+/// Owning handle to a cursor (const or mutable)
+template<typename CPtr>
+using CursorHandle = detail::
+ Copyable<CPtr, CursorDeleter<CPtr>, serd_cursor_copy, serd_cursor_equals>;
+
+/// A read-only view of a cursor
+class CursorView;
+
+/// @copydoc SerdCursor
+template<typename CPtr>
+class CursorWrapper : public CursorHandle<CPtr>
+{
+public:
+ template<class C>
+ explicit CursorWrapper(const CursorWrapper<C>& cursor)
+ : CursorHandle<CPtr>{cursor}
+ {}
+
+ 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;
+ }
+
+protected:
+ explicit CursorWrapper(CPtr* const ptr)
+ : CursorHandle<CPtr>{ptr}
+ {}
+
+private:
+ mutable StatementView _statement{nullptr};
+};
+
+/// A non-owning constant view of a cursor
+class CursorView : public CursorWrapper<const SerdCursor>
+{
+public:
+ /// Create a view of a C cursor pointer
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ CursorView(const SerdCursor* const ptr)
+ : CursorWrapper<const SerdCursor>{ptr}
+ {}
+
+ /// Create a view of some other cursor
+ template<class C>
+ // NOLINTNEXTLINE(google-explicit-constructor, hicpp-explicit-conversions)
+ CursorView(const CursorWrapper<C>& cursor)
+ : CursorWrapper<const SerdCursor>{cursor}
+ {}
+};
+
+/// An owning handle to a cursor
+class Cursor : public CursorWrapper<SerdCursor>
+{
+public:
+ /// Create a cursor by taking ownership of a C cursor
+ explicit Cursor(SerdCursor* const ptr)
+ : CursorWrapper<SerdCursor>{ptr}
+ {}
+
+ /// Create a cursor by copying another cursor
+ explicit Cursor(const CursorView& ptr)
+ : CursorWrapper<SerdCursor>{ptr}
+ {}
+
+ Cursor(const Cursor&) = default;
+ Cursor& operator=(const Cursor&) = default;
+
+ Cursor(Cursor&&) = default;
+ Cursor& operator=(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<DescribeFlag>;
+
+/**
+ @}
+ @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<ModelFlag>;
+
+/// @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
+};
+
+/// A wrapper for a cursor that acts as a collection
+class ModelRange
+{
+public:
+ ModelRange(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;
+};
+
+/// Deleter for a Model wrapper
+using ModelDeleter = detail::StandaloneDeleter<SerdModel, serd_model_free>;
+
+using ModelHandle =
+ detail::Copyable<SerdModel, ModelDeleter, serd_model_copy, serd_model_equals>;
+
+/// @copydoc SerdModel
+class Model : public ModelHandle
+{
+public:
+ using Range = ModelRange;
+
+ using value_type = Statement; ///< Element value type (ala std)
+ using iterator = Cursor; ///< Iterator type (ala std)
+ using const_iterator = Cursor; ///< Const iterator type (ala std)
+
+ Model(World& world,
+ const StatementOrder default_order,
+ const ModelFlags flags)
+ : ModelHandle{serd_model_new(world.cobj(),
+ static_cast<SerdStatementOrder>(default_order),
+ flags)}
+ , _end{serd_model_end(cobj())}
+ {}
+
+ /// @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<Status>(
+ serd_model_add_index(cobj(), static_cast<SerdStatementOrder>(order)));
+ }
+
+ /// @copydoc serd_model_drop_index
+ Status drop_index(const StatementOrder order)
+ {
+ return static_cast<Status>(
+ serd_model_drop_index(cobj(), static_cast<SerdStatementOrder>(order)));
+ }
+
+ /// @copydoc serd_model_insert
+ Status insert(StatementView s)
+ {
+ return static_cast<Status>(serd_model_insert(cobj(), s.cobj()));
+ }
+
+ /// @copydoc serd_model_add
+ Status insert(const NodeView& s,
+ const NodeView& p,
+ const NodeView& o,
+ Optional<NodeView> g = {})
+ {
+ return static_cast<Status>(
+ 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<Status>(
+ 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;
+ }
+
+ /**
+ 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<Status>(
+ serd_model_erase_statements(cobj(), range.cobj()));
+ }
+
+ /// Search for statements that match a pattern
+ ModelRange find(Optional<NodeView> s,
+ Optional<NodeView> p,
+ Optional<NodeView> o,
+ Optional<NodeView> g = {}) const
+ {
+ return ModelRange{
+ Cursor{serd_model_find(
+ nullptr, cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj())},
+ end()};
+ }
+
+ /// @copydoc serd_model_get
+ Optional<NodeView> get(Optional<NodeView> s,
+ Optional<NodeView> p,
+ Optional<NodeView> o,
+ Optional<NodeView> g = {}) const
+ {
+ return NodeView(
+ serd_model_get(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj()));
+ }
+
+ /// @copydoc serd_model_get_statement
+ Optional<StatementView> get_statement(Optional<NodeView> s,
+ Optional<NodeView> p,
+ Optional<NodeView> o,
+ Optional<NodeView> g = {}) const
+ {
+ return StatementView(
+ serd_model_get_statement(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj()));
+ }
+
+ /// @copydoc serd_model_ask
+ bool ask(Optional<NodeView> s,
+ Optional<NodeView> p,
+ Optional<NodeView> o,
+ Optional<NodeView> g = {}) const
+ {
+ return serd_model_ask(cobj(), s.cobj(), p.cobj(), o.cobj(), g.cobj());
+ }
+
+ /// @copydoc serd_model_count
+ size_t count(Optional<NodeView> s,
+ Optional<NodeView> p,
+ Optional<NodeView> o,
+ Optional<NodeView> 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(
+ nullptr, cobj(), static_cast<SerdStatementOrder>(order))};
+ }
+
+ /// @copydoc serd_model_begin
+ Cursor begin() const { return Cursor{serd_model_begin(nullptr, cobj())}; }
+
+ /// @copydoc serd_model_end
+ const Cursor& end() const { return _end; }
+
+private:
+ friend class Optional<Model>;
+
+ explicit Model(std::nullptr_t)
+ : ModelHandle{nullptr}
+ , _end{nullptr}
+ {}
+
+ Cursor _end;
+};
+
+/**
+ @}
+ @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())};
+}
+
+/**
+ @}
+ @}
+ @}
+*/
+
+} // namespace serd
+
+#endif // SERD_SERD_HPP
diff --git a/bindings/cpp/meson.build b/bindings/cpp/meson.build
new file mode 100644
index 00000000..7ff616ee
--- /dev/null
+++ b/bindings/cpp/meson.build
@@ -0,0 +1,110 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+versioned_cpp_name = 'serdpp' + version_suffix
+
+# Set ultra strict warnings for developers, if requested
+cpp_suppressions = []
+if cpp.get_id() == 'clang'
+ if get_option('warning_level') == 'everything'
+ cpp_suppressions += [
+ '-Wno-c++98-compat-pedantic',
+ '-Wno-documentation-unknown-command',
+ '-Wno-format-nonliteral',
+ '-Wno-padded',
+ '-Wno-unsafe-buffer-usage',
+ ]
+ endif
+
+ if get_option('warning_level') in ['everything', '3']
+ cpp_suppressions += [
+ '-Wno-nullability-extension',
+ ]
+ endif
+
+elif cpp.get_id() == 'gcc'
+ if get_option('warning_level') == 'everything'
+ cpp_suppressions += [
+ '-Wno-abi-tag',
+ '-Wno-float-equal',
+ '-Wno-multiple-inheritance',
+ '-Wno-padded',
+ '-Wno-switch-default',
+ '-Wno-unused-const-variable',
+ ]
+ endif
+
+elif cpp.get_id() == 'msvc'
+ if get_option('warning_level') == 'everything'
+ 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 = (
+ cpp_suppressions + platform_c_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'],
+)
+
+if not get_option('docs').disabled() and not get_option('docs_cpp').disabled()
+ subdir('doc')
+endif
diff --git a/bindings/cpp/test/.clang-tidy b/bindings/cpp/test/.clang-tidy
new file mode 100644
index 00000000..027f0ce5
--- /dev/null
+++ b/bindings/cpp/test/.clang-tidy
@@ -0,0 +1,27 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+Checks: >
+ -*-magic-numbers,
+ -*-non-private-member-variables-in-classes,
+ -*-uppercase-literal-suffix,
+ -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,
+ -misc-no-recursion,
+ -performance-unnecessary-value-param,
+ -readability-function-cognitive-complexity,
+ -readability-implicit-bool-conversion,
+ -readability-suspicious-call-argument,
+InheritParentConfig: true
diff --git a/bindings/cpp/test/test_serd_hpp.cpp b/bindings/cpp/test/test_serd_hpp.cpp
new file mode 100644
index 00000000..36689c93
--- /dev/null
+++ b/bindings/cpp/test/test_serd_hpp.cpp
@@ -0,0 +1,794 @@
+// Copyright 2018-2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+#undef NDEBUG
+
+#include "serd/serd.h"
+#include "serd/serd.hpp"
+
+#include <algorithm>
+#include <array>
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+#include <fstream> // IWYU pragma: keep
+#include <iostream>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <utility>
+
+template<class 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.cobj();
+
+ // Move construct
+ T moved{std::forward<T>(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<class 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); // 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});
+ 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<class Value>
+static int
+test_optional(const Value& value, const Value& other)
+{
+ test_copy_move(value);
+
+ // Truthiness
+ assert(!serd::Optional<Value>());
+ // assert(!serd::Optional<Value>(nullptr));
+ assert(serd::Optional<Value>(value));
+
+ // Comparison and general sanity
+ serd::Optional<Value> 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<Value> copied{optional};
+ assert(copied == nonconst);
+ assert(copied.cobj() != c_ptr);
+
+ optional = std::move(nonconst);
+ serd::Optional<Value> moved{std::move(optional)};
+ assert(moved.cobj() == c_ptr);
+ assert(!optional); // NOLINT
+
+ serd::Optional<Value> copy_assigned;
+ copy_assigned = optional;
+ assert(copy_assigned == optional);
+ assert(copy_assigned.cobj() != c_ptr);
+
+ serd::Optional<Value> move_assigned;
+ move_assigned = std::move(moved);
+ assert(move_assigned.cobj() == c_ptr);
+ assert(!optional);
+
+ serd::Optional<Value> 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;
+}
+
+template<class T>
+static int
+test_node(const T& 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<const char*>(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;
+}
+
+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<std::string>(resolved) == "http://example.org/rel/uri");
+ // assert(static_cast<serd::StringView>(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<bool>(serd::make(true)) == true);
+ assert(serd::get<bool>(serd::make(false)) == false);
+ assert(serd::get<double>(serd::make(1.5)) == 1.5);
+ assert(serd::get<double>(serd::make(-2.5)) == -2.5);
+ assert(serd::get<float>(serd::make(1.2f)) == 1.2f);
+ assert(serd::get<float>(serd::make(-2.5f)) == -2.5f);
+ assert(serd::get<int64_t>(serd::make(12)) == 12);
+ assert(serd::get<int64_t>(serd::make(-34)) == -34);
+
+ return 0;
+}
+
+static int
+test_uri()
+{
+ const auto uri = serd::make_uri("file:/path");
+ const auto no_authority = serd::URI{uri.string_view()};
+ 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<serd::Node> base_uri;
+ serd::Optional<serd::Node> ns_name;
+ serd::Optional<serd::Node> ns_uri;
+ serd::Optional<serd::Node> ended_node;
+ size_t n_statements{};
+ std::stringstream stream{};
+ serd::Sink sink{world};
+
+ sink.set_base_func([&](const serd::NodeView uri) {
+ base_uri = serd::Node{uri};
+ return serd::Status::success;
+ });
+
+ sink.set_prefix_func(
+ [&](const serd::NodeView name, const serd::NodeView uri) {
+ ns_name = serd::Node{name};
+ ns_uri = serd::Node{uri};
+ return serd::Status::success;
+ });
+
+ sink.set_statement_func(
+ [&](const serd::StatementFlags, const serd::StatementView& statement) {
+ ++n_statements;
+ stream << statement.subject() << " " << statement.predicate() << " "
+ << statement.object() << std::endl;
+ return serd::Status::success;
+ });
+
+ sink.set_end_func([&](const serd::NodeView node) {
+ ended_node = serd::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 <http://example.org/base> ."
+ "@prefix eg: <http://example.org/> ."
+ "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 <http://drobilla.net/base/> .\n"
+ "@prefix eg: <http://example.org/> .\n"
+ "\n"
+ "<http://drobilla.net/base/s>\n"
+ "\t<http://example.org/p> [\n"
+ "\t\t<http://example.org/p2> <http://drobilla.net/o>\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(base.string_view());
+ 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<serd::Node> ns_name;
+ serd::Optional<serd::Node> ns_uri;
+
+ sink.set_prefix_func([&](serd::NodeView name, serd::NodeView uri) {
+ ns_name = serd::Node{name};
+ ns_uri = serd::Node{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<TestFunc, 14> tests{{test_operators,
+ test_optional,
+ test_nodes,
+ test_string,
+ test_stringview,
+ 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;
+}