json-ld 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.markdown +36 -0
- data/{README → README.markdown} +1 -1
- data/VERSION +1 -1
- data/lib/json/ld.rb +92 -16
- data/lib/json/ld/api.rb +320 -0
- data/lib/json/ld/evaluation_context.rb +640 -0
- data/lib/json/ld/extensions.rb +6 -0
- data/lib/json/ld/reader.rb +120 -270
- data/lib/json/ld/writer.rb +210 -284
- metadata +53 -38
@@ -0,0 +1,640 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'json'
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module JSON::LD
|
6
|
+
class EvaluationContext # :nodoc:
|
7
|
+
# The base.
|
8
|
+
#
|
9
|
+
# The document base IRI, used for expanding relative IRIs.
|
10
|
+
#
|
11
|
+
# @attr_reader [RDF::URI]
|
12
|
+
attr_reader :base
|
13
|
+
|
14
|
+
# A list of current, in-scope mappings from term to IRI.
|
15
|
+
#
|
16
|
+
# @attr [Hash{String => String}]
|
17
|
+
attr :mappings, true
|
18
|
+
|
19
|
+
# Reverse mappings from IRI to a term or CURIE
|
20
|
+
#
|
21
|
+
# @attr [Hash{RDF::URI => String}]
|
22
|
+
attr :iri_to_curie, true
|
23
|
+
|
24
|
+
# Reverse mappings from IRI to term only for terms, not CURIEs
|
25
|
+
#
|
26
|
+
# @attr [Hash{RDF::URI => String}]
|
27
|
+
attr :iri_to_term, true
|
28
|
+
|
29
|
+
# Type coersion
|
30
|
+
#
|
31
|
+
# The @type keyword is used to specify type coersion rules for the data. For each key in the map, the
|
32
|
+
# key is a String representation of the property for which String values will be coerced and
|
33
|
+
# the value is the datatype (or @id) to coerce to. Type coersion for
|
34
|
+
# the value `@id` asserts that all vocabulary terms listed should undergo coercion to an IRI,
|
35
|
+
# including CURIE processing for compact IRI Expressions like `foaf:homepage`.
|
36
|
+
#
|
37
|
+
# @attr [Hash{String => String}]
|
38
|
+
attr :coercions, true
|
39
|
+
|
40
|
+
# List coercion
|
41
|
+
#
|
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
|
46
|
+
|
47
|
+
# Default language
|
48
|
+
#
|
49
|
+
# This adds a language to plain strings that aren't otherwise coerced
|
50
|
+
# @attr [String]
|
51
|
+
attr :language, true
|
52
|
+
|
53
|
+
# Global options used in generating IRIs
|
54
|
+
# @attr [Hash] options
|
55
|
+
attr :options, true
|
56
|
+
|
57
|
+
# A context provided to us that we can use without re-serializing
|
58
|
+
attr :provided_context, true
|
59
|
+
|
60
|
+
##
|
61
|
+
# Create new evaluation context
|
62
|
+
# @yield [ec]
|
63
|
+
# @yieldparam [EvaluationContext]
|
64
|
+
# @return [EvaluationContext]
|
65
|
+
def initialize(options = {})
|
66
|
+
@base = RDF::URI(options[:base_uri]) if options[:base_uri]
|
67
|
+
@mappings = {}
|
68
|
+
@coercions = {}
|
69
|
+
@lists = {}
|
70
|
+
@iri_to_curie = {}
|
71
|
+
@iri_to_term = {
|
72
|
+
RDF.to_uri.to_s => "rdf",
|
73
|
+
RDF::XSD.to_uri.to_s => "xsd"
|
74
|
+
}
|
75
|
+
|
76
|
+
@options = options
|
77
|
+
|
78
|
+
# Load any defined prefixes
|
79
|
+
(options[:prefixes] || {}).each_pair do |k, v|
|
80
|
+
@iri_to_term[v.to_s] = k
|
81
|
+
end
|
82
|
+
|
83
|
+
debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
|
84
|
+
|
85
|
+
yield(self) if block_given?
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create an Evaluation Context using an existing context as a start by parsing the input.
|
89
|
+
#
|
90
|
+
# @param [IO, Array, Hash, String] input
|
91
|
+
# @return [EvaluationContext] context
|
92
|
+
# @raise [InvalidContext]
|
93
|
+
# on a remote context load error, syntax error, or a reference to a term which is not defined.
|
94
|
+
def parse(context)
|
95
|
+
case context
|
96
|
+
when EvaluationContext
|
97
|
+
debug("parse") {"context: #{context.inspect}"}
|
98
|
+
context.dup
|
99
|
+
when IO, StringIO
|
100
|
+
debug("parse") {"io: #{context}"}
|
101
|
+
# Load context document, if it is a string
|
102
|
+
begin
|
103
|
+
ctx = JSON.load(context)
|
104
|
+
raise JSON::LD::InvalidContext::Syntax, "missing @context" unless ctx.is_a?(Hash) && ctx["@context"]
|
105
|
+
parse(ctx["@context"])
|
106
|
+
rescue JSON::ParserError => e
|
107
|
+
debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
|
108
|
+
raise JSON::LD::InvalidContext::Syntax, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
|
109
|
+
self.dup
|
110
|
+
end
|
111
|
+
when String, nil
|
112
|
+
debug("parse") {"remote: #{context}"}
|
113
|
+
# Load context document, if it is a string
|
114
|
+
ec = nil
|
115
|
+
begin
|
116
|
+
open(context.to_s) {|f| ec = parse(f)}
|
117
|
+
ec.provided_context = context
|
118
|
+
debug("parse") {"=> provided_context: #{context.inspect}"}
|
119
|
+
ec
|
120
|
+
rescue Exception => e
|
121
|
+
debug("parse") {"Failed to retrieve @context from remote document at #{context}: #{e.message}"}
|
122
|
+
raise JSON::LD::InvalidContext::LoadError, "Failed to parse remote context at #{context}: #{e.message}", e.backtrace if @options[:validate]
|
123
|
+
self.dup
|
124
|
+
end
|
125
|
+
when Array
|
126
|
+
# Process each member of the array in order, updating the active context
|
127
|
+
# Updates evaluation context serially during parsing
|
128
|
+
debug("parse") {"Array"}
|
129
|
+
ec = self
|
130
|
+
context.each {|c| ec = ec.parse(c)}
|
131
|
+
ec.provided_context = context
|
132
|
+
debug("parse") {"=> provided_context: #{context.inspect}"}
|
133
|
+
ec
|
134
|
+
when Hash
|
135
|
+
new_ec = self.dup
|
136
|
+
new_ec.provided_context = context
|
137
|
+
debug("parse") {"=> provided_context: #{context.inspect}"}
|
138
|
+
|
139
|
+
num_updates = 1
|
140
|
+
while num_updates > 0 do
|
141
|
+
num_updates = 0
|
142
|
+
|
143
|
+
# Map terms to IRIs first
|
144
|
+
context.each do |key, value|
|
145
|
+
# Expand a string value, unless it matches a keyword
|
146
|
+
debug("parse") {"Hash[#{key}] = #{value.inspect}"}
|
147
|
+
if (new_ec.mapping(key) || key) == '@language'
|
148
|
+
new_ec.language = value.to_s
|
149
|
+
elsif term_valid?(key)
|
150
|
+
# 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
|
155
|
+
raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{value.class}" unless value.is_a?(String) || value.nil?
|
156
|
+
|
157
|
+
iri = new_ec.expand_iri(value, :position => :predicate) if value.is_a?(String)
|
158
|
+
if iri && new_ec.mappings[key] != iri
|
159
|
+
# Record term definition
|
160
|
+
new_ec.mapping(key, iri)
|
161
|
+
num_updates += 1
|
162
|
+
end
|
163
|
+
elsif !new_ec.expand_iri(key).is_a?(RDF::URI)
|
164
|
+
raise InvalidContext::Syntax, "key #{key.inspect} is invalid"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# Next, look for coercion using new_ec
|
170
|
+
context.each do |key, value|
|
171
|
+
# Expand a string value, unless it matches a keyword
|
172
|
+
debug("parse") {"coercion/list: Hash[#{key}] = #{value.inspect}"}
|
173
|
+
prop = new_ec.expand_iri(key, :position => :predicate).to_s
|
174
|
+
case value
|
175
|
+
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?
|
180
|
+
value.each do |key2, value2|
|
181
|
+
expanded_key = new_ec.mapping(key2) || key2
|
182
|
+
iri = new_ec.expand_iri(value2, :position => :predicate) if value2.is_a?(String)
|
183
|
+
case expanded_key
|
184
|
+
when '@type'
|
185
|
+
raise InvalidContext::Syntax, "unknown mapping for '@type' to #{value2.class}" unless value2.is_a?(String) || value2.nil?
|
186
|
+
if new_ec.coerce(prop) != iri
|
187
|
+
raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless RDF::URI(iri).absolute? || iri == '@id'
|
188
|
+
# Record term coercion
|
189
|
+
debug("parse") {"coerce #{prop.inspect} to #{iri.inspect}"}
|
190
|
+
new_ec.coerce(prop, iri)
|
191
|
+
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)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
when String
|
201
|
+
# handled in previous loop
|
202
|
+
else
|
203
|
+
raise InvalidContext::Syntax, "attemp to map #{key.inspect} to #{value.class}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
new_ec
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Generate @context
|
213
|
+
#
|
214
|
+
# If a context was supplied in global options, use that, otherwise, generate one
|
215
|
+
# from this representation.
|
216
|
+
#
|
217
|
+
# @param [Hash{Symbol => Object}] options ({})
|
218
|
+
# @return [Hash]
|
219
|
+
def serialize(options = {})
|
220
|
+
depth(options) do
|
221
|
+
use_context = if provided_context
|
222
|
+
debug "serlialize: reuse context: #{provided_context.inspect}"
|
223
|
+
provided_context
|
224
|
+
else
|
225
|
+
debug("serlialize: generate context")
|
226
|
+
debug {"=> context: #{inspect}"}
|
227
|
+
ctx = Hash.new
|
228
|
+
ctx['@language'] = language.to_s if language
|
229
|
+
|
230
|
+
# Prefixes
|
231
|
+
mappings.keys.sort {|a,b| a.to_s <=> b.to_s}.each do |k|
|
232
|
+
next unless term_valid?(k.to_s)
|
233
|
+
debug {"=> mappings[#{k}] => #{mappings[k]}"}
|
234
|
+
ctx[k.to_s] = mappings[k].to_s
|
235
|
+
end
|
236
|
+
|
237
|
+
unless coercions.empty? && lists.empty?
|
238
|
+
# 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
|
244
|
+
|
245
|
+
# 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
|
251
|
+
end
|
252
|
+
|
253
|
+
debug {"=> coerce(#{k}) => #{coerce(k)}"}
|
254
|
+
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)
|
257
|
+
# Fold into existing definition
|
258
|
+
ctx[k_iri][self.alias("@type")] = dt
|
259
|
+
debug {"=> reuse datatype[#{k_iri}] => #{dt}"}
|
260
|
+
end
|
261
|
+
|
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"}
|
267
|
+
end
|
268
|
+
|
269
|
+
# Remove an empty definition
|
270
|
+
ctx.delete(k_iri) if ctx[k_iri].empty?
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
debug {"start_doc: context=#{ctx.inspect}"}
|
275
|
+
ctx
|
276
|
+
end
|
277
|
+
|
278
|
+
# Return hash with @context, or empty
|
279
|
+
r = Hash.new
|
280
|
+
r['@context'] = use_context unless use_context.nil? || use_context.empty?
|
281
|
+
r
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
##
|
286
|
+
# Retrieve term mapping, add it if `value` is provided
|
287
|
+
#
|
288
|
+
# @param [String, #to_s] term
|
289
|
+
# @param [RDF::URI, String] value (nil)
|
290
|
+
#
|
291
|
+
# @return [RDF::URI, String]
|
292
|
+
def mapping(term, value = nil)
|
293
|
+
if value
|
294
|
+
debug {"map #{term.inspect} to #{value}"} unless @mappings[term.to_s] == value
|
295
|
+
@mappings[term.to_s] = value
|
296
|
+
iri_to_term[value.to_s] = term
|
297
|
+
end
|
298
|
+
@mappings.has_key?(term.to_s) && @mappings[term.to_s]
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# Revered term mapping, typically used for finding aliases for keys.
|
303
|
+
#
|
304
|
+
# Returns either the original value, or a mapping for this value.
|
305
|
+
#
|
306
|
+
# @example
|
307
|
+
# {"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}
|
308
|
+
#
|
309
|
+
# @param [RDF::URI, String] value
|
310
|
+
# @return [RDF::URI, String]
|
311
|
+
def alias(value)
|
312
|
+
@mappings.invert.fetch(value, value)
|
313
|
+
end
|
314
|
+
|
315
|
+
##
|
316
|
+
# Retrieve term coercion, add it if `value` is provided
|
317
|
+
#
|
318
|
+
# @param [String] property in full IRI string representation
|
319
|
+
# @param [RDF::URI, '@id'] value (nil)
|
320
|
+
#
|
321
|
+
# @return [RDF::URI, '@id']
|
322
|
+
def coerce(property, value = nil)
|
323
|
+
# 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
|
+
return '@id' if [RDF.type, '@type'].include?(property) # '@type' always is an IRI
|
327
|
+
if value
|
328
|
+
debug {"coerce #{property.inspect} to #{value}"} unless @coercions[property.to_s] == value
|
329
|
+
@coercions[property.to_s] = value
|
330
|
+
end
|
331
|
+
@coercions[property.to_s] if @coercions.has_key?(property.to_s)
|
332
|
+
end
|
333
|
+
|
334
|
+
##
|
335
|
+
# Retrieve list mapping, add it if `value` is provided
|
336
|
+
#
|
337
|
+
# @param [String] property in full IRI string representation
|
338
|
+
# @param [Boolean] value (nil)
|
339
|
+
# @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
|
344
|
+
end
|
345
|
+
@lists[property.to_s] && @lists[property.to_s]
|
346
|
+
end
|
347
|
+
|
348
|
+
##
|
349
|
+
# Determine if `term` is a suitable term
|
350
|
+
#
|
351
|
+
# @param [String] term
|
352
|
+
# @return [Boolean]
|
353
|
+
def term_valid?(term)
|
354
|
+
term.empty? || term.match(NC_REGEXP)
|
355
|
+
end
|
356
|
+
|
357
|
+
##
|
358
|
+
# Expand an IRI. Relative IRIs are expanded against any document base.
|
359
|
+
#
|
360
|
+
# @param [String] iri
|
361
|
+
# A keyword, term, prefix:suffix or possibly relative IRI
|
362
|
+
# @param [Hash{Symbol => Object}] options
|
363
|
+
# @option options [:subject, :predicate, :object, :datatype] position
|
364
|
+
# Useful when determining how to serialize.
|
365
|
+
#
|
366
|
+
# @return [RDF::URI, String] IRI or String, if it's a keyword
|
367
|
+
# @raise [RDF::ReaderError] if the iri cannot be expanded
|
368
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
|
369
|
+
def expand_iri(iri, options = {})
|
370
|
+
return iri unless iri.is_a?(String)
|
371
|
+
prefix, suffix = iri.split(":", 2)
|
372
|
+
debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}"}
|
373
|
+
prefix = prefix.to_s
|
374
|
+
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)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
##
|
384
|
+
# Compact an IRI
|
385
|
+
#
|
386
|
+
# @param [RDF::URI] iri
|
387
|
+
# @param [Hash{Symbol => Object}] options ({})
|
388
|
+
# @option options [:subject, :predicate, :object, :datatype] position
|
389
|
+
# Useful when determining how to serialize.
|
390
|
+
#
|
391
|
+
# @return [String] compacted form of IRI
|
392
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#iri-compaction
|
393
|
+
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
|
395
|
+
|
396
|
+
depth(options) do
|
397
|
+
debug {"compact_iri(#{options.inspect}, #{iri.inspect})"}
|
398
|
+
|
399
|
+
result = self.alias('@type') if options[:position] == :predicate && iri == RDF.type
|
400
|
+
result ||= get_curie(iri) || self.alias(iri.to_s)
|
401
|
+
|
402
|
+
debug {"=> #{result.inspect}"}
|
403
|
+
result
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
##
|
408
|
+
# Expand a value from compacted to expanded form making the context
|
409
|
+
# 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
|
411
|
+
# coercion rules.
|
412
|
+
#
|
413
|
+
# @param [RDF::URI] predicate
|
414
|
+
# Associated predicate used to find coercion rules
|
415
|
+
# @param [Hash, String] value
|
416
|
+
# Value (literal or IRI) to be expanded
|
417
|
+
# @param [Hash{Symbol => Object}] options
|
418
|
+
#
|
419
|
+
# @return [Hash] Object representation of value
|
420
|
+
# @raise [RDF::ReaderError] if the iri cannot be expanded
|
421
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
|
422
|
+
def expand_value(predicate, value, options = {})
|
423
|
+
depth(options) do
|
424
|
+
debug("expand_value") {"predicate: #{predicate}, value: #{value.inspect}, coerce: #{coerce(predicate).inspect}"}
|
425
|
+
result = case value
|
426
|
+
when TrueClass, FalseClass, RDF::Literal::Boolean
|
427
|
+
{"@literal" => value.to_s, "@type" => RDF::XSD.boolean.to_s}
|
428
|
+
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}
|
432
|
+
when Float, RDF::Literal::Double
|
433
|
+
{"@literal" => value.to_s, "@type" => RDF::XSD.double.to_s}
|
434
|
+
when Date, Time, DateTime
|
435
|
+
l = RDF::Literal(value)
|
436
|
+
{"@literal" => l.to_s, "@type" => l.datatype.to_s}
|
437
|
+
when RDF::URI
|
438
|
+
{'@id' => value.to_s}
|
439
|
+
when RDF::Literal
|
440
|
+
res = Hash.new
|
441
|
+
res['@literal'] = value.to_s
|
442
|
+
res['@type'] = value.datatype.to_s if value.has_datatype?
|
443
|
+
res['@language'] = value.language.to_s if value.has_language?
|
444
|
+
res
|
445
|
+
else
|
446
|
+
case coerce(predicate)
|
447
|
+
when '@id'
|
448
|
+
{'@id' => expand_iri(value, :position => :object).to_s}
|
449
|
+
when nil
|
450
|
+
language ? {"@literal" => value.to_s, "@language" => language.to_s} : value.to_s
|
451
|
+
else
|
452
|
+
res = Hash.new
|
453
|
+
res['@literal'] = value.to_s
|
454
|
+
res['@type'] = coerce(predicate).to_s
|
455
|
+
res
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
debug {"=> #{result.inspect}"}
|
460
|
+
result
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
##
|
465
|
+
# Compact a value
|
466
|
+
#
|
467
|
+
# @param [RDF::URI] predicate
|
468
|
+
# Associated predicate used to find coercion rules
|
469
|
+
# @param [Hash] value
|
470
|
+
# Value (literal or IRI), in full object representation, to be compacted
|
471
|
+
# @param [Hash{Symbol => Object}] options
|
472
|
+
#
|
473
|
+
# @return [Hash] Object representation of value
|
474
|
+
# @raise [ProcessingError] if the iri cannot be expanded
|
475
|
+
# @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)
|
478
|
+
|
479
|
+
depth(options) do
|
480
|
+
debug("compact_value") {"predicate: #{predicate.inspect}, value: #{value.inspect}, coerce: #{coerce(predicate).inspect}"}
|
481
|
+
|
482
|
+
result = case
|
483
|
+
when %w(boolean integer double).any? {|t| expand_iri(value['@type'], :position => :datatype) == RDF::XSD[t]}
|
484
|
+
# Compact native type
|
485
|
+
debug {" (native)"}
|
486
|
+
l = RDF::Literal(value['@literal'], :datatype => expand_iri(value['@type'], :position => :datatype))
|
487
|
+
l.canonicalize.object
|
488
|
+
when coerce(predicate) == '@id' && value.has_key?('@id')
|
489
|
+
# Compact an @id coercion
|
490
|
+
debug {" (@id & coerce)"}
|
491
|
+
compact_iri(value['@id'], :position => :object)
|
492
|
+
when value['@type'] && expand_iri(value['@type'], :position => :datatype) == coerce(predicate)
|
493
|
+
# Compact common datatype
|
494
|
+
debug {" (@type & coerce) == #{coerce(predicate)}"}
|
495
|
+
value['@literal']
|
496
|
+
when value.has_key?('@id')
|
497
|
+
# Compact an IRI
|
498
|
+
value['@id'] = compact_iri(value['@id'], :position => :object)
|
499
|
+
debug {" (@id => #{value['@id']})"}
|
500
|
+
value
|
501
|
+
when value['@language'] && value['@language'] == language
|
502
|
+
# Compact language
|
503
|
+
debug {" (@language) == #{language}"}
|
504
|
+
value['@literal']
|
505
|
+
when value['@literal'] && !value['@language'] && !value['@type'] && !coerce(predicate) && !language
|
506
|
+
# Compact simple literal to string
|
507
|
+
debug {" (@literal && !@language && !@type && !coerce && !language)"}
|
508
|
+
value['@literal']
|
509
|
+
when value['@type']
|
510
|
+
# Compact datatype
|
511
|
+
debug {" (@type)"}
|
512
|
+
value['@type'] = compact_iri(value['@type'], :position => :datatype)
|
513
|
+
value
|
514
|
+
else
|
515
|
+
# Otherwise, use original value
|
516
|
+
debug {" (no change)"}
|
517
|
+
value
|
518
|
+
end
|
519
|
+
|
520
|
+
# If the result is an object, tranform keys using any term keyword aliases
|
521
|
+
if result.is_a?(Hash) && result.keys.any? {|k| self.alias(k) != k}
|
522
|
+
debug {" (map to key aliases)"}
|
523
|
+
new_element = {}
|
524
|
+
result.each do |k, v|
|
525
|
+
new_element[self.alias(k)] = v
|
526
|
+
end
|
527
|
+
result = new_element
|
528
|
+
end
|
529
|
+
|
530
|
+
debug {"=> #{result.inspect}"}
|
531
|
+
result
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def inspect
|
536
|
+
v = %w([EvaluationContext)
|
537
|
+
v << "mappings[#{mappings.keys.length}]=#{mappings}"
|
538
|
+
v << "coercions[#{coercions.keys.length}]=#{coercions}"
|
539
|
+
v << "lists[#{lists.length}]=#{lists}"
|
540
|
+
v.join(", ") + "]"
|
541
|
+
end
|
542
|
+
|
543
|
+
def dup
|
544
|
+
# Also duplicate mappings, coerce and list
|
545
|
+
ec = super
|
546
|
+
ec.mappings = mappings.dup
|
547
|
+
ec.coercions = coercions.dup
|
548
|
+
ec.lists = lists.dup
|
549
|
+
ec.language = language
|
550
|
+
ec.options = options
|
551
|
+
ec.iri_to_term = iri_to_term.dup
|
552
|
+
ec.iri_to_curie = iri_to_curie.dup
|
553
|
+
ec
|
554
|
+
end
|
555
|
+
|
556
|
+
private
|
557
|
+
|
558
|
+
def uri(value, append = nil)
|
559
|
+
value = RDF::URI.new(value)
|
560
|
+
value = value.join(append) if append
|
561
|
+
value.validate! if @options[:validate]
|
562
|
+
value.canonicalize! if @options[:canonicalize]
|
563
|
+
value = RDF::URI.intern(value) if @options[:intern]
|
564
|
+
value
|
565
|
+
end
|
566
|
+
|
567
|
+
# Keep track of allocated BNodes
|
568
|
+
#
|
569
|
+
# Don't actually use the name provided, to prevent name alias issues.
|
570
|
+
# @return [RDF::Node]
|
571
|
+
def bnode(value = nil)
|
572
|
+
@@bnode_cache ||= {}
|
573
|
+
@@bnode_cache[value.to_s] ||= RDF::Node.new(value)
|
574
|
+
end
|
575
|
+
|
576
|
+
##
|
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
|
585
|
+
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(/:$/, '')
|
608
|
+
else
|
609
|
+
debug "no mapping found for #{iri} in #{iri_to_term.inspect}"
|
610
|
+
nil
|
611
|
+
end
|
612
|
+
|
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
|
638
|
+
end
|
639
|
+
end
|
640
|
+
end
|