json-ld 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,185 @@
1
+ module JSON::LD
2
+ ##
3
+ # Expand module, used as part of API
4
+ module Expand
5
+ include Utils
6
+
7
+ ##
8
+ # Expand an Array or Object given an active context and performing local context expansion.
9
+ #
10
+ # @param [Array, Hash] input
11
+ # @param [String] active_property
12
+ # @param [EvaluationContext] context
13
+ # @param [Hash{Symbol => Object}] options
14
+ # @return [Array, Hash]
15
+ def expand(input, active_property, context, options = {})
16
+ debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
17
+ result = case input
18
+ when Array
19
+ # If element is an array, process each item in element recursively using this algorithm,
20
+ # passing copies of the active context and active property. If the expanded entry is null, drop it.
21
+ depth do
22
+ is_list = context.container(active_property) == '@list'
23
+ value = input.map do |v|
24
+ # If active property has a @container set to @list, and item is an array,
25
+ # or the result of expanding any item is an object containing an @list property,
26
+ # throw an exception as lists of lists are not allowed.
27
+ raise ProcessingError::ListOfLists, "A list may not contain another list" if v.is_a?(Array) && is_list
28
+
29
+ expand(v, active_property, context, options)
30
+ end.flatten.compact
31
+
32
+ if is_list && value.any? {|v| v.is_a?(Hash) && v.has_key?('@list')}
33
+ raise ProcessingError::ListOfLists, "A list may not contain another list"
34
+ end
35
+
36
+ value
37
+ end
38
+ when Hash
39
+ # Otherwise, if element is an object
40
+ # If element has a @context property, update the active context according to the steps outlined
41
+ # in Context Processing and remove the @context property.
42
+ if input.has_key?('@context')
43
+ context = context.parse(input.delete('@context'))
44
+ debug("expand") {"evaluation context: #{context.inspect}"}
45
+ end
46
+
47
+ depth do
48
+ output_object = Hash.ordered
49
+ # Then, proceed and process each property and value in element as follows:
50
+ input.each do |key, value|
51
+ # Remove property from element expand property according to the steps outlined in IRI Expansion
52
+ property = context.expand_iri(key, :position => :predicate, :quiet => true)
53
+
54
+ # Set active property to the original un-expanded property if property if not a keyword
55
+ active_property = key unless key[0,1] == '@'
56
+ debug("expand property") {"#{active_property}, expanded: #{property}, value: #{value.inspect}"}
57
+
58
+ # If property does not expand to a keyword or absolute IRI, remove property from element
59
+ # and continue to the next property from element
60
+ if property.nil?
61
+ debug(" => ") {"skip nil key"}
62
+ next
63
+ end
64
+ property = property.to_s
65
+
66
+ expanded_value = case property
67
+ when '@id'
68
+ # If the property is @id the value must be a string. Expand the value according to IRI Expansion.
69
+ context.expand_iri(value, :position => :subject, :quiet => true).to_s
70
+ when '@type'
71
+ # Otherwise, if the property is @type the value must be a string, an array of strings
72
+ # or an empty JSON Object.
73
+ # Expand value or each of it's entries according to IRI Expansion
74
+ case value
75
+ when Array
76
+ depth do
77
+ [value].flatten.map do |v|
78
+ context.expand_iri(v, options.merge(:position => :property, :quiet => true)).to_s
79
+ end
80
+ end
81
+ when Hash
82
+ # Empty object used for @type wildcard
83
+ raise ProcessingError, "Object value of @type must be empty: #{value.inspect}" unless value.empty?
84
+ value
85
+ else
86
+ context.expand_iri(value, options.merge(:position => :property, :quiet => true)).to_s
87
+ end
88
+ when '@value', '@language'
89
+ # Otherwise, if the property is @value or @language the value must not be a JSON object or an array.
90
+ raise ProcessingError::Lossy, "Value of #{property} must be a string, was #{value.inspect}" if value.is_a?(Hash) || value.is_a?(Array)
91
+ value
92
+ when '@list', '@set', '@graph'
93
+ # Otherwise, if the property is @list, @set, or @graph, expand value recursively
94
+ # using this algorithm, passing copies of the active context and active property.
95
+ # If the expanded value is not an array, convert it to an array.
96
+ value = [value] unless value.is_a?(Array)
97
+ value = depth { expand(value, active_property, context, options) }
98
+
99
+ # If property is @list, and any expanded value
100
+ # is an object containing an @list property, throw an exception, as lists of lists are not supported
101
+ if property == '@list' && value.any? {|v| v.is_a?(Hash) && v.has_key?('@list')}
102
+ raise ProcessingError::ListOfLists, "A list may not contain another list"
103
+ end
104
+
105
+ value
106
+ else
107
+ # Otherwise, expand value recursively using this algorithm, passing copies of the active context and active property.
108
+ depth { expand(value, active_property, context, options) }
109
+ end
110
+
111
+ # moved from step 2.2.3
112
+ # If expanded value is null and property is not @value, continue with the next property
113
+ # from element.
114
+ if property != '@value' && expanded_value.nil?
115
+ debug(" => skip nil value")
116
+ next
117
+ end
118
+
119
+ # If the expanded value is not null and property is not a keyword
120
+ # and the active property has a @container set to @list,
121
+ # convert value to an object with an @list property whose value is set to value
122
+ # (unless value is already in that form)
123
+ if expanded_value && property[0,1] != '@' && context.container(active_property) == '@list' &&
124
+ (!expanded_value.is_a?(Hash) || !expanded_value.fetch('@list', false))
125
+ debug(" => ") { "convert #{expanded_value.inspect} to list"}
126
+ expanded_value = {'@list' => [expanded_value].flatten}
127
+ end
128
+
129
+ # Convert value to array form unless value is null or property is @id, @type, @value, or @language.
130
+ if !%(@id @language @type @value).include?(property) && !expanded_value.is_a?(Array)
131
+ debug(" => make #{expanded_value.inspect} an array")
132
+ expanded_value = [expanded_value]
133
+ end
134
+
135
+ if output_object.has_key?(property)
136
+ # If element already contains a property property, append value to the existing value.
137
+ output_object[property] += expanded_value
138
+ else
139
+ # Otherwise, create a property property with value as value.
140
+ output_object[property] = expanded_value
141
+ end
142
+ debug {" => #{expanded_value.inspect}"}
143
+ end
144
+
145
+ debug("output object") {output_object.inspect}
146
+
147
+ # If the processed element has an @value property
148
+ if output_object.has_key?('@value')
149
+ output_object.delete('@language') if output_object['@language'].to_s.empty?
150
+ output_object.delete('@type') if output_object['@type'].to_s.empty?
151
+ if output_object.keys.length > 2 || (%w(@language @type) - output_object.keys).empty?
152
+ raise ProcessingError, "element must not have more than one other property, which can either be @language or @type with a string value." unless value.is_a?(String)
153
+ end
154
+
155
+ # if @value is the only property or the value of @value equals null, replace element with the value of @value.
156
+ if output_object['@value'].nil? || output_object.keys.length == 1
157
+ return output_object['@value']
158
+ end
159
+ elsif !output_object.fetch('@type', []).is_a?(Array)
160
+ # Otherwise, if element has an @type property and it's value is not in the form of an array,
161
+ # convert it to an array.
162
+ output_object['@type'] = [output_object['@type']]
163
+ end
164
+
165
+ # If element has an @set or @list property, it must be the only property. Set element to the value of @set;
166
+ # leave @list untouched.
167
+ if !(%w(@set @list) & output_object.keys).empty?
168
+ raise ProcessingError, "element must have only @set, @list or @graph" if output_object.keys.length > 1
169
+
170
+ output_object = output_object.values.first unless output_object.has_key?('@list')
171
+ end
172
+
173
+ # If element has just a @language property, set element to null.
174
+ output_object unless output_object.is_a?(Hash) && output_object.keys == %w(@language)
175
+ end
176
+ else
177
+ # Otherwise, unless the value is a number, expand the value according to the Value Expansion rules, passing active property.
178
+ context.expand_value(active_property, input, :position => :object, :depth => @depth) unless input.nil?
179
+ end
180
+
181
+ debug {" => #{result.inspect}"}
182
+ result
183
+ end
184
+ end
185
+ end
@@ -37,6 +37,29 @@ module RDF
37
37
  @uri.to_s
