json-ld 1.1.8 → 1.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,169 +5,118 @@ module JSON::LD
5
5
  ##
6
6
  # 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.
7
7
  #
8
- # @param [Array, Hash] element
9
- # Expanded element
10
- # @param [Hash{String => Hash}] node_map
11
- # map of nodes
12
- # @param [String] active_graph
8
+ # @param [Array, Hash] input
9
+ # Expanded JSON-LD input
10
+ # @param [Hash] graphs A map of graph name to subjects
11
+ # @param [String] graph
13
12
  # The name of the currently active graph that the processor should use when processing.
14
- # @param [String] active_subject
15
- # The currently active subject that the processor should use when processing.
16
- # @param [String] active_property
17
- # The currently active property or keyword that the processor should use when processing.
13
+ # @param [String] name
14
+ # The name assigned to the current input if it is a bnode
18
15
  # @param [Array] list
19
- # List for saving list elements
20
- def generate_node_map(element,
21
- node_map,
22
- active_graph = '@default',
23
- active_subject = nil,
24
- active_property = nil,
25
- list = nil)
16
+ # List to append to, nil for none
17
+ def create_node_map(input,
18
+ graphs,
19
+ graph = '@default',
20
+ name = nil,
21
+ list = nil)
26
22
  depth do
27
- debug("node_map") {"active_graph: #{active_graph}, element: #{element.inspect}"}
28
- debug(" =>") {"active_subject: #{active_subject.inspect}, active_property: #{active_property.inspect}, list: #{list.inspect}"}
29
- if element.is_a?(Array)
30
- # If element is an array, process each entry in element recursively by passing item for element, node map, active graph, active subject, active property, and list.
31
- element.map {|o|
32
- generate_node_map(o,
33
- node_map,
34
- active_graph,
35
- active_subject,
36
- active_property,
37
- list)
38
- }
39
- else
40
- # Otherwise element is a JSON object. Reference the JSON object which is the value of the active graph member of node map using the variable graph. If the active subject is null, set node to null otherwise reference the active subject member of graph using the variable node.
41
- # Spec FIXME: initializing it to an empty JSON object, if necessary
42
- raise "Expected element to be a hash, was #{element.class}" unless element.is_a?(Hash)
43
- graph = node_map[active_graph] ||= {}
44
- node = graph[active_subject] if active_subject
45
-
46
- # If element has an @type member, perform for each item the following steps:
47
- if element.has_key?('@type')
48
- types = Array(element['@type']).map do |item|
49
- # If item is a blank node identifier, replace it with a newly generated blank node identifier passing item for identifier.
50
- blank_node?(item) ? namer.get_name(item) : item
51
- end
52
-
53
- element['@type'] = element['@type'].is_a?(Array) ? types : types.first
54
- end
55
-
56
- # If element has an @value member, perform the following steps:
57
- if value?(element)
58
- unless list
59
- # If no list has been passed, merge element into the active property member of the active subject in graph.
60
- merge_value(node, active_property, element)
61
- else
62
- # Otherwise, append element to the @list member of list.
63
- merge_value(list, '@list', element)
64
- end
65
- elsif list?(element)
66
- # Otherwise, if element has an @list member, perform the following steps:
67
- # Initialize a new JSON object result having a single member @list whose value is initialized to an empty array.
68
- result = {'@list' => []}
69
-
70
- # Recursively call this algorithm passing the value of element's @list member as new element and result as list.
71
- generate_node_map(element['@list'],
72
- node_map,
73
- active_graph,
74
- active_subject,
75
- active_property,
76
- result)
77
-
78
- # Append result to the the value of the active property member of node.
79
- debug("node_map") {"@list: #{result.inspect}"}
80
- merge_value(node, active_property, result)
23
+ debug("node_map") {"graph: #{graph}, input: #{input.inspect}, name: #{name}"}
24
+ case input
25
+ when Array
26
+ # If input is an array, process each entry in input recursively by passing item for input, node map, active graph, active subject, active property, and list.
27
+ input.map {|o| create_node_map(o, graphs, graph, nil, list)}
28
+ when Hash
29
+ type = input['@type']
30
+ if value?(input)
31
+ # Rename blanknode @type
32
+ input['@type'] = namer.get_name(type) if type && blank_node?(type)
33
+ list << input if list
81
34
  else
82
- # Otherwise element is a node object, perform the following steps:
83
-
84
- # If element has an @id member, set id to its value and remove the member from element. If id is a blank node identifier, replace it with a newly generated blank node identifier passing id for identifier.
85
- # Otherwise, set id to the result of the Generate Blank Node Identifier algorithm passing null for identifier.
86
- id = element.delete('@id')
87
- id = namer.get_name(id) if blank_node?(id)
88
- debug("node_map") {"id: #{id.inspect}"}
35
+ # Input is a node definition
89
36
 
