json-ld 0.1.0 → 0.1.2

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