Skip to content

Commit 0e11f03

Browse files
committed
More framing updates for deep object patterns and value patterns.
1 parent 4fa248d commit 0e11f03

File tree

4 files changed

+526
-200
lines changed

4 files changed

+526
-200
lines changed

lib/json/ld/expand.rb

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def expand(input, active_property, context, ordered: true)
7878
when String
7979
context.expand_iri(value, documentRelative: true, log_depth: @options[:log_depth]).to_s
8080
when Array
81+
# Framing allows an array of IRIs, and always puts values in an array
8182
raise JsonLdError::InvalidIdValue,
8283
"value of @id must be a string, array of string or hash if framing: #{value.inspect}" unless framing
8384
context.expand_iri(value, documentRelative: true, log_depth: @options[:log_depth]).to_s
@@ -131,18 +132,43 @@ def expand(input, active_property, context, ordered: true)
131132
expand(value, '@graph', context, ordered: ordered)
132133
when '@value'
133134
# 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.
134-
raise JsonLdError::InvalidValueObjectValue,
135-
"Value of #{expanded_property} must be a scalar or null: #{value.inspect}" if value.is_a?(Hash) || value.is_a?(Array)
136-
if value.nil?
135+
# If framing, always use array form, unless null
136+
case value
137+
when String, TrueClass, FalseClass, Numeric then (framing ? [value] : value)
138+
when nil
137139
output_object['@value'] = nil
138140
next;
141+
when Array
142+
raise JsonLdError::InvalidValueObjectValue,
143+
"@value value may not be an array unless framing: #{value.inspect}" unless framing
144+
value
145+
when Hash
146+
raise JsonLdError::InvalidValueObjectValue,
147+
"@value value must be a an empty object for framing: #{value.inspect}" unless
148+
value.empty? && framing
149+
[value]
150+
else
151+
raise JsonLdError::InvalidValueObjectValue,
152+
"Value of #{expanded_property} must be a scalar or null: #{value.inspect}"
139153
end
140-
value
141154
when '@language'
142155
# If expanded property is @language and value is not a string, an invalid language-tagged string error has been detected and processing is aborted. Otherwise, set expanded value to lowercased value.
143-
raise JsonLdError::InvalidLanguageTaggedString,
144-
"Value of #{expanded_property} must be a string: #{value.inspect}" unless value.is_a?(String)
145-
value.downcase
156+
# If framing, always use array form, unless null
157+
case value
158+
when String then (framing ? [value.downcase] : value.downcase)
159+
when Array
160+
raise JsonLdError::InvalidLanguageTaggedString,
161+
"@language value may not be an array unless framing: #{value.inspect}" unless framing
162+
value.map(&:downcase)
163+
when Hash
164+
raise JsonLdError::InvalidLanguageTaggedString,
165+
"@language value must be a an empty object for framing: #{value.inspect}" unless
166+
value.empty? && framing
167+
[value]
168+
else
169+
raise JsonLdError::InvalidLanguageTaggedString,
170+
"Value of #{expanded_property} must be a string: #{value.inspect}"
171+
end
146172
when '@index'
147173
# If expanded property is @index and value is not a string, an invalid @index value error has been detected and processing is aborted. Otherwise, set expanded value to value.
148174
raise JsonLdError::InvalidIndexValue,
@@ -312,21 +338,22 @@ def expand(input, active_property, context, ordered: true)
312338
"value object has unknown keys: #{output_object.inspect}"
313339
end
314340

315-
output_object.delete('@language') if output_object['@language'].to_s.empty?
316-
output_object.delete('@type') if output_object['@type'].to_s.empty?
341+
output_object.delete('@language') if Array(output_object['@language']).join('').to_s.empty?
342+
output_object.delete('@type') if Array(output_object['@type']).join('').to_s.empty?
317343

318344
# If the value of result's @value key is null, then set result to null.
319-
return nil if output_object['@value'].nil?
345+
return nil if Array(output_object['@value']).empty?
320346

321-
if !output_object['@value'].is_a?(String) && output_object.has_key?('@language')
347+
if !Array(output_object['@value']).all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.has_key?('@language')
322348
# Otherwise, if the value of result's @value member is not a string and result contains the key @language, an invalid language-tagged value error has been detected (only strings can be language-tagged) and processing is aborted.
323349
raise JsonLdError::InvalidLanguageTaggedValue,
324-
"when @language is used, @value must be a string: #{@value.inspect}"
325-
elsif !output_object.fetch('@type', "").is_a?(String) ||
326-
!context.expand_iri(output_object.fetch('@type', ""), vocab: true, log_depth: @options[:log_depth]).is_a?(RDF::URI)
350+
"when @language is used, @value must be a string: #{output_object.inspect}"
351+
elsif !Array(output_object.fetch('@type', "")).all? {|t|
352+
t.is_a?(String) && context.expand_iri(t, vocab: true, log_depth: @options[:log_depth]).is_a?(RDF::URI) ||
353+
t.is_a?(Hash) && t.empty?}
327354
# Otherwise, if the result has a @type member and its value is not an IRI, an invalid typed value error has been detected and processing is aborted.
328355
raise JsonLdError::InvalidTypedValue,
329-
"value of @type must be an IRI: #{output_object['@type'].inspect}"
356+
"value of @type must be an IRI: #{output_object.inspect}"
330357
end
331358
elsif !output_object.fetch('@type', []).is_a?(Array)
332359
# Otherwise, if result contains the key @type and its associated value is not an array, set it to an array containing only the associated value.

