json-ld 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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