Skip to content

Commit 737f015

Browse files
committed
Finish 3.1.9
2 parents c8b2fe7 + b2d6e80 commit 737f015

19 files changed

+104
-36
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and
1515

1616
* If the [jsonlint][] gem is installed, it will be used when validating an input document.
1717
* If available, uses [Nokogiri][] and/or [Nokogumbo][] for parsing HTML, falls back to REXML otherwise.
18-
* Provisional support for [JSON-LD*][JSON-LD*].
18+
* Provisional support for [JSON-LD-star][JSON-LD-star].
1919

2020
[Implementation Report](https://ruby-rdf.github.io/json-ld/etc/earl.html)
2121

@@ -37,9 +37,9 @@ The order of triples retrieved from the `RDF::Enumerable` dataset determines the
3737
### MultiJson parser
3838
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.
3939

40-
### JSON-LD* (RDFStar)
40+
### JSON-LD-star (RDFStar)
4141

42-
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*].
42+
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].
4343

4444
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`.
4545

@@ -636,7 +636,7 @@ see <https://unlicense.org/> or the accompanying {file:UNLICENSE} file.
636636
[YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md
637637
[PDD]: https://unlicense.org/#unlicensing-contributions
638638
[RDF.rb]: https://rubygems.org/gems/rdf
639-
[JSON-LD*]: https://json-ld.github.io/json-ld-star/
639+
[JSON-LD-star]: https://json-ld.github.io/json-ld-star/
640640
[Rack::LinkedData]: https://rubygems.org/gems/rack-linkeddata
641641
[Backports]: https://rubygems.org/gems/backports
642642
[JSON-LD]: https://www.w3.org/TR/json-ld11/ "JSON-LD 1.1"

VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.1.8
1+
3.1.9

bin/jsonld

+10-8
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,34 @@ def run(input, options, parser_options)
2626
if options[:expand]
2727
parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context)
2828
input = JSON::LD::API.fromRdf(reader) if reader
29-
output = JSON::LD::API.expand(input, parser_options)
29+
output = JSON::LD::API.expand(input, **parser_options)
3030
secs = Time.new - start
3131
options[:output].puts output.to_json(JSON::LD::JSON_STATE)
3232
STDERR.puts "Expanded in #{secs} seconds." unless options[:quiet]
3333
elsif options[:compact]
3434
input = JSON::LD::API.fromRdf(reader) if reader
35-
output = JSON::LD::API.compact(input, parser_options[:context], parser_options)
35+
output = JSON::LD::API.compact(input, parser_options[:context], **parser_options)
3636
secs = Time.new - start
3737
options[:output].puts output.to_json(JSON::LD::JSON_STATE)
3838
STDERR.puts "Compacted in #{secs} seconds." unless options[:quiet]
3939
elsif options[:flatten]
4040
input = JSON::LD::API.fromRdf(reader) if reader
41-
output = JSON::LD::API.flatten(input, parser_options[:context], parser_options)
41+
output = JSON::LD::API.flatten(input, parser_options[:context], **parser_options)
4242
secs = Time.new - start
4343
options[:output].puts output.to_json(JSON::LD::JSON_STATE)
4444
STDERR.puts "Flattened in #{secs} seconds." unless options[:quiet]
4545
elsif options[:frame]
4646
input = JSON::LD::API.fromRdf(reader) if reader
47-
output = JSON::LD::API.frame(input, parser_options[:frame], parser_options)
47+
output = JSON::LD::API.frame(input, parser_options[:frame], **parser_options)
4848
secs = Time.new - start
4949
options[:output].puts output.to_json(JSON::LD::JSON_STATE)
5050
STDERR.puts "Framed in #{secs} seconds." unless options[:quiet]
5151
else
5252
parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context)
5353
parser_options[:standard_prefixes] = true
54-
reader ||= JSON::LD::Reader.new(input, parser_options)
54+
reader ||= JSON::LD::Reader.new(input, **parser_options)
5555
num = 0
56-
RDF::Writer.for(options[:output_format]).new(options[:output], parser_options) do |w|
56+
RDF::Writer.for(options[:output_format]).new(options[:output], **parser_options) do |w|
5757
reader.each do |statement|
5858
num += 1
5959
w << statement
@@ -113,6 +113,7 @@ OPT_ARGS = [
113113
["--processingMode",GetoptLong::REQUIRED_ARGUMENT,"Set processing mode, defaults to json-ld-1.1"],
114114
["--quiet", GetoptLong::NO_ARGUMENT, "Supress most output other than progress indicators"],
115115
["--rename_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"],
116+
["--rdfstar", GetoptLong::NO_ARGUMENT, "Parse JSON-LD-star"],
116117
["--requireAll", GetoptLong::OPTIONAL_ARGUMENT,"Rename bnodes as part of expansion, or keep them the same"],
117118
["--stream", GetoptLong::NO_ARGUMENT, "Use Streaming reader/writer"],
118119
["--unique_bnodes", GetoptLong::OPTIONAL_ARGUMENT,"Use unique bnode identifiers"],
@@ -140,7 +141,7 @@ opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]})
140141
opts.each do |opt, arg|
141142
case opt
142143
when '--debug' then logger.level = Logger::DEBUG
143-
when '--compact' then parser_options[:compact] = true
144+
when '--compact' then options[:compact] = true
144145
when "--compactArrays" then parser_options[:compactArrays] = (arg || 'true') == 'true'
145146
when '--context' then parser_options[:context] = RDF::URI(arg).absolute? ? arg : File.open(arg)
146147
when '--evaluate' then input = arg
@@ -158,6 +159,7 @@ opts.each do |opt, arg|
158159
when '--quiet'
159160
options[:quiet] = true
160161
logger.level = Logger::FATAL
162+
when "--rdfstar" then parser_options[:rdfstar] = true
161163
when "--rename_bnodes" then parser_options[:rename_bnodes] = (arg || 'true') == 'true'
162164
when "--requireAll" then parser_options[:requireAll] = (arg || 'true') == 'true'
163165
when '--stream' then parser_options[:stream] = true
@@ -193,7 +195,7 @@ if ARGV.empty?
193195
else
194196
ARGV.each do |file|
195197
# Call with opened files
196-
RDF::Util::File.open_file(file, options) {|f| run(f, options, parser_options)}
198+
RDF::Util::File.open_file(file, **options) {|f| run(f, options, parser_options)}
197199
end
198200
end
199201
puts

lib/json/ld/api.rb

+12-3
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class API
9090
# Processing mode, json-ld-1.0 or json-ld-1.1.
9191
# 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`.
9292
# @option options [Boolean] rdfstar (false)
93-
# support parsing JSON-LD* statement resources.
93+
# support parsing JSON-LD-star statement resources.
9494
# @option options [Boolean] :rename_bnodes (true)
9595
# Rename bnodes as part of expansion, or keep them the same.
9696
# @option options [Boolean] :unique_bnodes (false)
@@ -253,6 +253,8 @@ def self.compact(input, context, expanded: false, **options)
253253
# @param [Boolean] expanded (false) Input is already expanded
254254
# @param [Hash{Symbol => Object}] options
255255
# @option options (see #initialize)
256+
# @option options [Boolean] :createAnnotations
257+
# Unfold embedded nodes which can be represented using `@annotation`.
256258
# @yield jsonld
257259
# @yieldparam [Hash] jsonld
258260
# The flattened JSON-LD document
@@ -284,6 +286,13 @@ def self.flatten(input, context, expanded: false, **options)
284286
graph_maps = {'@default' => {}}
285287
create_node_map(value, graph_maps)
286288

