1. RDF Messages Interoperability Tests
This document proposes interoperability tests for implementations that support RDF Message Logs and RDF Message live streams for different serializations.
Implementations of parsers, serializers, and live stream adapters MUST provide conformance tests that verify message-level behavior.
An RDF Message Log parser MUST expose a message-level interface that emits one RDF Dataset per completed message.
A message is completed when a message delimiter or the end of the input is encountered. Empty messages MUST be preserved when they occur before the first triple or quad, or between two delimiters. A trailing delimiter after a non-empty message MUST finalize that message and MUST not by itself create an additional empty trailing message.
Blank node identifiers in RDF Message Logs MUST be scoped to the message in which they occur. Therefore, if the same blank node label appears in two different messages in the same input stream, the parser MUST produce two distinct blank nodes. This requirement applies even when the entire RDF Message Log is parsed from one continuous input string or stream by one parser instance.
Message delimiters are message-level syntax. They MUST only occur where the underlying RDF syntax permits directives or top-level statements, and MUST not occur inside an open graph block. A delimiter encountered inside a graph block MUST be a parse error.
Serializers should produce RDF Message Logs that round-trip to the same message sequence and the same quads per message. Conformance tests should not require a serializer to choose a specific delimiter spelling when more than one delimiter spelling is supported by the target syntax.
1.1. Message Stream Discovery Tests
These tests apply to adapters that expose protocol messages as RDF Messages.
1.1.1. Server-Sent Events With JSON-LD Context Link
Stream metadata:
Content-Type: text/event-stream Link: <https://example.org/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json" RDF-Message-Content-Type: application/ld+json
The context document at https://example.org/context.jsonld is:
{ "@context" :{ "ex" : "http://example.org/" , "p" :{ "@id" : "http://example.org/p" , "@type" : "@id" }}}
Input events:
event: rdf-message
data: {"@id":"ex:s1","p":"ex:o1"}
event: rdf-message
data: {"@id":"ex:s2","p":"ex:o2"}
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
1.1.2. WebSocket Subprotocol Negotiation
Handshake request:
Sec-WebSocket-Protocol: rdfm.ld-json, rdfm.n-triples
Handshake response:
Sec-WebSocket-Protocol: rdfm.n-triples
Input frames:
Frame 1: _:b0 <http://example.org/p> <http://example.org/o1> . Frame 2: _:b0 <http://example.org/p> <http://example.org/o2> .
Expected result:
Message 1 contains one triple whose subject is a blank node. Message 2 contains one triple whose subject is a blank node. The two blank nodes must not be the same RDF term.
1.1.3. Kafka Records
Record headers for both records:
RDF-Message-Content-Type: application/n-triples
Input records:
Record 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Record 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
1.1.4. MQTT 5 Publish Packets
PUBLISH properties for both packets:
Content Type: application/ld+json
Input packets:
Packet 1 payload:
{"@id":"http://example.org/s1","http://example.org/p":{"@id":"http://example.org/o1"}}
Packet 2 payload:
{"@id":"http://example.org/s2","http://example.org/p":{"@id":"http://example.org/o2"}}
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
1.1.5. Other Broker Messages
Message headers for both broker messages:
RDF-Message-Content-Type: application/trig
Input broker messages:
Message 1 body:
<http://example.org/g1> {
<http://example.org/s1> <http://example.org/p> <http://example.org/o1> .
}
Message 2 body:
<http://example.org/g2> {
<http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
}
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> <http://example.org/g1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> <http://example.org/g2> .
1.1.6. Jelly gRPC Frames
Input stream:
RdfStreamFrame 1: RDF dataset containing <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . RdfStreamFrame 2: RDF dataset containing <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
1.1.7. SSE Rejects Direct Binary RDF Payloads
Stream metadata:
Content-Type: text/event-stream RDF-Message-Content-Type: application/x-jelly-rdf
Expected result:
The adapter must reject the stream metadata because binary RDF serializations must not be sent directly in SSE data fields.
1.2. Parsing Tests for Turtle, TriG, N-Triples, and N-Quads
1.2.1. Single Message Without Delimiter
Input:
VERSION"1.2-messages" <http://example.org/s1> <http://example.org/p> <http://example.org/o1> .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> .
1.2.2. Two Messages With MESSAGE
Input:
VERSION"1.2-messages" <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . MESSAGE<http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
1.2.3. Two Messages With @message .
Input:
@version"1.2-messages" . @prefix ex: <http://example.org/> . ex : s1 ex : p ex : o1 . @message. ex : s2 ex : p ex : o2 .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
1.2.4. Empty First Message
Input:
VERSION"1.2-messages" MESSAGE<http://example.org/s1> <http://example.org/p> <http://example.org/o1> .
Expected messages:
Message 1: empty Message 2: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> .
1.2.5. Empty Message Between Non-Empty Messages
Input:
VERSION"1.2-messages" <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . MESSAGE MESSAGE<http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: empty Message 3: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
1.2.6. Final Delimiter Does Not Create Extra Empty Message
Input:
VERSION"1.2-messages" <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . MESSAGE
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> .
1.2.7. N-Quads Messages Preserve Graph Names
Input:
VERSION "1.2-messages" <http://example.org/s1> <http://example.org/p> <http://example.org/o1> <http://example.org/g1> . MESSAGE <http://example.org/s2> <http://example.org/p> <http://example.org/o2> <http://example.org/g2> .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> <http://example.org/g1> . Message 2: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> <http://example.org/g2> .
1.2.8. Message With Default Graph And Named Graph Quads
Input:
VERSION"1.2-messages" PREFIX ex: <http://example.org/> ex : s1 ex : p ex : o1 . ex : g { ex : s2 ex : p ex : o2 . ex : s3 ex : p ex : o3 . } MESSAGEex : s4 ex : p ex : o4 .
Expected messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . <http://example.org/s2> <http://example.org/p> <http://example.org/o2> <http://example.org/g> . <http://example.org/s3> <http://example.org/p> <http://example.org/o3> <http://example.org/g> . Message 2: <http://example.org/s4> <http://example.org/p> <http://example.org/o4> .
1.2.9. Blank Node Labels Are Scoped Per Message
Input:
VERSION"1.2-messages" _ : b0 <http://example.org/p> <http://example.org/o1> . MESSAGE_ : b0 <http://example.org/p> <http://example.org/o2> .
Expected result:
Message 1 contains one quad whose subject is a blank node. Message 2 contains one quad whose subject is a blank node. The two blank nodes must not be the same RDF term.
This test must be performed on one continuous input string or stream, not by creating two independent parser instances.
1.2.10. Repeated Prefixes Across Messages
Input:
VERSION"1.2-messages" PREFIX ex: <http://example.org/one/> ex : s ex : p ex : o . MESSAGEPREFIX ex: <http://example.org/two/> ex : s ex : p ex : o .
Expected messages:
Message 1: <http://example.org/one/s> <http://example.org/one/p> <http://example.org/one/o> . Message 2: <http://example.org/two/s> <http://example.org/two/p> <http://example.org/two/o> .
1.2.11. Message Boundary After A Graph Block
Input:
VERSION"1.2-messages" <http://example.org/g> { <http://example.org/a> <http://example.org/b> <http://example.org/c> . } MESSAGE<http://example.org/d> <http://example.org/e> <http://example.org/f> .
Expected messages:
Message 1: <http://example.org/a> <http://example.org/b> <http://example.org/c> <http://example.org/g> . Message 2: <http://example.org/d> <http://example.org/e> <http://example.org/f> .
1.3. Parsing Tests for NDJSON-LD
In these tests, each input line is one JSON object. Context messages update parser state but do not emit RDF messages. A parser maintains an active context that is updated by context messages.
1.3.1. Context Message Followed By Data Message
Input:
{ "@context" : "https://schema.org/" } { "@type" : "Product" , "name" : "Hairdryer DRY 2000" , "color" : "red" }
Expected messages:
Message 1: RDF dataset obtained by interpreting the second JSON object with @context = "https://schema.org/".
1.3.2. Empty Message Does Not Change Active Context
Input:
{ "@context" : "https://schema.org/" } {} { "@type" : "Product" , "name" : "27-inch 4K Monitor 2700P" , "color" : "black" }
Expected messages:
Message 1: empty Message 2: RDF dataset obtained by interpreting the third JSON object with @context = "https://schema.org/".
1.3.3. Context Switch Applies To Subsequent Data Messages
Input:
{ "@context" : "https://schema.org/" } { "@type" : "Product" , "name" : "Hairdryer DRY 2000" , "color" : "red" } { "@context" : "https://example.org/other-vocab/" } { "@type" : "OtherType" , "name" : "Piotr" }
Expected messages:
Message 1: RDF dataset obtained by interpreting the second JSON object with @context = "https://schema.org/". Message 2: RDF dataset obtained by interpreting the fourth JSON object with @context = "https://example.org/other-vocab/".
1.3.4. Data Message With Local @context Is Wrapped With Active Context
Input:
{ "@context" : { "ex" : "https://example.org/base/" , "name" : "ex:baseName" }} { "@context" : { "label" : "ex:label" }, "@type" : "ex:Thing" , "name" : "Alice" , "label" : "A" } { "@type" : "ex:Thing" , "name" : "Bob" }
Expected messages:
Message 1:
RDF dataset obtained by processing the effective JSON-LD document:
{"@context": {"ex": "https://example.org/base/", "name": "ex:baseName"}, "@graph": {"@context": {"label": "ex:label"}, "@type": "ex:Thing", "name": "Alice", "label": "A"}}
Message 2:
RDF dataset obtained by processing the effective JSON-LD document:
{"@context": {"ex": "https://example.org/base/", "name": "ex:baseName"}, "@graph": {"@type": "ex:Thing", "name": "Bob"}}
The local @context from line 2 does not change the active context C for subsequent messages.
1.3.5. Data Message Without Prior Context Is Allowed
Input:
{ "@id" : "https://example.org/p1" , "@type" : "https://example.org/Product" }
Expected messages:
Message 1: RDF dataset obtained by interpreting the JSON object as regular JSON-LD, without a stream-level active context.
1.4. Serialization Tests for Turtle, TriG, N-Triples, and N-Quads
Given a sequence of RDF Messages, a serializer should write a document that can be parsed back into the same sequence of messages and quads.
Serializers may choose the concrete message delimiter supported by their syntax. These tests should not assert whether @message . or MESSAGE was used.
Round-trip input messages:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: empty Message 3: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
Expected result after serializing and parsing the output:
Message 1: <http://example.org/s1> <http://example.org/p> <http://example.org/o1> . Message 2: empty Message 3: <http://example.org/s2> <http://example.org/p> <http://example.org/o2> .
Round-trip input messages with named graph quads:
Message 1: <http://example.org/a> <http://example.org/b> <http://example.org/c> <http://example.org/g> . Message 2: <http://example.org/d> <http://example.org/e> <http://example.org/f> <http://example.org/g> .
Expected result after serializing and parsing the output:
Message 1: <http://example.org/a> <http://example.org/b> <http://example.org/c> <http://example.org/g> . Message 2: <http://example.org/d> <http://example.org/e> <http://example.org/f> <http://example.org/g> .
1.5. Error Tests
1.5.1. Message Delimiter Without Message Support
If a parser is not in RDF Messages mode and encounters a message delimiter, it must reject the document.
Input:
<http://example.org/s> <http://example.org/p> <http://example.org/o> . MESSAGE
Expected result:
Parse error.
1.5.2. @message Without Trailing Dot
Input:
VERSION"1.2-messages" <http://example.org/s> <http://example.org/p> <http://example.org/o> . @message<http://example.org/invalid>
Expected result:
Parse error.
1.5.3. Message Delimiter Inside An Open Graph Block
A message delimiter must not be accepted inside an open graph block. It must be rejected instead of being interpreted as splitting the graph block across two messages.
Input:
VERSION"1.2-messages" <http://example.org/g> { <http://example.org/a> <http://example.org/b> <http://example.org/c> . MESSAGE<http://example.org/d> <http://example.org/e> <http://example.org/f> . }
Expected result:
Parse error.