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,126 @@
1
+ module JSON::LD
2
+ module Compact
3
+ include Utils
4
+
5
+ ##
6
+ # Compact an expanded Array or Hash given an active property and a context.
7
+ #
8
+ # @param [Array, Hash] element
9
+ # @param [String] property (nil)
10
+ # @param [EvaluationContext] context
11
+ # @return [Array, Hash]
12
+ def compact(element, property = nil)
13
+ if property.nil?
14
+ debug("compact") {"element: #{element.inspect}, ec: #{context.inspect}"}
15
+ else
16
+ debug("compact") {"property: #{property.inspect}"}
17
+ end
18
+ case element
19
+ when Array
20
+ # 1) If value is an array, process each item in value recursively using
21
+ # this algorithm, passing copies of the active context and the
22
+ # active property.
23
+ debug("compact") {"Array #{element.inspect}"}
24
+ result = depth {element.map {|v| compact(v, property)}}
25
+
26
+ # If element has a single member and the active property has no
27
+ # @container mapping to @list or @set, the compacted value is that
28
+ # member; otherwise the compacted value is element
29
+ if result.length == 1
30
+ debug("=> extract single element: #{result.first.inspect}")
31
+ result.first
32
+ else
33
+ debug("=> array result: #{result.inspect}")
34
+ result
35
+ end
36
+ when Hash
37
+ # 2) Otherwise, if element is an object:
38
+ result = {}
39
+
40
+ if k = %w(@list @set @value).detect {|container| element.has_key?(container)}
41
+ debug("compact") {"#{k}: container(#{property}) = #{context.container(property)}"}
42
+ end
43
+
44
+ k ||= '@id' if element.keys == ['@id']
45
+
46
+ case k
47
+ when '@value', '@id'
48
+ # If element has an @value property or element is a subject reference, return the result of performing
49
+ # Value Compaction on element using active property.
50
+ v = context.compact_value(property, element, :depth => @depth)
51
+ debug("compact") {"value optimization, return as #{v.inspect}"}
52
+ return v
53
+ when '@list'
54
+ # 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 and removing null values.
55
+ compacted_key = context.compact_iri(k, :position => :predicate, :depth => @depth)
56
+ v = depth { compact(element[k], property) }
57
+
58
+ # Return either the result as an array, as an object with a key of @list (or appropriate alias from active context
59
+ v = [v].compact unless v.is_a?(Array)
60
+ v = {compacted_key => v} unless context.container(property) == k
61
+ debug("compact") {"@list result, return as #{v.inspect}"}
62
+ return v
63
+ end
64
+
65
+ # Otherwise, for each property and value in element:
66
+ element.each do |key, value|
67
+ debug("compact") {"#{key}: #{value.inspect}"}
68
+
69
+ if %(@id @type).include?(key)
70
+ compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
71
+ result[compacted_key] = case value
72
+ when String
73
+ # If value is a string, the compacted value is the result of performing IRI Compaction on value.
74
+ debug {" => compacted string for #{key}"}
75
+ context.compact_iri(value, :position => :subject, :depth => @depth)
76
+ when Array
77
+ # 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
78
+ compacted_value = value.map {|v| context.compact_iri(v, :position => :subject, :depth => @depth)}
79
+ debug {" => compacted value(#{key}): #{compacted_value.inspect}"}
80
+ compacted_value = compacted_value.first if compacted_value.length == 1
81
+ compacted_value
82
+ end
83
+ else
84
+ if value.empty?
85
+ # Make sure that an empty array is preserved
86
+ compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
87
+ result[compacted_key] = value
88
+ end
89
+
90
+ # For each item in value:
91
+ raise ProcessingError, "found #{value.inspect} for #{key} if #{element.inspect}" unless value.is_a?(Array)
92
+ value.each do |item|
93
+ compacted_key = context.compact_iri(key, :position => :predicate, :value => item, :depth => @depth)
94
+ debug {" => compacted key: #{compacted_key.inspect} for #{item.inspect}"}
95
+
96
+ compacted_item = depth {self.compact(item, compacted_key)}
97
+ debug {" => compacted value: #{compacted_value.inspect}"}
98
+
99
+ case result[compacted_key]
100
+ when Array
101
+ result[compacted_key] << compacted_item
102
+ when nil
103
+ if !compacted_value.is_a?(Array) && context.container(compacted_key) == '@set'
104
+ compacted_item = [compacted_item].compact
105
+ debug {" => as @set: #{compacted_item.inspect}"}
106
+ end
107
+ result[compacted_key] = compacted_item
108
+ else
109
+ result[compacted_key] = [result[compacted_key], compacted_item]
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ # Re-order result keys
116
+ r = Hash.ordered
117
+ result.keys.sort.each {|k| r[k] = result[k]}
118
+ r
119
+ else
120
+ # For other types, the compacted value is the element value
121
+ debug("compact") {element.class.to_s}
122
+ element
123
+ end
124
+ end
125
+ end
126
+ end
@@ -4,6 +4,8 @@ require 'bigdecimal'
4
4
 
5
5
  module JSON::LD
6
6
  class EvaluationContext # :nodoc:
7
+ include Utils
8
+
7
9
  # The base.
8
10
  #
9
11
  # The document base IRI, used for expanding relative IRIs.
@@ -39,17 +41,30 @@ module JSON::LD
39
41
 
40
42
  # List coercion
41
43
  #
42
- # The @list keyword is used to specify that properties having an array value are to be treated
43
- # as an ordered list, rather than a normal unordered list
44
- # @attr [Hash{String => true}]
45
- attr :lists, true
44
+ # The @container keyword is used to specify how arrays are to be treated.
45
+ # A value of @list indicates that arrays of values are to be treated as an ordered list.
46
+ # A value of @set indicates that arrays are to be treated as unordered and that
47
+ # singular values are always coerced to an array form on expansion and compaction.
48
+ # @attr [Hash{String => String}]
49
+ attr :containers, true
50
+
51
+ # Language coercion
52
+ #
53
+ # The @language keyword is used to specify language coercion rules for the data. For each key in the map, the
54
+ # key is a String representation of the property for which String values will be coerced and
55
+ # the value is the language to coerce to. If no property-specific language is given,
56
+ # any default language from the context is used.
57
+ #
58
+ # @attr [Hash{String => String}]
59
+ attr :languages, true
46
60
 