90
- # If graph does not contain a member id, create one and initialize it to a JSON object consisting of a single member @id whose value is set to id.
91
- graph[id] ||= {'@id' => id}
37
+ # spec requires @type to be named first, so assign names early
38
+ Array(type).each {|t| namer.get_name(t) if blank_node?(t)}
92
39
 
93
- # If active property is not null, perform the following steps:
94
- if node?(active_subject) || node_reference?(active_subject)
95
- debug("node_map") {"active_subject is an object, merge into #{id}"}
96
- merge_value(graph[id], active_property, active_subject)
97
- elsif active_property
98
- # Create a new JSON object reference consisting of a single member @id whose value is id.
99
- reference = {'@id' => id}
100
-
101
- # If list is null:
102
- unless list
103
- merge_value(node, active_property, reference)
104
- else
105
- merge_value(list, '@list', reference)
106
- end
40
+ # get name for subject
41
+ if name.nil?
42
+ name ||= input['@id']
43
+ name = namer.get_name(name) if blank_node?(name)
107
44
  end
108
45
 
109
- # Reference the value of the id member of graph using the variable node.
110
- node = graph[id]
111
-
112
- # If element has an @type key, append each item of its associated array to the array associated with the @type key of node unless it is already in that array. Finally remove the @type member from element.
113
- if element.has_key?('@type')
114
- Array(element.delete('@type')).each do |t|
115
- merge_value(node, '@type', t)
116
- end
117
- end
118
-
119
- # If element has an @index member, set the @index member of node to its value. If node has already an @index member with a different value, a conflicting indexes error has been detected and processing is aborted. Otherwise, continue by removing the @index member from element.
120
- if element.has_key?('@index')
121
- raise JsonLdError::ConflictingIndexes,
122
- "Element already has index #{node['@index']} dfferent from #{element['@index']}" if
123
- node['@index'] && node['@index'] != element['@index']
124
- node['@index'] = element.delete('@index')
125
- end
126
-
127
- # If element has an @reverse member:
128
- if element.has_key?('@reverse')
129
- element.delete('@reverse').each do |property, values|
130
- values.each do |value|
131
- debug("node_map") {"@reverse(#{id}): #{value.inspect}"}
132
- # Recursively invoke this algorithm passing value for element, node map, and active graph.
133
- generate_node_map(value,
134
- node_map,
135
- active_graph,
136
- {'@id' => id},
137
- property)
46
+ # add subject reference to list
47
+ list << {'@id' => name} if list
48
+
49
+ # create new subject or merge into existing one
50
+ subject = (graphs[graph] ||= {})[name] ||= {'@id' => name}
51
+
52
+ input.keys.kw_sort.each do |property|
53
+ objects = input[property]
54
+ case property
55
+ when '@id'
56
+ # Skip
57
+ when '@reverse'
58
+ # handle reverse properties
59
+ referenced_node, reverse_map = {'@id' => name}, objects
60
+ reverse_map.each do |reverse_property, items|
61
+ items.each do |item|
62
+ item_name = item['@id']
63
+ item_name = namer.get_name(item_name) if blank_node?(item_name)
64
+ create_node_map(item, graphs, graph, item_name)
65
+ add_value(graphs[graph][item_name],
66
+ reverse_property,
67
+ referenced_node,
68
+ property_is_array: true,
69
+ allow_duplicate: false)
70
+ end
71
+ end
72
+ when '@graph'
73
+ graphs[name] ||= {}
74
+ g = graph == '@merged' ? graph : name
75
+ create_node_map(objects, graphs, g)
76
+ when /^@(?!type)/
77
+ # copy non-@type keywords
78
+ if property == '@index' && subject['@index']
79
+ raise JsonLdError::ConflictingIndexes,
80
+ "Element already has index #{subject['@index']} dfferent from #{input['@index']}" if
81
+ subject['@index'] != input['@index']
82
+ subject['@index'] = input.delete('@index')
83
+ end
84
+ subject[property] = objects
85
+ else
86
+ # if property is a bnode, assign it a new id
87
+ property = namer.get_name(property) if blank_node?(property)
88
+
89
+ add_value(subject, property, [], property_is_array: true) if objects.empty?
90
+
91
+ objects.each do |o|
92
+ o = namer.get_name(o) if property == '@type' && blank_node?(o)
93
+
94
+ case
95
+ when node?(o) || node_reference?(o)
96
+ id = o['@id']
97
+ id = namer.get_name(id) if blank_node?(id)
98
+
99
+ # add reference and recurse
100
+ add_value(subject, property, {'@id' => id}, property_is_array: true, allow_duplicate: false)
101
+ create_node_map(o, graphs, graph, id)
102
+ when list?(o)
103
+ olist = []
104
+ create_node_map(o['@list'], graphs, graph, name, olist)
105
+ o = {'@list' => olist}
106
+ add_value(subject, property, o, property_is_array: true, allow_duplicate: true)
107
+ else
108
+ # handle @value
109
+ create_node_map(o, graphs, graph, name)
110
+ add_value(subject, property, o, property_is_array: true, allow_duplicate: false)
111
+ end
138
112
  end
