Skip to content

Commit 2ac3fdf

Browse files
committed
Add support for compactToRelative flag.
For json-ld.org/json-ld#468.
1 parent 4118197 commit 2ac3fdf

File tree

4 files changed

+120
-27
lines changed

4 files changed

+120
-27
lines changed

lib/json/ld/api.rb

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class API
6363
# The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context. If not specified, and a base IRI is found from `input`, options[:base] will be modified with this value.
6464
# @option options [Boolean] :compactArrays (true)
6565
# If set to `true`, the JSON-LD processor replaces arrays with just one element with that element during compaction. If set to `false`, all arrays will remain arrays even if they have just one element.
66+
# @option options [Boolean] :compactToRelative (true)
67+
# Creates document relative IRIs when compacting, if `true`, otherwise leaves expanded.
6668
# @option options [Proc] :documentLoader
6769
# The callback of the loader to be used to retrieve remote documents and contexts. If specified, it must be used to retrieve remote documents and contexts; otherwise, if not specified, the processor's built-in loader must be used. See {documentLoader} for the method signature.
6870
# @option options [String, #read, Hash, Array, JSON::LD::Context] :expandContext
@@ -88,11 +90,11 @@ class API
8890
# @raise [JsonLdError]
8991
def initialize(input, context, options = {}, &block)
9092
@options = {
91-
compactArrays: true,
92-
rename_bnodes: true,
93-
documentLoader: self.class.method(:documentLoader)
94-
}
95-
@options = @options.merge(options)
93+
compactArrays: true,
94+
rename_bnodes: true,
95+
documentLoader: self.class.method(:documentLoader),
96+
compactToRelative: true
97+
}.merge(options)
9698
@namer = options[:unique_bnodes] ? BlankNodeUniqer.new : (@options[:rename_bnodes] ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
9799

98100
# For context via Link header
@@ -101,7 +103,7 @@ def initialize(input, context, options = {}, &block)
101103
@value = case input
102104
when Array, Hash then input.dup
103105
when IO, StringIO
104-
@options = {base: input.base_uri}.merge!(@options) if input.respond_to?(:base_uri)
106+
@options = {base: input.base_uri}.merge(@options) if input.respond_to?(:base_uri)
105107

106108
# if input impelements #links, attempt to get a contextUrl from that link
107109
content_type = input.respond_to?(:content_type) ? input.content_type : "application/json"
@@ -116,7 +118,7 @@ def initialize(input, context, options = {}, &block)
116118
when String
117119
remote_doc = @options[:documentLoader].call(input, @options)
118120

119-
@options = {base: remote_doc.documentUrl}.merge!(@options)
121+
@options = {base: remote_doc.documentUrl}.merge(@options)
120122
context_ref = remote_doc.contextUrl
121123

122124
case remote_doc.document
@@ -128,9 +130,6 @@ def initialize(input, context, options = {}, &block)
128130
end
129131
end
130132

131-
# Update calling context :base option, if not defined
132-
options[:base] ||= @options[:base] if @options[:base]
133-
134133
# If not provided, first use context from document, or from a Link header
135134
context ||= (@value['@context'] if @value.is_a?(Hash)) || context_ref
136135
@context = Context.parse(context || {}, @options)
@@ -139,6 +138,13 @@ def initialize(input, context, options = {}, &block)
139138
@options[:processingMode] ||= @context.processingMode || "json-ld-1.0"
140139
@options[:validate] ||= %w(json-ld-1.0 json-ld-1.1).include?(@options[:processingMode])
141140

141+
# If, after processing, the context does not have a _base IRI_, and the _compactToRelative_ option is set to true or processingMode is json-ld-1.0, set _base IRI_ in the active context to either the _base_ option from the API, if set, or the IRI of the currently being processed document.
142+
if !@context.base && @options[:base]
143+
doc_base = RDF::URI(@options[:base]).dup
144+
doc_base.canonicalize! if options[:canonicalize]
145+
@context.base = doc_base
146+
end
147+
142148
if block_given?
143149
case block.arity
144150
when 0, -1 then instance_eval(&block)
@@ -160,25 +166,38 @@ def initialize(input, context, options = {}, &block)
160166
# @param [Hash{Symbol => Object}] options
161167
# @option options (see #initialize)
162168
# @raise [JsonLdError]
163-
# @yield jsonld
169+
# @yield jsonld, base_iri
164170
# @yieldparam [Array<Hash>] jsonld
165171
# The expanded JSON-LD document
172+
# @yieldparam [RDF::URI] base_iri
173+
# The document base as determined during expansion
166174
# @yieldreturn [Object] returned object
167175
# @return [Object, Array<Hash>]
168176
# If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
169177
# @see http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
170-
def self.expand(input, options = {})
171-
result = nil
172-
API.new(input, options[:expandContext], options) do |api|
173-
result = api.expand(api.value, nil, api.context, ordered: options.fetch(:ordered, true))
178+
def self.expand(input, options = {}, &block)
179+
result, doc_base = nil
180+
API.new(input, options[:expandContext], options) do
181+
result = self.expand(self.value, nil, self.context, ordered: options.fetch(:ordered, true))
182+
doc_base = @options[:base]
174183
end
175184

176185
# If, after the algorithm outlined above is run, the resulting element is an JSON object with just a @graph property, element is set to the value of @graph's value.
177186
result = result['@graph'] if result.is_a?(Hash) && result.keys == %w(@graph)
178187

179188
# Finally, if element is a JSON object, it is wrapped into an array.
180189
result = [result].compact unless result.is_a?(Array)
181-
block_given? ? yield(result) : result
190+
191+
if block_given?
192+
case block.arity
193+
when 1 then yield(result)
194+
when 2 then yield(result, doc_base)
195+
else
196+
raise "Unexpected number of yield parameters to expand"
197+
end
198+
else
199+
result
200+
end
182201
end
183202

184203
##
@@ -204,12 +223,16 @@ def self.expand(input, options = {})
204223
# @raise [JsonLdError]
205224
# @see http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
206225
def self.compact(input, context, options = {})
207-
expanded = result = nil
226+
result = nil
208227

209228
# 1) Perform the Expansion Algorithm on the JSON-LD input.
210229
# This removes any existing context to allow the given context to be cleanly applied.
211-
expanded_input = options[:expanded] ? input : API.expand(input, options)
230+
expanded_input = options[:expanded] ? input : API.expand(input, options) do |result, base_iri|
231+
options[:base] ||= base_iri
232+
result
233+
end
212234

235+
#require 'byebug'; byebug
213236
API.new(expanded_input, context, options) do
214237
log_debug(".compact") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
215238
result = compact(value)
@@ -248,7 +271,10 @@ def self.flatten(input, context, options = {})
248271
flattened = []
249272

250273
# Expand input to simplify processing
251-
expanded_input = options[:expanded] ? input : API.expand(input, options)
274+
expanded_input = options[:expanded] ? input : API.expand(input, options) do |result, base_iri|
275+
options[:base] ||= base_iri
276+
result
277+
end
252278

253279
# Initialize input using
254280
API.new(expanded_input, context, options) do
@@ -322,7 +348,7 @@ def self.frame(input, frame, options = {})
322348
omitDefault: false,
323349
pruneBlankNodeIdentifiers: true,
324350
documentLoader: method(:documentLoader)
325-
}.merge!(options)
351+
}.merge(options)
326352