47
61
  # Default language
48
62
  #
63
+ #
49
64
  # This adds a language to plain strings that aren't otherwise coerced
50
65
  # @attr [String]
51
- attr :language, true
52
-
66
+ attr :default_language, true
67
+
53
68
  # Global options used in generating IRIs
54
69
  # @attr [Hash] options
55
70
  attr :options, true
@@ -63,10 +78,11 @@ module JSON::LD
63
78
  # @yieldparam [EvaluationContext]
64
79
  # @return [EvaluationContext]
65
80
  def initialize(options = {})
66
- @base = RDF::URI(options[:base_uri]) if options[:base_uri]
81
+ @base = RDF::URI(options[:base]) if options[:base]
67
82
  @mappings = {}
68
83
  @coercions = {}
69
- @lists = {}
84
+ @containers = {}
85
+ @languages = {}
70
86
  @iri_to_curie = {}
71
87
  @iri_to_term = {
72
88
  RDF.to_uri.to_s => "rdf",
@@ -77,7 +93,7 @@ module JSON::LD
77
93
 
78
94
  # Load any defined prefixes
79
95
  (options[:prefixes] || {}).each_pair do |k, v|
80
- @iri_to_term[v.to_s] = k
96
+ @iri_to_term[v.to_s] = k unless k.nil?
81
97
  end
82
98
 
83
99
  debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
@@ -87,12 +103,14 @@ module JSON::LD
87
103
 
88
104
  # Create an Evaluation Context using an existing context as a start by parsing the input.
89
105
  #
90
- # @param [IO, Array, Hash, String] input
106
+ # @param [String, #read, Array, Hash, EvaluatoinContext] input
91
107
  # @return [EvaluationContext] context
92
108
  # @raise [InvalidContext]
93
109
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
94
110
  def parse(context)
95
111
  case context
112
+ when nil
113
+ EvaluationContext.new
96
114
  when EvaluationContext
97
115
  debug("parse") {"context: #{context.inspect}"}
98
116
  context.dup
@@ -113,7 +131,7 @@ module JSON::LD
113
131
  # Load context document, if it is a string
114
132
  ec = nil
115
133
  begin
116
- open(context.to_s) {|f| ec = parse(f)}
134
+ RDF::Util::File.open_file(context.to_s) {|f| ec = parse(f)}
117
135
  ec.provided_context = context
118
136
  debug("parse") {"=> provided_context: #{context.inspect}"}
119
137
  ec
@@ -134,33 +152,36 @@ module JSON::LD
134
152
  when Hash
135
153
  new_ec = self.dup
136
154
  new_ec.provided_context = context
137
- debug("parse") {"=> provided_context: #{context.inspect}"}
138
155
 
139
156
  num_updates = 1
140
157
  while num_updates > 0 do
141
158
  num_updates = 0
142
159
 
143
- # Map terms to IRIs first
160
+ # Map terms to IRIs/keywords first
144
161
  context.each do |key, value|
145
162
  # Expand a string value, unless it matches a keyword
146
163
  debug("parse") {"Hash[#{key}] = #{value.inspect}"}
147
- if (new_ec.mapping(key) || key) == '@language'
148
- new_ec.language = value.to_s
164
+ if key == '@language'
165
+ new_ec.default_language = value
149
166
  elsif term_valid?(key)
167
+ # Remove all coercion information for the property
168
+ new_ec.set_coerce(key, nil)
169
+ new_ec.set_container(key, nil)
170
+ @languages.delete(key)
171
+
150
172
  # Extract IRI mapping. This is complicated, as @id may have been aliased
151
- if value.is_a?(Hash)
152
- id_key = value.keys.detect {|k| new_ec.mapping(k) == '@id'} || '@id'
153
- value = value[id_key]
154
- end
173
+ value = value.fetch('@id', nil) if value.is_a?(Hash)
155
174
  raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{value.class}" unless value.is_a?(String) || value.nil?
156
175
 
157
176
  iri = new_ec.expand_iri(value, :position => :predicate) if value.is_a?(String)
158
- if iri && new_ec.mappings[key] != iri
177
+ if iri && new_ec.mappings.fetch(key, nil) != iri
159
178
  # Record term definition
160
- new_ec.mapping(key, iri)
179
+ new_ec.set_mapping(key, iri)
161
180
  num_updates += 1
181
+ elsif value.nil?
182
+ new_ec.set_mapping(key, nil)
162
183
  end
163
- elsif !new_ec.expand_iri(key).is_a?(RDF::URI)
184
+ else
164
185
  raise InvalidContext::Syntax, "key #{key.inspect} is invalid"
165
186
  end
166
187
  end
@@ -170,37 +191,44 @@ module JSON::LD
170
191
  context.each do |key, value|
171
192
  # Expand a string value, unless it matches a keyword
172
193
  debug("parse") {"coercion/list: Hash[#{key}] = #{value.inspect}"}
173
- prop = new_ec.expand_iri(key, :position => :predicate).to_s
174
194
  case value
175
195
  when Hash
176
- # Must have one of @id, @type or @list
177
- expanded_keys = value.keys.map {|k| new_ec.mapping(k) || k}
178
- raise InvalidContext::Syntax, "mapping for #{key.inspect} missing one of @id, @type or @list" if (%w(@id @type @list) & expanded_keys).empty?
179
- raise InvalidContext::Syntax, "unknown mappings for #{key.inspect}: #{value.keys.inspect}" unless (expanded_keys - %w(@id @type @list)).empty?
196
+ # Must have one of @id, @language, @type or @container
197
+ raise InvalidContext::Syntax, "mapping for #{key.inspect} missing one of @id, @language, @type or @container" if (%w(@id @language @type @container) & value.keys).empty?
180
198
  value.each do |key2, value2|
181
- expanded_key = new_ec.mapping(key2) || key2
182
199
  iri = new_ec.expand_iri(value2, :position => :predicate) if value2.is_a?(String)
183
- case expanded_key
200
+ case key2
184
201
  when '@type'
185
202
  raise InvalidContext::Syntax, "unknown mapping for '@type' to #{value2.class}" unless value2.is_a?(String) || value2.nil?
186
- if new_ec.coerce(prop) != iri
203
+ if new_ec.coerce(key) != iri
187
204
  raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless RDF::URI(iri).absolute? || iri == '@id'
188
205
  # Record term coercion
189
- debug("parse") {"coerce #{prop.inspect} to #{iri.inspect}"}
190
- new_ec.coerce(prop, iri)
206
+ new_ec.set_coerce(key, iri)
207
+ end
208
+ when '@container'
209
+ raise InvalidContext::Syntax, "unknown mapping for '@container' to #{value2.class}" unless %w(@list @set).include?(value2)
210
+ if new_ec.container(key) != value2
211
+ debug("parse") {"container #{key.inspect} as #{value2.inspect}"}
212
+ new_ec.set_container(key, value2)
191
213
  end
192
- when '@list'
193
- raise InvalidContext::Syntax, "unknown mapping for '@list' to #{value2.class}" unless value2.is_a?(TrueClass) || value2.is_a?(FalseClass)
194
- if new_ec.list(prop) != value2
195
- debug("parse") {"list #{prop.inspect} as #{value2.inspect}"}
196
- new_ec.list(prop, value2)
214
+ when '@language'
215
+ if !new_ec.languages.has_key?(key) || new_ec.languages[key] != value2
216
+ debug("parse") {"language #{key.inspect} as #{value2.inspect}"}
217
+ new_ec.set_language(key, value2)
197
218
  end
198
219
  end
199
220
  end
200
- when String
221
+
222
+ # If value has no @id, create a mapping from key
223
+ # to the expanded key IRI
224
+ unless value.has_key?('@id')
225
+ iri = new_ec.expand_iri(key, :position => :predicate)
226
+ new_ec.set_mapping(key, iri)
227
+ end
228
+ when nil, String
201
229
  # handled in previous loop
202
230
  else
203
- raise InvalidContext::Syntax, "attemp to map #{key.inspect} to #{value.class}"
231
+ raise InvalidContext::Syntax, "attempt to map #{key.inspect} to #{value.class}"
204
232
  end
205
233
  end
206
234
 
@@ -224,50 +252,52 @@ module JSON::LD
224
252
  else
225
253
  debug("serlialize: generate context")
226
254
  debug {"=> context: #{inspect}"}
227
- ctx = Hash.new
228
- ctx['@language'] = language.to_s if language
255
+ ctx = Hash.ordered
256
+ ctx['@language'] = default_language.to_s if default_language
229
257
 
230
- # Prefixes
231
- mappings.keys.sort {|a,b| a.to_s <=> b.to_s}.each do |k|
258
+ # Mappings
259
+ mappings.keys.sort{|a, b| a.to_s <=> b.to_s}.each do |k|
232
260
  next unless term_valid?(k.to_s)
233
261
  debug {"=> mappings[#{k}] => #{mappings[k]}"}
234
- ctx[k.to_s] = mappings[k].to_s
262
+ ctx[k] = mappings[k].to_s
235
263
  end
236
264
 
237
- unless coercions.empty? && lists.empty?
265
+ unless coercions.empty? && containers.empty? && languages.empty?
238
266
  # Coerce
239
- (coercions.keys + lists.keys).uniq.sort.each do |k|
240
- next if ['@type', RDF.type.to_s].include?(k.to_s)
241
-
242
- k_iri = compact_iri(k, :position => :predicate, :depth => @depth).to_s
243
- k_prefix = k_iri.split(':').first
267
+ (coercions.keys + containers.keys + languages.keys).uniq.sort.each do |k|
268
+ next if k == '@type'
244
269
 
245
270
  # Turn into long form
246
- ctx[k_iri] ||= Hash.new
247
- if ctx[k_iri].is_a?(String)
248
- defn = Hash.new
249
- defn[self.alias("@id")] = ctx[k_iri]
250
- ctx[k_iri] = defn
271
+ ctx[k] ||= Hash.ordered
272
+ if ctx[k].is_a?(String)
273
+ defn = Hash.ordered
274
+ defn["@id"] = compact_iri(ctx[k], :position => :subject, :not_term => true)
275
+ ctx[k] = defn
251
276
  end
252
277
 
253
278
  debug {"=> coerce(#{k}) => #{coerce(k)}"}
254
279
  if coerce(k) && !NATIVE_DATATYPES.include?(coerce(k))
255
- # If coercion doesn't depend on any prefix definitions, it can be folded into the first context block
256
- dt = compact_iri(coerce(k), :position => :datatype, :depth => @depth)
280
+ dt = coerce(k)
281
+ dt = compact_iri(dt, :position => :datatype) unless dt == '@id'
257
282
  # Fold into existing definition
258
- ctx[k_iri][self.alias("@type")] = dt
259
- debug {"=> reuse datatype[#{k_iri}] => #{dt}"}
283
+ ctx[k]["@type"] = dt
284
+ debug {"=> datatype[#{k}] => #{dt}"}
285
+ end
286
+
287
+ debug {"=> container(#{k}) => #{container(k)}"}
288
+ if %w(@list @set).include?(container(k))
289
+ ctx[k]["@container"] = container(k)
290
+ debug {"=> container[#{k}] => #{container(k).inspect}"}
260
291
  end
261
292
 
262
- debug {"=> list(#{k}) => #{list(k)}"}
263
- if list(k)
264
- # It is not dependent on previously defined terms, fold into existing definition
265
- ctx[k_iri][self.alias("@list")] = true
266
- debug {"=> reuse list_range[#{k_iri}] => true"}
293
+ debug {"=> language(#{k}) => #{language(k)}"}
294
+ if language(k) != default_language
295
+ ctx[k]["@language"] = language(k) ? language(k) : nil
296
+ debug {"=> language[#{k}] => #{language(k).inspect}"}
267
297
  end
268
298
 
269
299
  # Remove an empty definition
270
- ctx.delete(k_iri) if ctx[k_iri].empty?
300
+ ctx.delete(k) if ctx[k].empty?
271
301
  end
272
302
  end
273
303
 
@@ -276,30 +306,46 @@ module JSON::LD
276
306
  end
277
307
 
278
308
  # Return hash with @context, or empty
279
- r = Hash.new
309
+ r = Hash.ordered
280
310
  r['@context'] = use_context unless use_context.nil? || use_context.empty?
281
311
  r
282
312
  end
283
313
  end
284
314
 
285
315
  ##
286
- # Retrieve term mapping, add it if `value` is provided
316
+ # Retrieve term mapping
287
317
  #
288
318
  # @param [String, #to_s] term
289
- # @param [RDF::URI, String] value (nil)
290
319
  #
291
320
  # @return [RDF::URI, String]
292
- def mapping(term, value = nil)
321
+ def mapping(term)
322
+ @mappings.fetch(term.to_s, nil)
323
+ end
324
+
325
+ ##
326
+ # Set term mapping
327
+ #
328
+ # @param [String] term
329
+ # @param [RDF::URI, String] value
330
+ #
331
+ # @return [RDF::URI, String]
332
+ def set_mapping(term, value)
333
+ # raise InvalidContext::Syntax, "mapping term #{term.inspect} must be a string" unless term.is_a?(String)
334
+ # raise InvalidContext::Syntax, "mapping value #{value.inspect} must be an RDF::URI" unless value.nil? || value.to_s[0,1] == '@' || value.is_a?(RDF::URI)
335
+ debug {"map #{term.inspect} to #{value}"} unless @mappings[term] == value
336
+ iri_to_term.delete(@mappings[term].to_s) if @mappings[term]
293
337
  if value
294
- debug {"map #{term.inspect} to #{value}"} unless @mappings[term.to_s] == value
295
- @mappings[term.to_s] = value
338
+ @mappings[term] = value
339
+ @options[:prefixes][term] = value if @options.has_key?(:prefixes)
296
340
  iri_to_term[value.to_s] = term
341
+ else
342
+ @mappings.delete(term)
343
+ nil
297
344
  end
298
- @mappings.has_key?(term.to_s) && @mappings[term.to_s]
299
345
  end
300
346
 
301
347
  ##
302
- # Revered term mapping, typically used for finding aliases for keys.
348
+ # Reverse term mapping, typically used for finding aliases for keys.
303
349
  #
304
350
  # Returns either the original value, or a mapping for this value.
305
351
  #
@@ -307,51 +353,90 @@ module JSON::LD
307
353
  # {"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}
308
354
  #
309
355
  # @param [RDF::URI, String] value
310
- # @return [RDF::URI, String]
356
+ # @return [String]
311
357
  def alias(value)
312
- @mappings.invert.fetch(value, value)
358
+ iri_to_term.fetch(value, value)
313
359
  end
314
-
360
+
315
361
  ##
316
- # Retrieve term coercion, add it if `value` is provided
362
+ # Retrieve term coercion
317
363
  #
318
- # @param [String] property in full IRI string representation
319
- # @param [RDF::URI, '@id'] value (nil)
364
+ # @param [String] property in unexpanded form
320
365
  #
321
366
  # @return [RDF::URI, '@id']
322
- def coerce(property, value = nil)
367
+ def coerce(property)
323
368
  # Map property, if it's not an RDF::Value
324
- debug("coerce") {"map #{property} to #{mapping(property)}"} if mapping(property)
325
- property = mapping(property) if mapping(property)
326
369
  return '@id' if [RDF.type, '@type'].include?(property) # '@type' always is an IRI
370
+ @coercions.fetch(property, nil)
371
+ end
372
+
373
+ ##
374
+ # Set term coercion
375
+ #
376
+ # @param [String] property in unexpanded form
377
+ # @param [RDF::URI, '@id'] value
378
+ #
379
+ # @return [RDF::URI, '@id']
380
+ def set_coerce(property, value)
381
+ debug {"coerce #{property.inspect} to #{value.inspect}"} unless @coercions[property.to_s] == value
327
382
  if value
328
- debug {"coerce #{property.inspect} to #{value}"} unless @coercions[property.to_s] == value
329
- @coercions[property.to_s] = value
383
+ @coercions[property] = value
384
+ else
385
+ @coercions.delete(property)
330
386
  end
331
- @coercions[property.to_s] if @coercions.has_key?(property.to_s)
332
387
  end
333
388
 
334
389
  ##
335
- # Retrieve list mapping, add it if `value` is provided
390
+ # Retrieve container mapping, add it if `value` is provided
391
+ #
392
+ # @param [String] property in unexpanded form
393
+ # @return [String]
394
+ def container(property)
395
+ @containers.fetch(property.to_s, nil)
396
+ end
397
+
398
+ ##
399
+ # Set container mapping
336
400
  #
337
- # @param [String] property in full IRI string representation
338
- # @param [Boolean] value (nil)
401
+ # @param [String] property
402
+ # @param [String] value one of @list, @set or nil
339
403
  # @return [Boolean]
340
- def list(property, value = nil)
341
- unless value.nil?
342
- debug {"coerce #{property.inspect} to @list"} unless @lists[property.to_s] == value
343
- @lists[property.to_s] = value
404
+ def set_container(property, value)
405
+ return if @containers[property.to_s] == value
406
+ debug {"coerce #{property.inspect} to #{value.inspect}"}
407
+ if value
408
+ @containers[property.to_s] = value
409
+ else
410
+ @containers.delete(value)
344
411
  end
345
- @lists[property.to_s] && @lists[property.to_s]
346
412
  end
347
413
 
348
414
  ##
349
- # Determine if `term` is a suitable term
415
+ # Retrieve the language associated with a property, or the default language otherwise
416
+ # @return [String]
417
+ def language(property)
418
+ @languages.fetch(property.to_s, @default_language) if !coerce(property)
419
+ end
420
+
421
+ ##
422
+ # Set language mapping
423
+ #
424
+ # @param [String] property
425
+ # @param [String] value
426
+ # @return [String]
427
+ def set_language(property, value)
428
+ # Use false for nil language
429
+ @languages[property.to_s] = value ? value : false
430
+ end
431
+
432
+ ##
433
+ # Determine if `term` is a suitable term.
434
+ # Term may be any valid JSON string.
350
435
  #
351
436
  # @param [String] term
352
437
  # @return [Boolean]
353
438
  def term_valid?(term)
354
- term.empty? || term.match(NC_REGEXP)
439
+ term.is_a?(String)
355
440
  end
356
441
 
357
442
  ##
@@ -368,15 +453,22 @@ module JSON::LD
368
453
  # @see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
369
454
  def expand_iri(iri, options = {})
370
455
  return iri unless iri.is_a?(String)
371
- prefix, suffix = iri.split(":", 2)
372
- debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}"}
456
+ prefix, suffix = iri.split(':', 2)
457
+ return mapping(iri) if mapping(iri) # If it's an exact match
458
+ debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}"} unless options[:quiet]
459
+ base = self.base unless [:predicate, :datatype].include?(options[:position])
373
460
  prefix = prefix.to_s
