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" => {