289+
# If create annotations flag is set, then update each node map in graph maps with the result of calling the create annotations algorithm.
290+
if options[:createAnnotations]
291+
graph_maps.values.each do |node_map|
292+
create_annotations(node_map)
293+
end
294+
end
295+
287296
default_graph = graph_maps['@default']
288297
graph_maps.keys.opt_sort(ordered: @options[:ordered]).each do |graph_name|
289298
next if graph_name == '@default'
@@ -302,7 +311,7 @@ def self.flatten(input, context, expanded: false, **options)
302311
if context && !flattened.empty?
303312
# 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.
304313
compacted = as_array(compact(flattened))
305-
kwgraph = self.context.compact_iri('@graph')
314+
kwgraph = self.context.compact_iri('@graph', vocab: true)
306315
flattened = self.context.
307316
serialize(provided_context: context).
308317
merge(kwgraph => compacted)
@@ -448,7 +457,7 @@ def self.frame(input, frame, expanded: false, **options)
448457
result = if !compacted.is_a?(Array)
449458
compacted
450459
else
451-
kwgraph = context.compact_iri('@graph')
460+
kwgraph = context.compact_iri('@graph', vocab: true)
452461
{kwgraph => compacted}
453462
end
454463
# Only add context if one was provided

lib/json/ld/compact.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def compact(element,
100100
if expanded_property == '@id'
101101
compacted_value = as_array(expanded_value).map do |expanded_id|
102102
if node?(expanded_id) && @options[:rdfstar]
103-
# This can only really happen for valid RDF*
103+
# This can only really happen for valid RDF-star
104104
compact(expanded_id, base: base,
105105
property: '@id',
106106
log_depth: log_depth.to_i + 1)
@@ -145,7 +145,7 @@ def compact(element,
145145
end
146146

147147
unless compacted_value.empty?
148-
al = context.compact_iri('@reverse')
148+
al = context.compact_iri('@reverse', vocab: true)
149149
log_debug("", depth: log_depth.to_i) {"remainder: #{al} => #{compacted_value.inspect}"}
150150
result[al] = compacted_value
151151
end

lib/json/ld/context.rb

+2
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,8 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
14551455
if !vocab
14561456
# transform iri to a relative IRI using the document's base IRI
14571457
iri = remove_base(self.base || base, iri)
1458+
# Make . relative if it has the form of a keyword.
1459+
iri = "./#{iri}" if iri.match?(/^@[a-zA-Z]+$/)
14581460
return iri
14591461
else
14601462
return iri

lib/json/ld/flatten.rb

+46-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module Flatten
99
##
1010
# 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.
1111
#
12-
# For RDF*/JSON-LD*:
12+
# For RDF-star/JSON-LD-star:
1313
# * 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.
1414
# * The presence of `@annotation` implies an embedded node and the annotation object is removed from the node/value object in which it appears.
1515
#
@@ -191,6 +191,51 @@ def create_node_map(element, graph_map,
191191
end
192192
end
193193

194+
##
195+
# Create annotations
196+
#
197+
# Updates a node map from which annotations have been folded into embedded triples to re-extract the annotations.
198+
#
199+
# 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.
200+
#
201+
# * Keys which are of the form of a canonicalized JSON object are examined in inverse order of length.
202+
# * Deserialize the key into a map, and re-serialize the value of `@id`.
203+
# * 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.
204+
#
205+
# @param [Hash{String => Hash}] input
206+
# @return [Hash{String => Hash}]
207+
def create_annotations(node_map)
208+
node_map.keys.
209+
select {|k| k.start_with?('{')}.
210+
sort_by(&:length).
211+
reverse.each do |key|
212+
213+
annotation = node_map[key]
214+
# Deserialize key, and re-serialize the `@id` value.
215+
emb = annotation['@id'].dup
216+
id = emb.delete('@id')
217+
property, value = emb.to_a.first
218+
219+
# If id is a map, set it to the result of canonicalizing that value, otherwise to itself.
220+
id = id.to_json_c14n if id.is_a?(Hash)
221+
222+
next unless node_map.key?(id)
223+
# If node map has an entry for id and that entry contains the same property and value from entry:
224+
node = node_map[id]
225+
226+
next unless node.key?(property)
227+
228+
node[property].each do |emb_value|
229+
next unless emb_value == value.first
230+
231+
node_map.delete(key)
232+
annotation.delete('@id')
233+
add_value(emb_value, '@annotation', annotation, property_is_array: true) unless
234+
annotation.empty?
235+
end
236+
end
237+
end
238+
194239
##
195240
# Rename blank nodes recursively within an embedded object
196241
#

lib/json/ld/format.rb

+7
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ def self.cli_commands
174174
use: :required,
175175
on: ["--context CONTEXT"],
176176
description: "Context to use when compacting.") {|arg| RDF::URI(arg)},
177+
RDF::CLI::Option.new(
178+
symbol: :createAnnotations,
179+
datatype: TrueClass,
180+
default: false,
181+
control: :checkbox,
182+
on: ["--[no-]create-annotations"],
183+
description: "Unfold embedded nodes which can be represented using `@annotation`."),
177184
]
178185
},
179186
frame: {

lib/json/ld/from_rdf.rb

+12-6
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
4040

4141
default_graph[name] ||= {'@id' => name} unless name == '@default'
4242

43-
subject = statement.subject.to_s
44-
node = node_map[subject] ||= resource_representation(statement.subject,useNativeTypes)
43+
subject = statement.subject.statement? ?
44+
resource_representation(statement.subject, useNativeTypes)['@id'].to_json_c14n :
45+
statement.subject.to_s
46+
node = node_map[subject] ||= resource_representation(statement.subject, useNativeTypes)
4547

4648
# If predicate is rdf:datatype, note subject in compound literal subjects map
4749
if @options[:rdfDirection] == 'compound-literal' && statement.predicate == RDF.to_uri + 'direction'
@@ -50,12 +52,14 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
5052

5153
# 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.
5254
unless statement.object.literal?
53-
node_map[statement.object.to_s] ||=
55+
object = statement.object.statement? ?
56+
resource_representation(statement.object, useNativeTypes)['@id'].to_json_c14n :
57+
statement.object.to_s
58+
node_map[object] ||=
5459
resource_representation(statement.object, useNativeTypes)
5560
end
5661

5762
# 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.
58-
# XXX JSON-LD* does not support embedded value of @type
5963
if statement.predicate == RDF.type && statement.object.resource? && !useRdfType
6064
merge_value(node, '@type', statement.object.to_s)
6165
next
@@ -112,8 +116,7 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
112116
end
113117
end
114118

115-
# Skip to next graph, unless this one has lists
116-
next unless nil_var = graph_object[RDF.nil.to_s]
119+
nil_var = graph_object.fetch(RDF.nil.to_s, {})
117120

118121
# For each item usage in the usages member of nil, perform the following steps:
119122
nil_var.fetch(:usages, []).each do |usage|
@@ -141,6 +144,9 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false)
141144
head['@list'] = list.reverse
142145
list_nodes.each {|node_id| graph_object.delete(node_id)}
143146
end
147+
148+
# Create annotations on graph object
149+
create_annotations(graph_object)
144150
end
145151