374
461
  case
375
- when prefix == '_' then bnode(suffix)
376
- when iri.to_s[0,1] == "@" then iri
377
- when mappings.has_key?(prefix) then uri(mappings[prefix] + suffix.to_s)
378
- when base then base.join(iri)
379
- else uri(iri)
462
+ when prefix == '_' && suffix then debug("=> bnode"); bnode(suffix)
463
+ when iri.to_s[0,1] == "@" then debug("=> keyword"); iri
464
+ when suffix.to_s[0,2] == '//' then debug("=> iri"); uri(iri)
465
+ when mappings.has_key?(prefix) then debug("=> curie"); uri(mappings[prefix] + suffix.to_s)
466
+ when base then debug("=> base"); base.join(iri)
467
+ else
468
+ # Otherwise, it must be an absolute IRI
469
+ u = uri(iri)
470
+ debug("=> absolute") {"#{u.inspect} abs? #{u.absolute?.inspect}"}
471
+ u if u.absolute? || [:subject, :object].include?(options[:position])
380
472
  end
381
473
  end
382
474
 
@@ -387,17 +479,123 @@ module JSON::LD
387
479
  # @param [Hash{Symbol => Object}] options ({})
388
480
  # @option options [:subject, :predicate, :object, :datatype] position
389
481
  # Useful when determining how to serialize.
