aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2021-01-13 20:22:49 +0100
committerDavid Robillard <d@drobilla.net>2022-01-13 23:04:09 -0500
commitd8e718453cf7a29045b648683b0645094bc66ae9 (patch)
treec3e0776fddbcb60caa90fbd984a3710365cdf8de
parentd30f90cae709a9192aa29f219a494c62b865e0f8 (diff)
downloadserd-d8e718453cf7a29045b648683b0645094bc66ae9.tar.gz
serd-d8e718453cf7a29045b648683b0645094bc66ae9.tar.bz2
serd-d8e718453cf7a29045b648683b0645094bc66ae9.zip
Add support for writing terse collections
-rw-r--r--NEWS1
-rw-r--r--include/serd/serd.h4
-rw-r--r--src/writer.c35
-rw-r--r--test/meson.build13
-rwxr-xr-xtest/run_test_suite.py12
-rw-r--r--test/terse/blank_object.ttl2
-rw-r--r--test/terse/blank_subject.ttl2
-rw-r--r--test/terse/collection_object.ttl2
-rw-r--r--test/terse/collection_subject.ttl2
-rw-r--r--test/terse/manifest.ttl38
-rw-r--r--test/test_terse_write.c114
11 files changed, 210 insertions, 15 deletions
diff --git a/NEWS b/NEWS
index 3f677a08..c28a7ab8 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,7 @@ serd (1.0.1) unstable;
* Add SerdBuffer for mutable buffers to keep SerdChunk const-correct
* Add SerdWorld for shared library state
* Add option for writing terse output without newlines
+ * Add support for writing terse collections
* Add support for xsd:float and xsd:double literals
* Bring read/write interface closer to C standard
* Make nodes opaque
diff --git a/include/serd/serd.h b/include/serd/serd.h
index 45af4069..bfab6192 100644
--- a/include/serd/serd.h
+++ b/include/serd/serd.h
@@ -1250,7 +1250,9 @@ typedef enum {
SERD_ANON_S = 1u << 1u, ///< Start of anonymous subject
SERD_ANON_O = 1u << 2u, ///< Start of anonymous object
SERD_LIST_S = 1u << 3u, ///< Start of list subject
- SERD_LIST_O = 1u << 4u ///< Start of list object
+ SERD_LIST_O = 1u << 4u, ///< Start of list object
+ SERD_TERSE_S = 1u << 5u, ///< Start of terse subject
+ SERD_TERSE_O = 1u << 6u ///< Start of terse object
} SerdStatementFlag;
/// Bitwise OR of SerdStatementFlag values
diff --git a/src/writer.c b/src/writer.c
index e0db3a06..8c96b2a8 100644
--- a/src/writer.c
+++ b/src/writer.c
@@ -61,8 +61,11 @@ typedef enum {
SEP_ANON_S_P, ///< Between start of anonymous node and predicate
SEP_ANON_END, ///< End of anonymous node (']')
SEP_LIST_BEGIN, ///< Start of list ('(')
- SEP_LIST_SEP, ///< List separator (whitespace)
+ SEP_LIST_SEP, ///< List separator (newline)
SEP_LIST_END, ///< End of list (')')
+ SEP_TLIST_BEGIN, ///< Start of terse list ('(')
+ SEP_TLIST_SEP, ///< Terse list separator (space)
+ SEP_TLIST_END, ///< End of terse list (')')
SEP_GRAPH_BEGIN, ///< Start of graph ('{')
SEP_GRAPH_END, ///< End of graph ('}')
} Sep;
@@ -88,12 +91,15 @@ static const SepRule rules[] = {
{",", 1, +0, SEP_ALL, SEP_NONE, ~(M(SEP_ANON_END) | M(SEP_LIST_END))},
{"", 0, +1, SEP_NONE, SEP_NONE, SEP_ALL},
{" ", 1, +0, SEP_NONE, SEP_NONE, SEP_NONE},
- {"[", 1, +1, M(SEP_END_O), SEP_NONE, SEP_NONE},
+ {"[", 1, +1, M(SEP_END_O), M(SEP_TLIST_BEGIN) | M(SEP_TLIST_SEP), SEP_NONE},
{"", 0, +0, SEP_NONE, SEP_ALL, SEP_NONE},
{"]", 1, -1, SEP_NONE, ~M(SEP_ANON_BEGIN), SEP_NONE},
{"(", 1, +1, M(SEP_END_O), SEP_NONE, SEP_ALL},
{"", 0, +0, SEP_NONE, SEP_ALL, SEP_NONE},
{")", 1, -1, SEP_NONE, SEP_ALL, SEP_NONE},
+ {"(", 1, +1, SEP_NONE, SEP_NONE, SEP_NONE},
+ {"", 0, +0, SEP_ALL, SEP_NONE, SEP_NONE},
+ {")", 1, -1, SEP_NONE, SEP_NONE, SEP_NONE},
{"{", 1, +1, SEP_ALL, SEP_NONE, SEP_NONE},
{"}", 1, -1, SEP_NONE, SEP_NONE, SEP_ALL},
};
@@ -483,9 +489,9 @@ uri_sink(const void* buf, size_t size, size_t nmemb, void* stream)
}
static void
-write_newline(SerdWriter* writer)
+write_newline(SerdWriter* writer, bool terse)
{
- if (writer->flags & SERD_WRITE_TERSE) {
+ if (terse || (writer->flags & SERD_WRITE_TERSE)) {
sink(" ", 1, writer);
} else {
sink("\n", 1, writer);
@@ -499,16 +505,21 @@ static void
write_top_level_sep(SerdWriter* writer)
{
if (!writer->empty && !(writer->flags & SERD_WRITE_TERSE)) {
- write_newline(writer);
+ write_newline(writer, false);
}
}
static bool
write_sep(SerdWriter* writer, const SerdStatementFlags flags, Sep sep)
{
- (void)flags;
+ const SepRule* const rule = &rules[sep];
- const SepRule* rule = &rules[sep];
+ const bool terse = (((flags & SERD_TERSE_S) && (flags & SERD_LIST_S)) ||
+ ((flags & SERD_TERSE_O) && (flags & SERD_LIST_O)));
+
+ if (terse && sep >= SEP_LIST_BEGIN && sep <= SEP_LIST_END) {
+ sep = (Sep)((int)sep + 3); // Switch to corresponding terse separator
+ }
// Adjust indent, but tolerate if it would become negative
if ((rule->pre_line_after & (1u << writer->last_sep)) ||
@@ -520,7 +531,7 @@ write_sep(SerdWriter* writer, const SerdStatementFlags flags, Sep sep)
// Write newline or space before separator if necessary
if (rule->pre_line_after & (1u << writer->last_sep)) {
- write_newline(writer);
+ write_newline(writer, terse);
} else if (rule->pre_space_after & (1u << writer->last_sep)) {
sink(" ", 1, writer);
}
@@ -530,7 +541,7 @@ write_sep(SerdWriter* writer, const SerdStatementFlags flags, Sep sep)
// Write newline after separator if necessary
if (rule->post_line_after & (1u << writer->last_sep)) {
- write_newline(writer);
+ write_newline(writer, terse);
writer->last_sep = SEP_NONE;
} else {
writer->last_sep = sep;
@@ -844,8 +855,10 @@ serd_writer_write_statement(SerdWriter* const writer,
const SerdNode* const graph = serd_statement_graph(statement);
if (!is_resource(subject) || !is_resource(predicate) || !object ||
- ((flags & SERD_ANON_S) && (flags & SERD_LIST_S)) ||
- ((flags & SERD_ANON_O) && (flags & SERD_LIST_O))) {
+ ((flags & SERD_ANON_S) && (flags & SERD_LIST_S)) || // Nonsense
+ ((flags & SERD_ANON_O) && (flags & SERD_LIST_O)) || // Nonsense
+ ((flags & SERD_ANON_S) && (flags & SERD_TERSE_S)) || // Unsupported
+ ((flags & SERD_ANON_O) && (flags & SERD_TERSE_O))) { // Unsupported
return SERD_ERR_BAD_ARG;
}
diff --git a/test/meson.build b/test/meson.build
index 8a76bbb6..cd748c0c 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -16,6 +16,7 @@ unit_tests = [
'sink',
'statement',
'string',
+ 'terse_write',
'uri',
'world',
'writer',
@@ -163,7 +164,7 @@ if get_option('utils')
serd_suites = ['good', 'bad']
serd_base = 'http://drobilla.net/sw/serd/test/'
- ### Run all suites with no special arguments
+ ### Run basic suites with no special arguments
foreach name : serd_suites
manifest = files(name / 'manifest.ttl')
base_uri = serd_base + name + '/'
@@ -174,6 +175,16 @@ if get_option('utils')
timeout: 240)
endforeach
+ ### The terse suite needs to be run with -t
+ test('terse', run_test_suite,
+ args: script_args + ['--osyntax', 'turtle',
+ files('terse/manifest.ttl'),
+ serd_base + 'terse/',
+ '--', '-t'],
+ env: test_env,
+ suite: ['rdf', 'serd'],
+ timeout: 240)
+
### The lax suite is special because it is run twice...
lax_manifest = files('lax/manifest.ttl')
lax_base_uri = serd_base + name + '/'
diff --git a/test/run_test_suite.py b/test/run_test_suite.py
index 5f1d29c8..5223060d 100755
--- a/test/run_test_suite.py
+++ b/test/run_test_suite.py
@@ -229,6 +229,7 @@ def test_suite(
base_uri,
report_filename,
input_syntax,
+ output_syntax,
command_prefix,
):
"""Run all tests in a test suite manifest."""
@@ -253,8 +254,11 @@ def test_suite(
def run_tests(test_class, tests, expected_return, results):
thru_flags = [["-e"], ["-f"], ["-b"], ["-r", "http://example.org/"]]
- osyntax = _test_output_syntax(test_class)
thru_options_iter = _option_combinations(thru_flags)
+ if output_syntax is not None:
+ osyntax = output_syntax
+ else:
+ osyntax = _test_output_syntax(test_class)
if input_syntax is not None:
isyntax = input_syntax
@@ -267,7 +271,9 @@ def test_suite(
test_name = os.path.basename(test_uri_path)
test_path = os.path.join(test_dir, test_name)
- command = command_prefix + ["-a"] + [test_path, test_uri]
+ command = (
+ command_prefix + ["-a", "-o", osyntax] + [test_path, test_uri]
+ )
command_string = " ".join(shlex.quote(c) for c in command)
out_filename = os.path.join(out_test_dir, test_name + ".out")
@@ -383,6 +389,7 @@ def main():
parser.add_argument("--report", help="path to write result report to")
parser.add_argument("--serdi", default="serdi", help="path to serdi")
parser.add_argument("--syntax", default=None, help="input syntax")
+ parser.add_argument("--osyntax", default=None, help="output syntax")
parser.add_argument("--wrapper", default="", help="executable wrapper")
parser.add_argument("manifest", help="test suite manifest.ttl file")
parser.add_argument("base_uri", help="base URI for tests")
@@ -400,6 +407,7 @@ def main():
args.base_uri,
args.report,
args.syntax,
+ args.osyntax,
command_prefix,
)
diff --git a/test/terse/blank_object.ttl b/test/terse/blank_object.ttl
new file mode 100644
index 00000000..270e406b
--- /dev/null
+++ b/test/terse/blank_object.ttl
@@ -0,0 +1,2 @@
+@prefix eg: <http://example.org/> .
+eg:s eg:p1 [ eg:p11 1 ; eg:p12 2 ] ; eg:p2 [ eg:p23 3 ; eg:p24 4 ] .
diff --git a/test/terse/blank_subject.ttl b/test/terse/blank_subject.ttl
new file mode 100644
index 00000000..5e3303f9
--- /dev/null
+++ b/test/terse/blank_subject.ttl
@@ -0,0 +1,2 @@
+@prefix eg: <http://example.org/> .
+[] eg:p1 1 ; eg:p2 2 .
diff --git a/test/terse/collection_object.ttl b/test/terse/collection_object.ttl
new file mode 100644
index 00000000..3310c418
--- /dev/null
+++ b/test/terse/collection_object.ttl
@@ -0,0 +1,2 @@
+@prefix eg: <http://example.org/> .
+eg:s eg:p1 ( 1 2 ) ; eg:p2 ( 3 4 ) .
diff --git a/test/terse/collection_subject.ttl b/test/terse/collection_subject.ttl
new file mode 100644
index 00000000..43670620
--- /dev/null
+++ b/test/terse/collection_subject.ttl
@@ -0,0 +1,2 @@
+@prefix eg: <http://example.org/> .
+( 1 2 ) eg:p3 3 ; eg:p4 4 .
diff --git a/test/terse/manifest.ttl b/test/terse/manifest.ttl
new file mode 100644
index 00000000..f8dca816
--- /dev/null
+++ b/test/terse/manifest.ttl
@@ -0,0 +1,38 @@
+@prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix rdft: <http://www.w3.org/ns/rdftest#> .
+
+<>
+ rdf:type mf:Manifest ;
+ rdfs:comment "Serd terse serialisation test cases" ;
+ mf:entries (
+ <#blank_object>
+ <#blank_subject>
+ <#collection_object>
+ <#collection_subject>
+ ) .
+
+<#blank_object>
+ rdf:type rdft:TestTurtleEval ;
+ mf:name "blank_object" ;
+ mf:action <blank_object.ttl> ;
+ mf:result <blank_object.ttl> .
+
+<#blank_subject>
+ rdf:type rdft:TestTurtleEval ;
+ mf:name "blank_subject" ;
+ mf:action <blank_subject.ttl> ;
+ mf:result <blank_subject.ttl> .
+
+<#collection_object>
+ rdf:type rdft:TestTurtleEval ;
+ mf:name "collection_object" ;
+ mf:action <collection_object.ttl> ;
+ mf:result <collection_object.ttl> .
+
+<#collection_subject>
+ rdf:type rdft:TestTurtleEval ;
+ mf:name "collection_subject" ;
+ mf:action <collection_subject.ttl> ;
+ mf:result <collection_subject.ttl> .
diff --git a/test/test_terse_write.c b/test/test_terse_write.c
new file mode 100644
index 00000000..9a19d493
--- /dev/null
+++ b/test/test_terse_write.c
@@ -0,0 +1,114 @@
+/*
+ Copyright 2019-2020 David Robillard <d@drobilla.net>
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted, provided that the above
+ copyright notice and this permission notice appear in all copies.
+
+ THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+#undef NDEBUG
+
+#include "serd/serd.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define NS_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+
+static void
+check_output(SerdWriter* writer, SerdBuffer* buffer, const char* expected)
+{
+ serd_writer_finish(writer);
+ serd_buffer_sink_finish(buffer);
+
+ const char* output = (const char*)buffer->buf;
+
+ fprintf(stderr, "%s", output);
+ assert(!strcmp(output, expected));
+
+ buffer->len = 0;
+}
+
+static int
+test(void)
+{
+ SerdBuffer buffer = {NULL, 0};
+ SerdWorld* world = serd_world_new();
+ SerdEnv* env = serd_env_new(SERD_EMPTY_STRING());
+ SerdNodes* nodes = serd_nodes_new();
+
+ const SerdNode* b1 = serd_nodes_blank(nodes, SERD_STRING("b1"));
+ const SerdNode* l1 = serd_nodes_blank(nodes, SERD_STRING("l1"));
+ const SerdNode* l2 = serd_nodes_blank(nodes, SERD_STRING("l2"));
+ const SerdNode* s1 = serd_nodes_string(nodes, SERD_STRING("s1"));
+ const SerdNode* s2 = serd_nodes_string(nodes, SERD_STRING("s2"));
+
+ const SerdNode* rdf_first =
+ serd_nodes_uri(nodes, SERD_STRING(NS_RDF "first"));
+
+ const SerdNode* rdf_value =
+ serd_nodes_uri(nodes, SERD_STRING(NS_RDF "value"));
+
+ const SerdNode* rdf_rest = serd_nodes_uri(nodes, SERD_STRING(NS_RDF "rest"));
+ const SerdNode* rdf_nil = serd_nodes_uri(nodes, SERD_STRING(NS_RDF "nil"));
+
+ serd_env_set_prefix(env, SERD_STRING("rdf"), SERD_STRING(NS_RDF));
+
+ SerdWriter* writer = serd_writer_new(
+ world, SERD_TURTLE, 0, env, (SerdWriteFunc)serd_buffer_sink, &buffer);
+
+ const SerdSink* sink = serd_writer_sink(writer);
+
+ // Simple lone list
+ serd_sink_write(sink, SERD_TERSE_S | SERD_LIST_S, l1, rdf_first, s1, NULL);
+ serd_sink_write(sink, 0, l1, rdf_rest, l2, NULL);
+ serd_sink_write(sink, 0, l2, rdf_first, s2, NULL);
+ serd_sink_write(sink, 0, l2, rdf_rest, rdf_nil, NULL);
+ check_output(writer, &buffer, "( \"s1\" \"s2\" ) .\n");
+
+ // Nested terse lists
+ serd_sink_write(sink,
+ SERD_TERSE_S | SERD_LIST_S | SERD_TERSE_O | SERD_LIST_O,
+ l1,
+ rdf_first,
+ l2,
+ NULL);
+ serd_sink_write(sink, 0, l2, rdf_first, s1, NULL);
+ serd_sink_write(sink, 0, l1, rdf_rest, rdf_nil, NULL);
+ serd_sink_write(sink, 0, l2, rdf_rest, rdf_nil, NULL);
+ check_output(writer, &buffer, "( ( \"s1\" ) ) .\n");
+
+ // List as object
+ serd_sink_write(
+ sink, SERD_EMPTY_S | SERD_LIST_O | SERD_TERSE_O, b1, rdf_value, l1, NULL);
+ serd_sink_write(sink, 0, l1, rdf_first, s1, NULL);
+ serd_sink_write(sink, 0, l1, rdf_rest, l2, NULL);
+ serd_sink_write(sink, 0, l2, rdf_first, s2, NULL);
+ serd_sink_write(sink, 0, l2, rdf_rest, rdf_nil, NULL);
+ check_output(writer, &buffer, "[]\n\trdf:value ( \"s1\" \"s2\" ) .\n");
+
+ serd_buffer_sink_finish(&buffer);
+ serd_writer_free(writer);
+ serd_nodes_free(nodes);
+ serd_env_free(env);
+ serd_world_free(world);
+ free(buffer.buf);
+
+ return 0;
+}
+
+int
+main(void)
+{
+ return test();
+}