json-ld 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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