327353
framing_state = {
328354
graphMap: {},
@@ -344,7 +370,10 @@ def self.frame(input, frame, options = {})
344370
end
345371

346372
# Expand input to simplify processing
347-
expanded_input = options[:expanded] ? input : API.expand(input, options)
373+
expanded_input = options[:expanded] ? input : API.expand(input, options) do |result, base_iri|
374+
options[:base] ||= base_iri
375+
result
376+
end
348377

349378
# Expand frame to simplify processing
350379
expanded_frame = API.expand(frame, options.merge(processingMode: "json-ld-1.1-expand-frame"))

lib/json/ld/context.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def inspect
194194
# @return [RDF::URI] Document base IRI, to initialize `base`.
195195
attr_reader :doc_base
196196

197-
# @return [RDF::URI] base IRI of the context, if loaded remotely. XXX
197+
# @return [RDF::URI] base IRI of the context, if loaded remotely.
198198
attr_accessor :context_base
199199

200200
# Term definitions
@@ -1189,7 +1189,7 @@ def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **opt
11891189
end
11901190
end
11911191

1192-
if !vocab
1192+
if !vocab && @options[:compactToRelative]
11931193
# transform iri to a relative IRI using the document's base IRI
11941194
iri = remove_base(iri)
11951195
#log_debug("") {"=> relative iri: #{iri.inspect}"} unless quiet

spec/compact_spec.rb

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,70 @@
299299
end
300300
end
301301

302+
context "IRI Compaction" do
303+
{
304+
"Expands and compacts to document base in 1.0" => {
305+
input: %({
306+
"@id": "a",
307+
"http://example.com/b": {"@id": "c"}
308+
}),
309+
context: %({"b": "http://example.com/b"}),
310+
output: %({
311+
"@context": {"b": "http://example.com/b"},
312+
"@id": "a",
313+
"b": {"@id": "c"}
314+
}),
315+
base: "http://example.org/"
316+
},
317+
"Expands and compacts to document base in 1.1 with compactToRelative true" => {
318+
input: %({
319+
"@id": "a",
320+
"http://example.com/b": {"@id": "c"}
321+
}),
322+
context: %({"b": "http://example.com/b"}),
323+
output: %({
324+
"@context": {"b": "http://example.com/b"},
325+
"@id": "a",
326+
"b": {"@id": "c"}
327+
}),
328+
base: "http://example.org/",
329+
compactToRelative: true,
330+
processingMode: 'json-ld-1.1'
331+
},
332+
"Expands but does not compact to document base in 1.1 with compactToRelative false" => {
333+
input: %({
334+
"@id": "a",
335+
"http://example.com/b": {"@id": "c"}
336+
}),
337+
context: %({"b": "http://example.com/b"}),
338+
output: %({
339+
"@context": {"b": "http://example.com/b"},
340+
"@id": "http://example.org/a",
341+
"b": {"@id": "http://example.org/c"}
342+
}),
343+
base: "http://example.org/",
344+
compactToRelative: false,
345+
processingMode: 'json-ld-1.1'
346+
},
347+
"Expands and compacts to document base in 1.1 by default" => {
348+
input: %({
349+
"@id": "a",
350+
"http://example.com/b": {"@id": "c"}
351+
}),
352+
context: %({"b": "http://example.com/b"}),
353+
output: %({
354+
"@context": {"b": "http://example.com/b"},
355+
"@id": "a",
356+
"b": {"@id": "c"}
357+
}),
358+
base: "http://example.org/",
359+
processingMode: 'json-ld-1.1'
360+
},
361+
}.each_pair do |title, params|
362+
it(title) {run_compact(params)}
363+
end
364+
end
365+
302366
context "@container: @reverse" do
303367
{
304368
"@container: @reverse" => {
@@ -1275,15 +1339,15 @@
12751339
end
12761340

12771341
def run_compact(params)
1278-
input, output, context, processingMode = params[:input], params[:output], params[:context], params[:processingMode]
1342+
input, output, context = params[:input], params[:output], params[:context]
12791343
input = ::JSON.parse(input) if input.is_a?(String)
12801344
output = ::JSON.parse(output) if output.is_a?(String)
12811345
context = ::JSON.parse(context) if context.is_a?(String)
12821346
pending params.fetch(:pending, "test implementation") unless input
12831347
if params[:exception]
1284-
expect {JSON::LD::API.compact(input, context, logger: logger, processingMode: processingMode)}.to raise_error(params[:exception])
1348+
expect {JSON::LD::API.compact(input, context, params.merge(logger: logger))}.to raise_error(params[:exception])
12851349
else
1286-
jld = JSON::LD::API.compact(input, context, logger: logger, processingMode: processingMode)
1350+
jld = JSON::LD::API.compact(input, context, params.merge(logger: logger))
12871351
expect(jld).to produce(output, logger)
12881352
end
12891353
end

spec/context_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def containers
2424

2525
describe JSON::LD::Context do
2626
let(:logger) {RDF::Spec.logger}
27-
let(:context) {JSON::LD::Context.new(logger: logger, validate: true, processingMode: "json-ld-1.1")}
27+
let(:context) {JSON::LD::Context.new(logger: logger, validate: true, processingMode: "json-ld-1.1", compactToRelative: true)}
2828
let(:remote_doc) do
2929
JSON::LD::API::RemoteDocument.new("http://example.com/context", %q({
3030
"@context": {

0 commit comments

Comments
 (0)