lib/json/ld/frame.rb

Lines changed: 74 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,6 @@ def frame(state, subjects, frame, **options)
4343
state = state.merge(uniqueEmbeds: {}) if property.nil?
4444

4545
if flags[:embed] == '@link' && state[:link].has_key?(id)
46-
# TODO: may want to also match an existing linked subject
47-
# against the current frame ... so different frames could
48-
# produce different subjects that are only shared in-memory
49-
# when the frames are the same
50-
5146
# add existing linked subject
5247
add_frame_output(parent, property, state[:link][id])
5348
next
@@ -90,6 +85,8 @@ def frame(state, subjects, frame, **options)
9085

9186
# add objects
9287
objects.each do |o|
88+
subframe = Array(frame[prop]).first || create_implicit_frame(flags)
89+
9390
case
9491
when list?(o)
9592
# add empty list
@@ -99,19 +96,16 @@ def frame(state, subjects, frame, **options)
9996
src = o['@list']
10097
src.each do |oo|
10198
if node_reference?(oo)
102-
subframe = frame[prop].first['@list'] if frame[prop].is_a?(Array) && frame[prop].first.is_a?(Hash)
103-
subframe ||= create_implicit_frame(flags)
10499
frame(state, [oo['@id']], subframe, options.merge(parent: list, property: '@list'))
105100
else
106101
add_frame_output(list, '@list', oo.dup)
107102
end
108103
end
109104
when node_reference?(o)
110105
# recurse into subject reference
111-
subframe = frame[prop] || create_implicit_frame(flags)
112106
frame(state, [o['@id']], subframe, options.merge(parent: output, property: prop))
113-
else
114-
# include other values automatically
107+
when value_match?(subframe, o)
108+
# Include values if they match
115109
add_frame_output(output, prop, o.dup)
116110
end
117111
end
@@ -191,7 +185,7 @@ def cleanup_preserve(input)
191185
#
192186
# @param [Hash{Symbol => Object}] state
193187
# Current framing state
194-
# @param [Hash{String => Hash}] subjects
188+
# @param [Array<String>] subjects
195189
# The subjects to filter
196190
# @param [Hash{String => Object}] frame
197191
# @param [Hash{Symbol => String}] flags the frame flags.
@@ -219,65 +213,77 @@ def filter_subjects(state, subjects, frame, flags)
219213
#
220214
# @return [Boolean] true if the node matches, false if not.
221215
def filter_subject(subject, frame, state, flags)
222-
types = frame.fetch('@type', [])
223-
subject_types = subject.fetch('@type', [])
224-
ids = frame.fetch('@id', [])
225-
subject_ids = subject.fetch('@id', [])
226-
subject_ids = [subject_ids] unless subject_ids.is_a?(Array)
227-
228-
# Match on specific @id.
229-
return !(ids & subject_ids).empty? if !ids.empty? && ids != [{}]
230-
231-
# Match on specific @type
232-
return !(types & subject_types).empty? if !types.empty? && types != [{}]
233-
234-
# Match on wildcard @type
235-
return true if types == [{}] && !subject_types.empty?
236-
237-
# Don't Match on no @type
238-
return false if frame['@type'] == [] && !subject_types.empty?
239-
240216
# Duck typing, for nodes not having a type, but having @id
241217
wildcard, matches_some = true, false
242218

243-
frame.reject {|k| k.start_with?('@')}.each do |k, v|
244-
is_empty = v.empty?
245-
if v = v.first
246-
validate_frame(v)
247-
has_default = v.has_key?('@default')
248-
# Exclude framing keywords
249-
v = v.dup.delete_if {|kk,vv| %w(@default @embed @explicit @omitDefault @requireAll).include?(kk)}
250-
end
251-
219+
frame.each do |k, v|
252220
node_values = subject.fetch(k, [])
253221