482
+ # @option options [Object] :value
483
+ # Value, used to select among various maps for the same IRI
484
+ # @option options [Boolean] :not_term (false)
485
+ # Don't return a term, but only a CURIE or IRI.
390
486
  #
391
487
  # @return [String] compacted form of IRI
392
488
  # @see http://json-ld.org/spec/latest/json-ld-api/#iri-compaction
393
489
  def compact_iri(iri, options = {})
394
- return iri.to_s if [RDF.first, RDF.rest, RDF.nil].include?(iri) # Don't cause these to be compacted
490
+ # Don't cause these to be compacted
491
+ return iri.to_s if [RDF.first, RDF.rest, RDF.nil].include?(iri)
492
+ return self.alias('@type') if options[:position] == :predicate && iri == RDF.type
395
493
 
396
494
  depth(options) do
397
- debug {"compact_iri(#{options.inspect}, #{iri.inspect})"}
495
+ debug {"compact_iri(#{iri.inspect}, #{options.inspect})"}
496
+
497
+ value = options.fetch(:value, nil)
498
+
499
+ # Get a list of terms which map to iri
500
+ terms = mappings.keys.select {|t| mapping(t).to_s == iri}
501
+
502
+ # Create an association term map for terms to their associated
503
+ # term rank.
504
+ term_map = {}
505
+
506
+ # If value is a @list add a term rank for each
507
+ # term mapping to iri which has @container @list.
508
+ debug("compact_iri", "#{value.inspect} is a list? #{list?(value).inspect}")
509
+ if list?(value)
510
+ list_terms = terms.select {|t| container(t) == '@list'}
511
+
512
+ term_map = list_terms.inject({}) do |memo, t|
513
+ memo[t] = term_rank(t, value)
514
+ memo
515
+ end unless list_terms.empty?
516
+ debug("term map") {"remove zero rank terms: #{term_map.keys.select {|t| term_map[t] == 0}}"} if term_map.any? {|t,r| r == 0}
517
+ term_map.delete_if {|t, r| r == 0}
518
+ end
519
+
520
+ # Otherwise, value is @value or a native type.
521
+ # Add a term rank for each term mapping to iri
522
+ # which does not have @container @list
523
+ if term_map.empty?
524
+ non_list_terms = terms.reject {|t| container(t) == '@list'}
525
+
526
+ # If value is a @list, exclude from term map those terms
527
+ # with @container @set
528
+ non_list_terms.reject {|t| container(t) == '@set'} if list?(value)
529
+
530
+ term_map = non_list_terms.inject({}) do |memo, t|
531
+ memo[t] = term_rank(t, value)
532
+ memo
533
+ end unless non_list_terms.empty?
534
+ debug("term map") {"remove zero rank terms: #{term_map.keys.select {|t| term_map[t] == 0}}"} if term_map.any? {|t,r| r == 0}
535
+ term_map.delete_if {|t, r| r == 0}
536
+ end
537
+
538
+ # If we don't want terms, remove anything that's not a CURIE or IRI
539
+ term_map.keep_if {|t, v| t.index(':') } if options.fetch(:not_term, false)
540
+
541
+ # Find terms having the greatest term match value
542
+ least_distance = term_map.values.max
543
+ terms = term_map.keys.select {|t| term_map[t] == least_distance}
544
+
545
+ # If the list of found terms is empty, append a compact IRI for
546
+ # each term which is a prefix of iri which does not have
547
+ # @type coercion, @container coercion or @language coercion rules
548
+ # along with the iri itself.
549
+ if terms.empty?
550
+ curies = mappings.keys.map {|k| iri.to_s.sub(mapping(k).to_s, "#{k}:") if
551
+ iri.to_s.index(mapping(k).to_s) == 0 &&
552
+ iri.to_s != mapping(k).to_s}.compact
553
+
554
+ debug("curies") do
555
+ curies.map do |c|
556
+ "#{c}: " +
557
+ "container: #{container(c).inspect}, " +
558
+ "coerce: #{coerce(c).inspect}, " +
559
+ "lang: #{language(c).inspect}"
560
+ end.inspect
561
+ end
398
562
 
