Skip to content

Commit 3ec34b8

Browse files
committed
Add framing and keep_free_floating_nodes option to expand and use when expanding frame. Fixes #28.
1 parent e9d9fe9 commit 3ec34b8

File tree

4 files changed

+94
-24
lines changed

4 files changed

+94
-24
lines changed

lib/json/ld/api.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ def initialize(input, context, options = {}, &block)
152152
# The JSON-LD object to copy and perform the expansion upon.
153153
# @param [Hash{Symbol => Object}] options
154154
# See options in {JSON::LD::API#initialize}
155+
# @option options [Boolean] :framing Internal use for framing
156+
# @option options [Boolean] :keep_free_floating_nodes Internal use for framing
155157
# @raise [JsonLdError]
156158
# @yield jsonld
157159
# @yieldparam [Array<Hash>] jsonld
@@ -163,7 +165,10 @@ def initialize(input, context, options = {}, &block)
163165
def self.expand(input, options = {})
164166
result = nil
165167
API.new(input, options[:expandContext], options) do |api|
166-
result = api.expand(api.value, nil, api.context)
168+
result = api.expand(api.value, nil, api.context,
169+
ordered: options.fetch(:ordered, true),
170+
framing: options.fetch(:framing, false),
171+
keep_free_floating_nodes: options.fetch(:keep_free_floating_nodes, false))
167172
end
168173

169174
# If, after the algorithm outlined above is run, the resulting element is an
@@ -347,7 +352,7 @@ def self.frame(input, frame, options = {})
347352
expanded_input = options[:expanded] ? input : API.expand(input, options)
348353

349354
# Expand frame to simplify processing
350-
expanded_frame = API.expand(frame, options)
355+
expanded_frame = API.expand(frame, options.merge(framing: true, keep_free_floating_nodes: true))
351356

352357
# Initialize input using frame as context
353358
API.new(expanded_input, nil, options) do

lib/json/ld/expand.rb

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ module Expand
1010
# @param [Array, Hash] input
1111
# @param [String] active_property
1212
# @param [Context] context
13-
# @param [Hash{Symbol => Object}] options
14-
# @option options [Boolean] :ordered (true)
13+
# @param [Boolean] ordered (true)
1514
# Ensure output objects have keys ordered properly
15+
# @param [Boolean] framing (false)
16+
# @param [Boolean] keep_free_floating_notes (false)
1617
# @return [Array, Hash]
17-
def expand(input, active_property, context, options = {})
18-
options = {ordered: true}.merge!(options)
18+
def expand(input, active_property, context, ordered: true, framing: false, keep_free_floating_nodes: false)
1919
log_debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
2020
result = case input
2121
when Array
@@ -24,7 +24,7 @@ def expand(input, active_property, context, options = {})
2424
is_list = context.container(active_property) == '@list'
2525
value = input.map do |v|
2626
# Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
27-
v = expand(v, active_property, context, options)
27+
v = expand(v, active_property, context, ordered: ordered)
2828

2929
# If the active property is @list or its container mapping is set to @list, the expanded item must not be an array or a list object, otherwise a list of lists error has been detected and processing is aborted.
3030
raise JsonLdError::ListOfLists,
@@ -45,7 +45,7 @@ def expand(input, active_property, context, options = {})
4545
log_depth do
4646
output_object = {}
4747
# Then, proceed and process each property and value in element as follows:
48-
keys = options[:ordered] ? input.keys.kw_sort : input.keys
48+
keys = ordered ? input.keys.kw_sort : input.keys
4949
keys.each do |key|
5050
# For each key and value in element, ordered lexicographically by key:
5151
value = input[key]
@@ -74,8 +74,15 @@ def expand(input, active_property, context, options = {})
7474
expanded_value = case expanded_property
7575
when '@id'
7676
# If expanded property is @id and value is not a string, an invalid @id value error has been detected and processing is aborted
77-
raise JsonLdError::InvalidIdValue,
78-
"value of @id must be a string: #{value.inspect}" unless value.is_a?(String)
77+
case value
78+
when String
79+
when Hash
80+
raise JsonLdError::InvalidIdValue,
81+
"value of @id must be a string unless framing: #{value.inspect}" unless framing
82+
else
83+
raise JsonLdError::InvalidIdValue,
84+
"value of @id must be a string or hash if framing: #{value.inspect}"
85+
end
7986

