Skip to content

Commit

Permalink
Finish 3.1.9
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Feb 25, 2021
2 parents c8b2fe7 + b2d6e80 commit 737f015
Show file tree
Hide file tree
Showing 19 changed files with 104 additions and 36 deletions.
8 changes: 4 additions & 4 deletions README.md
Expand Up @@ -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)

Expand All @@ -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`.

Expand Down Expand Up @@ -636,7 +636,7 @@ see <https://unlicense.org/> 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"
Expand Down
2 changes: 1 addition & 1 deletion VERSION
@@ -1 +1 @@
3.1.8
3.1.9
18 changes: 10 additions & 8 deletions bin/jsonld
Expand Up @@ -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
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
15 changes: 12 additions & 3 deletions lib/json/ld/api.rb
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/json/ld/compact.rb
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/json/ld/context.rb
Expand Up @@ -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
Expand Down
47 changes: 46 additions & 1 deletion lib/json/ld/flatten.rb
Expand Up @@ -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.
#
Expand Down Expand Up @@ -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
#
Expand Down
7 changes: 7 additions & 0 deletions lib/json/ld/format.rb
Expand Up @@ -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: {
Expand Down
18 changes: 12 additions & 6 deletions lib/json/ld/from_rdf.rb
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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|
Expand Down Expand Up @@ -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 = []
Expand Down
2 changes: 1 addition & 1 deletion script/parse
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion spec/compact_spec.rb
Expand Up @@ -3113,7 +3113,7 @@
end
end

context "JSON-LD*" do
context "JSON-LD-star" do
{
"subject-iii": {
input: %([{
Expand Down
2 changes: 0 additions & 2 deletions spec/context_spec.rb
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion spec/expand_spec.rb
Expand Up @@ -3413,7 +3413,7 @@
end
end

context "JSON-LD*" do
context "JSON-LD-star" do
{
"node with embedded subject without rdfstar option": {
input: %({
Expand Down
2 changes: 1 addition & 1 deletion spec/flatten_spec.rb
Expand Up @@ -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: %({
Expand Down
2 changes: 1 addition & 1 deletion spec/from_rdf_spec.rb
Expand Up @@ -766,7 +766,7 @@
end
end

context "RDF*" do
context "RDF-star" do
{
"subject-iii": {
input: RDF::Statement(
Expand Down
1 change: 0 additions & 1 deletion spec/spec_helper.rb
Expand Up @@ -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
Expand Down

0 comments on commit 737f015

Please sign in to comment.