139
113
  end
140
114
  end
141
-
142
- # If element has an @graph member, recursively invoke this algorithm passing the value of the @graph member for element, node map, and id for active graph before removing the @graph member from element.
143
- if element.has_key?('@graph')
144
- generate_node_map(element.delete('@graph'),
145
- node_map,
146
- id)
147
- end
148
-
149
- # Finally, for each key-value pair property-value in element ordered by property perform the following steps:
150
- # Note: Not ordering doesn't seem to affect results and is more performant
151
- element.keys.each do |property|
152
- value = element[property]
153
-
154
- # If property is a blank node identifier, replace it with a newly generated blank node identifier passing property for identifier.
155
- property = namer.get_name(property) if blank_node?(property)
156
-
157
- # If node does not have a property member, create one and initialize its value to an empty array.
158
- node[property] ||= []
159
-
160
- # Recursively invoke this algorithm passing value as new element, id as new active subject, and property as new active property.
161
- generate_node_map(value,
162
- node_map,
163
- active_graph,
164
- id,
165
- property)
166
- end
167
115
  end
116
+ else
117
+ # add non-object to list
118
+ list << input if list
168
119
  end
169
-
170
- debug("node_map") {node_map.to_json(JSON_STATE) rescue 'malformed json'}
171
120
  end
172
121
  end
173
122
  end
@@ -39,7 +39,9 @@ module JSON::LD
39
39
  # @param [String] sample Beginning several bytes (~ 1K) of input.
40
40
  # @return [Boolean]
41
41
  def self.detect(sample)
