json-ld 0.0.8 → 0.1.0

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.
@@ -106,4 +106,10 @@ if RUBY_VERSION < "1.9"
106
106
  self.dup.merge!(other)
107
107
  end
108
108
  end
109
+
110
+ class Hash
111
+ def new(obj = nil, &block)
112
+ InsertOrderPreservingHash.new(obj, &block)
113
+ end
114
+ end
109
115
  end
@@ -15,84 +15,6 @@ module JSON::LD
15
15
  # @return [RDF::Graph]
16
16
  attr_reader :graph
17
17
 
18
- ##
19
- # Context
20
- #
21
- # The `@context` keyword is used to change how the JSON-LD processor evaluates key- value pairs. In this
22
- # case, it was used to map one string (`'myvocab'`) to another string, which is interpreted as a IRI. In the
23
- # example above, the `myvocab` string is replaced with "http://example.org/myvocab#" when it is detected. In
24
- # the example above, `"myvocab:personality"` would expand to "http://example.org/myvocab#personality".
25
- #
26
- # This mechanism is a short-hand for RDF, called a `CURIE`, and provides developers an unambiguous way to
27
- # map any JSON value to RDF.
28
- #
29
- # @private
30
- class EvaluationContext # :nodoc:
31
- # The base.
32
- #
33
- # The `@base` string is a special keyword that states that any relative IRI MUST be appended to the string
34
- # specified by `@base`.
35
- #
36
- # @attr [RDF::URI]
37
- attr :base, true
38
-
39
- # A list of current, in-scope URI mappings.
40
- #
41
- # @attr [Hash{String => String}]
42
- attr :mappings, true
43
-
44
- # The default vocabulary
45
- #
46
- # A value to use as the prefix URI when a term is used.
47
- # This specification does not define an initial setting for the default vocabulary.
48
- # Host Languages may define an initial setting.
49
- #
50
- # @attr [String]
51
- attr :vocab, true
52
-
53
- # Type coersion
54
- #
55
- # The @coerce keyword is used to specify type coersion rules for the data. For each key in the map, the
56
- # key is the type to be coerced to and the value is the vocabulary term to be coerced. Type coersion for
57
- # the key `@iri` asserts that all vocabulary terms listed should undergo coercion to an IRI,
58
- # including `@base` processing for relative IRIs and CURIE processing for compact URI Expressions like
59
- # `foaf:homepage`.
60
- #
61
- # As the value may be an array, this is maintained as a reverse mapping of `property` => `type`.
62
- #
63
- # @attr [Hash{String => String}]
64
- attr :coerce
65
-
66
- # List coercion
67
- #
68
- # The @list keyword is used to specify that properties having an array value are to be treated
69
- # as an ordered list, rather than a normal unordered list
70
- # @attr [Array<String>]
71
- attr :list
72
-
73
- ##
74
- # Create new evaluation context
75
- # @yield [ec]
76
- # @yieldparam [EvaluationContext]
77
- # @return [EvaluationContext]
78
- def initialize
79
- @base = nil
80
- @mappings = {}
81
- @vocab = nil
82
- @coerce = {}
83
- @list = []
84
- yield(self) if block_given?
85
- end
86
-
87
- def inspect
88
- v = %w([EvaluationContext) + %w(base vocab).map {|a| "#{a}='#{self.send(a).inspect}'"}
89
- v << "mappings[#{mappings.keys.length}]=#{mappings}"
90
- v << "coerce[#{coerce.keys.length}]=#{coerce}"
91
- v << "list[#{list.length}]=#{list}"
92
- v.join(", ") + "]"
93
- end
94
- end
95
-
96
18
  ##
97
19
  # Initializes the RDF/JSON reader instance.
98
20
  #
@@ -105,7 +27,6 @@ module JSON::LD
105
27
  # @raise [RDF::ReaderError] if the JSON document cannot be loaded
106
28
  def initialize(input = $stdin, options = {}, &block)
107
29
  super do
108
- @base_uri = uri(options[:base_uri]) if options[:base_uri]
109
30
  begin
110
31
  @doc = JSON.load(input)
111
32
  rescue JSON::ParserError => e
@@ -128,11 +49,8 @@ module JSON::LD
128
49
  def each_statement(&block)
129
50
  @callback = block
130
51
 
131
- # initialize the evaluation context with the appropriate base
132
- ec = EvaluationContext.new do |e|
133
- e.base = @base_uri if @base_uri
134
- parse_context(e, DEFAULT_CONTEXT)
135
- end
52
+ # initialize the evaluation context with initial context
53
+ ec = EvaluationContext.new(@options)
136
54
 
137
55
  traverse("", @doc, nil, nil, ec)
138
56
  end
@@ -159,206 +77,140 @@ module JSON::LD
159
77
  # Inherited property
160
78
  # @param [EvaluationContext] ec
161
79
  # The active context
80
+ # @return [RDF::Resource] defined by this element
81
+ # @yield :resource
82
+ # @yieldparam [RDF::Resource] :resource
162
83
  def traverse(path, element, subject, property, ec)
163
- add_debug(path) {"traverse: s=#{subject.inspect}, p=#{property.inspect}, e=#{ec.inspect}"}
164
- object = nil
84
+ debug(path) {"traverse: e=#{element.class.inspect}, s=#{subject.inspect}, p=#{property.inspect}, e=#{ec.inspect}"}
165
85
 
166
- case element
86
+ traverse_result = case element
167
87
  when Hash
168
- # 2) ... For each key-value
169
- # pair in the associative array, using the newly created processor state do the
170
- # following:
171
-
172
88
  # 2.1) If a @context keyword is found, the processor merges each key-value pair in
173
89
  # the local context into the active context ...
174
90
  if element['@context']
175
91
  # Merge context
176
- ec = parse_context(ec.dup, element['@context'])
92
+ ec = ec.parse(element['@context'])
177
93
  prefixes.merge!(ec.mappings) # Update parsed prefixes
178
94
  end
179
95
 
180
- # 2.2) Create a new associative array by mapping the keys from the current associative array ...
96
+ # 2.2) Create a copy of the current JSON object, changing keys that map to JSON-LD keywords with those keywords.
97
+ # Use the new JSON object in subsequent steps
181
98
  new_element = {}
182
99
  element.each do |k, v|
183
- k = ec.mappings[k.to_s] while ec.mappings.has_key?(k.to_s)
100
+ k = ec.mapping(k) if ec.mapping(k).to_s[0,1] == '@'
184
101
  new_element[k] = v
185
102
  end
186
103
  unless element == new_element
187
- add_debug(path) {"traverse: keys after map: #{new_element.keys.inspect}"}
104
+ debug(path) {"traverse: keys after map: #{new_element.keys.inspect}"}
188
105
  element = new_element
189
106
  end
190
107
 
191
108
  # Other shortcuts to allow use of this method for terminal associative arrays
192
- if element['@iri'].is_a?(String)
193
- # 2.3 Return the IRI found from the value
194
- object = expand_term(element['@iri'], ec.base, ec)
195
- add_triple(path, subject, property, object) if subject && property
196
- return
197
- elsif element['@literal']
198
- # 2.4
109
+ object = if element['@literal']
110
+ # 2.3) If the JSON object has a @literal key, set the active object to a literal value as follows ...
199
111
  literal_opts = {}
200
- literal_opts[:datatype] = expand_term(element['@datatype'], ec.vocab.to_s, ec) if element['@datatype']
112
+ literal_opts[:datatype] = ec.expand_iri(element['@type'], :position => :datatype) if element['@type']
201
113
  literal_opts[:language] = element['@language'].to_sym if element['@language']
202
- object = RDF::Literal.new(element['@literal'], literal_opts)
203
- add_triple(path, subject, property, object) if subject && property
204
- return
114
+ RDF::Literal.new(element['@literal'], literal_opts)
205
115
  elsif element['@list']
206
- # 2.4a (Lists)
207
- parse_list("#{path}[#{'@list'}]", element['@list'], subject, property, ec)
208
- return
209
- elsif element['@subject'].is_a?(String)
116
+ # 2.4 (Lists)
117
+ parse_list("#{path}[#{'@list'}]", element['@list'], property, ec) do |resource|
118
+ add_triple(path, subject, property, resource) if subject && property
119
+ end
120
+ end
121
+
122
+ if object
123
+ yield object if block_given?
124
+ return object
125
+ end
126
+
127
+ active_subject = if element['@id'].is_a?(String)
210
128
  # 2.5 Subject
211
129
  # 2.5.1 Set active object (subject)
212
- active_subject = expand_term(element['@subject'], ec.base, ec)
213
- elsif element['@subject']
130
+ ec.expand_iri(element['@id'], :position => :subject)
131
+ elsif element['@id']
214
132
  # 2.5.2 Recursively process hash or Array values
215
- traverse("#{path}[#{'@subject'}]", element['@subject'], subject, property, ec)
133
+ traverse("#{path}[#{'@id'}]", element['@id'], subject, property, ec) do |resource|
134
+ add_triple(path, subject, property, resource) if subject && property
135
+ end
216
136
  else
217
137
  # 2.6) Generate a blank node identifier and set it as the active subject.
218
- active_subject = RDF::Node.new
138
+ RDF::Node.new
219
139
  end
220
140
 
221
- add_triple(path, subject, property, active_subject) if subject && property
222
141
  subject = active_subject
223
142
 
143
+ # 2.7) For each key in the JSON object that has not already been processed, perform the following steps:
224
144
  element.each do |key, value|
225
- # 2.7) If a key that is not @context, @subject, or @type, set the active property by
145
+ # 2.7.1) If a key that is not @context, @id, or @type, set the active property by
226
146
  # performing Property Processing on the key.
227
147
  property = case key
228
- when '@type' then '@type'
148
+ when '@type' then RDF.type
229
149
  when /^@/ then next
230
- else expand_term(key, ec.vocab, ec)
150
+ else ec.expand_iri(key, :position => :predicate)
231
151
  end
232
152
 
233
- # 2.7.3
234
- if ec.list.include?(property.to_s) && value.is_a?(Array)
235
- # 2.7.3.1 (Lists) If the active property is the target of a @list coercion, and the value is an array,
236
- # process the value as a list starting at Step 3a.
237
- parse_list("#{path}[#{key}]", value, subject, property, ec)
153
+ # 2.7.3) List expansion
154
+ object = if ec.list(property) && value.is_a?(Array)
155
+ # If the active property is the target of a @list coercion, and the value is an array,
156
+ # process the value as a list starting at Step 3.1.
157
+ parse_list("#{path}[#{key}]", value, property, ec) do |resource|
158
+ # Adds triple for head BNode only, the rest of the list is done within the method
159
+ add_triple(path, subject, property, resource) if subject && property
160
+ end
238
161
  else
239
- traverse("#{path}[#{key}]", value, subject, property, ec)
162
+ traverse("#{path}[#{key}]", value, subject, property, ec) do |resource|
163
+ # Adds triples for each value
164
+ add_triple(path, subject, property, resource) if subject && property
165
+ end
240
166
  end
241
167
  end
168
+
169
+ # 2.8) The subject is returned
170
+ subject
242
171
  when Array
243
- # 3) If a regular array is detected, process each value in the array by doing the following:
172
+ # 3) If a regular array is detected ...
244
173
  element.each_with_index do |v, i|
245
- traverse("#{path}[#{i}]", v, subject, property, ec)
174
+ traverse("#{path}[#{i}]", v, subject, property, ec) do |resource|
175
+ add_triple(path, subject, property, resource) if subject && property
176
+ end
246
177
  end
178
+ nil # No real value returned from an array
247
179
  when String
248
- # Perform coersion of the value, or generate a literal
249
- add_debug(path) do
250
- "traverse(#{element}): coerce?(#{property.inspect}) == #{ec.coerce[property.to_s].inspect}, " +
251
- "ec=#{ec.coerce.inspect}"
180
+ # 4) Perform coersion of the value, or generate a literal
181
+ debug(path) do
182
+ "traverse(#{element}): coerce(#{property.inspect}) == #{ec.coerce(property).inspect}, " +
183
+ "ec=#{ec.coercions.inspect}"
252
184
  end
253
- object = if ec.coerce[property.to_s] == '@iri'
254
- expand_term(element, ec.base, ec)
255
- elsif ec.coerce[property.to_s]
256
- RDF::Literal.new(element, :datatype => ec.coerce[property.to_s])
185
+ if ec.coerce(property) == '@id'
186
+ # 4.1) If the active property is the target of a @id coercion ...
187
+ ec.expand_iri(element, :position => :object)
188
+ elsif ec.coerce(property)
189
+ # 4.2) Otherwise, if the active property is the target of coercion ..
190
+ RDF::Literal.new(element, :datatype => ec.coerce(property))
257
191
  else
258
- RDF::Literal.new(element)
192
+ # 4.3) Otherwise, set the active object to a plain literal value created from the string.
193
+ RDF::Literal.new(element, :language => ec.language)
259
194
  end
260
- property = RDF.type if property == '@type'
261
- add_triple(path, subject, property, object) if subject && property
262
195
  when Float
263
196
  object = RDF::Literal::Double.new(element)
264
- add_debug(path) {"traverse(#{element}): native: #{object.inspect}"}
265
- add_triple(path, subject, property, object) if subject && property
197
+ debug(path) {"traverse(#{element}): native: #{object.inspect}"}
198
+ object
266
199
  when Fixnum
267
200
  object = RDF::Literal.new(element)
268
- add_debug(path) {"traverse(#{element}): native: #{object.inspect}"}
269
- add_triple(path, subject, property, object) if subject && property
201
+ debug(path) {"traverse(#{element}): native: #{object.inspect}"}
202
+ object
270
203
  when TrueClass, FalseClass
271
204
  object = RDF::Literal::Boolean.new(element)
272
- add_debug(path) {"traverse(#{element}): native: #{object.inspect}"}
273
- add_triple(path, subject, property, object) if subject && property
205
+ debug(path) {"traverse(#{element}): native: #{object.inspect}"}
206
+ object
274
207
  else
275
208
  raise RDF::ReaderError, "Traverse to unknown element: #{element.inspect} of type #{element.class}"
276
209
  end
277
- end
278
-
279
- ##
280
- # add a statement, object can be literal or URI or bnode
281
- #
282
- # @param [String] path
283
- # @param [URI, BNode] subject the subject of the statement
284
- # @param [URI] predicate the predicate of the statement
285
- # @param [URI, BNode, Literal] object the object of the statement
286
- # @return [Statement] Added statement
287
- # @raise [ReaderError] Checks parameter types and raises if they are incorrect if parsing mode is _validate_.
288
- def add_triple(path, subject, predicate, object)
289
- statement = RDF::Statement.new(subject, predicate, object)
290
- add_debug(path) {"statement: #{statement.to_ntriples}"}
291
- @callback.call(statement)
292
- end
293
-
294
- ##
295
- # Add debug event to debug array, if specified
296
- #
297
- # @param [XML Node, any] node:: XML Node or string for showing context
298
- # @param [String] message
299
- # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
300
- def add_debug(node, message = "")
301
- return unless ::JSON::LD.debug? || @options[:debug]
302
- message = message + yield if block_given?
303
- puts "#{node}: #{message}" if JSON::LD::debug?
304
- @options[:debug] << "#{node}: #{message}" if @options[:debug].is_a?(Array)
305
- end
306
-
307
- ##
308
- # Parse a JSON context, into a new EvaluationContext
309
- # @param [Hash{String => String,Hash}, String] context
310
- # JSON representation of @context
311
- # @return [EvaluationContext]
312
- # @raise [RDF::ReaderError]
313
- # on a remote context load error, syntax error, or a reference to a term which is not defined.
314
- def parse_context(ec, context)
315
- # Load context document, if it is a string
316
- if context.is_a?(String)
317
- begin
318
- context = open(context.to_s) {|f| JSON.load(f)}
319
- rescue JSON::ParserError => e
320
- raise RDF::ReaderError, "Failed to parse remote context at #{context}: #{e.message}"
321
- end
322
- end
323
210
 
324
- context.each do |key, value|
325
- add_debug("parse_context(#{key})") {value.inspect}
326
- case key
327
- when '@vocab' then ec.vocab = value
328
- when '@base' then ec.base = uri(value)
329
- when '@coerce'
330
- # Process after prefix mapping
331
- else
332
- # Spec confusion: The text indicates to merge each key-value pair into the active context. Is any
333
- # processing performed on the values. For instance, could a value be a CURIE, or {"@iri": <value>}?
334
- # Examples indicate that there is no such processing, and each value should be an absolute IRI. The
335
- # wording makes this unclear.
336
- ec.mappings[key.to_s] = value
337
- end
338
- end
339
-
340
- if context['@coerce']
341
- # Spec confusion: doc says to merge each key-value mapping to the local context's @coerce mapping,
342
- # overwriting duplicate values. In the case where a mapping is indicated to a list of properties
343
- # (e.g., { "@iri": ["foaf:homepage", "foaf:member"] }, does this overwrite a previous mapping
344
- # of { "@iri": "foaf:knows" }, or add to it.
345
- add_error RDF::ReaderError, "Expected @coerce to reference an associative array" unless context['@coerce'].is_a?(Hash)
346
- context['@coerce'].each do |type, property|
347
- add_debug("parse_context: @coerce") {"type=#{type}, prop=#{property}"}
348
- type_uri = expand_term(type, ec.vocab, ec).to_s
349
- [property].flatten.compact.each do |prop|
350
- p = expand_term(prop, ec.vocab, ec).to_s
351
- if type == '@list'
352
- # List is managed separate from types, as it is maintained in normal form.
353
- ec.list << p unless ec.list.include?(p)
354
- else
355
- ec.coerce[p] = type_uri
356
- end
357
- end
358
- end
359
- end
360
-
361
- ec
211
+ # Yield and return traverse_result
212
+ yield traverse_result if traverse_result && block_given?
213
+ traverse_result
362
214
  end
363
215
 
364
216
  ##
@@ -368,71 +220,69 @@ module JSON::LD
368
220
  # location within JSON hash
369
221
  # @param [Array] list
370
222
  # The Array to serialize as a list
371
- # @param [RDF::URI] subject
372
- # Inherited subject
373
223
  # @param [RDF::URI] property
374
224
  # Inherited property
375
225
  # @param [EvaluationContext] ec
376
226
  # The active context
377
- def parse_list(path, list, subject, property, ec)
378
- add_debug(path) {"list: #{list.inspect}, s=#{subject.inspect}, p=#{property.inspect}, e=#{ec.inspect}"}
227
+ # @return [RDF::Resource] BNode or nil for head of list
228
+ # @yield :resource
229
+ # BNode or nil for head of list
230
+ # @yieldparam [RDF::Resource] :resource
231
+ def parse_list(path, list, property, ec)
232
+ debug(path) {"list: #{list.inspect}, p=#{property.inspect}, e=#{ec.inspect}"}
379
233
 
380
234
  last = list.pop
381
- first_bnode = last ? RDF::Node.new : RDF.nil
382
- add_triple("#{path}", subject, property, first_bnode)
235
+ result = first_bnode = last ? RDF::Node.new : RDF.nil
383
236
 
384
237
  list.each do |list_item|
385
- traverse("#{path}", list_item, first_bnode, RDF.first, ec)
238
+ # Traverse the value, using _property_, not rdf:first, to ensure that
239
+ # proper type coercion is performed
240
+ traverse("#{path}", list_item, first_bnode, property, ec) do |resource|
241
+ add_triple("#{path}", first_bnode, RDF.first, resource)
242
+ end
386
243
  rest_bnode = RDF::Node.new
387
244
  add_triple("#{path}", first_bnode, RDF.rest, rest_bnode)
388
245
  first_bnode = rest_bnode
389
246
  end
390
247
  if last
391
- traverse("#{path}", last, first_bnode, RDF.first, ec)
248
+ traverse("#{path}", last, first_bnode, property, ec) do |resource|
249
+ add_triple("#{path}", first_bnode, RDF.first, resource)
250
+ end
392
251
  add_triple("#{path}", first_bnode, RDF.rest, RDF.nil)
393
252
  end
253
+
254
+ yield result if block_given?
255
+ result
394
256
  end
395
257
 
396
258
  ##
397
- # Expand a term using the specified context
398
- #
399
- # @param [String] term
400
- # @param [String] base Base to apply to URIs
401
- # @param [EvaluationContext] ec
259
+ # add a statement, object can be literal or URI or bnode
402
260
  #
403
- # @return [RDF::URI]
404
- # @raise [RDF::ReaderError] if the term cannot be expanded
405
- # @see http://json-ld.org/spec/ED/20110507/#markup-of-rdf-concepts
406
- def expand_term(term, base, ec)
407
- #add_debug("expand_term", {"term=#{term.inspect}, base=#{base.inspect}, ec=#{ec.inspect}"}
408
- prefix, suffix = term.split(":", 2)
409
- if prefix == '_'
410
- bnode(suffix)
411
- elsif ec.mappings.has_key?(prefix)
412
- uri(ec.mappings[prefix] + suffix.to_s)
413
- elsif base
414
- base.respond_to?(:join) ? base.join(term) : uri(base + term)
415
- else
416
- uri(term)
417
- end
418
- end
419
-
420
- def uri(value, append = nil)
421
- value = RDF::URI.new(value)
422
- value = value.join(append) if append
423
- value.validate! if validate?
424
- value.canonicalize! if canonicalize?
425
- value = RDF::URI.intern(value) if intern?
426
- value
261
+ # @param [String] path
262
+ # @param [URI, BNode] subject the subject of the statement
263
+ # @param [URI] predicate the predicate of the statement
264
+ # @param [URI, BNode, Literal] object the object of the statement
265
+ # @return [Statement] Added statement
266
+ # @raise [ReaderError] Checks parameter types and raises if they are incorrect if parsing mode is _validate_.
267
+ def add_triple(path, subject, predicate, object)
268
+ predicate = RDF.type if predicate == '@type'
269
+ statement = RDF::Statement.new(subject, predicate, object)
270
+ debug(path) {"statement: #{statement.to_ntriples}"}
271
+ @callback.call(statement)
427
272
  end
428
273
 
429
- # Keep track of allocated BNodes
274
+ ##
275
+ # Add debug event to debug array, if specified
430
276
  #
431
- # Don't actually use the name provided, to prevent name alias issues.
432
- # @return [RDF::Node]
433
- def bnode(value = nil)
434
- @bnode_cache ||= {}
435
- @bnode_cache[value.to_s] ||= RDF::Node.new
277
+ # @param [XML Node, any] node:: XML Node or string for showing context
278
+ # @param [String] message
279
+ # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
280
+ def debug(*args)
281
+ return unless ::JSON::LD.debug? || @options[:debug]
282
+ message = " " * (@depth || 0) * 2 + (args.empty? ? "" : args.join(": "))
283
+ message += yield if block_given?
284
+ puts message if JSON::LD::debug?
285
+ @options[:debug] << message if @options[:debug].is_a?(Array)
436
286
  end
437
287
  end
438
288
  end