38
38
  end
39
39
  end
40
+
41
+ class Literal
42
+ class Double
43
+ ##
44
+ # Converts this literal into its canonical lexical representation.
45
+ # Update to use %.15E to avoid precision problems
46
+ def canonicalize!
47
+ @string = case
48
+ when @object.nan? then 'NaN'
49
+ when @object.infinite? then @object.to_s[0...-'inity'.length].upcase
50
+ when @object.zero? then '0.0E0'
51
+ else
52
+ i, f, e = ('%.15E' % @object.to_f).split(/[\.E]/)
53
+ f.sub!(/0*$/, '') # remove any trailing zeroes
54
+ f = '0' if f.empty? # ...but there must be a digit to the right of the decimal point
55
+ e.sub!(/^\+?0+(\d)$/, '\1') # remove the optional leading '+' sign and any extra leading zeroes
56
+ "#{i}.#{f}E#{e}"
57
+ end
58
+ @object = Float(@string) unless @object.nil?
59
+ self
60
+ end
61
+ end
62
+ end
40
63
  end
41
64
 
42
65
  if RUBY_VERSION < "1.9"
@@ -54,7 +77,7 @@ if RUBY_VERSION < "1.9"
54
77
  end
55
78
 
56
79
  def each
57
- @ordered_keys.each {|k| yield(k, super[k])}
80
+ @ordered_keys.each {|k| yield(k, self[k])}
58
81
  end