254-
# No longer a wildcard pattern if frame has any non-keyword properties
255-
wildcard = false
222+
case k
223+
when '@id'
224+
ids = v || []
225+
226+
# Match on specific @id.
227+
return ids.include?(subject['@id']) if !ids.empty? && ids != [{}]
228+
match_this = true
229+
when '@type'
230+
# No longer a wildcard pattern
231+
wildcard = false
232+
233+
match_this = case v
234+
when []
235+
# Don't Match on no @type
236+
return false if !node_values.empty?
237+
true
238+
when [{}]
239+
# Match on wildcard @type
240+
!node_values.empty?
241+
else
242+
# Match on specific @type
243+
return !(v & node_values).empty?
244+
false
245+
end
246+
when /@/
247+
# Skip other keywords
248+
next
249+
else
250+
is_empty = v.empty?
251+
if v = v.first
252+
validate_frame(v)
253+
has_default = v.has_key?('@default')
254+
# Exclude framing keywords
255+
v = v.dup.delete_if {|kk,vv| %w(@default @embed @explicit @omitDefault @requireAll).include?(kk)}
256+
end
256257

257-
# Skip, but allow match if node has no value for property, and frame has a default value
258-
next if node_values.empty? && has_default
259258

260-
# If frame value is empty, don't match if subject has any value
261-
return false if !node_values.empty? && is_empty
259+
# No longer a wildcard pattern if frame has any non-keyword properties
260+
wildcard = false
262261

263-
match_this = case v
264-
when nil
265-
# node does not match if values is not empty and the value of property in frame is match none.
266-
return false unless node_values.empty?
267-
true
268-
when {}
269-
# node matches if values is not empty and the value of property in frame is wildcard
270-
!node_values.empty?
271-
else
272-
if value?(v)
273-
# Match on any matching value
274-
node_values.any? {|nv| value_match?(v, nv)}
275-
elsif node?(v) || node_reference?(v)
276-
node_values.any? do |nv|
277-
node_match?(v, nv, state, flags)
278-
end
262+
# Skip, but allow match if node has no value for property, and frame has a default value
263+
next if node_values.empty? && has_default
264+
265+
# If frame value is empty, don't match if subject has any value
266+
return false if !node_values.empty? && is_empty
267+
268+
match_this = case v
269+
when nil
270+
# node does not match if values is not empty and the value of property in frame is match none.
271+
return false unless node_values.empty?
272+
true
273+
when {}
274+
# node matches if values is not empty and the value of property in frame is wildcard
275+
!node_values.empty?
279276
else
280-
false # No matching on non-value or node values
277+
if value?(v)
278+
# Match on any matching value
279+
node_values.any? {|nv| value_match?(v, nv)}
280+
elsif node?(v) || node_reference?(v)
281+
node_values.any? do |nv|
282+
node_match?(v, nv, state, flags)
283+
end
284+
else
285+
false # No matching on non-value or node values
286+
end
281287
end
282288
end
283289

@@ -398,7 +404,7 @@ def add_frame_output(parent, property, output)
398404
# @param [Hash] flags the current framing flags.
399405
# @return [Array<Hash>] the implicit frame.
400406
def create_implicit_frame(flags)
401-
[flags.keys.inject({}) {|memo, key| memo["@#{key}"] = [flags[key]]; memo}]
407+
flags.keys.inject({}) {|memo, key| memo["@#{key}"] = [flags[key]]; memo}
402408
end
403409

404410
private
@@ -411,15 +417,17 @@ def node_match?(pattern, value, state, flags)
411417

412418
# Value matches if it is a value, and matches the value pattern.
413419
#
420+
# * `pattern` is empty
414421
# * @values are the same, or `pattern[@value]` is a wildcard, and
415422
# * @types are the same or `value[@type]` is not null and `pattern[@type]` is `{}`, or `value[@type]` is null and `pattern[@type]` is null or `[]`, and
416423
# * @languages are the same or `value[@language]` is not null and `pattern[@language]` is `{}`, or `value[@language]` is null and `pattern[@language]` is null or `[]`.
417424
def value_match?(pattern, value)
418425
v1, t1, l1 = value['@value'], value['@type'], value['@language']
419-
v2, t2, l2 = pattern['@value'], pattern['@type'], pattern['@language']
420-
return false unless v1 == v2 || v1 && v2 == {}
421-
return false unless t1 == t2 || t1 && t2 == {} || t1.nil? && (t2 || []) == []
422-
return false unless l1 == l2 || l1 && l2 == {} || l1.nil? && (l2 || []) == []
426+
v2, t2, l2 = Array(pattern['@value']), Array(pattern['@type']), Array(pattern['@language'])
427+
return true if (v2 + t2 + l2).empty?
428+
return false unless v2.include?(v1) || v2 == [{}]
429+
return false unless t2.include?(t1) || t1 && t2 == [{}] || t1.nil? && (t2 || []) == []
430+
return false unless l2.include?(l1) || l1 && l2 == [{}] || l1.nil? && (l2 || []) == []
423431
true
424432
end
425433
end

0 commit comments

Comments
 (0)