diff --git a/README.md b/README.md index 035a5d4..ece1fbc 100755 --- a/README.md +++ b/README.md @@ -15,7 +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*]. +* Provisional support for [JSON-LD-star][JSON-LD-star]. [Implementation Report](https://ruby-rdf.github.io/json-ld/etc/earl.html) @@ -37,9 +37,9 @@ 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) +### JSON-LD-star (RDFStar) -The {JSON::LD::API.expand}, {JSON::LD::API.compact}, {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*]. +The {JSON::LD::API.expand}, {JSON::LD::API.compact}, {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-star][JSON-LD-star]. 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`. @@ -636,7 +636,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/ +[JSON-LD-star]: 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 c848fb9..7148b0a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.8 +3.1.9 diff --git a/bin/jsonld b/bin/jsonld index e71864f..58bb9cf 100755 --- a/bin/jsonld +++ b/bin/jsonld @@ -26,34 +26,34 @@ def run(input, options, parser_options) if options[:expand] parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context) input = JSON::LD::API.fromRdf(reader) if reader - output = JSON::LD::API.expand(input, parser_options) + output = JSON::LD::API.expand(input, **parser_options) secs = Time.new - start options[:output].puts output.to_json(JSON::LD::JSON_STATE) STDERR.puts "Expanded in #{secs} seconds." unless options[:quiet] elsif options[:compact] input = JSON::LD::API.fromRdf(reader) if reader - output = JSON::LD::API.compact(input, parser_options[:context], parser_options) + output = JSON::LD::API.compact(input, parser_options[:context], **parser_options) secs = Time.new - start options[:output].puts output.to_json(JSON::LD::JSON_STATE) STDERR.puts "Compacted in #{secs} seconds." unless options[:quiet] elsif options[:flatten] input = JSON::LD::API.fromRdf(reader) if reader - output = JSON::LD::API.flatten(input, parser_options[:context], parser_options) + output = JSON::LD::API.flatten(input, parser_options[:context], **parser_options) secs = Time.new - start options[:output].puts output.to_json(JSON::LD::JSON_STATE) STDERR.puts "Flattened in #{secs} seconds." unless options[:quiet] elsif options[:frame] input = JSON::LD::API.fromRdf(reader) if reader - output = JSON::LD::API.frame(input, parser_options[:frame], parser_options) + output = JSON::LD::API.frame(input, parser_options[:frame], **parser_options) secs = Time.new - start options[:output].puts output.to_json(JSON::LD::JSON_STATE) STDERR.puts "Framed in #{secs} seconds." unless options[:quiet] else parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context) parser_options[:standard_prefixes] = true - reader ||= JSON::LD::Reader.new(input, parser_options) + reader ||= JSON::LD::Reader.new(input, **parser_options) num = 0 - RDF::Writer.for(options[:output_format]).new(options[:output], parser_options) do |w| + RDF::Writer.for(options[:output_format]).new(options[:output], **parser_options) do |w| reader.each do |statement| num += 1 w << statement @@ -113,6 +113,7 @@ OPT_ARGS = [ ["--processingMode",GetoptLong::REQUIRED_ARGUMENT,"Set processing mode, defaults to json-ld-1.1"], ["--quiet", GetoptLong::NO_ARGUMENT, "Supress most output other than progress indicators"], ["--rename_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"], + ["--rdfstar", GetoptLong::NO_ARGUMENT, "Parse JSON-LD-star"], ["--requireAll", GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"], ["--stream", GetoptLong::NO_ARGUMENT, "Use Streaming reader/writer"], ["--unique_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Use unique bnode identifiers"], @@ -140,7 +141,7 @@ opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]}) opts.each do |opt, arg| case opt when '--debug' then logger.level = Logger::DEBUG - when '--compact' then parser_options[:compact] = true + when '--compact' then options[:compact] = true when "--compactArrays" then parser_options[:compactArrays] = (arg || 'true') == 'true' when '--context' then parser_options[:context] = RDF::URI(arg).absolute? ? arg : File.open(arg) when '--evaluate' then input = arg @@ -158,6 +159,7 @@ opts.each do |opt, arg| when '--quiet' options[:quiet] = true logger.level = Logger::FATAL + when "--rdfstar" then parser_options[:rdfstar] = true when "--rename_bnodes" then parser_options[:rename_bnodes] = (arg || 'true') == 'true' when "--requireAll" then parser_options[:requireAll] = (arg || 'true') == 'true' when '--stream' then parser_options[:stream] = true @@ -193,7 +195,7 @@ if ARGV.empty? else ARGV.each do |file| # Call with opened files - RDF::Util::File.open_file(file, options) {|f| run(f, options, parser_options)} + RDF::Util::File.open_file(file, **options) {|f| run(f, options, parser_options)} end end puts diff --git a/lib/json/ld/api.rb b/lib/json/ld/api.rb index 39a3ba7..cf20320 100644 --- a/lib/json/ld/api.rb +++ b/lib/json/ld/api.rb @@ -90,7 +90,7 @@ class API # 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. + # support parsing JSON-LD-star 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) @@ -253,6 +253,8 @@ def self.compact(input, context, expanded: false, **options) # @param [Boolean] expanded (false) Input is already expanded # @param [Hash{Symbol => Object}] options # @option options (see #initialize) + # @option options [Boolean] :createAnnotations + # Unfold embedded nodes which can be represented using `@annotation`. # @yield jsonld # @yieldparam [Hash] jsonld # The flattened JSON-LD document @@ -284,6 +286,13 @@ def self.flatten(input, context, expanded: false, **options) graph_maps = {'@default' => {}} create_node_map(value, graph_maps) + # If create annotations flag is set, then update each node map in graph maps with the result of calling the create annotations algorithm. + if options[:createAnnotations] + graph_maps.values.each do |node_map| + create_annotations(node_map) + end + end + default_graph = graph_maps['@default'] graph_maps.keys.opt_sort(ordered: @options[:ordered]).each do |graph_name| next if graph_name == '@default' @@ -302,7 +311,7 @@ def self.flatten(input, context, expanded: false, **options) if context && !flattened.empty? # Otherwise, return the result of compacting flattened according the Compaction algorithm passing context ensuring that the compaction result uses the @graph keyword (or its alias) at the top-level, even if the context is empty or if there is only one element to put in the @graph array. This ensures that the returned document has a deterministic structure. compacted = as_array(compact(flattened)) - kwgraph = self.context.compact_iri('@graph') + kwgraph = self.context.compact_iri('@graph', vocab: true) flattened = self.context. serialize(provided_context: context). merge(kwgraph => compacted) @@ -448,7 +457,7 @@ def self.frame(input, frame, expanded: false, **options) result = if !compacted.is_a?(Array) compacted else - kwgraph = context.compact_iri('@graph') + kwgraph = context.compact_iri('@graph', vocab: true) {kwgraph => compacted} end # Only add context if one was provided diff --git a/lib/json/ld/compact.rb b/lib/json/ld/compact.rb index c13a723..deee309 100644 --- a/lib/json/ld/compact.rb +++ b/lib/json/ld/compact.rb @@ -100,7 +100,7 @@ def compact(element, if expanded_property == '@id' compacted_value = as_array(expanded_value).map do |expanded_id| if node?(expanded_id) && @options[:rdfstar] - # This can only really happen for valid RDF* + # This can only really happen for valid RDF-star compact(expanded_id, base: base, property: '@id', log_depth: log_depth.to_i + 1) @@ -145,7 +145,7 @@ def compact(element, end unless compacted_value.empty? - al = context.compact_iri('@reverse') + al = context.compact_iri('@reverse', vocab: true) log_debug("", depth: log_depth.to_i) {"remainder: #{al} => #{compacted_value.inspect}"} result[al] = compacted_value end diff --git a/lib/json/ld/context.rb b/lib/json/ld/context.rb index bc6c9a0..67ec566 100644 --- a/lib/json/ld/context.rb +++ b/lib/json/ld/context.rb @@ -1455,6 +1455,8 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) if !vocab # transform iri to a relative IRI using the document's base IRI iri = remove_base(self.base || base, iri) + # Make . relative if it has the form of a keyword. + iri = "./#{iri}" if iri.match?(/^@[a-zA-Z]+$/) return iri else return iri diff --git a/lib/json/ld/flatten.rb b/lib/json/ld/flatten.rb index 836c4e9..2790046 100644 --- a/lib/json/ld/flatten.rb +++ b/lib/json/ld/flatten.rb @@ -9,7 +9,7 @@ module Flatten ## # This algorithm creates a JSON object node map holding an indexed representation of the graphs and nodes represented in the passed expanded document. All nodes that are not uniquely identified by an IRI get assigned a (new) blank node identifier. The resulting node map will have a member for every graph in the document whose value is another object with a member for every node represented in the document. The default graph is stored under the @default member, all other graphs are stored under their graph name. # - # For RDF*/JSON-LD*: + # For RDF-star/JSON-LD-star: # * Values of `@id` can be an object (embedded node); when these are used as keys in a Node Map, they are serialized as canonical JSON, and de-serialized when flattening. # * The presence of `@annotation` implies an embedded node and the annotation object is removed from the node/value object in which it appears. # @@ -191,6 +191,51 @@ def create_node_map(element, graph_map, end end + ## + # Create annotations + # + # Updates a node map from which annotations have been folded into embedded triples to re-extract the annotations. + # + # Map entries where the key is of the form of a canonicalized JSON object are used to find keys with the `@id` and property components. If found, the original map entry is removed and entries added to an `@annotation` property of the associated value. + # + # * Keys which are of the form of a canonicalized JSON object are examined in inverse order of length. + # * Deserialize the key into a map, and re-serialize the value of `@id`. + # * If the map contains an entry with that value (after re-canonicalizing, as appropriate), and the associated antry has a item which matches the non-`@id` item from the map, the node is used to create an `@annotation` entry within that value. + # + # @param [Hash{String => Hash}] input + # @return [Hash{String => Hash}] + def create_annotations(node_map) + node_map.keys. + select {|k| k.start_with?('{')}. + sort_by(&:length). + reverse.each do |key| + + annotation = node_map[key] + # Deserialize key, and re-serialize the `@id` value. + emb = annotation['@id'].dup + id = emb.delete('@id') + property, value = emb.to_a.first + + # If id is a map, set it to the result of canonicalizing that value, otherwise to itself. + id = id.to_json_c14n if id.is_a?(Hash) + + next unless node_map.key?(id) + # If node map has an entry for id and that entry contains the same property and value from entry: + node = node_map[id] + + next unless node.key?(property) + + node[property].each do |emb_value| + next unless emb_value == value.first + + node_map.delete(key) + annotation.delete('@id') + add_value(emb_value, '@annotation', annotation, property_is_array: true) unless + annotation.empty? + end + end + end + ## # Rename blank nodes recursively within an embedded object # diff --git a/lib/json/ld/format.rb b/lib/json/ld/format.rb index d971459..706c6b5 100644 --- a/lib/json/ld/format.rb +++ b/lib/json/ld/format.rb @@ -174,6 +174,13 @@ def self.cli_commands use: :required, on: ["--context CONTEXT"], description: "Context to use when compacting.") {|arg| RDF::URI(arg)}, + RDF::CLI::Option.new( + symbol: :createAnnotations, + datatype: TrueClass, + default: false, + control: :checkbox, + on: ["--[no-]create-annotations"], + description: "Unfold embedded nodes which can be represented using `@annotation`."), ] }, frame: { diff --git a/lib/json/ld/from_rdf.rb b/lib/json/ld/from_rdf.rb index 27788e0..e911c23 100644 --- a/lib/json/ld/from_rdf.rb +++ b/lib/json/ld/from_rdf.rb @@ -40,8 +40,10 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false) default_graph[name] ||= {'@id' => name} unless name == '@default' - subject = statement.subject.to_s - node = node_map[subject] ||= resource_representation(statement.subject,useNativeTypes) + subject = statement.subject.statement? ? + resource_representation(statement.subject, useNativeTypes)['@id'].to_json_c14n : + 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' @@ -50,12 +52,14 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false) # 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] ||= + object = statement.object.statement? ? + resource_representation(statement.object, useNativeTypes)['@id'].to_json_c14n : + statement.object.to_s + node_map[object] ||= 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 @@ -112,8 +116,7 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false) end end - # Skip to next graph, unless this one has lists - next unless nil_var = graph_object[RDF.nil.to_s] + nil_var = graph_object.fetch(RDF.nil.to_s, {}) # For each item usage in the usages member of nil, perform the following steps: nil_var.fetch(:usages, []).each do |usage| @@ -141,6 +144,9 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false) head['@list'] = list.reverse list_nodes.each {|node_id| graph_object.delete(node_id)} end + + # Create annotations on graph object + create_annotations(graph_object) end result = [] diff --git a/script/parse b/script/parse index 6d0ad29..a5eda62 100755 --- a/script/parse +++ b/script/parse @@ -116,7 +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"], + ["--rdfstar", GetoptLong::NO_ARGUMENT, "RDF-star mode"], ["--stream", GetoptLong::NO_ARGUMENT, "Streaming reader/writer"], ["--uri", GetoptLong::REQUIRED_ARGUMENT, "Run with argument value as base"], ["--validate", GetoptLong::NO_ARGUMENT, "Validate input"], diff --git a/spec/compact_spec.rb b/spec/compact_spec.rb index 103cf40..cedbc07 100644 --- a/spec/compact_spec.rb +++ b/spec/compact_spec.rb @@ -3113,7 +3113,7 @@ end end - context "JSON-LD*" do + context "JSON-LD-star" do { "subject-iii": { input: %([{ diff --git a/spec/context_spec.rb b/spec/context_spec.rb index 51654e6..ae83c98 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -1148,7 +1148,6 @@ def containers "nil" => [nil, nil], "absolute IRI" => ["http://example.com/", "http://example.com/"], "prefix:suffix" => ["ex:suffix", "http://example.org/suffix"], - "keyword" => ["@type", "@type"], "unmapped" => ["foo", "foo"], "bnode" => [JSON::LD::JsonLdError:: IRIConfusedWithPrefix, RDF::Node("a")], "relative" => ["foo/bar", "http://base/foo/bar"], @@ -1338,7 +1337,6 @@ def containers "nil" => [nil, nil], "absolute IRI" => ["http://example.com/", "http://example.com/"], "prefix:suffix" => ["ex:suffix", "http://example.org/suffix"], - "keyword" => ["@type", "@type"], "unmapped" => ["foo", "foo"], "bnode" => [JSON::LD::JsonLdError:: IRIConfusedWithPrefix, RDF::Node("a")], "relative" => ["foo/bar", "http://base/foo/bar"], diff --git a/spec/expand_spec.rb b/spec/expand_spec.rb index 4f09ae4..ccd4473 100644 --- a/spec/expand_spec.rb +++ b/spec/expand_spec.rb @@ -3413,7 +3413,7 @@ end end - context "JSON-LD*" do + context "JSON-LD-star" do { "node with embedded subject without rdfstar option": { input: %({ diff --git a/spec/flatten_spec.rb b/spec/flatten_spec.rb index b5763c3..1115e73 100644 --- a/spec/flatten_spec.rb +++ b/spec/flatten_spec.rb @@ -666,7 +666,7 @@ end end - context "JSON-LD*" do + context "JSON-LD-star" do { "node object with @annotation property is ignored without rdfstar option": { input: %({ diff --git a/spec/from_rdf_spec.rb b/spec/from_rdf_spec.rb index 87f88c5..c509432 100644 --- a/spec/from_rdf_spec.rb +++ b/spec/from_rdf_spec.rb @@ -766,7 +766,7 @@ end end - context "RDF*" do + context "RDF-star" do { "subject-iii": { input: RDF::Statement( diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 82c74ef..2500bc7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -77,7 +77,6 @@ def remap_bnodes(actual, expected) bijection = bijection.inject({}) {|memo, (k, v)| memo.merge(k.to_s => v.to_s)} # Recursively replace blank nodes in actual with the bijection - #require 'byebug'; byebug replace_nodes(actual, bijection) else actual diff --git a/spec/suite_to_rdf_spec.rb b/spec/suite_to_rdf_spec.rb index 293d56d..48f664d 100644 --- a/spec/suite_to_rdf_spec.rb +++ b/spec/suite_to_rdf_spec.rb @@ -9,7 +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' + pending "RDF-star" 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 26017d6..69b3b90 100644 --- a/spec/to_rdf_spec.rb +++ b/spec/to_rdf_spec.rb @@ -1175,7 +1175,7 @@ end end - context "JSON-LD*" do + context "JSON-LD-star" do { "node with embedded subject without rdfstar option": { input: %({ diff --git a/spec/writer_spec.rb b/spec/writer_spec.rb index f871dd9..e18222e 100644 --- a/spec/writer_spec.rb +++ b/spec/writer_spec.rb @@ -189,7 +189,7 @@ end end - context "RDF*" do + context "RDF-star" do { "subject-iii": { input: RDF::Statement(