Skip to content

Commit 4fa248d

Browse files
committed
Partial update for frame matching.
1 parent 9d64e12 commit 4fa248d

File tree

3 files changed

+118
-49
lines changed

3 files changed

+118
-49
lines changed

lib/json/ld/expand.rb

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ module Expand
1414
# @param [Context] context
1515
# @param [Boolean] ordered (true)
1616
# Ensure output objects have keys ordered properly
17-
# @param [Boolean] framing (false)
1817
# @param [Hash{Symbol => Object}] options
1918
# See {JSON::LD::API.expand}
2019
# @return [Array<Hash{String => Object}>]
@@ -75,18 +74,36 @@ def expand(input, active_property, context, ordered: true)
7574
expanded_value = case expanded_property
7675
when '@id'
7776
# If expanded property is @id and value is not a string, an invalid @id value error has been detected and processing is aborted
78-
case value
77+
e_id = case value
7978
when String
79+
context.expand_iri(value, documentRelative: true, log_depth: @options[:log_depth]).to_s
80+
when Array
81+
raise JsonLdError::InvalidIdValue,
82+
"value of @id must be a string, array of string or hash if framing: #{value.inspect}" unless framing
83+
context.expand_iri(value, documentRelative: true, log_depth: @options[:log_depth]).to_s
84+
value.map do |v|
85+
raise JsonLdError::InvalidTypeValue,
86+
"@id value must be a string or array of strings for framing: #{v.inspect}" unless v.is_a?(String)
87+
context.expand_iri(v, documentRelative: true, quiet: true, log_depth: @options[:log_depth]).to_s
88+
end
8089
when Hash
8190
raise JsonLdError::InvalidIdValue,
8291
"value of @id must be a string unless framing: #{value.inspect}" unless framing
92+
raise JsonLdError::InvalidTypeValue,
93+
"value of @id must be a an empty object for framing: #{value.inspect}" unless
94+
value.empty? && framing
95+
[{}]
8396
else
8497
raise JsonLdError::InvalidIdValue,
8598
"value of @id must be a string or hash if framing: #{value.inspect}"
8699
end
87100

88-
# Otherwise, set expanded value to the result of using the IRI Expansion algorithm, passing active context, value, and true for document relative.
89-
context.expand_iri(value, documentRelative: true, log_depth: @options[:log_depth]).to_s
101+
# Use array form if framing
102+
if framing && !e_id.is_a?(Array)
103+
[e_id]
104+
else
105+
e_id
106+
end
90107
when '@type'
91108
# If expanded property is @type and value is neither a string nor an array of strings, an invalid type value error has been detected and processing is aborted. Otherwise, set expanded value to the result of using the IRI Expansion algorithm, passing active context, true for vocab, and true for document relative to expand the value or each of its items.
92109
#log_debug("@type") {"value: #{value.inspect}"}
@@ -103,7 +120,8 @@ def expand(input, active_property, context, ordered: true)
103120
# For framing
104121
raise JsonLdError::InvalidTypeValue,
105122
"@type value must be a an empty object for framing: #{value.inspect}" unless
106-
value.empty?
123+
value.empty? && framing
124+
[{}]
107125
else
108126
raise JsonLdError::InvalidTypeValue,
109127
"@type value must be a string or array of strings: #{value.inspect}"

lib/json/ld/frame.rb

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ module Frame
2121
def frame(state, subjects, frame, **options)
2222
parent, property = options[:parent], options[:property]
2323
# Validate the frame
24-
validate_frame(state, frame)
24+
validate_frame(frame)
2525
frame = frame.first if frame.is_a?(Array)
2626

2727
# Get values for embedOn and explicitOn
@@ -200,7 +200,7 @@ def cleanup_preserve(input)
200200
def filter_subjects(state, subjects, frame, flags)
201201
subjects.inject({}) do |memo, id|
202202
subject = state[:subjects][id]
203-
memo[id] = subject if filter_subject(subject, frame, flags)
203+
memo[id] = subject if filter_subject(subject, frame, state, flags)
204204
memo
205205
end
206206
end
@@ -214,55 +214,84 @@ def filter_subjects(state, subjects, frame, flags)
214214
#
215215
# @param [Hash{String => Object}] subject the subject to check.
216216
# @param [Hash{String => Object}] frame the frame to check.
217+
# @param [Hash{Symbol => Object}] state Current framing state
217218
# @param [Hash{Symbol => Object}] flags the frame flags.
218219
#
219220
# @return [Boolean] true if the node matches, false if not.
220-
def filter_subject(subject, frame, flags)
221+
def filter_subject(subject, frame, state, flags)
221222
types = frame.fetch('@type', [])
222223
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)
223227

