diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..f3768924
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,47 @@
+# This workflow runs continuous CI across different versions of ruby on all branches and pull requests to develop.
+
+name: CI
+
+# Controls when the action will run.
+on:
+ # Triggers the workflow on push or pull request events but only for the develop branch
+ push:
+ branches: [ '**' ]
+ pull_request:
+ branches: [ develop ]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ # This workflow contains a single job called "build"
+ tests:
+ name: Ruby ${{ matrix.ruby }}
+ if: "contains(github.event.commits[0].message, '[ci skip]') == false"
+ runs-on: ubuntu-latest
+ env:
+ CI: true
+ ALLOW_FAILURES: false ${{ endsWith(matrix.ruby, 'head') }}
+ strategy:
+ fail-fast: false
+ matrix:
+ ruby:
+ - 2.4
+ - 2.5
+ - 2.6
+ - 2.7
+ # - ruby-head # net-http-persistent
+ - jruby
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v2
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ - name: Install dependencies
+ run: bundle install --jobs 4 --retry 3
+ - name: Run tests
+ run: bundle exec rspec spec || $ALLOW_FAILURES
+
diff --git a/README.md b/README.md
index e6e112f5..cb522c7c 100755
--- a/README.md
+++ b/README.md
@@ -2,9 +2,10 @@
[JSON-LD][] reader/writer for [RDF.rb][RDF.rb] and fully conforming [JSON-LD API][] processor. Additionally this gem implements [JSON-LD Framing][].
-[![Gem Version](https://badge.fury.io/rb/json-ld.png)](https://badge.fury.io/rb/json-ld)
-[![Build Status](https://secure.travis-ci.org/ruby-rdf/json-ld.png?branch=master)](https://travis-ci.org/ruby-rdf/json-ld)
-[![Coverage Status](https://coveralls.io/repos/ruby-rdf/json-ld/badge.svg)](https://coveralls.io/r/ruby-rdf/json-ld)
+[![Gem Version](https://badge.fury.io/rb/json-ld.png)](https://rubygems.org/gems/json-ld)
+[![Build Status](https://secure.travis-ci.org/ruby-rdf/json-ld.png?branch=develop)](https://github.com/ruby-rdf/json-ld/actions?query=workflow%3ACI)
+[![Coverage Status](https://coveralls.io/repos/ruby-rdf/json-ld/badge.svg)](https://coveralls.io/github/ruby-rdf/json-ld)
+[![Gitter chat](https://badges.gitter.im/ruby-rdf.png)](https://gitter.im/gitterHQ/gitter)
## Features
@@ -14,6 +15,7 @@ JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and
* If the [jsonlint][] gem is installed, it will be used when validating an input document.
* If available, uses [Nokogiri][] and/or [Nokogumbo][] for parsing HTML, falls back to REXML otherwise.
+* Provisional support for [JSON-LD*][JSON-LD*].
[Implementation Report](file.earl.html)
@@ -35,6 +37,59 @@ The order of triples retrieved from the `RDF::Enumerable` dataset determines the
### MultiJson parser
The [MultiJson](https://rubygems.org/gems/multi_json) gem is used for parsing JSON; this defaults to the native JSON parser, but will use a more performant parser if one is available. A specific parser can be specified by adding the `:adapter` option to any API call. See [MultiJson](https://rubygems.org/gems/multi_json) for more information.
+### JSON-LD* (RDFStar)
+
+The {JSON::LD::API.toRdf} and {JSON::LD::API.fromRdf} API methods, along with the {JSON::LD::Reader} and {JSON::LD::Writer}, include provisional support for [JSON-LD*][JSON-LD*].
+
+Internally, an `RDF::Statement` is treated as another resource, along with `RDF::URI` and `RDF::Node`, which allows an `RDF::Statement` to have a `#subject` or `#object` which is also an `RDF::Statement`.
+
+In JSON-LD, with the `rdfstar` option set, the value of `@id`, in addition to an IRI or Blank Node Identifier, can be a JSON-LD node object having exactly one property with an optional `@id`, which may also be an embedded object. (It may also have `@context` and `@index` values).
+
+ {
+ "@id": {
+ "@context": {"foaf": "http://xmlns.com/foaf/0.1/"},
+ "@index": "ignored",
+ "@id": "bob",
+ "foaf:age" 23
+ },
+ "ex:certainty": 0.9
+ }
+
+**Note: This feature is subject to change or elimination as the standards process progresses.**
+
+#### Serializing a Graph containing embedded statements
+
+ require 'json-ld'
+ statement = RDF::Statement(RDF::URI('bob'), RDF::Vocab::FOAF.age, RDF::Literal(23))
+ graph = RDF::Graph.new << [statement, RDF::URI("ex:certainty"), RDF::Literal(0.9)]
+ graph.dump(:jsonld, validate: false, standard_prefixes: true)
+ # => {"@id": {"@id": "bob", "foaf:age" 23}, "ex:certainty": 0.9}
+
+Alternatively, using the {JSON::LD::API.fromRdf} method:
+
+ JSON::LD::API::fromRdf(graph)
+ # => {"@id": {"@id": "bob", "foaf:age" 23}, "ex:certainty": 0.9}
+
+#### Reading a Graph containing embedded statements
+
+By default, {JSON::LD::API.toRdf} (and {JSON::LD::Reader}) will reject a document containing a subject resource.
+
+ jsonld = %({
+ "@id": {
+ "@id": "bob", "foaf:age" 23
+ },
+ "ex:certainty": 0.9
+ })
+ graph = RDF::Graph.new << JSON::LD::API.toRdf(input)
+ # => JSON::LD::JsonLdError::InvalidIdValue
+
+{JSON::LD::API.toRdf} (and {JSON::LD::Reader}) support a boolean valued `rdfstar` option; only one statement is asserted, although the reified statement is contained within the graph.
+
+ graph = RDF::Graph.new do |graph|
+ JSON::LD::Reader.new(jsonld, rdfstar: true) {|reader| graph << reader}
+ end
+ graph.count #=> 1
+
## Examples
```ruby
@@ -568,6 +623,7 @@ see or the accompanying {file:UNLICENSE} file.
[YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md
[PDD]: https://unlicense.org/#unlicensing-contributions
[RDF.rb]: https://rubygems.org/gems/rdf
+[JSON-LD*]: https://json-ld.github.io/json-ld-star/
[Rack::LinkedData]: https://rubygems.org/gems/rack-linkeddata
[Backports]: https://rubygems.org/gems/backports
[JSON-LD]: https://www.w3.org/TR/json-ld11/ "JSON-LD 1.1"
diff --git a/VERSION b/VERSION
index 3ad0595a..9cec7165 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.1.5
+3.1.6
diff --git a/etc/doap.jsonld b/etc/doap.jsonld
index 239c807a..79c42887 100644
--- a/etc/doap.jsonld
+++ b/etc/doap.jsonld
@@ -26,7 +26,7 @@
"@type": "doap:Project",
"doap:name": "JSON::LD",
"doap:homepage": "https://github.com/ruby-rdf/json-ld/",
- "doap:license": "https://unlicense.org/",
+ "doap:license": "https://unlicense.org/1.0/",
"doap:shortdesc": "JSON-LD support for RDF.rb.",
"doap:description": "RDF.rb extension for parsing/serializing JSON-LD data.",
"doap:created": "2011-05-07",
diff --git a/etc/doap.nt b/etc/doap.nt
index deb3306d..0cfcd495 100644
--- a/etc/doap.nt
+++ b/etc/doap.nt
@@ -14,7 +14,7 @@
.
.
.
- .
+ .
.
"JSON::LD" .
"Ruby" .
diff --git a/etc/doap.ttl b/etc/doap.ttl
index 4f382f23..5f9b3c1b 100644
--- a/etc/doap.ttl
+++ b/etc/doap.ttl
@@ -19,7 +19,7 @@
doap:implements ,
,
;
- doap:license ;
+ doap:license ;
doap:maintainer ;
doap:name "JSON::LD"^^xsd:string;
doap:programming-language "Ruby";
diff --git a/etc/earl-stream.ttl b/etc/earl-stream.ttl
index c5029885..e10ea1a0 100644
--- a/etc/earl-stream.ttl
+++ b/etc/earl-stream.ttl
@@ -18,7 +18,7 @@
doap:implements ,
,
;
- doap:license ;
+ doap:license ;
doap:maintainer ;
doap:name "JSON::LD"^^xsd:string;
doap:programming-language "Ruby"^^xsd:string;
diff --git a/etc/earl.ttl b/etc/earl.ttl
index a70b4fa6..7604951b 100644
--- a/etc/earl.ttl
+++ b/etc/earl.ttl
@@ -18,7 +18,7 @@
doap:implements ,
,
;
- doap:license ;
+ doap:license ;
doap:maintainer ;
doap:name "JSON::LD"^^xsd:string;
doap:programming-language "Ruby"^^xsd:string;
diff --git a/example-files/bob-star.jsonld b/example-files/bob-star.jsonld
new file mode 100644
index 00000000..8268b74e
--- /dev/null
+++ b/example-files/bob-star.jsonld
@@ -0,0 +1,12 @@
+{
+ "@context": {
+ "@base": "http://example.org/",
+ "ex": "http://example.org/",
+ "foaf": "http://xmlns.com/foaf/0.1/"
+ },
+ "@id": {
+ "@id": "bob",
+ "foaf:age": 23
+ },
+ "ex:certainty": 0.8
+}
diff --git a/lib/json/ld.rb b/lib/json/ld.rb
index efcb5bc7..5f3b2635 100644
--- a/lib/json/ld.rb
+++ b/lib/json/ld.rb
@@ -137,6 +137,7 @@ class InvalidLocalContext < JsonLdError; @code = "invalid local context"; end
class InvalidNestValue < JsonLdError; @code = "invalid @nest value"; end
class InvalidPrefixValue < JsonLdError; @code = "invalid @prefix value"; end
class InvalidPropagateValue < JsonLdError; @code = "invalid @propagate value"; end
+ class InvalidEmbeddedNode < JsonLdError; @code = "invalid reified node"; end
class InvalidRemoteContext < JsonLdError; @code = "invalid remote context"; end
class InvalidReverseProperty < JsonLdError; @code = "invalid reverse property"; end
class InvalidReversePropertyMap < JsonLdError; @code = "invalid reverse property map"; end
diff --git a/lib/json/ld/api.rb b/lib/json/ld/api.rb
index c7c7a252..2a7373d2 100644
--- a/lib/json/ld/api.rb
+++ b/lib/json/ld/api.rb
@@ -89,6 +89,8 @@ class API
# @option options [String] :processingMode
# Processing mode, json-ld-1.0 or json-ld-1.1.
# If `processingMode` is not specified, a mode of `json-ld-1.0` or `json-ld-1.1` is set, the context used for `expansion` or `compaction`.
+ # @option options [Boolean] rdfstar (false)
+ # support parsing JSON-LD* statement resources.
# @option options [Boolean] :rename_bnodes (true)
# Rename bnodes as part of expansion, or keep them the same.
# @option options [Boolean] :unique_bnodes (false)
diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb
index 87d56005..a8a9a97f 100644
--- a/lib/json/ld/expand.rb
+++ b/lib/json/ld/expand.rb
@@ -273,12 +273,23 @@ def expand_object(input, active_property, context, output_object,
context.expand_iri(v, as_string: true, base: @options[:base], documentRelative: true)
end
when Hash
- raise JsonLdError::InvalidIdValue,
- "value of @id must be a string unless framing: #{value.inspect}" unless framing
- raise JsonLdError::InvalidTypeValue,
- "value of @id must be a an empty object for framing: #{value.inspect}" unless
- value.empty?
- [{}]
+ if framing
+ raise JsonLdError::InvalidTypeValue,
+ "value of @id must be a an empty object for framing: #{value.inspect}" unless
+ value.empty?
+ [{}]
+ elsif @options[:rdfstar]
+ # Result must have just a single statement
+ rei_node = expand(value, active_property, context, log_depth: log_depth.to_i + 1)
+ statements = to_enum(:item_to_rdf, rei_node)
+ raise JsonLdError::InvalidEmbeddedNode,
+ "Embedded node with #{statements.size} statements" unless
+ statements.count == 1
+ rei_node
+ else
+ raise JsonLdError::InvalidIdValue,
+ "value of @id must be a string unless framing: #{value.inspect}" unless framing
+ end
else
raise JsonLdError::InvalidIdValue,
"value of @id must be a string or hash if framing: #{value.inspect}"
diff --git a/lib/json/ld/format.rb b/lib/json/ld/format.rb
index 8d73210d..b482300e 100644
--- a/lib/json/ld/format.rb
+++ b/lib/json/ld/format.rb
@@ -165,7 +165,16 @@ def self.cli_commands
end
end
end
- end
+ end,
+ options: [
+ RDF::CLI::Option.new(
+ symbol: :context,
+ datatype: RDF::URI,
+ control: :url2,
+ use: :required,
+ on: ["--context CONTEXT"],
+ description: "Context to use when compacting.") {|arg| RDF::URI(arg)},
+ ]
},
frame: {
description: "Frame JSON-LD or parsed RDF",
diff --git a/lib/json/ld/from_rdf.rb b/lib/json/ld/from_rdf.rb
index 27a088b4..f7c09550 100644
--- a/lib/json/ld/from_rdf.rb
+++ b/lib/json/ld/from_rdf.rb
@@ -22,7 +22,6 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
referenced_once = {}
value = nil
- ec = @context
# Create an entry for compound-literal node detection
compound_literal_subjects = {}
@@ -33,7 +32,7 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
dataset.each do |statement|
#log_debug("statement") { statement.to_nquads.chomp}
- name = statement.graph_name ? ec.expand_iri(statement.graph_name, base: @options[:base]).to_s : '@default'
+ name = statement.graph_name ? @context.expand_iri(statement.graph_name, base: @options[:base]).to_s : '@default'
# Create a graph entry as needed
node_map = graph_map[name] ||= {}
@@ -41,30 +40,29 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
default_graph[name] ||= {'@id' => name} unless name == '@default'
- subject = ec.expand_iri(statement.subject, as_string: true, base: @options[:base])
- node = node_map[subject] ||= {'@id' => subject}
+ subject = statement.subject.to_s
+ node = node_map[subject] ||= resource_representation(statement.subject,useNativeTypes)
# If predicate is rdf:datatype, note subject in compound literal subjects map
if @options[:rdfDirection] == 'compound-literal' && statement.predicate == RDF.to_uri + 'direction'
compound_literal_subjects[name][subject] ||= true
end
- # If object is an IRI or blank node identifier, and node map does not have an object member, create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
- node_map[statement.object.to_s] ||= {'@id' => statement.object.to_s} unless
- statement.object.literal?
+ # If object is an IRI, blank node identifier, or statement, and node map does not have an object member, create one and initialize its value to a new JSON object consisting of a single member @id whose value is set to object.
+ unless statement.object.literal?
+ node_map[statement.object.to_s] ||=
+ resource_representation(statement.object, useNativeTypes)
+ end
# If predicate equals rdf:type, and object is an IRI or blank node identifier, append object to the value of the @type member of node. If no such member exists, create one and initialize it to an array whose only item is object. Finally, continue to the next RDF triple.
+ # XXX JSON-LD* does not support embedded value of @type
if statement.predicate == RDF.type && statement.object.resource? && !useRdfType
merge_value(node, '@type', statement.object.to_s)
next
end
# Set value to the result of using the RDF to Object Conversion algorithm, passing object, rdfDirection, and use native types.
- value = ec.expand_value(nil,
- statement.object,
- rdfDirection: @options[:rdfDirection],
- useNativeTypes: useNativeTypes,
- base: @options[:base])
+ value = resource_representation(statement.object, useNativeTypes)
merge_value(node, statement.predicate.to_s, value)
@@ -162,5 +160,31 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
#log_debug("fromRdf") {result.to_json(JSON_STATE) rescue 'malformed json'}
result
end
+
+ private
+ def resource_representation(resource, useNativeTypes)
+ case resource
+ when RDF::Statement
+ # Note, if either subject or object are a BNode which is used elsewhere,
+ # this might not work will with the BNode accounting from above.
+ rep = {'@id' => resource_representation(resource.subject, false)}
+ if resource.predicate == RDF.type
+ rep['@id'].merge!('@type' => resource.object.to_s)
+ else
+ rep['@id'].merge!(
+ resource.predicate.to_s =>
+ as_array(resource_representation(resource.object, useNativeTypes)))
+ end
+ rep
+ when RDF::Literal
+ @context.expand_value(nil,
+ resource,
+ rdfDirection: @options[:rdfDirection],
+ useNativeTypes: useNativeTypes,
+ base: @options[:base])
+ else
+ {'@id' => resource.to_s}
+ end
+ end
end
end
diff --git a/lib/json/ld/to_rdf.rb b/lib/json/ld/to_rdf.rb
index 09628b7a..d3840d6c 100644
--- a/lib/json/ld/to_rdf.rb
+++ b/lib/json/ld/to_rdf.rb
@@ -16,6 +16,8 @@ module ToRDF
# @return RDF::Resource the subject of this item
def item_to_rdf(item, graph_name: nil, &block)
# Just return value object as Term
+ return unless item
+
if value?(item)
value, datatype = item.fetch('@value'), item.fetch('@type', nil)
@@ -76,11 +78,13 @@ def item_to_rdf(item, graph_name: nil, &block)
return parse_list(item['@list'], graph_name: graph_name, &block)
end
- # Skip if '@id' is nil
- subject = if item.has_key?('@id')
- item['@id'].nil? ? nil : as_resource(item['@id'])
- else
- node
+ subject = case item['@id']
+ when nil then node
+ when String then as_resource(item['@id'])
+ when Object
+ # Embedded statement
+ # (No error checking, as this is done in expansion)
+ to_enum(:item_to_rdf, item['@id']).to_a.first
end
#log_debug("item_to_rdf") {"subject: #{subject.to_ntriples rescue 'malformed rdf'}"}
diff --git a/script/parse b/script/parse
index cc131686..9c7bff78 100755
--- a/script/parse
+++ b/script/parse
@@ -116,6 +116,7 @@ OPT_ARGS = [
["--output", "-o", GetoptLong::REQUIRED_ARGUMENT, "Where to store output (default STDOUT)"],
["--profile", GetoptLong::NO_ARGUMENT, "Run profiler with output to doc/profiles/"],
["--quiet", GetoptLong::NO_ARGUMENT, "Reduce output"],
+ ["--rdfstar", GetoptLong::NO_ARGUMENT, "RDF* mode"],
["--stream", GetoptLong::NO_ARGUMENT, "Streaming reader/writer"],
["--uri", GetoptLong::REQUIRED_ARGUMENT, "Run with argument value as base"],
["--validate", GetoptLong::NO_ARGUMENT, "Validate input"],
@@ -156,6 +157,7 @@ opts.each do |opt, arg|
when '--quiet'
options[:quiet] = true
logger.level = Logger::FATAL
+ when '--rdfstar' then parser_options[:rdfstar] = true
when '--stream' then parser_options[:stream] = true
when '--uri' then parser_options[:base] = arg
when '--validate' then parser_options[:validate] = true
diff --git a/spec/expand_spec.rb b/spec/expand_spec.rb
index 52671c51..e4e04118 100644
--- a/spec/expand_spec.rb
+++ b/spec/expand_spec.rb
@@ -3376,6 +3376,254 @@
end
end
+ context "JSON-LD*" do
+ {
+ "node with embedded subject without rdfstar option": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidIdValue
+ },
+ }.each do |title, params|
+ it(title) {run_expand params}
+ end
+
+ {
+ "node with embedded subject having no @id": {
+ input: %({
+ "@id": {
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ output: %([{
+ "@id": {
+ "ex:prop": [{"@value": "value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }])
+ },
+ "node with embedded subject having IRI @id": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ output: %([{
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": [{"@value": "value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }])
+ },
+ "node with embedded subject having BNode @id": {
+ input: %({
+ "@id": {
+ "@id": "_:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ output: %([{
+ "@id": {
+ "@id": "_:rei",
+ "ex:prop": [{"@value": "value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }])
+ },
+ "node with embedded subject having a type": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "@type": "ex:Type"
+ },
+ "ex:prop": "value2"
+ }),
+ output: %([{
+ "@id": {
+ "@id": "ex:rei",
+ "@type": ["ex:Type"]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }])
+ },
+ "node with embedded subject having an IRI value": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": {"@id": "ex:value"}
+ },
+ "ex:prop": "value2"
+ }),
+ output: %([{
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": [{"@id": "ex:value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }])
+ },
+ "node with embedded subject having an BNode value": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": {"@id": "_:value"}
+ },
+ "ex:prop": "value2"
+ }),
+ output: %([{
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": [{"@id": "_:value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }])
+ },
+ "node with recursive embedded subject": {
+ input: %({
+ "@id": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value3"
+ },
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ output: %([{
+ "@id": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": [{"@value": "value3"}]
+ },
+ "ex:prop": [{"@value": "value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }])
+ },
+ "illegal node with subject having no property": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei"
+ },
+ "ex:prop": "value3"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "illegal node with subject having multiple properties": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": ["value1", "value2"]
+ },
+ "ex:prop": "value3"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "illegal node with subject having multiple types": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "@type": ["ex:Type1", "ex:Type2"]
+ },
+ "ex:prop": "value3"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "illegal node with subject having type and property": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "@type": "ex:Type",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "node with embedded object": {
+ input: %({
+ "@id": "ex:subj",
+ "ex:value": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ }
+ }
+ }),
+ output: %([{
+ "@id": "ex:subj",
+ "ex:value": [{
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": [{"@value": "value"}]
+ }
+ }]
+ }])
+ },
+ "illegal node with embedded object having properties": {
+ input: %({
+ "@id": "ex:subj",
+ "ex:value": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }
+ }),
+ output: %([{
+ "@id": "ex:subj",
+ "ex:value": [{
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": [{"@value": "value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }]
+ }])
+ },
+ "node with recursive embedded object": {
+ input: %({
+ "@id": "ex:subj",
+ "ex:value": {
+ "@id": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value3"
+ },
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }
+ }),
+ output: %([{
+ "@id": "ex:subj",
+ "ex:value": [{
+ "@id": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": [{"@value": "value3"}]
+ },
+ "ex:prop":[{"@value": "value"}]
+ },
+ "ex:prop": [{"@value": "value2"}]
+ }]
+ }])
+ },
+ }.each do |title, params|
+ it(title) {run_expand params.merge(rdfstar: true)}
+ end
+ end
+
begin
require 'nokogiri'
rescue LoadError
diff --git a/spec/from_rdf_spec.rb b/spec/from_rdf_spec.rb
index ebea3175..87f88c51 100644
--- a/spec/from_rdf_spec.rb
+++ b/spec/from_rdf_spec.rb
@@ -766,6 +766,187 @@
end
end
+ context "RDF*" do
+ {
+ "subject-iii": {
+ input: RDF::Statement(
+ RDF::Statement(
+ RDF::URI('http://example/s1'),
+ RDF::URI('http://example/p1'),
+ RDF::URI('http://example/o1')),
+ RDF::URI('http://example/p'),
+ RDF::URI('http://example/o')),
+ output: %([{
+ "@id": {
+ "@id": "http://example/s1",
+ "http://example/p1": [{"@id": "http://example/o1"}]
+ },
+ "http://example/p": [{"@id": "http://example/o"}]
+ }])
+ },
+ "subject-iib": {
+ input: RDF::Statement(
+ RDF::Statement(
+ RDF::URI('http://example/s1'),
+ RDF::URI('http://example/p1'),
+ RDF::Node.new('o1')),
+ RDF::URI('http://example/p'),
+ RDF::URI('http://example/o')),
+ output: %([{
+ "@id": {
+ "@id": "http://example/s1",
+ "http://example/p1": [{"@id": "_:o1"}]
+ },
+ "http://example/p": [{"@id": "http://example/o"}]
+ }])
+ },
+ "subject-iil": {
+ input: RDF::Statement(
+ RDF::Statement(
+ RDF::URI('http://example/s1'),
+ RDF::URI('http://example/p1'),
+ RDF::Literal('o1')),
+ RDF::URI('http://example/p'),
+ RDF::URI('http://example/o')),
+ output: %([{
+ "@id": {
+ "@id": "http://example/s1",
+ "http://example/p1": [{"@value": "o1"}]
+ },
+ "http://example/p": [{"@id": "http://example/o"}]
+ }])
+ },
+ "subject-bii": {
+ input: RDF::Statement(
+ RDF::Statement(
+ RDF::Node('s1'),
+ RDF::URI('http://example/p1'),
+ RDF::URI('http://example/o1')),
+ RDF::URI('http://example/p'),
+ RDF::URI('http://example/o')),
+ output: %([{
+ "@id": {
+ "@id": "_:s1",
+ "http://example/p1": [{"@id": "http://example/o1"}]
+ },
+ "http://example/p": [{"@id": "http://example/o"}]
+ }])
+ },
+ "subject-bib": {
+ input: RDF::Statement(
+ RDF::Statement(
+ RDF::Node('s1'),
+ RDF::URI('http://example/p1'),
+ RDF::Node.new('o1')),
+ RDF::URI('http://example/p'), RDF::URI('http://example/o')),
+ output: %([{
+ "@id": {
+ "@id": "_:s1",
+ "http://example/p1": [{"@id": "_:o1"}]
+ },
+ "http://example/p": [{"@id": "http://example/o"}]
+ }])
+ },
+ "subject-bil": {
+ input: RDF::Statement(
+ RDF::Statement(
+ RDF::Node('s1'),
+ RDF::URI('http://example/p1'),
+ RDF::Literal('o1')),
+ RDF::URI('http://example/p'),
+ RDF::URI('http://example/o')),
+ output: %([{
+ "@id": {
+ "@id": "_:s1",
+ "http://example/p1": [{"@value": "o1"}]
+ },
+ "http://example/p": [{"@id": "http://example/o"}]
+ }])
+ },
+ "object-iii": {
+ input: RDF::Statement(
+ RDF::URI('http://example/s'),
+ RDF::URI('http://example/p'),
+ RDF::Statement(
+ RDF::URI('http://example/s1'),
+ RDF::URI('http://example/p1'),
+ RDF::URI('http://example/o1'))),
+ output: %([{
+ "@id": "http://example/s",
+ "http://example/p": [{
+ "@id": {
+ "@id": "http://example/s1",
+ "http://example/p1": [{"@id": "http://example/o1"}]
+ }
+ }]
+ }])
+ },
+ "object-iib": {
+ input: RDF::Statement(
+ RDF::URI('http://example/s'),
+ RDF::URI('http://example/p'),
+ RDF::Statement(
+ RDF::URI('http://example/s1'),
+ RDF::URI('http://example/p1'),
+ RDF::Node.new('o1'))),
+ output: %([{
+ "@id": "http://example/s",
+ "http://example/p": [{
+ "@id": {
+ "@id": "http://example/s1",
+ "http://example/p1": [{"@id": "_:o1"}]
+ }
+ }]
+ }])
+ },
+ "object-iil": {
+ input: RDF::Statement(
+ RDF::URI('http://example/s'),
+ RDF::URI('http://example/p'),
+ RDF::Statement(
+ RDF::URI('http://example/s1'),
+ RDF::URI('http://example/p1'),
+ RDF::Literal('o1'))),
+ output: %([{
+ "@id": "http://example/s",
+ "http://example/p": [{
+ "@id": {
+ "@id": "http://example/s1",
+ "http://example/p1": [{"@value": "o1"}]
+ }
+ }]
+ }])
+ },
+ "recursive-subject": {
+ input: RDF::Statement(
+ RDF::Statement(
+ RDF::Statement(
+ RDF::URI('http://example/s2'),
+ RDF::URI('http://example/p2'),
+ RDF::URI('http://example/o2')),
+ RDF::URI('http://example/p1'),
+ RDF::URI('http://example/o1')),
+ RDF::URI('http://example/p'),
+ RDF::URI('http://example/o')),
+ output: %([{
+ "@id": {
+ "@id": {
+ "@id": "http://example/s2",
+ "http://example/p2": [{"@id": "http://example/o2"}]
+ },
+ "http://example/p1": [{"@id": "http://example/o1"}]
+ },
+ "http://example/p": [{"@id": "http://example/o"}]
+ }])
+ },
+ }.each do |name, params|
+ it name do
+ graph = RDF::Graph.new {|g| g << params[:input]}
+ do_fromRdf(params.merge(input: graph, prefixes: {ex: 'http://example/'}))
+ end
+ end
+ end
+
context "problems" do
{
"xsd:boolean as value" => {
diff --git a/spec/suite_to_rdf_spec.rb b/spec/suite_to_rdf_spec.rb
index e1fa5cce..293d56d9 100644
--- a/spec/suite_to_rdf_spec.rb
+++ b/spec/suite_to_rdf_spec.rb
@@ -9,6 +9,7 @@
m.entries.each do |t|
specify "#{t.property('@id')}: #{t.name}#{' (negative test)' unless t.positiveTest?}" do
pending "Generalized RDF" if t.options[:produceGeneralizedRdf]
+ pending "RDF*" if t.property('@id') == '#te122'
if %w(#t0118).include?(t.property('@id'))
expect {t.run self}.to write(/Statement .* is invalid/).to(:error)
elsif %w(#te075).include?(t.property('@id'))
diff --git a/spec/to_rdf_spec.rb b/spec/to_rdf_spec.rb
index 9bbcbea7..bfabfda8 100644
--- a/spec/to_rdf_spec.rb
+++ b/spec/to_rdf_spec.rb
@@ -1175,6 +1175,212 @@
end
end
+ context "JSON-LD*" do
+ {
+ "node with embedded subject without rdfstar option": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidIdValue
+ },
+ }.each do |title, params|
+ it(title) {run_to_rdf params}
+ end
+
+ {
+ "node with embedded subject having no @id": {
+ input: %({
+ "@id": {
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ expected: %(
+ <<_:b0 "value">> "value2" .
+ ),
+ },
+ "node with embedded subject having IRI @id": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ expected: %(
+ << "value">> "value2" .
+ ),
+ },
+ "node with embedded subject having BNode @id": {
+ input: %({
+ "@id": {
+ "@id": "_:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ expected: %(
+ <<_:b0 "value">> "value2" .
+ ),
+ },
+ "node with embedded subject having a type": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "@type": "ex:Type"
+ },
+ "ex:prop": "value2"
+ }),
+ expected: %(
+ << >> "value2" .
+ ),
+ },
+ "node with embedded subject having an IRI value": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": {"@id": "ex:value"}
+ },
+ "ex:prop": "value2"
+ }),
+ expected: %(
+ << >> "value2" .
+ ),
+ },
+ "node with embedded subject having an BNode value": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": {"@id": "_:value"}
+ },
+ "ex:prop": "value2"
+ }),
+ expected: %(
+ << _:b0>> "value2" .
+ ),
+ },
+ "node with recursive embedded subject": {
+ input: %({
+ "@id": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value3"
+ },
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ expected: %(
+ <<<< "value3">> "value">> "value2" .
+ ),
+ },
+ "illegal node with subject having no property": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei"
+ },
+ "ex:prop": "value3"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "illegal node with subject having multiple properties": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": ["value1", "value2"]
+ },
+ "ex:prop": "value3"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "illegal node with subject having multiple types": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "@type": ["ex:Type1", "ex:Type2"]
+ },
+ "ex:prop": "value3"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "illegal node with subject having type and property": {
+ input: %({
+ "@id": {
+ "@id": "ex:rei",
+ "@type": "ex:Type",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }),
+ exception: JSON::LD::JsonLdError::InvalidEmbeddedNode
+ },
+ "node with embedded object": {
+ input: %({
+ "@id": "ex:subj",
+ "ex:value": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ }
+ }
+ }),
+ expected: %(
+ << "value">> .
+ ),
+ },
+ "node with embedded object having properties": {
+ input: %({
+ "@id": "ex:subj",
+ "ex:value": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }
+ }),
+ expected: %(
+ << "value">> .
+ << "value">> "value2" .
+ ),
+ },
+ "node with recursive embedded object": {
+ input: %({
+ "@id": "ex:subj",
+ "ex:value": {
+ "@id": {
+ "@id": {
+ "@id": "ex:rei",
+ "ex:prop": "value3"
+ },
+ "ex:prop": "value"
+ },
+ "ex:prop": "value2"
+ }
+ }),
+ expected: %(
+ <<<< "value3">> "value">> .
+ <<<< "value3">> "value">> "value2" .
+ ),
+ },
+ }.each do |title, params|
+ context(title) do
+ it "Generates statements" do
+ output_graph = RDF::Graph.new {|g| g << RDF::NTriples::Reader.new(params[:expected], rdfstar: true)}
+ run_to_rdf params.merge(rdfstar: true, output: output_graph)
+ end if params[:expected]
+
+ it "Exception" do
+ run_to_rdf params.merge(rdfstar: true)
+ end if params[:exception]
+ end
+ end
+ end
+
context "exceptions" do
{
"Invalid subject" => {