8087
# Otherwise, set expanded value to the result of using the IRI Expansion algorithm, passing active context, value, and true for document relative.
8188
context.expand_iri(value, documentRelative: true, log_depth: @options[:log_depth]).to_s
@@ -104,7 +111,7 @@ def expand(input, active_property, context, options = {})
104111
end
105112
when '@graph'
106113
# If expanded property is @graph, set expanded value to the result of using this algorithm recursively passing active context, @graph for active property, and value for element.
107-
log_depth { expand(value, '@graph', context, options) }
114+
log_depth { expand(value, '@graph', context, ordered: ordered) }
108115
when '@value'
109116
# If expanded property is @value and value is not a scalar or null, an invalid value object value error has been detected and processing is aborted. Otherwise, set expanded value to value. If expanded value is null, set the @value member of result to null and continue with the next key from element. Null values need to be preserved in this case as the meaning of an @type member depends on the existence of an @value member.
110117
raise JsonLdError::InvalidValueObjectValue,
@@ -131,7 +138,7 @@ def expand(input, active_property, context, options = {})
131138
next if (active_property || '@graph') == '@graph'
132139

133140
# Otherwise, initialize expanded value to the result of using this algorithm recursively passing active context, active property, and value for element.
134-
value = log_depth { expand(value, active_property, context, options) }
141+
value = log_depth { expand(value, active_property, context, ordered: ordered) }
135142

136143
# Spec FIXME: need to be sure that result is an array
137144
value = [value] unless value.is_a?(Array)
@@ -144,15 +151,15 @@ def expand(input, active_property, context, options = {})
144151
value
145152
when '@set'
146153
# If expanded property is @set, set expanded value to the result of using this algorithm recursively, passing active context, active property, and value for element.
147-
log_depth { expand(value, active_property, context, options) }
154+
log_depth { expand(value, active_property, context, ordered: ordered) }
148155
when '@reverse'
149156
# If expanded property is @reverse and value is not a JSON object, an invalid @reverse value error has been detected and processing is aborted.
150157
raise JsonLdError::InvalidReverseValue,
151158
"@reverse value must be an object: #{value.inspect}" unless value.is_a?(Hash)
152159

153160
# Otherwise
154161
# Initialize expanded value to the result of using this algorithm recursively, passing active context, @reverse as active property, and value as element.
155-
value = log_depth { expand(value, '@reverse', context, options) }
162+
value = log_depth { expand(value, '@reverse', context, ordered: ordered) }
156163

157164
# If expanded value contains an @reverse member, i.e., properties that are reversed twice, execute for each of its property and item the following steps:
158165
if value.has_key?('@reverse')
@@ -184,7 +191,7 @@ def expand(input, active_property, context, options = {})
184191
next
185192
when '@explicit', '@default', '@embed', '@explicit', '@omitDefault', '@preserve', '@requireAll'
186193
# Framing keywords
187-
log_depth { [expand(value, expanded_property, context, options)].flatten }
194+
log_depth { [expand(value, expanded_property, context, ordered: ordered)].flatten }
188195
else
189196
# Skip unknown keyword
190197
next
@@ -203,7 +210,7 @@ def expand(input, active_property, context, options = {})
203210
ary = []
204211

205212
# For each key-value pair language-language value in value, ordered lexicographically by language
206-
keys = options[:ordered] ? value.keys.sort : value.keys
213+
keys = ordered ? value.keys.sort : value.keys
207214
keys.each do |k|
208215
[value[k]].flatten.each do |item|
209216
# item must be a string, otherwise an invalid language map value error has been detected and processing is aborted.
@@ -226,10 +233,10 @@ def expand(input, active_property, context, options = {})
226233
ary = []
227234