224-
# check @type (object value means 'any' type, fall through to ducktyping)
225-
if !types.empty? && types != [{}]
226-
# A subject must match if node has a @type property including any IRI from the corresponding @type property in frame.
227-
return types.any? {|t| subject_types.include?(t)}
228-
elsif types == [{}]
229-
# Otherwise, a subject must match if node has a @type property and frame has a @type property containing only an empty dictionary.
230-
return !subject_types.empty?
231-
else
232-
# Duck typing, for nodes not having a type, but having @id
233-
wildcard, matches_some = true, false
234-
235-
frame.each do |k, v|
236-
case k
237-
when '@id'
238-
return false if v.is_a?(String) && subject['@id'] != v
239-
wildcard, matches_some = false, true
240-
when '@type'
241-
wildcard, matches_some = false, false
242-
when /^@/
243-
else
244-
wildcard = false
245-
246-
if subject.has_key?(k)
247-
matches_some = true
248-
next
249-
elsif v == []
250-
# v == [] means do not match if property is present
251-
return false
252-
end
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+
240+
# Duck typing, for nodes not having a type, but having @id
241+
wildcard, matches_some = true, false
242+
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
253251

254-
# all properties must match to be a duck unless a @default is specified
255-
has_default = v.is_a?(Array) && v.length == 1 && v.first.is_a?(Hash) && v.first.has_key?('@default')
256-
return false if flags[:requireAll] && !has_default
252+
node_values = subject.fetch(k, [])
253+
254+
# No longer a wildcard pattern if frame has any non-keyword properties
255+
wildcard = false
256+
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
259+
260+
# If frame value is empty, don't match if subject has any value
261+
return false if !node_values.empty? && is_empty
262+
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
279+
else
280+
false # No matching on non-value or node values
257281
end
258282
end
259283

260-
# return true if wildcard or subject matches some properties
261-
wildcard || matches_some
284+
# All non-defaulted values must match if @requireAll is set
285+
return false if !match_this && flags[:requireAll]
286+
287+
matches_some ||= match_this
262288
end
289+
290+
# return true if wildcard or subject matches some properties
291+
wildcard || matches_some
263292
end
264293

265-
def validate_frame(state, frame)
294+
def validate_frame(frame)
266295
raise InvalidFrame::Syntax,
267296
"Invalid JSON-LD syntax; a JSON-LD frame must be an object: #{frame.inspect}" unless
268297
frame.is_a?(Hash) || (frame.is_a?(Array) && frame.first.is_a?(Hash) && frame.length == 1)
@@ -371,5 +400,27 @@ def add_frame_output(parent, property, output)
371400
def create_implicit_frame(flags)
372401
[flags.keys.inject({}) {|memo, key| memo["@#{key}"] = [flags[key]]; memo}]
373402
end
403+
404+
private
405+
# Node matches if it is a node, and matches the pattern as a frame
406+
def node_match?(pattern, value, state, flags)
407+
return false unless value['@id']
408+
node_object = state[:subjects][value['@id']]
409+
node_object && filter_subject(node_object, pattern, state, flags)
410+
end
411+
412+
# Value matches if it is a value, and matches the value pattern.
413+
#
414+
# * @values are the same, or `pattern[@value]` is a wildcard, and
415+
# * @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
416+
# * @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 `[]`.
417+
def value_match?(pattern, value)
418+
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 || []) == []
423+
true
424+
end
374425
end
375426
end

spec/frame_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
describe ".frame" do
99
{
10-
"A subject must match if node has a @type property including any IRI from the corresponding @type property in frame" => {
10+
"exact @type match" => {
1111
frame: %({
1212
"@context": {"ex": "http://example.org/"},
1313
"@type": "ex:Type1"
@@ -31,7 +31,7 @@
3131
}]
3232
})
3333
},
34-
"A subject must match if node has a @type any property and frame has a @type property containing only an empty JSON object" => {
34+
"wildcard type match" => {
3535
frame: %({
3636
"@context": {"ex": "http://example.org/"},
3737
"@type": {}
@@ -58,7 +58,7 @@
5858
}]
5959
})
6060
},
61-
"A subject must match if node and frame both have the same @id property" => {
61+
"@id match" => {
6262
frame: %({
6363
"@context": {"ex": "http://example.org/"},
6464
"@id": "ex:Sub1"
@@ -82,7 +82,7 @@
8282
}]
8383
})
8484
},
85-
"A subject must not match if node has a property where frame has an empty array for that same property" => {
85+
"wildcard with empty property no-match" => {
8686
frame: %({
8787
"@context": {"ex": "http://example.org/"},
8888
"ex:p": [],
@@ -103,8 +103,8 @@
103103
output: %({
104104
"@context": {"ex": "http://example.org/"},
105105
"@graph": [{
106-
"@id": "ex:Sub2",
107-
"ex:p": "foo",
106+
"@id": "ex:Sub1",
107+
"ex:p": null,
108108
"ex:q": "bar"
109109
}]
110110
})

0 commit comments

Comments
 (0)