42
- !!sample.match(/\{\s*"@(id|context|type)"/m)
42
+ !!sample.match(/\{\s*"@(id|context|type)"/m) &&
43
+ # Exclude CSVW metadata
44
+ !sample.include?("http://www.w3.org/ns/csvw")
43
45
  end
44
46
 
45
47
  ##
@@ -7,158 +7,140 @@ module JSON::LD
7
7
  #
8
8
  # @param [Hash{Symbol => Object}] state
9
9
  # Current framing state
10
- # @param [Hash{String => Hash}] nodes
11
- # Map of flattened nodes
10
+ # @param [Array<String>] subjects
11
+ # The subjects to filter
12
12
  # @param [Hash{String => Object}] frame
13
13
  # @param [Hash{Symbol => Object}] options ({})
14
- # @option options [Hash{String => Object}] :parent
15
- # Parent node or top-level array
16
- # @option options [String] :property
17
- # Property referencing this frame, or null for array.
14
+ # @option options [Hash{String => Object}] :parent (nil)
15
+ # Parent subject or top-level array
16
+ # @option options [String] :property (nil)
17
+ # The parent property.
18
18
  # @raise [JSON::LD::InvalidFrame]
19
- def frame(state, nodes, frame, options = {})
19
+ def frame(state, subjects, frame, options = {})
20
20
  depth do
21
21
  parent, property = options[:parent], options[:property]
22
- debug("frame") {"state: #{state.inspect}"}
23
- debug("frame") {"nodes: #{nodes.keys.inspect}"}
24
- debug("frame") {"frame: #{frame.to_json(JSON_STATE) rescue 'malformed json'}"}
25
- debug("frame") {"parent: #{parent.to_json(JSON_STATE) rescue 'malformed json'}"}
26
- debug("frame") {"property: #{property.inspect}"}
27
22
  # Validate the frame
28
23
  validate_frame(state, frame)
24
+ frame = frame.first if frame.is_a?(Array)
29
25
 
30
- # Create a set of matched nodes by filtering nodes by checking the map of flattened nodes against frame
26
+ # Get values for embedOn and explicitOn
27
+ flags = {
28
+ embed: get_frame_flag(frame, options, :embed),
29
+ explicit: get_frame_flag(frame, options, :explicit),
30
+ requireAll: get_frame_flag(frame, options, :requireAll),
31
+ }
32
+
33
+ # Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
31
34
  # This gives us a hash of objects indexed by @id
32
- matches = filter_nodes(state, nodes, frame)
33
- debug("frame") {"matches: #{matches.keys.inspect}"}
35
+ matches = filter_subjects(state, subjects, frame, flags)
34
36
 
35
- # Get values for embedOn and explicitOn
36
- embed = get_frame_flag(state, frame, 'embed');
37
- explicit = get_frame_flag(state, frame, 'explicit');
38
- debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
39
-
40
- # For each id and node from the set of matched nodes ordered by id
37
+ # For each id and node from the set of matched subjects ordered by id
41
38
  matches.keys.kw_sort.each do |id|
42
- element = matches[id]
43
- # If the active property is null, set the map of embeds in state to an empty map
44
- state = state.merge(embeds: {}) if property.nil?
39
+ subject = matches[id]
40
+
41
+ if flags[:embed] == '@link' && state[:link].has_key?(id)
42
+ # TODO: may want to also match an existing linked subject
43
+ # against the current frame ... so different frames could
44
+ # produce different subjects that are only shared in-memory
45
+ # when the frames are the same
46
+
47
+ # add existing linked subject
48
+ add_frame_output(parent, property, state[:link][id])
49
+ next
50
+ end
51
+
52
+ # Note: In order to treat each top-level match as a
53
+ # compartmentalized result, clear the unique embedded subjects map
54
+ # when the property is None, which only occurs at the top-level.
55
+ state = state.merge(uniqueEmbeds: {}) if property.nil?
45
56
 
46
57
  output = {'@id' => id}
47
-
48
- # prepare embed meta info
49
- embedded_node = {parent: parent, property: property}
50
-
51
- # If embedOn is true, and id is in map of embeds from state
52
- if embed && (existing = state[:embeds].fetch(id, nil))
53
- # only overwrite an existing embed if it has already been added to its parent -- otherwise its parent is somewhere up the tree from this embed and the embed would occur twice once the tree is added
54
- embed = false
55
-
56
- embed = if existing[:parent].is_a?(Array)
57
- # If existing has a parent which is an array containing a JSON object with @id equal to id, element has already been embedded and can be overwritten, so set embedOn to true
58
- existing[:parent].detect {|p| p['@id'] == id}
59
- else
60
- # Otherwise, existing has a parent which is a node definition. Set embedOn to true if any of the items in parent property is a node definition or node reference for id because the embed can be overwritten
61
- existing[:parent].fetch(existing[:property], []).any? do |v|
62
- v.is_a?(Hash) && v.fetch('@id', nil) == id
63
- end
64
- end
65
- debug("frame") {"embed now: #{embed.inspect}"}
58
+ state[:link][id] = output
66
59
 
67
- # If embedOn is true, existing is already embedded but can be overwritten
68
- remove_embed(state, id) if embed
60
+ # if embed is @never or if a circular reference would be created
61
+ # by an embed, the subject cannot be embedded, just add the
62
+ # reference; note that a circular reference won't occur when the
63
+ # embed flag is `@link` as the above check will short-circuit
64
+ # before reaching this point
65
+ if flags[:embed] == '@never' || creates_circular_reference(subject, state[:subjectStack])
66
+ add_frame_output(parent, property, output)
67
+ next
69
68
  end
70
69
 
71
- unless embed
72
- # not embedding, add output without any other properties
73
- add_frame_output(state, parent, property, output)
74
- else
75
- # Add embed to map of embeds for id
76
- state[:embeds][id] = embedded_node
77
- debug("frame") {"add embedded_node: #{embedded_node.inspect}"}
78
-
79
- # Process each property and value in the matched node as follows
80
- element.keys.kw_sort.each do |prop|
81
- value = element[prop]
82
- if prop[0,1] == '@'
83
- # If property is a keyword, add property and a copy of value to output and continue with the next property from node
84
- output[prop] = value.dup
85
- next
86
- end
70
+ # if only the last match should be embedded
71
+ if flags[:embed] == '@last'
72
+ # remove any existing embed
73
+ remove_embed(state, id) if state[:uniqueEmbeds].include?(id)
74
+ state[:uniqueEmbeds][id] = {
75
+ parent: parent,
76
+ property: property
77
+ }
78
+ end
87
79
 
88
- # If property is not in frame:
89
- unless frame.has_key?(prop)
90
- debug("frame") {"non-framed property #{prop}"}
91
- # If explicitOn is false, Embed values from node in output using node as element and property as active property
92
- embed_values(state, element, prop, output) unless explicit
93
-
94
- # Continue to next property
95
- next
96
- end
97
-
98
- # Process each item from value as follows
99
- value.each do |item|
100
- debug("frame") {"value property #{prop.inspect} == #{item.inspect}"}
101
-
102
- # FIXME: If item is a JSON object with the key @list
103
- if list?(item)
104
- # create a JSON object named list with the key @list and the value of an empty array
105
- list = {'@list' => []}
106
-
107
- # Append list to property in output
108
- add_frame_output(state, output, prop, list)
109
-
110
- # Process each listitem in the @list array as follows
111
- item['@list'].each do |listitem|
112
- if node_reference?(listitem)
113
- itemid = listitem['@id']
114
- debug("frame") {"list item of #{prop} recurse for #{itemid.inspect}"}
115
-
116
- # If listitem is a node reference process listitem recursively using this algorithm passing a new map of nodes that contains the @id of listitem as the key and the node reference as the value. Pass the first value from frame for property as frame, list as parent, and @list as active property.
117
- frame(state, {itemid => @node_map[itemid]}, frame[prop].first, parent: list, property: '@list')
118
- else
119
- # Otherwise, append a copy of listitem to @list in list.
120
- debug("frame") {"list item of #{prop} non-node ref #{listitem.inspect}"}
121
- add_frame_output(state, list, '@list', listitem)
122
- end
123
- end
124
- elsif node_reference?(item)
125
- # If item is a node reference process item recursively
126
- # Recurse into sub-objects
127
- itemid = item['@id']
128
- debug("frame") {"value property #{prop} recurse for #{itemid.inspect}"}
129
-
130
- # passing a new map as nodes that contains the @id of item as the key and the node reference as the value. Pass the first value from frame for property as frame, output as parent, and property as active property
131
- frame(state, {itemid => @node_map[itemid]}, frame[prop].first, parent: output, property: prop)
132
- else
133
- # Otherwise, append a copy of item to active property in output.
134
- debug("frame") {"value property #{prop} non-node ref #{item.inspect}"}
135
- add_frame_output(state, output, prop, item)
136
- end
137
- end
80
+ # push matching subject onto stack to enable circular embed checks
81
+ state[:subjectStack] << subject
82
+
83
+ # iterate over subject properties in order
84
+ subject.keys.kw_sort.each do |prop|
85
+ objects = subject[prop]
86
+
87
+ # copy keywords to output
88
+ if prop.start_with?('@')
89
+ output[prop] = objects.dup
90
+ next
138
91
  end
139
92
 
140
- # Process each property and value in frame in lexographical order, where property is not a keyword, as follows:
141
- frame.keys.kw_sort.each do |prop|
142
- next if prop[0,1] == '@' || output.has_key?(prop)
143
- property_frame = frame[prop]
144
- debug("frame") {"frame prop: #{prop.inspect}. property_frame: #{property_frame.inspect}"}
93
+ # explicit is on and property isn't in frame, skip processing
94
+ next if flags[:explicit] && !frame.has_key?(prop)
145
95
 
146
- # Set property frame to the first item in value or a newly created JSON object if value is empty.
147
- property_frame = property_frame.first || {}
96
+ # add objects
97
+ objects.each do |o|
98
+ case
99
+ when list?(o)
100
+ # add empty list
101
+ list = {'@list' => []}
102
+ add_frame_output(output, prop, list)
148
103
 
149
- # Skip to the next property in frame if property is in output or if property frame contains @omitDefault which is true or if it does not contain @omitDefault but the value of omit default flag true.
150
- next if output.has_key?(prop) || get_frame_flag(state, property_frame, 'omitDefault')
104
+ src = o['@list']
105
+ src.each do |oo|
106
+ if node_reference?(oo)
107
+ subframe = frame[prop].first['@list'] if frame[prop].is_a?(Array) && frame[prop].first.is_a?(Hash)
108
+ subframe ||= create_implicit_frame(flags)
109
+ frame(state, [oo['@id']], subframe, options.merge(parent: list, property: '@list'))
110
+ else
111
+ add_frame_output(list, '@list', oo.dup)
112
+ end
113
+ end
114
+ when node_reference?(o)
115
+ # recurse into subject reference
116
+ subframe = frame[prop] || create_implicit_frame(flags)
117
+ frame(state, [o['@id']], subframe, options.merge(parent: output, property: prop))
118
+ else
119
+ # include other values automatically
120
+ add_frame_output(output, prop, o.dup)
121
+ end
122
+ end
123
+ end
151
124
 
152
- # Set the value of property in output to a new JSON object with a property @preserve and a value that is a copy of the value of @default in frame if it exists, or the string @null otherwise
153
- default = property_frame.fetch('@default', '@null').dup
154
- default = [default] unless default.is_a?(Array)
155
- output[prop] = [{"@preserve" => default.compact}]
156
- debug("=>") {"add default #{output[prop].inspect}"}
125
+ # handle defaults in order
126
+ frame.keys.kw_sort.reject {|p| p.start_with?('@')}.each do |prop|
127
+ # if omit default is off, then include default values for
128
+ # properties that appear in the next frame but are not in
129
+ # the matching subject
130
+ n = frame[prop].first || {}
131
+ omit_default_on = get_frame_flag(n, options, :omitDefault)
132
+ if !omit_default_on && !output[prop]
133
+ preserve = n.fetch('@default', '@null').dup
134
+ preserve = [preserve] unless preserve.is_a?(Array)
135
+ output[prop] = [{'@preserve' => preserve}]
157
136
  end
158
-
159
- # Add output to parent
160
- add_frame_output(state, parent, property, output)
161
137
  end
138
+
139
+ # add output to parent
140
+ add_frame_output(parent, property, output)
141
+
142
+ # pop matching subject from circular ref-checking stack
143
+ state[:subjectStack].pop()
162
144
  end
163
145
  end
164
146
  end
@@ -170,10 +152,9 @@ module JSON::LD
170
152
  # @return [Array, Hash]
171
153
  def cleanup_preserve(input)
172
154
  depth do
173
- #debug("cleanup preserve") {input.inspect}
174
155
  result = case input
175
156
  when Array
176
- # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
157
+ # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
177
158
  input.map {|o| cleanup_preserve(o)}.compact
178
159
  when Hash
179
160
  output = Hash.new(input.size)
@@ -183,7 +164,7 @@ module JSON::LD
183
164
  output = cleanup_preserve(value)
184
165
  else
185
166
  v = cleanup_preserve(value)
186
-
167
+
187
168
  # Because we may have added a null value to an array, we need to clean that up, if we possible
188
169
  v = v.first if v.is_a?(Array) && v.length == 1 &&
189
170
  context.expand_iri(key) != "@graph" && context.container(key).nil?
@@ -197,23 +178,29 @@ module JSON::LD
197
178
  else
198
179
  input
199
180
  end
200
- #debug(" => ") {result.inspect}
201
181
  result
202
182
  end
203
183
  end
204
184
 
205
185
  private
206
-
186
+
207
187
  ##
208
- # Returns a map of all of the nodes that match a parsed frame.
209
- #
210
- # @param state the current framing state.
211
- # @param nodes the set of nodes to filter.
212
- # @param frame the parsed frame.
213
- #
214
- # @return all of the matched nodes.
215
- def filter_nodes(state, nodes, frame)
216
- nodes.dup.keep_if {|id, element| element && filter_node(state, element, frame)}
188
+ # Returns a map of all of the subjects that match a parsed frame.
189
+ #
190
+ # @param [Hash{Symbol => Object}] state
191
+ # Current framing state
192
+ # @param [Hash{String => Hash}] subjects
193
+ # The subjects to filter
194
+ # @param [Hash{String => Object}] frame
195
+ # @param [Hash{Symbol => String}] flags the frame flags.
196
+ #
197
+ # @return all of the matched subjects.
198
+ def filter_subjects(state, subjects, frame, flags)
199
+ subjects.inject({}) do |memo, id|
200
+ subject = state[:subjects][id]
201
+ memo[id] = subject if filter_subject(subject, frame, flags)
202
+ memo
203
+ end
217
204
  end
218
205
 
219
206
  ##
@@ -226,42 +213,95 @@ module JSON::LD
226
213
  #
227
214
  # Otherwise, does duck typing, where the node must have all of the properties
228
215
  # defined in the frame.
229
- #
230
- # @param [Hash{Symbol => Object}] state the current frame state.
231
- # @param [Hash{String => Object}] node the node to check.
216
+ #
217
+ # @param [Hash{String => Object}] subject the subject to check.
232
218
  # @param [Hash{String => Object}] frame the frame to check.
233
- #
234
- # @return true if the node matches, false if not.
235
- def filter_node(state, node, frame)
236
- debug("frame") {"filter node: #{node.inspect}"}
237
- if types = frame.fetch('@type', nil)
238
- node_types = node.fetch('@type', [])
239
- raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
240
- raise InvalidFrame::Syntax, "node @type must be an array: #{node_types.inspect}" unless node_types.is_a?(Array)
241
- # If frame has an @type, use it for selecting appropriate nodes.
242
- debug("frame") {"filter node: #{node_types.inspect} has any of #{types.inspect}"}
219
+ # @param [Hash{Symbol => Object}] flags the frame flags.
220
+ #
221
+ # @return [Boolean] true if the node matches, false if not.
222
+ def filter_subject(subject, frame, flags)
223
+ types = frame.fetch('@type', [])
224
+ raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
225
+ subject_types = subject.fetch('@type', [])
226
+ raise InvalidFrame::Syntax, "node @type must be an array: #{node_types.inspect}" unless subject_types.is_a?(Array)
243
227
 
244
- # Check for type wild-card, or intersection
245
- types == [{}] ? !node_types.empty? : node_types.any? {|t| types.include?(t)}
228
+ # check @type (object value means 'any' type, fall through to ducktyping)
229
+ if !types.empty? &&
230
+ !(types.length == 1 && types.first.is_a?(Hash))
231
+ # If frame has an @type, use it for selecting appropriate nodes.
232
+ return types.any? {|t| subject_types.include?(t)}
246
233
  else
247
234
  # Duck typing, for nodes not having a type, but having @id
248
-
249
- # Subject matches if it has all the properties in the frame
250
- frame_keys = frame.keys.reject {|k| k[0,1] == '@'}
251
- node_keys = node.keys.reject {|k| k[0,1] == '@'}
252
- (frame_keys & node_keys) == frame_keys
235
+ wildcard, matches_some = true, false
236
+
237
+ frame.each do |k, v|
238
+ case k
239
+ when '@id'
240
+ return false if v.is_a?(String) && subject['@id'] != v
241
+ wildcard, matches_some = true, true
242
+ when '@type'
243
+ wildcard, matches_some = true, true
244
+ when /^@/
245
+ else
246
+ wildcard = false
247
+
248
+ # v == [] means do not match if property is present
249
+ if subject.has_key?(k)
250
+ return false if v == []
251
+ matches_some = true
252
+ next
253
+ end
254
+
255
+ # all properties must match to be a duck unless a @default is
256
+ # specified
257
+ has_default = v.is_a?(Array) && v.length == 1 && v.first.is_a?(Hash) && v.first.has_key?('@default')
258
+ return false if flags[:requireAll] && !has_default
259
+ end
260
+ end
261
+
262
+ # return true if wildcard or subject matches some properties
263
+ wildcard || matches_some
253
264
  end
254
265
  end
255
266
 
256
267
  def validate_frame(state, frame)
257
268
  raise InvalidFrame::Syntax,
258
- "Invalid JSON-LD syntax; a JSON-LD frame must be an object: #{frame.inspect}" unless frame.is_a?(Hash)
269
+ "Invalid JSON-LD syntax; a JSON-LD frame must be an object: #{frame.inspect}" unless
270
+ frame.is_a?(Hash) || (frame.is_a?(Array) && frame.first.is_a?(Hash) && frame.length == 1)
259
271
  end
260
-
261
- # Return value of @name in frame, or default from state if it doesn't exist
262
- def get_frame_flag(state, frame, name)
263
- value = frame.fetch("@#{name}", [state[name.to_sym]]).first
264
- !!(value?(value) ? value['@value'] : value)
272
+
273
+ # Checks the current subject stack to see if embedding the given subject
274
+ # would cause a circular reference.
275
+ #
276
+ # @param subject_to_embed the subject to embed.
277
+ # @param subject_stack the current stack of subjects.
278
+ #
279
+ # @return true if a circular reference would be created, false if not.
280
+ def creates_circular_reference(subject_to_embed, subject_stack)
281
+ subject_stack[0..-2].any? do |subject|
282
+ subject['@id'] == subject_to_embed['@id']
283
+ end
284
+ end
285
+
286
+ # Gets the frame flag value for the given flag name.
287
+ #
288
+ # @param frame the frame.
289
+ # @param options the framing options.
290
+ # @param name the flag name.
291
+ #
292
+ # @return the flag value.
293
+ def get_frame_flag(frame, options, name)
294
+ rval = frame.fetch("@#{name}", [options[name]]).first
295
+ rval = rval.values.first if value?(rval)
296
+ if name == :embed
297
+ rval = case rval
298
+ when true then '@last'
299
+ when false then '@never'
300
+ when '@always', '@never', '@link' then rval
301
+ else '@last'
302
+ end
303
+ end
304
+ rval
265
305
  end
266
306
 
267
307
  ##
@@ -270,29 +310,32 @@ module JSON::LD
270
310
  # @param state the current framing state.
271
311
  # @param id the @id of the embed to remove.
272
312
  def remove_embed(state, id)
273
- debug("frame") {"remove embed #{id.inspect}"}
274
313
  # get existing embed
275
- embeds = state[:embeds];
314
+ embeds = state[:uniqueEmbeds];
276
315
  embed = embeds[id];
277
- parent = embed[:parent];
278
316
  property = embed[:property];
279
317
 
280
318
  # create reference to replace embed
281
- node = {}
282
- node['@id'] = id
283
- ref = {'@id' => id}
284
-
285
- # remove existing embed
286
- if node?(parent)
319
+ subject = {'@id' => id}
320
+
321
+ if embed[:parent].is_a?(Array)
322
+ # replace subject with reference
323
+ embed[:parent].map! do |parent|
324
+ compare_values(parent, subject) ? subject : parent
325
+ end
326
+ else
327
+ parent = embed[:parent]
287
328
  # replace node with reference
288
- parent[property].map! do |v|
289
- v.is_a?(Hash) && v.fetch('@id', nil) == id ? ref : v
329
+ if parent[property].is_a?(Array)
330
+ parent[property].reject! {|v| compare_values(v, subject)}
331
+ parent[property] << subject
332
+ elsif compare_values(parent[property], subject)
333
+ parent[property] = subject
290
334
  end
291
335
  end
292
336
 
293
337
  # recursively remove dependent dangling embeds
294
338
  def remove_dependents(id, embeds)
295
- debug("frame") {"remove dependents for #{id}"}
296
339
 
297
340
  depth do
298
341
  # get embed keys as a separate array to enable deleting keys in map
@@ -301,70 +344,40 @@ module JSON::LD
301
344
  next unless p.is_a?(Hash)
302
345
  pid = p.fetch('@id', nil)
303
346
  if pid == id
304
- debug("frame") {"remove #{id_dep} from embeds"}
305
347
  embeds.delete(id_dep)
306
348
  remove_dependents(id_dep, embeds)
307
349
  end
308
350
  end
309
351
  end
310
352
  end
311
-
353
+
312
354
  remove_dependents(id, embeds)
313
355
  end
314
356
 
315
357
  ##
316
358
  # Adds framing output to the given parent.
317
359
  #
318
- # @param state the current framing state.
319
360
  # @param parent the parent to add to.
320
361
  # @param property the parent property, null for an array parent.
321
362
  # @param output the output to add.
322
- def add_frame_output(state, parent, property, output)
363
+ def add_frame_output(parent, property, output)
323
364
  if parent.is_a?(Hash)
324
- debug("frame") { "add for property #{property.inspect}: #{output.inspect}"}
325
365
  parent[property] ||= []
326
366
  parent[property] << output
327
367
  else
328
- debug("frame") { "add top-level: #{output.inspect}"}
329
368
  parent << output
330
369
  end
331
370
  end
332
-
333
- ##
334
- # Embeds values for the given element and property into output.
335
- def embed_values(state, element, property, output)
336
- element[property].each do |o|
337
- # Get element @id, if this is an object
338
- sid = o['@id'] if node_reference?(o)
339
- if sid
340
- unless state[:embeds].has_key?(sid)
341
- debug("frame") {"embed element #{sid.inspect}"}
342
- # Embed full element, if it isn't already embedded
343
- embed = {parent: output, property: property}
344
- state[:embeds][sid] = embed
345
-
346
- # Recurse into element
347
- s = @node_map.fetch(sid, {'@id' => sid})
348
- o = {}
349
- s.each do |prop, value|
350
- if prop[0,1] == '@'
351
- # Copy keywords
352
- o[prop] = s[prop].dup
353
- else
354
- depth do
355
- debug("frame") {"embed property #{prop.inspect} value #{value.inspect}"}
356
- embed_values(state, s, prop, o)
357
- end
358
- end
359
- end
360
- else
361
- debug("frame") {"don't embed element #{sid.inspect}"}
362
- end
363
- else
364
- debug("frame") {"embed property #{property.inspect}, value #{o.inspect}"}
365
- end
366
- add_frame_output(state, output, property, o.dup)
367
- end
371
+
372
+ # Creates an implicit frame when recursing through subject matches. If
373
+ # a frame doesn't have an explicit frame for a particular property, then
374
+ # a wildcard child frame will be created that uses the same flags that
375
+ # the parent frame used.
376
+ #
377
+ # @param [Hash] flags the current framing flags.
378
+ # @return [Array<Hash>] the implicit frame.
379
+ def create_implicit_frame(flags)
380
+ [flags.keys.inject({}) {|memo, key| memo["@#{key}"] = [flags[key]]; memo}]
368
381
  end
369
382
  end
370
383
  end