json-ld 0.3.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -220,8 +220,8 @@ Full documentation available on [RubyDoc](http://rubydoc.info/gems/json-ld/file/
220
220
 
221
221
  ## Dependencies
222
222
  * [Ruby](http://ruby-lang.org/) (>= 1.8.7) or (>= 1.8.1 with [Backports][])
223
- * [RDF.rb](http://rubygems.org/gems/rdf) (>= 0.3.4)
224
- * [JSON](https://rubygems.org/gems/json) (>= 1.5.1)
223
+ * [RDF.rb](http://rubygems.org/gems/rdf) (>= 1.0)
224
+ * [JSON](https://rubygems.org/gems/json) (>= 1.5)
225
225
 
226
226
  ## Installation
227
227
  The recommended installation method is via [RubyGems](http://rubygems.org/).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.9.0
data/lib/json/ld.rb CHANGED
@@ -89,6 +89,9 @@ module JSON
89
89
  # A list containing another list was detected.
90
90
  LIST_OF_LISTS_DETECTED = 3
91
91
 
92
+ # When processing a language map, a value not a
93
+ ILLEGAL_LANGUAGE_MAP_DETECTED = 4
94
+
92
95
  attr_reader :code
93
96
 
94
97
  class Lossy < ProcessingError
@@ -105,6 +108,13 @@ module JSON
105
108
  end
106
109
  end
107
110
 
111
+ class LanguageMap < ProcessingError
112
+ def initialize(*args)
113
+ super
114
+ @code = ILLEGAL_LANGUAGE_MAP_DETECTED
115
+ end
116
+ end
117
+
108
118
  class ListOfLists < ProcessingError
109
119
  def initialize(*args)
110
120
  super
data/lib/json/ld/api.rb CHANGED
@@ -28,9 +28,22 @@ module JSON::LD
28
28
  OPEN_OPTS = {
29
29
  :headers => %w(Accept: application/ld+json, application/json)
30
30
  }
31
+
32
+ # Current input
33
+ # @!attribute [rw] input
34
+ # @return [String, #read, Hash, Array]
31
35
  attr_accessor :value
36
+
37
+ # Input evaluation context
38
+ # @!attribute [rw] context
39
+ # @return [JSON::LD::EvaluationContext]
32
40
  attr_accessor :context
33
41
 
42
+ # Current Blank Node Namer
43
+ # @!attribute [r] namer
44
+ # @return [JSON::LD::BlankNodeNamer]
45
+ attr_reader :namer
46
+
34
47
  ##
35
48
  # Initialize the API, reading in any document and setting global options
36
49
  #
@@ -49,13 +62,18 @@ module JSON::LD
49
62
  # @option options [Boolean] :optimize (false)
50
63
  # If set to `true`, the JSON-LD processor is allowed to optimize the output of the Compaction Algorithm to produce even compacter representations. The algorithm for compaction optimization is beyond the scope of this specification and thus not defined. Consequently, different implementations *MAY* implement different optimization algorithms.
51
64
  # (Presently, this is a noop).
52
- # @option options [Boolean] :useNativeDatatypes (true)
53
- # If set to `true`, the JSON-LD processor will use the expanded `rdf:type` IRI as the property instead of `@type` when converting from RDF.
54
- # @option options [Boolean] :useRdfType (false) If set to `true`, the JSON-LD processor will try to convert datatyped literals to JSON native types instead of using the expanded object form when converting from RDF. `xsd:boolean` values will be converted to `true` or `false`. `xsd:integer` and `xsd:double` values will be converted to JSON numbers.
65
+ # @option options [Boolean] :useNativeTypes (true)
66
+ # If set to `true`, the JSON-LD processor will use native datatypes for expression xsd:integer, xsd:boolean, and xsd:double values, otherwise, it will use the expanded form.
67
+ # @option options [Boolean] :useRdfType (false)
68
+ # If set to `true`, the JSON-LD processor will try to convert datatyped literals to JSON native types instead of using the expanded object form when converting from RDF. `xsd:boolean` values will be converted to `true` or `false`. `xsd:integer` and `xsd:double` values will be converted to JSON numbers.
69
+ # @option options [Boolean] :rename_bnodes (true)
70
+ # Rename bnodes as part of expansion, or keep them the same.
55
71
  # @yield [api]
56
72
  # @yieldparam [API]
57
73
  def initialize(input, context, options = {}, &block)
58
74
  @options = {:compactArrays => true}.merge(options)
75
+ options = {:rename_bnodes => true}.merge(options)
76
+ @namer = options[:rename_bnodes] ? BlankNodeNamer.new("t") : BlankNodeMapper.new
59
77
  @value = case input
60
78
  when Array, Hash then input.dup
61
79
  when IO, StringIO then JSON.parse(input.read)
@@ -110,7 +128,7 @@ module JSON::LD
110
128
  result = result['@graph'] if result.is_a?(Hash) && result.keys == %w(@graph)
111
129
 
112
130
  # Finally, if element is a JSON object, it is wrapped into an array.
113
- result = [result] unless result.is_a?(Array)
131
+ result = [result].compact unless result.is_a?(Array)
114
132
  callback.call(result) if callback
115
133
  yield result if block_given?
116
134
  result
@@ -207,18 +225,14 @@ module JSON::LD
207
225
 
208
226
  # Generate _nodeMap_
209
227
  node_map = Hash.ordered
210
- self.generate_node_map(value,
211
- node_map,
212
- (graph.to_s == '@merged' ? '@merged' : '@default'),
213
- nil,
214
- BlankNodeNamer.new("t"))
228
+ self.generate_node_map(value, node_map, (graph.to_s == '@merged' ? '@merged' : '@default'))
215
229
 
216
230
  result = []
217
231
 
218
232
  # If nodeMap has no property graph, return result, otherwise set definitions to its value.
219
233
  definitions = node_map.fetch(graph.to_s, {})
220
234
 
221
- # Foreach property and valud of definitions
235
+ # Foreach property and value of definitions
222
236
  definitions.keys.sort.each do |prop|
223
237
  value = definitions[prop]
224
238
  result << value
@@ -304,11 +318,7 @@ module JSON::LD
304
318
  # Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
305
319
  all_nodes = Hash.ordered
306
320
  depth do
307
- generate_node_map(value,
308
- all_nodes,
309
- '@merged',
310
- nil,
311
- BlankNodeNamer.new("t"))
321
+ generate_node_map(value, all_nodes, '@merged')
312
322
  end
313
323
  @node_map = all_nodes['@merged']
314
324
  debug(".frame") {"node_map: #{@node_map.to_json(JSON_STATE)}"}
@@ -350,20 +360,27 @@ module JSON::LD
350
360
  # See options in {JSON::LD::API#initialize}
351
361
  # Options passed to {JSON::LD::API.expand}
352
362
  # @raise [InvalidContext]
363
+ # @return [Array<RDF::Statement>] if no block given
353
364
  # @yield statement
354
365
  # @yieldparam [RDF::Statement] statement
355
- def self.toRDF(input, context = nil, callback = nil, options = {})
356
- # 1) Perform the Expansion Algorithm on the JSON-LD input.
357
- # This removes any existing context to allow the given context to be cleanly applied.
358
- expanded = expand(input, context, nil, options)
366
+ def self.toRDF(input, context = nil, callback = nil, options = {}, &block)
367
+ API.new(input, context, options) do |api|
368
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
369
+ # This removes any existing context to allow the given context to be cleanly applied.
370
+ result = api.expand(api.value, nil, api.context)
359
371
 
360
- API.new(expanded, nil, options) do
361
- debug(".expand") {"expanded input: #{value.to_json(JSON_STATE)}"}
372
+ api.send(:debug, ".expand") {"expanded input: #{result.to_json(JSON_STATE)}"}
362
373
  # Start generating statements
363
- statements("", value, nil, nil, nil) do |statement|
364
- callback.call(statement) if callback
365
- yield statement if block_given?
374
+ results = []
375
+ api.statements("", result, nil, nil, nil) do |statement|
376
+ callback ||= block if block_given?
377
+ if callback
378
+ callback.call(statement)
379
+ else
380
+ results << statement
381
+ end
366
382
  end
383
+ results
367
384
  end
368
385
  end
369
386
 
@@ -384,7 +401,7 @@ module JSON::LD
384
401
  # The JSON-LD document in expanded form
385
402
  # @return [Array<Hash>]
386
403
  # The JSON-LD document in expanded form
387
- def self.fromRDF(input, callback = nil, options = {})
404
+ def self.fromRDF(input, callback = nil, options = {}, &block)
388
405
  options = {:useNativeTypes => true}.merge(options)
389
406
  result = nil
390
407
 
@@ -392,8 +409,8 @@ module JSON::LD
392
409
  result = api.from_statements(input)
393
410
  end
394
411
 
412
+ callback ||= block if block_given?
395
413
  callback.call(result) if callback
396
- yield result if block_given?
397
414
  result
398
415
  end
399
416
  end
@@ -21,7 +21,7 @@ module JSON::LD
21
21
  # active property.
22
22
  debug("compact") {"Array #{element.inspect}"}
23
23
  result = depth {element.map {|v| compact(v, property)}}
24
-
24
+
25
25
  # If element has a single member and the active property has no
26
26
  # @container mapping to @list or @set, the compacted value is that
27
27
  # member; otherwise the compacted value is element
@@ -35,36 +35,92 @@ module JSON::LD
35
35
  when Hash
36
36
  # 2) Otherwise, if element is an object:
37
37
  result = {}
38
-
38
+
39
39
  if k = %w(@list @set @value).detect {|container| element.has_key?(container)}
40
40
  debug("compact") {"#{k}: container(#{property}) = #{context.container(property)}"}
41
41
  end
42
42
 
43
43
  k ||= '@id' if element.keys == ['@id']
44
-
44
+
45
45
  case k
46
46
  when '@value', '@id'
47
47
  # If element has an @value property or element is a node reference, return the result of performing Value Compaction on element using active property.
48
48
  v = context.compact_value(property, element, :depth => @depth)
49
- debug("compact") {"value optimization, return as #{v.inspect}"}
49
+ debug("compact") {"value optimization for #{property}, return as #{v.inspect}"}
50
50
  return v
51
51
  when '@list'
52
52
  # Otherwise, if the active property has a @container mapping to @list and element has a corresponding @list property, recursively compact that property's value passing a copy of the active context and the active property ensuring that the result is an array with all null values removed.
53
-
53
+
54
54
  # If there already exists a value for active property in element and the full IRI of property is also coerced to @list, return an error.
55
55
  # FIXME: check for full-iri list coercion
56
56
 
57
57
  # Otherwise store the resulting array as value of active property if empty or property otherwise.
58
- compacted_key = context.compact_iri(k, :position => :predicate, :depth => @depth)
58
+ compacted_key = context.compact_iri('@list', :position => :predicate, :depth => @depth)
59
59
  v = depth { compact(element[k], property) }
60
-
60
+
61
61
  # Return either the result as an array, as an object with a key of @list (or appropriate alias from active context
62
62
  v = [v].compact unless v.is_a?(Array)
63
- v = {compacted_key => v} unless context.container(property) == k
63
+ unless context.container(property) == '@list'
64
+ v = {compacted_key => v}
65
+ if element['@annotation']
66
+ compacted_key = context.compact_iri('@annotation', :position => :predicate, :depth => @depth)
67
+ v[compacted_key] = element['@annotation']
68
+ end
69
+ end
64
70
  debug("compact") {"@list result, return as #{v.inspect}"}
65
71
  return v
66
72
  end
67
73
 
74
+ # Check for property generators before continuing with other elements
75
+ # For each term pg in the active context which is a property generator
76
+ # Select property generator terms by shortest term
77
+ context.mappings.keys.sort.each do |term|
78
+ next unless context.mapping(term).is_a?(Array)
79
+ # Using the first expanded IRI p associated with the property generator
80
+ expanded_iris = context.mapping(term).map(&:to_s)
81
+ p = expanded_iris.first.to_s
82
+
83
+ # Skip to the next property generator term unless p is a property of element
84
+ next unless element.has_key?(p)
85
+
86
+ debug("compact") {"check pg #{term}: #{expanded_iris}"}
87
+
88
+ # For each node n which is a value of p in element
89
+ node_values = []
90
+ element[p].dup.each do |n|
91
+ # For each expanded IRI pi associated with the property generator other than p
92
+ next unless expanded_iris[1..-1].all? do |pi|
93
+ debug("compact") {"check #{pi} for (#{n.inspect})"}
94
+ element.has_key?(pi) && element[pi].any? do |ni|
95
+ nodesEquivalent?(n, ni)
96
+ end
97
+ end
98
+
99
+ # Remove n as a value of all p and pi in element
100
+ debug("compact") {"removed matched value #{n.inspect} from #{expanded_iris.inspect}"}
101
+ expanded_iris.each do |pi|
102
+ # FIXME: This removes all values equivalent to n, not just the first
103
+ element[pi] = element[pi].reject {|ni| nodesEquivalent?(n, ni)}
104
+ end
105
+
106
+ # Add the result of performing the compaction algorithm on n to pg to output
107
+ node_values << n
108
+ end
109
+
110
+ # If there are node_values, or all the values from expanded_iris are empty, add node_values to result, and remove the expanded_iris as keys from element
111
+ if node_values.length > 0 || expanded_iris.all? {|pi| element.has_key?(pi) && element[pi].empty?}
112
+ debug("compact") {"compact extracted pg values"}
113
+ result[term] = depth { compact(node_values, term)}
114
+ result[term] = [result[term]] if !result[term].is_a?(Array) && context.container(term) == '@set'
115
+
116
+ debug("compact") {"remove empty pg keys from element"}
117
+ expanded_iris.each do |pi|
118
+ debug(" =>") {"#{pi}? #{element.fetch(pi, []).empty?}"}
119
+ element.delete(pi) if element.fetch(pi, []).empty?
120
+ end
121
+ end
122
+ end
123
+
68
124
  # Otherwise, for each property and value in element:
69
125
  element.each do |key, value|
70
126
  debug("compact") {"#{key}: #{value.inspect}"}
@@ -80,48 +136,63 @@ module JSON::LD
80
136
  context.compact_iri(value, :position => position, :depth => @depth)
81
137
  when Array
82
138
  # Otherwise, value must be an array. Perform IRI Compaction on every entry of value. If value contains just one entry, value is set to that entry
83
- compacted_value = value.map {|v| context.compact_iri(v, :position => position, :depth => @depth)}
139
+ compacted_value = value.map {|v2| context.compact_iri(v2, :position => position, :depth => @depth)}
84
140
  debug {" => compacted value(#{key}): #{compacted_value.inspect}"}
85
141
  compacted_value = compacted_value.first if compacted_value.length == 1 && @options[:compactArrays]
86
142
  compacted_value
87
143
  end
144
+ elsif key == '@annotation' && context.container(property) == '@annotation'
145
+ # Skip the annotation key if annotations being applied
146
+ next
88
147
  else
89
148
  if value.empty?
90
149
  # Make sure that an empty array is preserved
91
150
  compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
92
151
  next if compacted_key.nil?
93
152
  result[compacted_key] = value
153
+ next
94
154
  end
95
155
 
96
156
  # For each item in value:
97
- raise ProcessingError, "found #{value.inspect} for #{key} if #{element.inspect}" unless value.is_a?(Array)
157
+ value = [value] if key == '@annotation' && value.is_a?(String)
158
+ raise ProcessingError, "found #{value.inspect} for #{key} of #{element.inspect}" unless value.is_a?(Array)
98
159
  value.each do |item|
99
160
  compacted_key = context.compact_iri(key, :position => :predicate, :value => item, :depth => @depth)
161
+
162
+ # Result for this item, typically the output object itself
163
+ item_result = result
164
+ item_key = compacted_key
100
165
  debug {" => compacted key: #{compacted_key.inspect} for #{item.inspect}"}
101
166
  next if compacted_key.nil?
102
167
 
168
+ # Language maps and annotations
169
+ if field = %w(@language @annotation).detect {|kk| context.container(compacted_key) == kk}
170
+ item_result = result[compacted_key] ||= Hash.new
171
+ item_key = item[field]
172
+ end
173
+
103
174
  compacted_item = depth {self.compact(item, compacted_key)}
104
175
  debug {" => compacted value: #{compacted_value.inspect}"}
105
-
106
- case result[compacted_key]
176
+
177
+ case item_result[item_key]
107
178
  when Array
108
- result[compacted_key] << compacted_item
179
+ item_result[item_key] << compacted_item
109
180
  when nil
110
181
  if !compacted_value.is_a?(Array) && context.container(compacted_key) == '@set'
111
182
  compacted_item = [compacted_item].compact
112
183
  debug {" => as @set: #{compacted_item.inspect}"}
113
184
  end
114
- result[compacted_key] = compacted_item
185
+ item_result[item_key] = compacted_item
115
186
  else
116
- result[compacted_key] = [result[compacted_key], compacted_item]
187
+ item_result[item_key] = [item_result[item_key], compacted_item]
117
188
  end
118
189
  end
119
190
  end
120
191
  end
121
-
192
+
122
193
  # Re-order result keys
123
194
  r = Hash.ordered
124
- result.keys.sort.each {|k| r[k] = result[k]}
195
+ result.keys.kw_sort.each {|kk| r[kk] = result[kk]}
125
196
  r
126
197
  else
127
198
  # For other types, the compacted value is the element value
@@ -129,5 +200,36 @@ module JSON::LD
129
200
  element
130
201
  end
131
202
  end
203
+
204
+ private
205
+
206
+ # Determines if two nodes are equivalent.
207
+ # * Value nodes are equivalent using a deep comparison
208
+ # * Arrays are equivalent if they have the same number of elements and each element is equivalent to the matching element
209
+ # * Node Defintions/References are equivalent IFF the have the same @id
210
+ def nodesEquivalent?(n1, n2)
211
+ depth do
212
+ r = if n1.is_a?(Array) && n2.is_a?(Array) && n1.length == n2.length
213
+ equiv = true
214
+ n1.each_with_index do |v1, i|
215
+ equiv &&= nodesEquivalent?(v1, n2[i]) if equiv
216
+ end
217
+ equiv
218
+ elsif value?(n1) && value?(n2)
219
+ n1 == n2
220
+ elsif list?(n1)
221
+ list?(n2) &&
222
+ n1.fetch('@annotation', true) == n2.fetch('@annotation', true) &&
223
+ nodesEquivalent?(n1['@list'], n2['@list'])
224
+ elsif (node?(n1) || node_reference?(n2))
225
+ (node?(n2) || node_reference?(n2)) && n1['@id'] == n2['@id']
226
+ else
227
+ false
228
+ end
229
+
230
+ debug("nodesEquivalent?(#{n1.inspect}, #{n2.inspect}): #{r.inspect}")
231
+ r
232
+ end
233
+ end
132
234
  end
133
235
  end
@@ -75,6 +75,10 @@ module JSON::LD
75
75
  # @return [EvaluationContext] A context provided to us that we can use without re-serializing
76
76
  attr_accessor :provided_context
77
77
 
78
+ # @!attribute [r] remote_contexts
79
+ # @return [Array<String>] The list of remote contexts already processed
80
+ attr_accessor :remote_contexts
81
+
78
82
  ##
79
83
  # Create new evaluation context
80
84
  # @yield [ec]
@@ -91,6 +95,7 @@ module JSON::LD
91
95
  RDF.to_uri.to_s => "rdf",
92
96
  RDF::XSD.to_uri.to_s => "xsd"
93
97
  }
98
+ @remote_contexts = []
94
99
 
95
100
  @options = options
96
101
 
@@ -121,6 +126,7 @@ module JSON::LD
121
126
  # Load context document, if it is a string
122
127
  begin
123
128
  ctx = JSON.load(context)
129
+ raise JSON::LD::InvalidContext::LoadError, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
124
130
  parse(ctx["@context"] || {})
125
131
  rescue JSON::ParserError => e
126
132
  debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
@@ -137,6 +143,8 @@ module JSON::LD
137
143
  ec = nil
138
144
  begin
139
145
  url = expand_iri(context, :base => context_base || base, :position => :subject)
146
+ raise JSON::LD::InvalidContext::LoadError if remote_contexts.include?(url)
147
+ @remote_contexts = @remote_contexts + [url]
140
148
  ecdup = self.dup
141
149
  ecdup.context_base = url # Set context_base for recursive remote contexts
142
150
  RDF::Util::File.open_file(url) {|f| ec = ecdup.parse(f)}
@@ -162,6 +170,7 @@ module JSON::LD
162
170
  new_ec = self.dup
163
171
  new_ec.provided_context = context.dup
164
172
 
173
+ # If context has a @vocab member: if its value is not a valid absolute IRI or null trigger an INVALID_VOCAB_MAPPING error; otherwise set the active context's vocabulary mapping to its value and remove the @vocab member from context.
165
174
  {
166
175
  '@language' => :default_language=,
167
176
  '@vocab' => :vocab=
@@ -171,7 +180,7 @@ module JSON::LD
171
180
  context.delete(key)
172
181
  debug("parse") {"Set #{key} to #{v.inspect}"}
173
182
  new_ec.send(setter, v)
174
- elsif v
183
+ elsif v && @options[:validate]
175
184
  raise InvalidContext::Syntax, "#{key.inspect} is invalid"
176
185
  end
177
186
  end
@@ -186,26 +195,48 @@ module JSON::LD
186
195
  debug("parse") {"Hash[#{key}] = #{value.inspect}"}
187
196
 
188
197
  if KEYWORDS.include?(key)
189
- raise InvalidContext::Syntax, "key #{key.inspect} must not be a keyword"
198
+ raise InvalidContext::Syntax, "key #{key.inspect} must not be a keyword" if @options[:validate]
199
+ next
190
200
  elsif term_valid?(key)
191
201
  # Remove all coercion information for the property
192
202
  new_ec.set_coerce(key, nil)
193
203
  new_ec.set_container(key, nil)
194
204
  @languages.delete(key)
195
205
 
196
- # Extract IRI mapping. This is complicated, as @id may have been aliased
197
- value = value.fetch('@id', nil) if value.is_a?(Hash)
198
- raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{value.class}" unless value.is_a?(String) || value.nil?
206
+ # Extract IRI mapping. This is complicated, as @id may have been aliased. Also, if @id is explicitly set to nil, it inhibits and automatic mapping, so treat it as false, to distinguish from no mapping at all.
207
+ value = case value
208
+ when Hash
209
+ value.has_key?('@id') && value['@id'].nil? ? false : value.fetch('@id', nil)
210
+ when nil
211
+ false
212
+ else
213
+ value
214
+ end
215
+
216
+ # Explicitly say this is not mapped
217
+ if value == false
218
+ debug("parse") {"Map #{key} to nil"}
219
+ new_ec.set_mapping(key, nil)
220
+ next
221
+ end
222
+
223
+ iri = if value.is_a?(Array)
224
+ # expand each item according the IRI Expansion algorithm. If an item does not expand to a valid absolute IRI, raise an INVALID_PROPERTY_GENERATOR error; otherwise sort val and store it as IRI mapping in definition.
225
+ value.map do |v|
226
+ raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{v.inspect}" unless v.is_a?(String)
227
+ new_ec.expand_iri(v, :position => :predicate)
228
+ end.sort
229
+ elsif value
230
+ raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{value.inspect}" unless value.is_a?(String)
231
+ new_ec.expand_iri(value, :position => :predicate)
232
+ end
199
233
 
200
- iri = new_ec.expand_iri(value, :position => :predicate) if value.is_a?(String)
201
234
  if iri && new_ec.mappings.fetch(key, nil) != iri
202
235
  # Record term definition
203
236
  new_ec.set_mapping(key, iri)
204
237
  num_updates += 1
205
- elsif value.nil?
206
- new_ec.set_mapping(key, nil)
207
238
  end
208
- else
239
+ elsif @options[:validate]
209
240
  raise InvalidContext::Syntax, "key #{key.inspect} is invalid"
210
241
  end
211
242
  end
@@ -223,14 +254,18 @@ module JSON::LD
223
254
  iri = new_ec.expand_iri(value2, :position => :predicate) if value2.is_a?(String)
224
255
  case key2
225
256
  when '@type'
226
- raise InvalidContext::Syntax, "unknown mapping for '@type' to #{value2.class}" unless value2.is_a?(String) || value2.nil?
257
+ raise InvalidContext::Syntax, "unknown mapping for '@type' to #{value2.inspect}" unless value2.is_a?(String) || value2.nil?
227
258
  if new_ec.coerce(key) != iri
228
- raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless RDF::URI(iri).absolute? || iri == '@id'
259
+ case iri
260
+ when '@id', /_:/, RDF::Node
261
+ else
262
+ raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless (RDF::URI(iri).absolute? rescue false)
263
+ end
229
264
  # Record term coercion
230
265
  new_ec.set_coerce(key, iri)
231
266
  end
232
267
  when '@container'
233
- raise InvalidContext::Syntax, "unknown mapping for '@container' to #{value2.class}" unless %w(@list @set).include?(value2)
268
+ raise InvalidContext::Syntax, "unknown mapping for '@container' to #{value2.inspect}" unless %w(@list @set @language @annotation).include?(value2)
234
269
  if new_ec.container(key) != value2
235
270
  debug("parse") {"container #{key.inspect} as #{value2.inspect}"}
236
271
  new_ec.set_container(key, value2)
@@ -281,10 +316,10 @@ module JSON::LD
281
316
  ctx['@vocab'] = vocab.to_s if vocab
282
317
 
283
318
  # Mappings
284
- mappings.keys.sort{|a, b| a.to_s <=> b.to_s}.each do |k|
319
+ mappings.keys.kw_sort{|a, b| a.to_s <=> b.to_s}.each do |k|
285
320
  next unless term_valid?(k.to_s)
286
321
  debug {"=> mappings[#{k}] => #{mappings[k]}"}
287
- ctx[k] = mappings[k].to_s
322
+ ctx[k] = mappings[k]
288
323
  end
289
324
 
290
325
  unless coercions.empty? && containers.empty? && languages.empty?
@@ -310,7 +345,7 @@ module JSON::LD
310
345
  end
311
346
 
312
347
  debug {"=> container(#{k}) => #{container(k)}"}
313
- if %w(@list @set).include?(container(k))
348
+ if %w(@list @set @language @annotation).include?(container(k))
314
349
  ctx[k]["@container"] = container(k)
315
350
  debug {"=> container[#{k}] => #{container(k).inspect}"}
316
351
  end
@@ -344,14 +379,14 @@ module JSON::LD
344
379
  #
345
380
  # @return [RDF::URI, String]
346
381
  def mapping(term)
347
- @mappings.fetch(term.to_s, nil)
382
+ @mappings.fetch(term.to_s, false)
348
383
  end
349
384
 
350
385
  ##
351
386
  # Set term mapping
352
387
  #
353
388
  # @param [#to_s] term
354
- # @param [RDF::URI, String] value
389
+ # @param [RDF::URI, String, nil] value
355
390
  #
356
391
  # @return [RDF::URI, String]
357
392
  def set_mapping(term, value)
@@ -388,8 +423,8 @@ module JSON::LD
388
423
  # @return [RDF::URI, '@id']
389
424
  def coerce(property)
390
425
  # Map property, if it's not an RDF::Value
391
- # @type and @graph always is an IRI
392
- return '@id' if [RDF.type, '@type', '@graph'].include?(property)
426
+ # @type is always is an IRI
427
+ return '@id' if [RDF.type, '@type'].include?(property)
393
428
  @coercions.fetch(property, nil)
394
429
  end
395
430
 
@@ -415,6 +450,7 @@ module JSON::LD
415
450
  # @param [String] property in unexpanded form
416
451
  # @return [String]
417
452
  def container(property)
453
+ return '@set' if property == '@graph'
418
454
  @containers.fetch(property.to_s, nil)
419
455
  end
420
456
 
@@ -472,33 +508,60 @@ module JSON::LD
472
508
  # Useful when determining how to serialize.
473
509
  # @option options [RDF::URI] base (self.base)
474
510
  # Base IRI to use when expanding relative IRIs.
475
- #
476
- # @return [RDF::URI, String] IRI or String, if it's a keyword
511
+ # @option options [Array<String>] path ([])
512
+ # Array of looked up iris, used to find cycles
513
+ # @option options [BlankNodeNamer] namer
514
+ # Blank Node namer to use for renaming Blank Nodes
515
+ #
516
+ # @return [RDF::Term, String, Array<RDF::URI>]
517
+ # IRI or String, if it's a keyword, or array of IRI, if it matches
518
+ # a property generator
477
519
  # @raise [RDF::ReaderError] if the iri cannot be expanded
478
520
  # @see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
479
521
  def expand_iri(iri, options = {})
480
522
  return iri unless iri.is_a?(String)
523
+
481
524
  prefix, suffix = iri.split(':', 2)
482
- return mapping(iri) if mapping(iri) # If it's an exact match
525
+ unless (m = mapping(iri)) == false
526
+ # It's an exact match
527
+ debug("expand_iri") {"match: #{iri.inspect} to #{m.inspect}"} unless options[:quiet]
528
+ return case m
529
+ when nil
530
+ nil
531
+ when Array
532
+ # Return array of IRIs, if it's a property generator
533
+ m.map {|mm| uri(mm.to_s, options[:namer])}
534
+ else
535
+ uri(m.to_s, options[:namer])
536
+ end
537
+ end
483
538
  debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{vocab.inspect}"} unless options[:quiet]
484
- base = [:subject].include?(options[:position]) ? options.fetch(:base, self.base) : nil
539
+ base = [:subject, :type].include?(options[:position]) ? options.fetch(:base, self.base) : nil
485
540
  prefix = prefix.to_s
486
541
  case
487
- when prefix == '_' && suffix then bnode(suffix)
542
+ when prefix == '_' && suffix then uri(bnode(suffix), options[:namer])
488
543
  when iri.to_s[0,1] == "@" then iri
489
544
  when suffix.to_s[0,2] == '//' then uri(iri)
490
- when mappings.fetch(prefix, false) then uri(mappings[prefix] + suffix.to_s)
545
+ when (mapping = mapping(prefix)) != false
546
+ debug("expand_iri") {"mapping: #{mapping(prefix).inspect}"} unless options[:quiet]
547
+ case mapping
548
+ when Array
549
+ # Return array of IRIs, if it's a property generator
550
+ mapping.map {|m| uri(m.to_s + suffix.to_s, options[:namer])}
551
+ else
552
+ uri(mapping.to_s + suffix.to_s, options[:namer])
553
+ end
491
554
  when base then base.join(iri)
492
555
  when vocab then uri("#{vocab}#{iri}")
493
556
  else
494
557
  # Otherwise, it must be an absolute IRI
495
558
  u = uri(iri)
496
- u if u.absolute? || [:subject].include?(options[:position])
559
+ u if u.absolute? || [:subject, :type].include?(options[:position])
497
560
  end
498
561
  end
499
562
 
500
563
  ##
501
- # Compact an IRI
564
+ # Compacts an absolute IRI to the shortest matching term or compact IRI
502
565
  #
503
566
  # @param [RDF::URI] iri
504
567
  # @param [Hash{Symbol => Object}] options ({})
@@ -524,10 +587,9 @@ module JSON::LD
524
587
  # Create an empty list of terms _terms_ that will be populated with terms that are ranked according to how closely they match value. Initialize highest rank to 0, and set a flag list container to false.
525
588
  terms = {}
526
589
 
527
- # If value is a @list add a term rank for each
528
- # term mapping to iri which has @container @list.
529
- debug("compact_iri", "#{value.inspect} is a list? #{list?(value).inspect}")
530
- if list?(value)
590
+ # If value is a @list select terms that match every item equivalently.
591
+ debug("compact_iri", "#{value.inspect} is a list? #{list?(value).inspect}") if value
592
+ if list?(value) && !annotation?(value)
531
593
  list_terms = matched_terms.select {|t| container(t) == '@list'}
532
594
 
533
595
  terms = list_terms.inject({}) do |memo, t|
@@ -563,13 +625,6 @@ module JSON::LD
563
625
  least_distance = terms.values.max
564
626
  terms = terms.keys.select {|t| terms[t] == least_distance}
565
627
 
566
- # If terms is empty, and the active context has a @vocab which is a prefix of iri where the resulting relative IRI is not a term in the active context. The resulting relative IRI is the unmatched part of iri.
567
- if vocab && terms.empty? && iri.to_s.index(vocab) == 0 &&
568
- [:predicate, :type].include?(options[:position])
569
- terms << iri.to_s.sub(vocab, '')
570
- debug("vocab") {"vocab: #{vocab}, rel: #{terms.first}"}
571
- end
572
-
573
628
  # If terms is empty, add a compact IRI representation of iri for each
574
629
  # term in the active context which maps to an IRI which is a prefix for
575
630
  # iri where the resulting compact IRI is not a term in the active
@@ -605,6 +660,15 @@ module JSON::LD
605
660
  debug("curies") {"selected #{terms.inspect}"}
606
661
  end
607
662
 
663
+ # If terms is empty, and the active context has a @vocab which is a prefix of iri where the resulting relative IRI is not a term in the active context. The resulting relative IRI is the unmatched part of iri.
664
+ # Don't use vocab, if the result would collide with a term
665
+ if vocab && terms.empty? && iri.to_s.index(vocab) == 0 &&
666
+ !mapping(iri.to_s.sub(vocab, '')) &&
667
+ [:predicate, :type].include?(options[:position])
668
+ terms << iri.to_s.sub(vocab, '')
669
+ debug("vocab") {"vocab: #{vocab}, rel: #{terms.first}"}
670
+ end
671
+
608
672
  # If we still don't have any terms and we're using standard_prefixes,
609
673
  # try those, and add to mapping
610
674
  if terms.empty? && @options[:standard_prefixes]
@@ -650,8 +714,8 @@ module JSON::LD
650
714
  ##
651
715
  # Expand a value from compacted to expanded form making the context
652
716
  # unnecessary. This method is used as part of more general expansion
653
- # and operates on RHS values, using a supplied key to determine @type and @container
654
- # coercion rules.
717
+ # and operates on RHS values, using a supplied key to determine @type and
718
+ # @container coercion rules.
655
719
  #
656
720
  # @param [String] property
657
721
  # Associated property used to find coercion rules
@@ -659,6 +723,8 @@ module JSON::LD
659
723
  # Value (literal or IRI) to be expanded
660
724
  # @param [Hash{Symbol => Object}] options
661
725
  # @option options [Boolean] :useNativeTypes (true) use native representations
726
+ # @option options [BlankNodeNamer] namer
727
+ # Blank Node namer to use for renaming Blank Nodes
662
728
  #
663
729
  # @return [Hash] Object representation of value
664
730
  # @raise [RDF::ReaderError] if the iri cannot be expanded
@@ -667,95 +733,40 @@ module JSON::LD
667
733
  options = {:useNativeTypes => true}.merge(options)
668
734
  depth(options) do
669
735
  debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
670
- value = RDF::Literal(value) if RDF::Literal(value).has_datatype?
671
- dt = case value
672
- when RDF::Literal
673
- case value.datatype
674
- when RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double then value.datatype
675
- else value
676
- end
677
- when RDF::Term then value.class.name
678
- else value
679
- end
680
736
 
681
- result = case dt
682
- when RDF::XSD.boolean
683
- debug("xsd:boolean")
684
- case coerce(property)
685
- when RDF::XSD.double.to_s
686
- {"@value" => value.to_s, "@type" => RDF::XSD.double.to_s}
687
- else
688
- if options[:useNativeTypes]
689
- # Unless there's coercion, to not modify representation
690
- {"@value" => (value.is_a?(RDF::Literal::Boolean) ? value.object : value)}
691
- else
692
- {"@value" => value.to_s, "@type" => RDF::XSD.boolean.to_s}
693
- end
694
- end
695
- when RDF::XSD.integer
696
- debug("xsd:integer")
697
- case coerce(property)
698
- when RDF::XSD.double.to_s
699
- {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
700
- when RDF::XSD.integer.to_s, nil
701
- # Unless there's coercion, to not modify representation
702
- if options[:useNativeTypes]
703
- {"@value" => value.is_a?(RDF::Literal::Integer) ? value.object : value}
704
- else
705
- {"@value" => value.to_s, "@type" => RDF::XSD.integer.to_s}
706
- end
707
- else
708
- res = Hash.ordered
709
- res['@value'] = value.to_s
710
- res['@type'] = coerce(property)
711
- res
712
- end
713
- when RDF::XSD.double
714
- debug("xsd:double")
715
- case coerce(property)
716
- when RDF::XSD.integer.to_s
717
- {"@value" => value.to_int.to_s, "@type" => RDF::XSD.integer.to_s}
718
- when RDF::XSD.double.to_s
719
- {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
720
- when nil
721
- if options[:useNativeTypes]
722
- # Unless there's coercion, to not modify representation
723
- {"@value" => value.is_a?(RDF::Literal::Double) ? value.object : value}
724
- else
725
- {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
726
- end
727
- else
728
- res = Hash.ordered
729
- res['@value'] = value.to_s
730
- res['@type'] = coerce(property)
731
- res
732
- end
733
- when "RDF::URI", "RDF::Node"
737
+ value = if value.is_a?(RDF::Value)
738
+ value
739
+ elsif coerce(property) == '@id'
740
+ expand_iri(value, :position => :subject, :namer => options[:namer])
741
+ else
742
+ RDF::Literal(value)
743
+ end
744
+ debug("expand_value") {"normalized: #{value.inspect}"}
745
+
746
+ result = case value
747
+ when RDF::URI, RDF::Node
734
748
  debug("URI | BNode") { value.to_s }
735
749
  {'@id' => value.to_s}
736
750
  when RDF::Literal
737
- debug("Literal")
751
+ debug("Literal") {"datatype: #{value.datatype.inspect}"}
738
752
  res = Hash.ordered
739
- res['@value'] = value.to_s
740
- res['@type'] = value.datatype.to_s if value.has_datatype?
741
- res['@language'] = value.language.to_s if value.has_language?
742
- res
743
- else
744
- debug("else")
745
- case coerce(property)
746
- when '@id'
747
- {'@id' => expand_iri(value, :position => :subject).to_s}
748
- when nil
749
- debug("expand value") {"lang(prop): #{language(property).inspect}, def: #{default_language.inspect}"}
750
- language(property) ? {"@value" => value.to_s, "@language" => language(property)} : {"@value" => value.to_s}
753
+ if options[:useNativeTypes] && [RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double].include?(value.datatype)
754
+ res['@value'] = value.object
755
+ res['@type'] = uri(coerce(property), options[:namer]) if coerce(property)
751
756
  else
752
- res = Hash.ordered
757
+ value.canonicalize! if value.datatype == RDF::XSD.double
753
758
  res['@value'] = value.to_s
754
- res['@type'] = coerce(property).to_s
755
- res
759
+ if coerce(property)
760
+ res['@type'] = uri(coerce(property), options[:namer]).to_s
761
+ elsif value.has_datatype?
762
+ res['@type'] = uri(value.datatype, options[:namer]).to_s
763
+ elsif value.has_language? || language(property)
764
+ res['@language'] = (value.language || language(property)).to_s
765
+ end
756
766
  end
767
+ res
757
768
  end
758
-
769
+
759
770
  debug {"=> #{result.inspect}"}
760
771
  result
761
772
  end
@@ -780,12 +791,14 @@ module JSON::LD
780
791
  depth(options) do
781
792
  debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
782
793
 
794
+ # Remove @annotation if property has annotation
795
+ value.delete('@annotation') if container(property) == '@annotation'
796
+
783
797
  result = case
784
- #when %w(boolean integer double).any? {|t| expand_iri(value['@type'], :position => :type) == RDF::XSD[t]}
785
- # # Compact native type
786
- # debug {" (native)"}
787
- # l = RDF::Literal(value['@value'], :datatype => expand_iri(value['@type'], :position => :type))
788
- # l.canonicalize.object
798
+ when value.has_key?('@annotation')
799
+ # Don't compact the value
800
+ debug {" (@annotation without container @annotation)"}
801
+ value
789
802
  when coerce(property) == '@id' && value.has_key?('@id')
790
803
  # Compact an @id coercion
791
804
  debug {" (@id & coerce)"}
@@ -799,11 +812,11 @@ module JSON::LD
799
812
  value[self.alias('@id')] = compact_iri(value['@id'], :position => :subject)
800
813
  debug {" (#{self.alias('@id')} => #{value['@id']})"}
801
814
  value
802
- when value['@language'] && value['@language'] == language(property)
815
+ when value['@language'] && (value['@language'] == language(property) || container(property) == '@language')
803
816
  # Compact language
804
817
  debug {" (@language) == #{language(property).inspect}"}
805
818
  value['@value']
806
- when value['@value'] && !value['@value'].is_a?(String)
819
+ when !value.fetch('@value', "").is_a?(String)
807
820
  # Compact simple literal to string
808
821
  debug {" (@value not string)"}
809
822
  value['@value']
@@ -853,27 +866,36 @@ module JSON::LD
853
866
 
854
867
  def dup
855
868
  # Also duplicate mappings, coerce and list
869
+ that = self
856
870
  ec = super
857
- ec.mappings = mappings.dup
858
- ec.coercions = coercions.dup
859
- ec.containers = containers.dup
860
- ec.languages = languages.dup
861
- ec.default_language = default_language
862
- ec.options = options
863
- ec.iri_to_term = iri_to_term.dup
864
- ec.iri_to_curie = iri_to_curie.dup
871
+ ec.instance_eval do
872
+ @mappings = that.mappings.dup
873
+ @coerceions = that.coercions.dup
874
+ @containers = that.containers.dup
875
+ @languages = that.languages.dup
876
+ @default_language = that.default_language
877
+ @options = that.options
878
+ @iri_to_term = that.iri_to_term.dup
879
+ @iri_to_curie = that.iri_to_curie.dup
880
+ end
865
881
  ec
866
882
  end
867
883
 
868
884
  private
869
885
 
870
- def uri(value, append = nil)
871
- value = RDF::URI.new(value)
872
- value = value.join(append) if append
873
- value.validate! if @options[:validate]
874
- value.canonicalize! if @options[:canonicalize]
875
- value = RDF::URI.intern(value) if @options[:intern]
876
- value
886
+ def uri(value, namer = nil)
887
+ case value.to_s
888
+ when /^_:(.*)$/
889
+ # Map BlankNodes if a namer is given
890
+ debug "uri(bnode)#{value}: #{$1}"
891
+ bnode(namer ? namer.get_sym($1) : $1)
892
+ else
893
+ value = RDF::URI.new(value)
894
+ value.validate! if @options[:validate]
895
+ value.canonicalize! if @options[:canonicalize]
896
+ value = RDF::URI.intern(value) if @options[:intern]
897
+ value
898
+ end
877
899
  end
878
900
 
879
901
  # Keep track of allocated BNodes
@@ -910,25 +932,42 @@ module JSON::LD
910
932
  elsif list?(value)
911
933
  if value['@list'].empty?
912
934
  # If the @list property is an empty array, if term has @container set to @list, term rank is 1, otherwise 0.
935
+ debug("term rank") { "empty list"}
913
936
  container(term) == '@list' ? 1 : 0
914
937
  else
915
- # Otherwise, return the sum of the term ranks for every entry in the list.
916
- depth {value['@list'].inject(0) {|memo, v| memo + term_rank(term, v)}}
938
+ debug("term rank") { "non-empty list"}
939
+ # Otherwise, return the most specific term, for which the term has some match against every value.
940
+ depth {value['@list'].map {|v| term_rank(term, v)}}.min
917
941
  end
918
942
  elsif value?(value)
919
943
  val_type = value.fetch('@type', nil)
920
944
  val_lang = value['@language'] || false if value.has_key?('@language')
921
945
  debug("term rank") {"@val_type: #{val_type.inspect}, val_lang: #{val_lang.inspect}"}
922
946
  if val_type
947
+ debug("term rank") { "typed value"}
923
948
  coerce(term) == val_type ? 3 : (default_term ? 1 : 0)
924
949
  elsif !value['@value'].is_a?(String)
950
+ debug("term rank") { "native value"}
925
951
  default_term ? 2 : 1
926
952
  elsif val_lang.nil?
927
953
  debug("val_lang.nil") {"#{language(term).inspect} && #{coerce(term).inspect}"}
928
- language(term) == false || (default_term && default_language.nil?) ? 3 : 0
954
+ if language(term) == false || (default_term && default_language.nil?)
955
+ # Value has no language, and there is no default language and the term has no language
956
+ 3
957
+ elsif default_term
958
+ # The term has no language (or type), but it's different than the default
959
+ 2
960
+ else
961
+ 0
962
+ end
929
963
  else
930
- if val_lang == language(term) || (default_term && default_language == val_lang)
964
+ debug("val_lang") {"#{language(term).inspect} && #{coerce(term).inspect}"}
965
+ if val_lang && container(term) == '@language'
931
966
  3
967
+ elsif val_lang == language(term) || (default_term && default_language == val_lang)
968
+ 2
969
+ elsif default_term && container(term) == '@set'
970
+ 2 # Choose a set term before a non-set term, if there's a language
932
971
  elsif default_term
933
972
  1
934
973
  else
@@ -936,6 +975,7 @@ module JSON::LD
936
975
  end
937
976
  end
938
977
  else # node definition/reference
978
+ debug("node dev/ref")
939
979
  coerce(term) == '@id' ? 3 : (default_term ? 1 : 0)
940
980
  end
941
981