59
82
  alias :each_pair :each
60
83
 
@@ -70,10 +93,6 @@ if RUBY_VERSION < "1.9"
70
93
  @ordered_keys
71
94
  end
72
95
 
73
- def values
74
- @ordered_keys.map {|k| super[k]}
75
- end
76
-
77
96
  def clear
78
97
  @ordered_keys.clear
79
98
  super
@@ -97,7 +116,9 @@ if RUBY_VERSION < "1.9"
97
116
  end
98
117
 
99
118
  def merge!(other)
100
- @ordered_keys += other.instance_variable_get(:@ordered_keys) || other.keys
119
+ new_keys = other.instance_variable_get(:@ordered_keys) || other.keys
120
+ new_keys -= @ordered_keys
121
+ @ordered_keys += new_keys
101
122
  super
102
123
  self
103
124
  end
@@ -108,8 +129,14 @@ if RUBY_VERSION < "1.9"
108
129
  end
109
130
 
110
131
  class Hash
111
- def new(obj = nil, &block)
132
+ def self.ordered(obj = nil, &block)
112
133
  InsertOrderPreservingHash.new(obj, &block)
113
134
  end
114
135
  end
136
+ else
137
+ class Hash
138
+ def self.ordered(obj = nil, &block)
139
+ Hash.new(obj, &block)
140
+ end
141
+ end
115
142
  end
@@ -22,7 +22,7 @@ module JSON::LD
22
22
  # @see http://www.w3.org/TR/rdf-testcases/#ntriples
23
23
  class Format < RDF::Format
24
24
  content_type 'application/ld+json',
25
- :extensions => [:jsonld, :json, :ld],
25
+ :extension => :jsonld,
26
26
  :alias => 'application/x-ld+json'
27
27
  content_encoding 'utf-8'
28
28
 