228235
# For each key-value in the object:
229-
keys = options[:ordered] ? value.keys.sort : value.keys
236+
keys = ordered ? value.keys.sort : value.keys
230237
keys.each do |k|
231238
# Initialize index value to the result of using this algorithm recursively, passing active context, key as active property, and index value as element.
232-
index_value = log_depth { expand([value[k]].flatten, key, context, options) }
239+
index_value = log_depth { expand([value[k]].flatten, key, context, ordered: ordered) }
233240
index_value.each do |item|
234241
item['@index'] ||= k
235242
ary << item
@@ -238,7 +245,7 @@ def expand(input, active_property, context, options = {})
238245
ary
239246
else
240247
# Otherwise, initialize expanded value to the result of using this algorithm recursively, passing active context, key for active property, and value for element.
241-
log_depth { expand(value, key, context, options) }
248+
log_depth { expand(value, key, context, ordered: ordered) }
242249
end
243250

244251
# If expanded value is null, ignore key by continuing to the next key from element.
@@ -323,13 +330,13 @@ def expand(input, active_property, context, options = {})
323330
# If active property is null or @graph, drop free-floating values as follows:
324331
if (active_property || '@graph') == '@graph' &&
325332
(output_object.keys.any? {|k| %w(@value @list).include?(k)} ||
326-
(output_object.keys - %w(@id)).empty?)
333+
(output_object.keys - %w(@id)).empty? && !keep_free_floating_nodes)
327334
log_debug(" =>") { "empty top-level: " + output_object.inspect}
328335
return nil
329336
end
330337

331338
# Re-order result keys if ordering
332-
if options[:ordered]
339+
if ordered
333340
output_object.keys.kw_sort.inject({}) {|map, kk| map[kk] = output_object[kk]; map}
334341
else
335342
output_object

lib/json/ld/frame.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,16 @@ def filter_subject(subject, frame, flags)
238238
case k
239239
when '@id'
240240
return false if v.is_a?(String) && subject['@id'] != v
241-
wildcard, matches_some = true, true
241+
wildcard, matches_some = false, true
242242
when '@type'
243-
wildcard, matches_some = true, true
243+
wildcard, matches_some = false, false
244244
when /^@/
245245
else
246246
wildcard = false
247247

248248
# v == [] means do not match if property is present
249249
if subject.has_key?(k)
250-
return false if v == []
250+
return false if v == [] && !subject[k].nil?
251251
matches_some = true
252252
next
253253
end

spec/frame_spec.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,5 +621,63 @@
621621
data = framed["@graph"].first
622622
expect(data["mising_value"]).to be_nil
623623
end
624+
625+
it "issue #28" do
626+
input = JSON.parse %({
627+
"@context": {
628+
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
629+
},
630+
"@id": "http://www.myresource/uuid",
631+
"http://www.myresource.com/ontology/1.0#talksAbout": [
632+
{
633+
"@id": "http://rdf.freebase.com/ns/m.018w8",
634+
"rdfs:label": [
635+
{
636+
"@value": "Basketball",
637+
"@language": "en"
638+
}
639+
]
640+
}
641+
]
642+
})
643+
frame = JSON.parse %({
644+
"@context": {
645+
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
646+
"talksAbout": {
647+
"@id": "http://www.myresource.com/ontology/1.0#talksAbout",
648+
"@type": "@id"
649+
},
650+
"label": {
651+
"@id": "rdfs:label",
652+
"@language": "en"
653+
}
654+
},
655+
"@id": "http://www.myresource/uuid"
656+
})
657+
expected = JSON.parse %({
658+
"@context": {
659+
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
660+
"talksAbout": {
661+
"@id": "http://www.myresource.com/ontology/1.0#talksAbout",
662+
"@type": "@id"
663+
},
664+
"label": {
665+
"@id": "rdfs:label",
666+
"@language": "en"
667+
}
668+
},
669+
"@graph": [
670+
{
671+
"@id": "http://www.myresource/uuid",
672+
"talksAbout": {
673+
"@id": "http://rdf.freebase.com/ns/m.018w8",
674+
"label": "Basketball"
675+
}
676+
}
677+
]
678+
})
679+
framed = JSON::LD::API.frame(input, frame, logger: logger)
680+
expect(framed).to produce(expected, logger)
681+
end
624682
end
625683
end

0 commit comments

Comments
 (0)