399
- result = self.alias('@type') if options[:position] == :predicate && iri == RDF.type
400
- result ||= get_curie(iri) || self.alias(iri.to_s)
563
+ terms = curies.select do |curie|
564
+ container(curie) != '@list' &&
565
+ coerce(curie).nil? &&
566
+ language(curie) == default_language
567
+ end
568
+
569
+ debug("curies") {"selected #{terms.inspect}"}
570
+
571
+ # If we still don't have any terms and we're using standard_prefixes,
572
+ # try those, and add to mapping
573
+ if terms.empty? && @options[:standard_prefixes]
574
+ terms = RDF::Vocabulary.
575
+ select {|v| iri.index(v.to_uri.to_s) == 0}.
576
+ map do |v|
577
+ prefix = v.__name__.to_s.split('::').last.downcase
578
+ set_mapping(prefix, v.to_uri.to_s)
579
+ iri.sub(v.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
580
+ end
581
+ debug("curies") {"using standard prefies: #{terms.inspect}"}
582
+ end
583
+
584
+ terms << iri.to_s
585
+ end
586
+
587
+ # Get the first term based on distance and lexecographical order
588
+ # Prefer terms that don't have @container @set over other terms, unless as set is true
589
+ terms = terms.sort do |a, b|
590
+ debug("term sort") {"c(a): #{container(a).inspect}, c(b): #{container(b)}"}
591
+ if a.length == b.length
592
+ a <=> b
593
+ else
594
+ a.length <=> b.length
595
+ end
596
+ end
597
+ debug("sorted terms") {terms.inspect}
598
+ result = terms.first
401
599
 
402
600
  debug {"=> #{result.inspect}"}
403
601
  result
@@ -407,11 +605,11 @@ module JSON::LD
407
605
  ##
408
606
  # Expand a value from compacted to expanded form making the context
409
607
  # unnecessary. This method is used as part of more general expansion
410
- # and operates on RHS values, using a supplied key to determine @type and @list
608
+ # and operates on RHS values, using a supplied key to determine @type and @container
411
609
  # coercion rules.
412
610
  #
413
- # @param [RDF::URI] predicate
414
- # Associated predicate used to find coercion rules
611
+ # @param [String] property
612
+ # Associated property used to find coercion rules
415
613
  # @param [Hash, String] value
416
614
  # Value (literal or IRI) to be expanded
417
615
  # @param [Hash{Symbol => Object}] options
@@ -419,39 +617,70 @@ module JSON::LD
419
617
  # @return [Hash] Object representation of value
420
618
  # @raise [RDF::ReaderError] if the iri cannot be expanded
421
619
  # @see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
422
- def expand_value(predicate, value, options = {})
620
+ def expand_value(property, value, options = {})
423
621
  depth(options) do
424
- debug("expand_value") {"predicate: #{predicate}, value: #{value.inspect}, coerce: #{coerce(predicate).inspect}"}
622
+ debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
425
623
  result = case value
426
624
  when TrueClass, FalseClass, RDF::Literal::Boolean
427
- {"@literal" => value.to_s, "@type" => RDF::XSD.boolean.to_s}
625
+ case coerce(property)
626
+ when RDF::XSD.double.to_s
627
+ {"@value" => value.to_s, "@type" => RDF::XSD.double.to_s}
628
+ else
629
+ # Unless there's coercion, to not modify representation
630
+ value.is_a?(RDF::Literal::Boolean) ? value.object : value
631
+ end
428
632
  when Integer, RDF::Literal::Integer
429
- {"@literal" => value.to_s, "@type" => RDF::XSD.integer.to_s}
430
- when BigDecimal, RDF::Literal::Decimal
431
- {"@literal" => value.to_s, "@type" => RDF::XSD.decimal.to_s}
633
+ case coerce(property)
634
+ when RDF::XSD.double.to_s
635
+ {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
636
+ when RDF::XSD.integer.to_s, nil
637
+ # Unless there's coercion, to not modify representation
638
+ value.is_a?(RDF::Literal::Integer) ? value.object : value
639
+ else
640
+ res = Hash.ordered
641
+ res['@value'] = value.to_s
642
+ res['@type'] = coerce(property)
643
+ res
644
+ end
432
645
  when Float, RDF::Literal::Double
433
- {"@literal" => value.to_s, "@type" => RDF::XSD.double.to_s}
646
+ case coerce(property)
647
+ when RDF::XSD.integer.to_s
648
+ {"@value" => value.to_int.to_s, "@type" => RDF::XSD.integer.to_s}
649
+ when RDF::XSD.double.to_s
650
+ {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
651
+ when nil
652
+ # Unless there's coercion, to not modify representation
653
+ value.is_a?(RDF::Literal::Double) ? value.object : value
654
+ else
655
+ res = Hash.ordered
656
+ res['@value'] = value.to_s
657
+ res['@type'] = coerce(property)
658
+ res
659
+ end
660
+ when BigDecimal, RDF::Literal::Decimal
661
+ {"@value" => value.to_s, "@type" => RDF::XSD.decimal.to_s}
434
662
  when Date, Time, DateTime
435
663
  l = RDF::Literal(value)
436
- {"@literal" => l.to_s, "@type" => l.datatype.to_s}
437
- when RDF::URI
664
+ {"@value" => l.to_s, "@type" => l.datatype.to_s}
665
+ when RDF::URI, RDF::Node
438
666
  {'@id' => value.to_s}
439
667
  when RDF::Literal
440
- res = Hash.new
441
- res['@literal'] = value.to_s
668
+ res = Hash.ordered
669
+ res['@value'] = value.to_s
442
670
  res['@type'] = value.datatype.to_s if value.has_datatype?
443
671
  res['@language'] = value.language.to_s if value.has_language?
444
672
  res
445
673
  else
446
- case coerce(predicate)
674
+ case coerce(property)
447
675
  when '@id'
448
676
  {'@id' => expand_iri(value, :position => :object).to_s}
449
677
  when nil
450
- language ? {"@literal" => value.to_s, "@language" => language.to_s} : value.to_s
678
+ debug("expand value") {"lang(prop): #{language(property).inspect}, def: #{default_language.inspect}"}
679
+ language(property) ? {"@value" => value.to_s, "@language" => language(property)} : value.to_s
451
680
  else
452
- res = Hash.new
453
- res['@literal'] = value.to_s
454
- res['@type'] = coerce(predicate).to_s
681
+ res = Hash.ordered
682
+ res['@value'] = value.to_s
683
+ res['@type'] = coerce(property).to_s
455
684
  res
456
685
  end
457
686
  end
@@ -464,8 +693,8 @@ module JSON::LD
464
693
  ##
465
694
  # Compact a value
466
695
  #
467
- # @param [RDF::URI] predicate
468
- # Associated predicate used to find coercion rules
696
+ # @param [String] property
697
+ # Associated property used to find coercion rules
469
698
  # @param [Hash] value
470
699
  # Value (literal or IRI), in full object representation, to be compacted
471
700
  # @param [Hash{Symbol => Object}] options
@@ -473,43 +702,43 @@ module JSON::LD
473
702
  # @return [Hash] Object representation of value
474
703
  # @raise [ProcessingError] if the iri cannot be expanded
475
704
  # @see http://json-ld.org/spec/latest/json-ld-api/#value-compaction
476
- def compact_value(predicate, value, options = {})
477
- raise ProcessingError::Lossy, "attempt to compact a non-object value" unless value.is_a?(Hash)
705
+ def compact_value(property, value, options = {})
706
+ raise ProcessingError::Lossy, "attempt to compact a non-object value: #{value.inspect}" unless value.is_a?(Hash)
478
707
 
479
708
  depth(options) do
480
- debug("compact_value") {"predicate: #{predicate.inspect}, value: #{value.inspect}, coerce: #{coerce(predicate).inspect}"}
709
+ debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
481
710
 
482
711
  result = case
483
712
  when %w(boolean integer double).any? {|t| expand_iri(value['@type'], :position => :datatype) == RDF::XSD[t]}
484
713
  # Compact native type
485
714
  debug {" (native)"}
486
- l = RDF::Literal(value['@literal'], :datatype => expand_iri(value['@type'], :position => :datatype))
715
+ l = RDF::Literal(value['@value'], :datatype => expand_iri(value['@type'], :position => :datatype))
487
716
  l.canonicalize.object
488
- when coerce(predicate) == '@id' && value.has_key?('@id')
717
+ when coerce(property) == '@id' && value.has_key?('@id')
489
718
  # Compact an @id coercion
490
719
  debug {" (@id & coerce)"}
491
720
  compact_iri(value['@id'], :position => :object)
492
- when value['@type'] && expand_iri(value['@type'], :position => :datatype) == coerce(predicate)
721
+ when value['@type'] && expand_iri(value['@type'], :position => :datatype) == coerce(property)
493
722
  # Compact common datatype
494
- debug {" (@type & coerce) == #{coerce(predicate)}"}
495
- value['@literal']
723
+ debug {" (@type & coerce) == #{coerce(property)}"}
724
+ value['@value']
496
725
  when value.has_key?('@id')
497
726
  # Compact an IRI
498
- value['@id'] = compact_iri(value['@id'], :position => :object)
499
- debug {" (@id => #{value['@id']})"}
727
+ value[self.alias('@id')] = compact_iri(value['@id'], :position => :object)
728
+ debug {" (#{self.alias('@id')} => #{value['@id']})"}
500
729
  value
501
- when value['@language'] && value['@language'] == language
730
+ when value['@language'] && value['@language'] == language(property)
502
731
  # Compact language
503
- debug {" (@language) == #{language}"}
504
- value['@literal']
505
- when value['@literal'] && !value['@language'] && !value['@type'] && !coerce(predicate) && !language
732
+ debug {" (@language) == #{language(property).inspect}"}
733
+ value['@value']
734
+ when value['@value'] && !value['@language'] && !value['@type'] && !coerce(property) && !default_language
506
735
  # Compact simple literal to string
507
- debug {" (@literal && !@language && !@type && !coerce && !language)"}
508
- value['@literal']
736
+ debug {" (@value && !@language && !@type && !coerce && !language)"}
737
+ value['@value']
509
738
  when value['@type']
510
739
  # Compact datatype
511
740
  debug {" (@type)"}
512
- value['@type'] = compact_iri(value['@type'], :position => :datatype)
741
+ value[self.alias('@type')] = compact_iri(value['@type'], :position => :datatype)
513
742
  value
514
743
  else
515
744
  # Otherwise, use original value
@@ -534,9 +763,11 @@ module JSON::LD
534
763
 
535
764
  def inspect
536
765
  v = %w([EvaluationContext)
766
+ v << "def_language=#{default_language}"
767
+ v << "languages[#{languages.keys.length}]=#{languages}"
537
768
  v << "mappings[#{mappings.keys.length}]=#{mappings}"
538
769
  v << "coercions[#{coercions.keys.length}]=#{coercions}"
539
- v << "lists[#{lists.length}]=#{lists}"
770
+ v << "containers[#{containers.length}]=#{containers}"
540
771
  v.join(", ") + "]"
541
772
  end
542
773
 
@@ -545,8 +776,9 @@ module JSON::LD
545
776
  ec = super
546
777
  ec.mappings = mappings.dup
547
778
  ec.coercions = coercions.dup
548
- ec.lists = lists.dup
549
- ec.language = language
779
+ ec.containers = containers.dup
780
+ ec.languages = languages.dup
781
+ ec.default_language = default_language
550
782
  ec.options = options
551
783
  ec.iri_to_term = iri_to_term.dup
552
784
  ec.iri_to_curie = iri_to_curie.dup
@@ -574,67 +806,59 @@ module JSON::LD
574
806
  end
575
807
 
576
808
  ##
577
- # Return a CURIE for the IRI, or nil. Adds namespace of CURIE to defined prefixes
578
- # @param [RDF::Resource] resource
579
- # @return [String, nil] value to use to identify IRI
580
- def get_curie(resource)
581
- debug {"get_curie(#{resource.inspect})"}
582
- case resource
583
- when RDF::Node, /^_:/
584
- return resource.to_s
809
+ # Get a "match value" given a term and a value. The value
810
+ # is lowest when the relative match between the term and the value
811
+ # is closest.
812
+ #
813
+ # @param [String] term
814
+ # @param [Object] value
815
+ # @return [Integer]
816
+ def term_rank(term, value)
817
+ debug("term rank") { "term: #{term.inspect}, value: #{value.inspect}"}
818
+ debug("term rank") { "coerce: #{coerce(term).inspect}, lang: #{languages.fetch(term, nil).inspect}"}
819
+
820
+ # A term without @language or @type can be used with rank 1 for any value
821
+ default_term = !coerce(term) && !languages.has_key?(term)
822
+ debug("term rank") { "default_term: #{default_term.inspect}"}
823
+
824
+ rank = case value
825
+ when TrueClass, FalseClass
826
+ coerce(term) == RDF::XSD.boolean.to_s ? 3 : (default_term ? 2 : 1)
827
+ when Integer
828
+ coerce(term) == RDF::XSD.integer.to_s ? 3 : (default_term ? 2 : 1)
829
+ when Float
830
+ coerce(term) == RDF::XSD.double.to_s ? 3 : (default_term ? 2 : 1)
831
+ when nil
832
+ # A value of null probably means it's an @id
833
+ 3
585
834
  when String
586
- iri = resource
587
- resource = RDF::URI(resource)
588
- return nil unless resource.absolute?
589
- when RDF::URI
590
- iri = resource.to_s
591
- return iri if options[:expand]
592
- else
593
- return nil
594
- end
595
-
596
- curie = case
597
- when iri_to_curie.has_key?(iri)
598
- return iri_to_curie[iri]
599
- when u = iri_to_term.keys.detect {|i| iri.index(i.to_s) == 0}
600
- # Use a defined prefix
601
- prefix = iri_to_term[u]
602
- mapping(prefix, u)
603
- iri.sub(u.to_s, "#{prefix}:").sub(/:$/, '')
604
- when @options[:standard_prefixes] && vocab = RDF::Vocabulary.detect {|v| iri.index(v.to_uri.to_s) == 0}
605
- prefix = vocab.__name__.to_s.split('::').last.downcase
606
- mapping(prefix, vocab.to_uri.to_s)
607
- iri.sub(vocab.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
835
+ # When compacting a string, the string has no language, so the term can be used if the term has @language null or it is a default term and there is no default language
836
+ debug("term rank") {"string: lang: #{languages.fetch(term, false).inspect}, def: #{default_language.inspect}"}
837
+ !languages.fetch(term, true) || (default_term && !default_language) ? 3 : 0
838
+ when Hash
839
+ if list?(value)
840
+ if value['@list'].empty?
841
+ # If the @list property is an empty array, if term has @container set to @list, term rank is 1, otherwise 0.
842
+ container(term) == '@list' ? 1 : 0
843
+ else
844
+ # Otherwise, return the sum of the term ranks for every entry in the list.
845
+ depth {value['@list'].inject(0) {|memo, v| memo + term_rank(term, v)}}
846
+ end
847
+ elsif subject?(value) || subject_reference?(value)
848
+ coerce(term) == '@id' ? 3 : (default_term ? 1 : 0)
849
+ elsif val_type = value.fetch('@type', nil)
850
+ coerce(term) == val_type ? 3 : (default_term ? 1 : 0)
851
+ elsif val_lang = value.fetch('@language', nil)
852
+ val_lang == language(term) ? 3 : (default_term ? 1 : 0)
853
+ else
854
+ default_term ? 3 : 0
855
+ end
608
856
  else
609
- debug "no mapping found for #{iri} in #{iri_to_term.inspect}"
610
- nil
857
+ raise ProcessingError, "Unexpected value for term_rank: #{value.inspect}"
611
858
  end
612
859
 
613
- iri_to_curie[iri] = curie
614
- rescue Addressable::URI::InvalidURIError => e
615
- raise RDF::WriterError, "Invalid IRI #{resource.inspect}: #{e.message}"
616
- end
617
-
618
- # Add debug event to debug array, if specified
619
- #
620
- # @param [String] message
621
- # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
622
- def debug(*args)
623
- return unless ::JSON::LD.debug? || @options[:debug]
624
- list = args
625
- list << yield if block_given?
626
- message = " " * (@depth || 0) * 2 + (list.empty? ? "" : list.join(": "))
627
- puts message if JSON::LD::debug?
628
- @options[:debug] << message if @options[:debug].is_a?(Array)
629
- end
630
-
631
- # Increase depth around a method invocation
632
- def depth(options = {})
633
- old_depth = @depth || 0
634
- @depth = (options[:depth] || old_depth) + 1
635
- ret = yield
636
- @depth = old_depth
637
- ret
860
+ # If term has @container @set, and rank is not 0, increase rank by 1.
861
+ rank > 0 && container(term) == '@set' ? rank + 1 : rank
638
862
  end
639
863
  end
640
864
  end