146152
result = []

script/parse

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ OPT_ARGS = [
116116
["--output", "-o", GetoptLong::REQUIRED_ARGUMENT, "Where to store output (default STDOUT)"],
117117
["--profile", GetoptLong::NO_ARGUMENT, "Run profiler with output to doc/profiles/"],
118118
["--quiet", GetoptLong::NO_ARGUMENT, "Reduce output"],
119-
["--rdfstar", GetoptLong::NO_ARGUMENT, "RDF* mode"],
119+
["--rdfstar", GetoptLong::NO_ARGUMENT, "RDF-star mode"],
120120
["--stream", GetoptLong::NO_ARGUMENT, "Streaming reader/writer"],
121121
["--uri", GetoptLong::REQUIRED_ARGUMENT, "Run with argument value as base"],
122122
["--validate", GetoptLong::NO_ARGUMENT, "Validate input"],

spec/compact_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -3113,7 +3113,7 @@
31133113
end
31143114
end
31153115

3116-
context "JSON-LD*" do
3116+
context "JSON-LD-star" do
31173117
{
31183118
"subject-iii": {
31193119
input: %([{

spec/context_spec.rb

-2
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,6 @@ def containers
11481148
"nil" => [nil, nil],
11491149
"absolute IRI" => ["http://example.com/", "http://example.com/"],
11501150
"prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
1151-
"keyword" => ["@type", "@type"],
11521151
"unmapped" => ["foo", "foo"],
11531152
"bnode" => [JSON::LD::JsonLdError:: IRIConfusedWithPrefix, RDF::Node("a")],
11541153
"relative" => ["foo/bar", "http://base/foo/bar"],
@@ -1338,7 +1337,6 @@ def containers
13381337
"nil" => [nil, nil],
13391338
"absolute IRI" => ["http://example.com/", "http://example.com/"],
13401339
"prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
1341-
"keyword" => ["@type", "@type"],
13421340
"unmapped" => ["foo", "foo"],
13431341
"bnode" => [JSON::LD::JsonLdError:: IRIConfusedWithPrefix, RDF::Node("a")],
13441342
"relative" => ["foo/bar", "http://base/foo/bar"],

spec/expand_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -3413,7 +3413,7 @@
34133413
end
34143414
end
34153415

3416-
context "JSON-LD*" do
3416+
context "JSON-LD-star" do
34173417
{
34183418
"node with embedded subject without rdfstar option": {
34193419
input: %({

spec/flatten_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@
666666
end
667667
end
668668

669-
context "JSON-LD*" do
669+
context "JSON-LD-star" do
670670
{
671671
"node object with @annotation property is ignored without rdfstar option": {
672672
input: %({

spec/from_rdf_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@
766766
end
767767
end
768768

769-
context "RDF*" do
769+
context "RDF-star" do
770770
{
771771
"subject-iii": {
772772
input: RDF::Statement(

spec/spec_helper.rb

-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ def remap_bnodes(actual, expected)
7777
bijection = bijection.inject({}) {|memo, (k, v)| memo.merge(k.to_s => v.to_s)}
7878

7979
# Recursively replace blank nodes in actual with the bijection
80-
#require 'byebug'; byebug
8180
replace_nodes(actual, bijection)
8281
else
8382
actual

0 commit comments

Comments
 (0)