@@ -39,7 +39,7 @@ 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*"@(subject|context|type|iri)"/m)
42
+ !!sample.match(/\{\s*"@(id|context|type)"/m)
43
43
  end
44
44
 
45
45
  ##
@@ -48,19 +48,4 @@ module JSON::LD
48
48
  :jsonld
49
49
  end
50
50
  end
51
-
52
- # Alias for JSON-LD format
53
- #
54
- # This allows the following:
55
- #
56
- # @example Obtaining an Notation3 format class
57
- # RDF::Format.for(:jsonld) #=> JSON::LD::JSONLD
58
- # RDF::Format.for(:jsonld).reader #=> JSON::LD::Reader
59
- # RDF::Format.for(:jsonld).writer #=> JSON::LD::Writer
60
- class JSONLD < RDF::Format
61
- content_encoding 'utf-8'
62
-
63
- reader { JSON::LD::Reader }
64
- writer { JSON::LD::Writer }
65
- end
66
51
  end
@@ -0,0 +1,452 @@
1
+ module JSON::LD
2
+ module Frame
3
+ include Utils
4
+
5
+ ##
6
+ # Frame input. Input is expected in expanded form, but frame is in compacted form.
7
+ #
8
+ # @param [Hash{Symbol => Object}] state
9
+ # Current framing state
10
+ # @param [Hash{String => Hash}] subjects
11
+ # Map of flattened subjects
12
+ # @param [Hash{String => Object}] frame
13
+ # @param [Hash{String => Object}] parent
14
+ # Parent subject or top-level array
15
+ # @param [String] property
16
+ # Property referencing this frame, or null for array.
17
+ # @raise [JSON::LD::InvalidFrame]
18
+ def frame(state, subjects, frame, parent, property)
19
+ raise ProcessingError, "why isn't @subjects a hash?: #{@subjects.inspect}" unless @subjects.is_a?(Hash)
20
+ depth do
21
+ debug("frame") {"state: #{state.inspect}"}
22
+ debug("frame") {"subjects: #{subjects.keys.inspect}"}
23
+ debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
24
+ debug("frame") {"parent: #{parent.to_json(JSON_STATE)}"}
25
+ debug("frame") {"property: #{property.inspect}"}
26
+ # Validate the frame
27
+ validate_frame(state, frame)
28
+
29
+ # Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
30
+ # This gives us a hash of objects indexed by @id
31
+ matches = filter_subjects(state, subjects, frame)
32
+ debug("frame") {"matches: #{matches.keys.inspect}"}
33
+
34
+ # Get values for embedOn and explicitOn
35
+ embed = get_frame_flag(state, frame, 'embed');
36
+ explicit = get_frame_flag(state, frame, 'explicit');
37
+ debug("frame") {"embed: #{embed.inspect}, explicit: #{explicit.inspect}"}
38
+
39
+ # For each id and subject from the set of matched subjects ordered by id
40
+ matches.keys.sort.each do |id|
41
+ element = matches[id]
42
+ # If the active property is null, set the map of embeds in state to an empty map
43
+ state = state.merge(:embeds => {}) if property.nil?
44
+
45
+ output = {'@id' => id}
46
+
47
+ # prepare embed meta info
48
+ embedded_subject = {:parent => parent, :property => property}
49
+
50
+ # If embedOn is true, and id is in map of embeds from state
51
+ if embed && (existing = state[:embeds].fetch(id, nil))
52
+ # only overwrite an existing embed if it has already been added to its
53
+ # parent -- otherwise its parent is somewhere up the tree from this
54
+ # embed and the embed would occur twice once the tree is added
55
+ embed = false
56
+
57
+ embed = if existing[:parent].is_a?(Array)
58
+ # 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
59
+ existing[:parent].detect {|p| p['@id'] == id}
60
+ else
61
+ # Otherwise, existing has a parent which is a subject definition. Set embedOn to true if any of the items in parent property is a subject definition or subject reference for id because the embed can be overwritten
62
+ existing[:parent].fetch(existing[:property], []).any? do |v|
63
+ v.is_a?(Hash) && v.fetch('@id', nil) == id
64
+ end
65
+ end
66
+ debug("frame") {"embed now: #{embed.inspect}"}
67
+
68
+ # If embedOn is true, existing is already embedded but can be overwritten
69
+ remove_embed(state, id) if embed
70
+ end
71
+
72
+ unless embed
73
+ # not embedding, add output without any other properties
74
+ add_frame_output(state, parent, property, output)
75
+ else
76
+ # Add embed to map of embeds for id
77
+ state[:embeds][id] = embedded_subject
78
+ debug("frame") {"add embedded_subject: #{embedded_subject.inspect}"}
79
+
80
+ # Process each property and value in the matched subject as follows
81
+ element.keys.sort.each do |prop|
82
+ value = element[prop]
83
+ if prop[0,1] == '@'
84
+ # If property is a keyword, add property and a copy of value to output and continue with the next property from subject
85
+ output[prop] = value.dup
86
+ next
87
+ end
88
+
89
+ # If property is not in frame:
90
+ unless frame.has_key?(prop)
91
+ debug("frame") {"non-framed property #{prop}"}
92
+ # If explicitOn is false, Embed values from subject in output using subject as element and property as active property
93
+ embed_values(state, element, prop, output) unless explicit
94
+
95
+ # Continue to next property
96
+ next
97
+ end
98
+
99
+ # Process each item from value as follows
100
+ value.each do |item|
101
+ debug("frame") {"value property #{prop.inspect} == #{item.inspect}"}
102
+
103
+ # FIXME: If item is a JSON object with the key @list
104
+ if list?(item)
105
+ # create a JSON object named list with the key @list and the value of an empty array
106
+ list = {'@list' => []}
107
+
108
+ # Append list to property in output
109
+ add_frame_output(state, output, prop, list)
110
+
111
+ # Process each listitem in the @list array as follows
112
+ item['@list'].each do |listitem|
113
+ if subject_reference?(listitem)
114
+ itemid = listitem['@id']
115
+ debug("frame") {"list item of #{prop} recurse for #{itemid.inspect}"}
116
+
117
+ # If listitem is a subject reference process listitem recursively using this algorithm passing a new map of subjects that contains the @id of listitem as the key and the subject reference as the value. Pass the first value from frame for property as frame, list as parent, and @list as active property.
118
+ frame(state, {itemid => @subjects[itemid]}, frame[prop].first, list, '@list')
119
+ else
120
+ # Otherwise, append a copy of listitem to @list in list.
121
+ debug("frame") {"list item of #{prop} non-subject ref #{listitem.inspect}"}
122
+ add_frame_output(state, list, '@list', listitem)
123
+ end
124
+ end
125
+ elsif subject_reference?(item)
126
+ # If item is a subject reference process item recursively
127
+ # Recurse into sub-objects
128
+ itemid = item['@id']
129
+ debug("frame") {"value property #{prop} recurse for #{itemid.inspect}"}
130
+
131
+ # passing a new map as subjects that contains the @id of item as the key and the subject reference as the value. Pass the first value from frame for property as frame, output as parent, and property as active property
132
+ frame(state, {itemid => @subjects[itemid]}, frame[prop].first, output, prop)
133
+ else
134
+ # Otherwise, append a copy of item to active property in output.
135
+ debug("frame") {"value property #{prop} non-subject ref #{item.inspect}"}
136
+ add_frame_output(state, output, prop, item)
137
+ end
138
+ end
139
+ end
140
+
141
+ # Process each property and value in frame in lexographical order, where property is not a keyword, as follows:
142
+ frame.keys.sort.each do |prop|
143
+ next if prop[0,1] == '@' || output.has_key?(prop)
144
+ property_frame = frame[prop]
145
+ debug("frame") {"frame prop: #{prop.inspect}. property_frame: #{property_frame.inspect}"}
146
+
147
+ # Set property frame to the first item in value or a newly created JSON object if value is empty.
148
+ property_frame = property_frame.first || {}
149
+
150
+ # 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.
151
+ next if output.has_key?(prop) || get_frame_flag(state, property_frame, 'omitDefault')
152
+
153
+ # 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
154
+ default = property_frame.fetch('@default', '@null').dup
155
+ default = [default] unless default.is_a?(Array)
156
+ output[prop] = [{"@preserve" => default.compact}]
157
+ debug("=>") {"add default #{output[prop].inspect}"}
158
+ end
159
+
160
+ # Add output to parent
161
+ add_frame_output(state, parent, property, output)
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ ##
168
+ # Build hash of subjects used for framing. Also returns flattened representation
169
+ # of input.
170
+ #
171
+ # @param [Hash{String => Hash}] subjects
172
+ # destination for mapped subjects and their Object representations
173
+ # @param [Array, Hash] input
174
+ # JSON-LD in expanded form
175
+ # @param [BlankNodeNamer] namer
176
+ # @return
177
+ # input with subject definitions changed to references
178
+ def get_framing_subjects(subjects, input, namer)
179
+ depth do
180
+ debug("framing subjects") {"input: #{input.inspect}"}
181
+ case input
182
+ when Array
183
+ input.map {|o| get_framing_subjects(subjects, o, namer)}
184
+ when Hash
185
+ case
186
+ when subject?(input) || subject_reference?(input)
187
+ # Get name for subject, mapping old blank node identifiers to new
188
+ name = blank_node?(input) ? namer.get_name(input.fetch('@id', nil)) : input['@id']
189
+ debug("framing subjects") {"new subject: #{name.inspect}"} unless subjects.has_key?(name)
190
+ subject = subjects[name] ||= {'@id' => name}
191
+
192
+ # In property order
193
+ input.keys.sort.each do |prop|
194
+ value = input[prop]
195
+ case prop
196
+ when '@id'
197
+ # Skip @id, already assigned
198
+ when /^@/
199
+ # Copy other keywords
200
+ subject[prop] = value
201
+ else
202
+ case value
203
+ when Hash
204
+ # Special case @list, which is not in expanded form
205
+ raise InvalidFrame::Syntax, "Unexpected hash value: #{value.inspect}" unless value.has_key?('@list')
206
+
207
+ # Map entries replacing subjects with subject references
208
+ subject[prop] = {"@list" =>
209
+ value['@list'].map {|o| get_framing_subjects(subjects, o, namer)}
210
+ }
211
+ when Array
212
+ # Map array entries
213
+ subject[prop] = get_framing_subjects(subjects, value, namer)
214
+ else
215
+ raise InvalidFrame::Syntax, "unexpected value: #{value.inspect}"
216
+ end
217
+ end
218
+ end
219
+
220
+ # Return as subject reference
221
+ {"@id" => name}
222
+ else
223
+ # At this point, it's not a subject or a reference, just return input
224
+ input
225
+ end
226
+ else
227
+ # Returns equivalent representation
228
+ input
229
+ end
230
+ end
231
+ end
232
+
233
+ ##
234
+ # Flatten input, used in framing.
235
+ #
236
+ # This algorithm works by transforming input to statements, and then back to JSON-LD
237
+ #
238
+ # @return [Array{Hash}]
239
+ def flatten
240
+ debug("flatten")
241
+ expanded = depth {self.expand(self.value, nil, context)}
242
+ statements = []
243
+ depth {self.statements("", expanded, nil, nil, nil ) {|s| statements << s}}
244
+ debug("flatten") {"statements: #{statements.map(&:to_nquads).join("\n")}"}
245
+
246
+ # Transform back to JSON-LD, not flattened
247
+ depth {self.from_statements(statements, BlankNodeNamer.new("t"))}
248
+ end
249
+
250
+ ##
251
+ # Replace @preserve keys with the values, also replace @null with null
252
+ #
253
+ # @param [Array, Hash] input
254
+ # @return [Array, Hash]
255
+ def cleanup_preserve(input)
256
+ depth do
257
+ #debug("cleanup preserve") {input.inspect}
258
+ result = case input
259
+ when Array
260
+ # If, after replacement, an array contains only the value null remove the value, leaving an empty array.
261
+ input.map {|o| cleanup_preserve(o)}.compact
262
+ when Hash
263
+ output = Hash.ordered
264
+ input.each do |key, value|
265
+ if key == '@preserve'
266
+ # replace all key-value pairs where the key is @preserve with the value from the key-pair
267
+ output = cleanup_preserve(value)
268
+ else
269
+ v = cleanup_preserve(value)
270
+
271
+ # Because we may have added a null value to an array, we need to clean that up, if we possible
272
+ v = v.first if v.is_a?(Array) && v.length == 1 &&
273
+ context.expand_iri(key) != "@graph" && context.container(key).nil?
274
+ output[key] = v
275
+ end
276
+ end
277
+ output
278
+ when '@null'
279
+ # If the value from the key-pair is @null, replace the value with nul
280
+ nil
281
+ else
282
+ input
283
+ end
284
+ #debug(" => ") {result.inspect}
285
+ result
286
+ end
287
+ end
288
+
289
+ private
290
+
291
+ ##
292
+ # Returns a map of all of the subjects that match a parsed frame.
293
+ #
294
+ # @param state the current framing state.
295
+ # @param subjects the set of subjects to filter.
296
+ # @param frame the parsed frame.
297
+ #
298
+ # @return all of the matched subjects.
299
+ def filter_subjects(state, subjects, frame)
300
+ subjects.dup.keep_if {|id, element| filter_subject(state, element, frame)}
301
+ end
302
+
303
+ ##
304
+ # Returns true if the given subject matches the given frame.
305
+ #
306
+ # Matches either based on explicit type inclusion where the subject
307
+ # has any type listed in the frame. If the frame has empty types defined
308
+ # matches subjects not having a @type. If the frame has a type of {} defined
309
+ # matches subjects having any type defined.
310
+ #
311
+ # Otherwise, does duck typing, where the subject must have all of the properties
312
+ # defined in the frame.
313
+ #
314
+ # @param [Hash{Symbol => Object}] state the current frame state.
315
+ # @param [Hash{String => Object}] subject the subject to check.
316
+ # @param [Hash{String => Object}] frame the frame to check.
317
+ #
318
+ # @return true if the subject matches, false if not.
319
+ def filter_subject(state, subject, frame)
320
+ if types = frame.fetch('@type', nil)
321
+ subject_types = subject.fetch('@type', [])
322
+ raise InvalidFrame::Syntax, "frame @type must be an array: #{types.inspect}" unless types.is_a?(Array)
323
+ raise InvalidFrame::Syntax, "subject @type must be an array: #{subject_types.inspect}" unless subject_types.is_a?(Array)
324
+ # If frame has an @type, use it for selecting appropriate subjects.
325
+ debug("frame") {"filter subject: #{subject_types.inspect} has any of #{types.inspect}"}
326
+
327
+ # Check for type wild-card, or intersection
328
+ types == [{}] ? !subject_types.empty? : subject_types.any? {|t| types.include?(t)}
329
+ else
330
+ # Duck typing, for subjects not having a type, but having @id
331
+
332
+ # Subject matches if it has all the properties in the frame
333
+ frame_keys = frame.keys.reject {|k| k[0,1] == '@'}
334
+ subject_keys = subject.keys.reject {|k| k[0,1] == '@'}
335
+ (frame_keys & subject_keys) == frame_keys
336
+ end
337
+ end
338
+
339
+ def validate_frame(state, frame)
340
+ raise InvalidFrame::Syntax,
341
+ "Invalid JSON-LD syntax; a JSON-LD frame must be an object" unless frame.is_a?(Hash)
342
+ end
343
+
344
+ # Return value of @name in frame, or default from state if it doesn't exist
345
+ def get_frame_flag(state, frame, name)
346
+ !!(frame.fetch("@#{name}", [state[name.to_sym]]).first)
347
+ end
348
+
349
+ ##
350
+ # Removes an existing embed.
351
+ #
352
+ # @param state the current framing state.
353
+ # @param id the @id of the embed to remove.
354
+ def remove_embed(state, id)
355
+ debug("frame") {"remove embed #{id.inspect}"}
356
+ # get existing embed
357
+ embeds = state[:embeds];
358
+ embed = embeds[id];
359
+ parent = embed[:parent];
360
+ property = embed[:property];
361
+
362
+ # create reference to replace embed
363
+ subject = {}
364
+ subject['@id'] = id
365
+ ref = {'@id' => id}
366
+
367
+ # remove existing embed
368
+ if subject?(parent)
369
+ # replace subject with reference
370
+ parent[property].map! do |v|
371
+ v.is_a?(Hash) && v.fetch('@id', nil) == id ? ref : v
372
+ end
373
+ end
374
+
375
+ # recursively remove dependent dangling embeds
376
+ def remove_dependents(id, embeds)
377
+ debug("frame") {"remove dependents for #{id}"}
378
+
379
+ depth do
380
+ # get embed keys as a separate array to enable deleting keys in map
381
+ embeds.each do |id_dep, e|
382
+ p = e.fetch(:parent, {}) if e.is_a?(Hash)
383
+ next unless p.is_a?(Hash)
384
+ pid = p.fetch('@id', nil)
385
+ if pid == id
386
+ debug("frame") {"remove #{id_dep} from embeds"}
387
+ embeds.delete(id_dep)
388
+ remove_dependents(id_dep, embeds)
389
+ end
390
+ end
391
+ end
392
+ end
393
+
394
+ remove_dependents(id, embeds)
395
+ end
396
+
397
+ ##
398
+ # Adds framing output to the given parent.
399
+ #
400
+ # @param state the current framing state.
401
+ # @param parent the parent to add to.
402
+ # @param property the parent property, null for an array parent.
403
+ # @param output the output to add.
404
+ def add_frame_output(state, parent, property, output)
405
+ if parent.is_a?(Hash)
406
+ debug("frame") { "add for property #{property.inspect}: #{output.inspect}"}
407
+ parent[property] ||= []
408
+ parent[property] << output
409
+ else
410
+ debug("frame") { "add top-level: #{output.inspect}"}
411
+ parent << output
412
+ end
413
+ end
414
+
415
+ ##
416
+ # Embeds values for the given element and property into output.
417
+ def embed_values(state, element, property, output)
418
+ element[property].each do |o|
419
+ # Get element @id, if this is an object
420
+ sid = o['@id'] if subject_reference?(o)
421
+ if sid
422
+ unless state[:embeds].has_key?(sid)
423
+ debug("frame") {"embed element #{sid.inspect}"}
424
+ # Embed full element, if it isn't already embedded
425
+ embed = {:parent => output, :property => property}
426
+ state[:embeds][sid] = embed
427
+
428
+ # Recurse into element
429
+ s = @subjects.fetch(sid, {'@id' => sid})
430
+ o = {}
431
+ s.each do |prop, value|
432
+ if prop[0,1] == '@'
433
+ # Copy keywords
434
+ o[prop] = s[prop].dup
435
+ else
436
+ depth do
437
+ debug("frame") {"embed property #{prop.inspect} value #{value.inspect}"}
438
+ embed_values(state, s, prop, o)
439
+ end
440
+ end
441
+ end
442
+ else
443
+ debug("frame") {"don't embed element #{sid.inspect}"}
444
+ end
445
+ else
446
+ debug("frame") {"embed property #{property.inspect}, value #{o.inspect}"}
447
+ end
448
+ add_frame_output(state, output, property, o.dup)
449
+ end
450
+ end
451
+ end
452
+ end