json-ld 3.0.2 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/AUTHORS +1 -1
- data/README.md +90 -53
- data/UNLICENSE +1 -1
- data/VERSION +1 -1
- data/bin/jsonld +4 -4
- data/lib/json/ld.rb +27 -10
- data/lib/json/ld/api.rb +325 -96
- data/lib/json/ld/compact.rb +75 -27
- data/lib/json/ld/conneg.rb +188 -0
- data/lib/json/ld/context.rb +677 -292
- data/lib/json/ld/expand.rb +240 -75
- data/lib/json/ld/flatten.rb +5 -3
- data/lib/json/ld/format.rb +19 -19
- data/lib/json/ld/frame.rb +135 -85
- data/lib/json/ld/from_rdf.rb +44 -17
- data/lib/json/ld/html/nokogiri.rb +151 -0
- data/lib/json/ld/html/rexml.rb +186 -0
- data/lib/json/ld/reader.rb +25 -5
- data/lib/json/ld/resource.rb +2 -2
- data/lib/json/ld/streaming_writer.rb +3 -1
- data/lib/json/ld/to_rdf.rb +47 -17
- data/lib/json/ld/utils.rb +4 -2
- data/lib/json/ld/writer.rb +75 -14
- data/spec/api_spec.rb +13 -34
- data/spec/compact_spec.rb +968 -9
- data/spec/conneg_spec.rb +373 -0
- data/spec/context_spec.rb +447 -53
- data/spec/expand_spec.rb +1872 -416
- data/spec/flatten_spec.rb +434 -47
- data/spec/frame_spec.rb +979 -344
- data/spec/from_rdf_spec.rb +305 -5
- data/spec/spec_helper.rb +177 -0
- data/spec/streaming_writer_spec.rb +4 -4
- data/spec/suite_compact_spec.rb +2 -2
- data/spec/suite_expand_spec.rb +14 -2
- data/spec/suite_flatten_spec.rb +10 -2
- data/spec/suite_frame_spec.rb +3 -2
- data/spec/suite_from_rdf_spec.rb +2 -2
- data/spec/suite_helper.rb +55 -20
- data/spec/suite_html_spec.rb +22 -0
- data/spec/suite_http_spec.rb +35 -0
- data/spec/suite_remote_doc_spec.rb +2 -2
- data/spec/suite_to_rdf_spec.rb +14 -3
- data/spec/support/extensions.rb +5 -1
- data/spec/test-files/test-4-input.json +3 -3
- data/spec/test-files/test-5-input.json +2 -2
- data/spec/test-files/test-8-framed.json +14 -18
- data/spec/to_rdf_spec.rb +606 -16
- data/spec/writer_spec.rb +5 -5
- metadata +144 -88
data/lib/json/ld/compact.rb
CHANGED
@@ -5,7 +5,13 @@ module JSON::LD
|
|
5
5
|
include Utils
|
6
6
|
|
7
7
|
# The following constant is used to reduce object allocations in #compact below
|
8
|
+
CONTAINER_MAPPING_ID = %w(@id).freeze
|
9
|
+
CONTAINER_MAPPING_INDEX = %w(@index).freeze
|
10
|
+
CONTAINER_MAPPING_LANGUAGE = %w(@language).freeze
|
8
11
|
CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE = Set.new(%w(@language @index @id @type)).freeze
|
12
|
+
CONTAINER_MAPPING_LIST = %w(@list).freeze
|
13
|
+
CONTAINER_MAPPING_TYPE = %w(@type).freeze
|
14
|
+
EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE = %w(@direction @index @language @value).freeze
|
9
15
|
|
10
16
|
##
|
11
17
|
# This algorithm compacts a JSON-LD document, such that the given context is applied. This must result in shortening any applicable IRIs to terms or compact IRIs, any applicable keywords to keyword aliases, and any applicable JSON-LD values expressed in expanded form to simple values such as strings or numbers.
|
@@ -24,8 +30,6 @@ module JSON::LD
|
|
24
30
|
|
25
31
|
# If the term definition for active property itself contains a context, use that for compacting values.
|
26
32
|
input_context = self.context
|
27
|
-
td = self.context.term_definitions[property] if property
|
28
|
-
self.context = (td && td.context && self.context.parse(td.context)) || input_context
|
29
33
|
|
30
34
|
case element
|
31
35
|
when Array
|
@@ -47,22 +51,30 @@ module JSON::LD
|
|
47
51
|
# Otherwise element is a JSON object.
|
48
52
|
|
49
53
|
# @null objects are used in framing
|
50
|
-
return nil if element.
|
54
|
+
return nil if element.key?('@null')
|
55
|
+
|
56
|
+
# Revert any previously type-scoped (non-preserved) context
|
57
|
+
if context.previous_context && !element.key?('@value') && element.keys != %w(@id)
|
58
|
+
self.context = context.previous_context
|
59
|
+
end
|
60
|
+
|
61
|
+
# Look up term definintions from property using the original type-scoped context, if it exists, but apply them to the now current previous context
|
62
|
+
td = input_context.term_definitions[property] if property
|
63
|
+
self.context = context.parse(td.context, override_protected: true) if td && td.context
|
51
64
|
|
52
65
|
if element.key?('@id') || element.key?('@value')
|
53
66
|
result = context.compact_value(property, element, log_depth: @options[:log_depth])
|
54
|
-
|
67
|
+
if !result.is_a?(Hash) || context.coerce(property) == '@json'
|
55
68
|
#log_debug("") {"=> scalar result: #{result.inspect}"}
|
56
69
|
return result
|
57
70
|
end
|
58
71
|
end
|
59
72
|
|
60
73
|
# If expanded property is @list and we're contained within a list container, recursively compact this item to an array
|
61
|
-
if list?(element) && context.container(property) ==
|
74
|
+
if list?(element) && context.container(property) == CONTAINER_MAPPING_LIST
|
62
75
|
return compact(element['@list'], property: property, ordered: ordered)
|
63
76
|
end
|
64
77
|
|
65
|
-
|
66
78
|
inside_reverse = property == '@reverse'
|
67
79
|
result, nest_result = {}, nil
|
68
80
|
|
@@ -72,24 +84,34 @@ module JSON::LD
|
|
72
84
|
map {|expanded_type| context.compact_iri(expanded_type, vocab: true)}.
|
73
85
|
sort.
|
74
86
|
each do |term|
|
75
|
-
term_context =
|
76
|
-
self.context = context.parse(term_context) if term_context
|
87
|
+
term_context = input_context.term_definitions[term].context if input_context.term_definitions[term]
|
88
|
+
self.context = context.parse(term_context, propagate: false) if term_context
|
77
89
|
end
|
78
90
|
|
79
91
|
element.keys.opt_sort(ordered: ordered).each do |expanded_property|
|
80
92
|
expanded_value = element[expanded_property]
|
81
93
|
#log_debug("") {"#{expanded_property}: #{expanded_value.inspect}"}
|
82
94
|
|
83
|
-
if expanded_property == '@id'
|
84
|
-
compacted_value = Array(expanded_value).map
|
85
|
-
|
86
|
-
|
95
|
+
if expanded_property == '@id'
|
96
|
+
compacted_value = Array(expanded_value).map {|expanded_id| context.compact_iri(expanded_id)}
|
97
|
+
|
98
|
+
kw_alias = context.compact_iri('@id', vocab: true)
|
99
|
+
as_array = compacted_value.length > 1
|
100
|
+
compacted_value = compacted_value.first unless as_array
|
101
|
+
result[kw_alias] = compacted_value
|
102
|
+
next
|
103
|
+
end
|
87
104
|
|
88
|
-
|
105
|
+
if expanded_property == '@type'
|
106
|
+
compacted_value = Array(expanded_value).map {|expanded_type| input_context.compact_iri(expanded_type, vocab: true)}
|
89
107
|
|
90
|
-
|
91
|
-
|
92
|
-
|
108
|
+
kw_alias = context.compact_iri('@type', vocab: true)
|
109
|
+
as_array = compacted_value.length > 1 ||
|
110
|
+
(context.as_array?(kw_alias) &&
|
111
|
+
!value?(element) &&
|
112
|
+
context.processingMode('json-ld-1.1'))
|
113
|
+
compacted_value = compacted_value.first unless as_array
|
114
|
+
result[kw_alias] = compacted_value
|
93
115
|
next
|
94
116
|
end
|
95
117
|
|
@@ -124,13 +146,13 @@ module JSON::LD
|
|
124
146
|
next
|
125
147
|
end
|
126
148
|
|
127
|
-
if expanded_property == '@index' && context.container(property) ==
|
149
|
+
if expanded_property == '@index' && context.container(property) == CONTAINER_MAPPING_INDEX
|
128
150
|
#log_debug("@index") {"drop @index"}
|
129
151
|
next
|
130
152
|
end
|
131
153
|
|
132
|
-
# Otherwise, if expanded property is @index, @value, or @language:
|
133
|
-
if expanded_property
|
154
|
+
# Otherwise, if expanded property is @direction, @index, @value, or @language:
|
155
|
+
if EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE.include?(expanded_property)
|
134
156
|
al = context.compact_iri(expanded_property, vocab: true, quiet: true)
|
135
157
|
#log_debug(expanded_property) {"#{al} => #{expanded_value.inspect}"}
|
136
158
|
result[al] = expanded_value
|
@@ -187,7 +209,7 @@ module JSON::LD
|
|
187
209
|
# handle @list
|
188
210
|
if list?(expanded_item)
|
189
211
|
compacted_item = as_array(compacted_item)
|
190
|
-
unless container ==
|
212
|
+
unless container == CONTAINER_MAPPING_LIST
|
191
213
|
al = context.compact_iri('@list', vocab: true, quiet: true)
|
192
214
|
compacted_item = {al => compacted_item}
|
193
215
|
if expanded_item.has_key?('@index')
|
@@ -219,7 +241,12 @@ module JSON::LD
|
|
219
241
|
property_is_array: as_array)
|
220
242
|
elsif container.include?('@graph') && simple_graph?(expanded_item)
|
221
243
|
# container includes @graph but not @id or @index and value is a simple graph object
|
222
|
-
|
244
|
+
if compacted_item.is_a?(Array) && compacted_item.length > 1
|
245
|
+
# Mutple objects in the same graph can't be represented directly, as they would be interpreted as two different graphs. Need to wrap in @included.
|
246
|
+
included_key = context.compact_iri('@included', vocab: true).to_s
|
247
|
+
compacted_item = {included_key => compacted_item}
|
248
|
+
end
|
249
|
+
# Drop through, where compacted_item will be added
|
223
250
|
add_value(nest_result, item_active_property, compacted_item,
|
224
251
|
property_is_array: as_array)
|
225
252
|
else
|
@@ -242,24 +269,45 @@ module JSON::LD
|
|
242
269
|
c = container.first
|
243
270
|
container_key = context.compact_iri(c, vocab: true, quiet: true)
|
244
271
|
compacted_item = case container
|
245
|
-
when
|
272
|
+
when CONTAINER_MAPPING_ID
|
246
273
|
map_key = compacted_item[container_key]
|
247
274
|
compacted_item.delete(container_key)
|
248
275
|
compacted_item
|
249
|
-
when
|
250
|
-
|
251
|
-
|
276
|
+
when CONTAINER_MAPPING_INDEX
|
277
|
+
index_key = context.term_definitions[item_active_property].index || '@index'
|
278
|
+
if index_key == '@index'
|
279
|
+
map_key = expanded_item['@index']
|
280
|
+
compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
|
281
|
+
else
|
282
|
+
container_key = context.compact_iri(index_key, vocab: true, quiet: true)
|
283
|
+
map_key, *others = Array(compacted_item[container_key])
|
284
|
+
if map_key.is_a?(String)
|
285
|
+
case others.length
|
286
|
+
when 0 then compacted_item.delete(container_key)
|
287
|
+
when 1 then compacted_item[container_key] = others.first
|
288
|
+
else compacted_item[container_key] = others
|
289
|
+
end
|
290
|
+
else
|
291
|
+
map_key = context.compact_iri('@none', vocab: true, quiet: true)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
# Note, if compacted_item is a node reference and key is @id-valued, then this could be compacted further.
|
252
295
|
compacted_item
|
253
|
-
when
|
296
|
+
when CONTAINER_MAPPING_LANGUAGE
|
254
297
|
map_key = expanded_item['@language']
|
255
298
|
value?(expanded_item) ? expanded_item['@value'] : compacted_item
|
256
|
-
when
|
299
|
+
when CONTAINER_MAPPING_TYPE
|
257
300
|
map_key, *types = Array(compacted_item[container_key])
|
258
301
|
case types.length
|
259
302
|
when 0 then compacted_item.delete(container_key)
|
260
303
|
when 1 then compacted_item[container_key] = types.first
|
261
304
|
else compacted_item[container_key] = types
|
262
305
|
end
|
306
|
+
|
307
|
+
# if compacted_item contains a single entry who's key maps to @id, then recompact the item without @type
|
308
|
+
if compacted_item.keys.length == 1 && expanded_item.keys.include?('@id')
|
309
|
+
compacted_item = compact({'@id' => expanded_item['@id']}, property: item_active_property)
|
310
|
+
end
|
263
311
|
compacted_item
|
264
312
|
end
|
265
313
|
map_key ||= context.compact_iri('@none', vocab: true, quiet: true)
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
require 'rack'
|
4
|
+
require 'link_header'
|
5
|
+
|
6
|
+
module JSON::LD
|
7
|
+
##
|
8
|
+
# Rack middleware for JSON-LD content negotiation.
|
9
|
+
#
|
10
|
+
# Uses HTTP Content Negotiation to serialize `Array` and `Hash` results as JSON-LD using 'profile' accept-params to invoke appropriate JSON-LD API methods.
|
11
|
+
#
|
12
|
+
# Allows black-listing and white-listing of two-part profiles where the second part denotes a URL of a _context_ or _frame_. (See {JSON::LD::Writer.accept?})
|
13
|
+
#
|
14
|
+
# Works along with `rack-linkeddata` for serializing data which is not in the form of an `RDF::Repository`.
|
15
|
+
#
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# use JSON::LD::Rack
|
19
|
+
#
|
20
|
+
# @see https://www.w3.org/TR/json-ld11/#iana-considerations
|
21
|
+
# @see https://www.rubydoc.info/github/rack/rack/master/file/SPEC
|
22
|
+
class ContentNegotiation
|
23
|
+
VARY = {'Vary' => 'Accept'}.freeze
|
24
|
+
|
25
|
+
# @return [#call]
|
26
|
+
attr_reader :app
|
27
|
+
|
28
|
+
##
|
29
|
+
# * Registers JSON::LD::Rack, suitable for Sinatra application
|
30
|
+
# * adds helpers
|
31
|
+
#
|
32
|
+
# @param [Sinatra::Base] app
|
33
|
+
# @return [void]
|
34
|
+
def self.registered(app)
|
35
|
+
options = {}
|
36
|
+
app.use(JSON::LD::Rack, **options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(app)
|
40
|
+
@app = app
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Handles a Rack protocol request.
|
45
|
+
# Parses Accept header to find appropriate mime-type and sets content_type accordingly.
|
46
|
+
#
|
47
|
+
# @param [Hash{String => String}] env
|
48
|
+
# @return [Array(Integer, Hash, #each)] Status, Headers and Body
|
49
|
+
# @see http://rack.rubyforge.org/doc/SPEC.html
|
50
|
+
def call(env)
|
51
|
+
response = app.call(env)
|
52
|
+
body = response[2].respond_to?(:body) ? response[2].body : response[2]
|
53
|
+
case body
|
54
|
+
when Array, Hash
|
55
|
+
response[2] = body # Put it back in the response, it might have been a proxy
|
56
|
+
serialize(env, *response)
|
57
|
+
else response
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Serializes objects as JSON-LD. Defaults to expanded form, other forms
|
63
|
+
# determined by presense of `profile` in accept-parms.
|
64
|
+
#
|
65
|
+
# @param [Hash{String => String}] env
|
66
|
+
# @param [Integer] status
|
67
|
+
# @param [Hash{String => Object}] headers
|
68
|
+
# @param [RDF::Enumerable] body
|
69
|
+
# @return [Array(Integer, Hash, #each)] Status, Headers and Body
|
70
|
+
def serialize(env, status, headers, body)
|
71
|
+
# This will only return json-ld content types, possibly with parameters
|
72
|
+
content_types = parse_accept_header(env['HTTP_ACCEPT'] || 'application/ld+json')
|
73
|
+
content_types = content_types.select do |content_type|
|
74
|
+
_, *params = content_type.split(';').map(&:strip)
|
75
|
+
accept_params = params.inject({}) do |memo, pv|
|
76
|
+
p, v = pv.split('=').map(&:strip)
|
77
|
+
memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
|
78
|
+
end
|
79
|
+
JSON::LD::Writer.accept?(accept_params)
|
80
|
+
end
|
81
|
+
if content_types.empty?
|
82
|
+
not_acceptable("No appropriate combinaion of media-type and parameters found")
|
83
|
+
else
|
84
|
+
ct, *params = content_types.first.split(';').map(&:strip)
|
85
|
+
accept_params = params.inject({}) do |memo, pv|
|
86
|
+
p, v = pv.split('=').map(&:strip)
|
87
|
+
memo.merge(p.downcase.to_sym => v.sub(/^["']?([^"']*)["']?$/, '\1'))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Determine API method from profile
|
91
|
+
profile = accept_params[:profile].to_s.split(' ')
|
92
|
+
|
93
|
+
# Get context from Link header
|
94
|
+
links = LinkHeader.parse(env['HTTP_LINK'])
|
95
|
+
context = links.find_link(['rel', JSON_LD_NS+"context"]).href rescue nil
|
96
|
+
frame = links.find_link(['rel', JSON_LD_NS+"frame"]).href rescue nil
|
97
|
+
|
98
|
+
if profile.include?(JSON_LD_NS+"framed") && frame.nil?
|
99
|
+
return not_acceptable("framed profile without a frame")
|
100
|
+
end
|
101
|
+
|
102
|
+
# accept? already determined that there are appropriate contexts
|
103
|
+
# If profile also includes a URI which is not a namespace, use it for compaction.
|
104
|
+
context ||= Writer.default_context if profile.include?(JSON_LD_NS+"compacted")
|
105
|
+
|
106
|
+
result = if profile.include?(JSON_LD_NS+"flattened")
|
107
|
+
API.flatten(body, context)
|
108
|
+
elsif profile.include?(JSON_LD_NS+"framed")
|
109
|
+
API.frame(body, frame)
|
110
|
+
elsif context
|
111
|
+
API.compact(body, context)
|
112
|
+
elsif profile.include?(JSON_LD_NS+"expanded")
|
113
|
+
API.expand(body)
|
114
|
+
else
|
115
|
+
body
|
116
|
+
end
|
117
|
+
|
118
|
+
headers = headers.merge(VARY).merge('Content-Type' => ct)
|
119
|
+
[status, headers, [result.to_json]]
|
120
|
+
end
|
121
|
+
rescue
|
122
|
+
http_error(500, $!.message)
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
|
127
|
+
##
|
128
|
+
# Parses an HTTP `Accept` header, returning an array of MIME content
|
129
|
+
# types ordered by the precedence rules defined in HTTP/1.1 §14.1.
|
130
|
+
#
|
131
|
+
# @param [String, #to_s] header
|
132
|
+
# @return [Array<String>]
|
133
|
+
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
|
134
|
+
def parse_accept_header(header)
|
135
|
+
entries = header.to_s.split(',')
|
136
|
+
entries = entries.
|
137
|
+
map { |e| accept_entry(e) }.
|
138
|
+
sort_by(&:last).
|
139
|
+
map(&:first)
|
140
|
+
entries.map { |e| find_content_type_for_media_range(e) }.compact
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns an array of quality, number of '*' in content-type, and number of non-'q' parameters
|
144
|
+
def accept_entry(entry)
|
145
|
+
type, *options = entry.split(';').map(&:strip)
|
146
|
+
quality = 0 # we sort smallest first
|
147
|
+
options.delete_if { |e| quality = 1 - e[2..-1].to_f if e.start_with? 'q=' }
|
148
|
+
[options.unshift(type).join(';'), [quality, type.count('*'), 1 - options.size]]
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Returns a content type appropriate for the given `media_range`,
|
153
|
+
# returns `nil` if `media_range` contains a wildcard subtype
|
154
|
+
# that is not mapped.
|
155
|
+
#
|
156
|
+
# @param [String, #to_s] media_range
|
157
|
+
# @return [String, nil]
|
158
|
+
def find_content_type_for_media_range(media_range)
|
159
|
+
media_range = media_range.sub('*/*', 'application/ld+json') if media_range.to_s.start_with?('*/*')
|
160
|
+
media_range = media_range.sub('application/*', 'application/ld+json') if media_range.to_s.start_with?('application/*')
|
161
|
+
media_range = media_range.sub('application/json', 'application/ld+json') if media_range.to_s.start_with?('application/json')
|
162
|
+
|
163
|
+
media_range.start_with?('application/ld+json') ? media_range : nil
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Outputs an HTTP `406 Not Acceptable` response.
|
168
|
+
#
|
169
|
+
# @param [String, #to_s] message
|
170
|
+
# @return [Array(Integer, Hash, #each)]
|
171
|
+
def not_acceptable(message = nil)
|
172
|
+
http_error(406, message, VARY)
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# Outputs an HTTP `4xx` or `5xx` response.
|
177
|
+
#
|
178
|
+
# @param [Integer, #to_i] code
|
179
|
+
# @param [String, #to_s] message
|
180
|
+
# @param [Hash{String => String}] headers
|
181
|
+
# @return [Array(Integer, Hash, #each)]
|
182
|
+
def http_error(code, message = nil, headers = {})
|
183
|
+
message = [code, Rack::Utils::HTTP_STATUS_CODES[code]].join(' ') +
|
184
|
+
(message.nil? ? "\n" : " (#{message})\n")
|
185
|
+
[code, {'Content-Type' => "text/plain"}.merge(headers), [message]]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/json/ld/context.rb
CHANGED
@@ -24,6 +24,14 @@ module JSON::LD
|
|
24
24
|
def add_preloaded(url, context = nil, &block)
|
25
25
|
PRELOADED[url.to_s.freeze] = context || block
|
26
26
|
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Alias a previousliy loaded context
|
30
|
+
# @param [String, RDF::URI] a
|
31
|
+
# @param [String, RDF::URI] url
|
32
|
+
def alias_preloaded(a, url)
|
33
|
+
PRELOADED[a.to_s.freeze] = PRELOADED[url.to_s.freeze]
|
34
|
+
end
|
27
35
|
end
|
28
36
|
|
29
37
|
# Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
|
@@ -38,16 +46,20 @@ module JSON::LD
|
|
38
46
|
attr_accessor :type_mapping
|
39
47
|
|
40
48
|
# Base container mapping, without @set
|
41
|
-
# @return Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'> Container mapping
|
49
|
+
# @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
|
42
50
|
attr_reader :container_mapping
|
43
51
|
|
44
52
|
# @return [String] Term used for nest properties
|
45
53
|
attr_accessor :nest
|
46
54
|
|
47
|
-
# Language mapping of term, `false` is used if there is
|
55
|
+
# Language mapping of term, `false` is used if there is an explicit language mapping for this term.
|
48
56
|
# @return [String] Language mapping
|
49
57
|
attr_accessor :language_mapping
|
50
58
|
|
59
|
+
# Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
|
60
|
+
# @return ["ltr", "rtl"] direction_mapping
|
61
|
+
attr_accessor :direction_mapping
|
62
|
+
|
51
63
|
# @return [Boolean] Reverse Property
|
52
64
|
attr_accessor :reverse_property
|
53
65
|
|
@@ -55,6 +67,10 @@ module JSON::LD
|
|
55
67
|
# @return [Boolean]
|
56
68
|
attr_accessor :simple
|
57
69
|
|
70
|
+
# Property used for data indexing; defaults to @index
|
71
|
+
# @return [Boolean]
|
72
|
+
attr_accessor :index
|
73
|
+
|
58
74
|
# Indicate that term may be used as a prefix
|
59
75
|
attr_writer :prefix
|
60
76
|
|
@@ -62,6 +78,10 @@ module JSON::LD
|
|
62
78
|
# @return [Hash{String => Object}]
|
63
79
|
attr_accessor :context
|
64
80
|
|
81
|
+
# Term is protected.
|
82
|
+
# @return [Boolean]
|
83
|
+
attr_writer :protected
|
84
|
+
|
65
85
|
# This is a simple term definition, not an expanded term definition
|
66
86
|
# @return [Boolean] simple
|
67
87
|
def simple?; simple; end
|
@@ -76,8 +96,11 @@ module JSON::LD
|
|
76
96
|
# @param [String] type_mapping Type mapping
|
77
97
|
# @param [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
|
78
98
|
# @param [String] language_mapping
|
79
|
-
# Language mapping of term, `false` is used if there is
|
99
|
+
# Language mapping of term, `false` is used if there is an explicit language mapping for this term
|
100
|
+
# @param ["ltr", "rtl"] direction_mapping
|
101
|
+
# Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
|
80
102
|
# @param [Boolean] reverse_property
|
103
|
+
# @param [Boolean] protected
|
81
104
|
# @param [String] nest term used for nest properties
|
82
105
|
# @param [Boolean] simple
|
83
106
|
# This is a simple term definition, not an expanded term definition
|
@@ -85,26 +108,36 @@ module JSON::LD
|
|
85
108
|
# Term may be used as a prefix
|
86
109
|
def initialize(term,
|
87
110
|
id: nil,
|
111
|
+
index: nil,
|
88
112
|
type_mapping: nil,
|
89
113
|
container_mapping: nil,
|
90
114
|
language_mapping: nil,
|
115
|
+
direction_mapping: nil,
|
91
116
|
reverse_property: false,
|
92
117
|
nest: nil,
|
118
|
+
protected: false,
|
93
119
|
simple: false,
|
94
120
|
prefix: nil,
|
95
121
|
context: nil)
|
96
122
|
@term = term
|
97
123
|
@id = id.to_s unless id.nil?
|
124
|
+
@index = index.to_s unless index.nil?
|
98
125
|
@type_mapping = type_mapping.to_s unless type_mapping.nil?
|
99
126
|
self.container_mapping = container_mapping
|
100
127
|
@language_mapping = language_mapping unless language_mapping.nil?
|
128
|
+
@direction_mapping = direction_mapping unless direction_mapping.nil?
|
101
129
|
@reverse_property = reverse_property
|
130
|
+
@protected = protected
|
102
131
|
@nest = nest unless nest.nil?
|
103
132
|
@simple = simple
|
104
133
|
@prefix = prefix unless prefix.nil?
|
105
134
|
@context = context unless context.nil?
|
106
135
|
end
|
107
136
|
|
137
|
+
# Term is protected.
|
138
|
+
# @return [Boolean]
|
139
|
+
def protected?; !!@protected; end
|
140
|
+
|
108
141
|
# Set container mapping, from an array which may include @set
|
109
142
|
def container_mapping=(mapping)
|
110
143
|
mapping = Array(mapping)
|
@@ -113,6 +146,7 @@ module JSON::LD
|
|
113
146
|
mapping.delete('@set')
|
114
147
|
end
|
115
148
|
@container_mapping = mapping.sort
|
149
|
+
@index ||= '@index' if mapping.include?('@index')
|
116
150
|
end
|
117
151
|
|
118
152
|
##
|
@@ -143,14 +177,16 @@ module JSON::LD
|
|
143
177
|
end
|
144
178
|
end
|
145
179
|
|
146
|
-
cm =
|
180
|
+
cm = Array(container_mapping)
|
181
|
+
cm << "@set" if as_set? && !cm.include?("@set")
|
147
182
|
cm = cm.first if cm.length == 1
|
148
183
|
defn['@container'] = cm unless cm.empty?
|
149
184
|
# Language set as false to be output as null
|
150
185
|
defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
|
151
186
|
defn['@context'] = @context if @context
|
152
187
|
defn['@nest'] = @nest if @nest
|
153
|
-
defn['@
|
188
|
+
defn['@index'] = @index if @index
|
189
|
+
defn['@prefix'] = @prefix unless @prefix.nil?
|
154
190
|
defn
|
155
191
|
end
|
156
192
|
end
|
@@ -161,7 +197,7 @@ module JSON::LD
|
|
161
197
|
# @return [String]
|
162
198
|
def to_rb
|
163
199
|
defn = [%(TermDefinition.new\(#{term.inspect})]
|
164
|
-
%w(id type_mapping container_mapping language_mapping reverse_property nest simple prefix context).each do |acc|
|
200
|
+
%w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
|
165
201
|
v = instance_variable_get("@#{acc}".to_sym)
|
166
202
|
v = v.to_s if v.is_a?(RDF::Term)
|
167
203
|
if acc == 'container_mapping'
|
@@ -177,17 +213,39 @@ module JSON::LD
|
|
177
213
|
# @return [Boolean]
|
178
214
|
def as_set?; @as_set || false; end
|
179
215
|
|
216
|
+
# Check if term definitions are identical, modulo @protected
|
217
|
+
# @return [Boolean]
|
218
|
+
def ==(other)
|
219
|
+
other.is_a?(TermDefinition) &&
|
220
|
+
id == other.id &&
|
221
|
+
term == other.term &&
|
222
|
+
type_mapping == other.type_mapping &&
|
223
|
+
container_mapping == other.container_mapping &&
|
224
|
+
nest == other.nest &&
|
225
|
+
language_mapping == other.language_mapping &&
|
226
|
+
direction_mapping == other.direction_mapping &&
|
227
|
+
reverse_property == other.reverse_property &&
|
228
|
+
simple == other.simple &&
|
229
|
+
index == other.index &&
|
230
|
+
context == other.context &&
|
231
|
+
prefix? == other.prefix? &&
|
232
|
+
as_set? == other.as_set?
|
233
|
+
end
|
234
|
+
|
180
235
|
def inspect
|
181
236
|
v = %w([TD)
|
182
237
|
v << "id=#{@id}"
|
238
|
+
v << "index=#{index.inspect}" unless index.nil?
|
183
239
|
v << "term=#{@term}"
|
184
240
|
v << "rev" if reverse_property
|
185
241
|
v << "container=#{container_mapping}" if container_mapping
|
186
242
|
v << "as_set=#{as_set?.inspect}"
|
187
243
|
v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
|
244
|
+
v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
|
188
245
|
v << "type=#{type_mapping}" unless type_mapping.nil?
|
189
246
|
v << "nest=#{nest.inspect}" unless nest.nil?
|
190
247
|
v << "simple=true" if @simple
|
248
|
+
v << "protected=true" if @protected
|
191
249
|
v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
|
192
250
|
v << "has-context" unless context.nil?
|
193
251
|
v.join(" ") + "]"
|
@@ -214,13 +272,26 @@ module JSON::LD
|
|
214
272
|
# @return [Hash{RDF::URI => String}] Reverse mappings from IRI to term only for terms, not CURIEs XXX
|
215
273
|
attr_accessor :iri_to_term
|
216
274
|
|
275
|
+
# Previous definition for this context. This is used for rolling back type-scoped contexts.
|
276
|
+
# @return [Context]
|
277
|
+
attr_accessor :previous_context
|
278
|
+
|
279
|
+
# Context is property-scoped
|
280
|
+
# @return [Boolean]
|
281
|
+
attr_accessor :property_scoped
|
282
|
+
|
217
283
|
# Default language
|
218
284
|
#
|
219
|
-
#
|
220
285
|
# This adds a language to plain strings that aren't otherwise coerced
|
221
286
|
# @return [String]
|
222
287
|
attr_reader :default_language
|
223
|
-
|
288
|
+
|
289
|
+
# Default direction
|
290
|
+
#
|
291
|
+
# This adds a direction to plain strings that aren't otherwise coerced
|
292
|
+
# @return ["lrt", "rtl"]
|
293
|
+
attr_reader :default_direction
|
294
|
+
|
224
295
|
# Default vocabulary
|
225
296
|
#
|
226
297
|
# Sets the default vocabulary used for expanding terms which
|
@@ -237,9 +308,6 @@ module JSON::LD
|
|
237
308
|
# @return [BlankNodeNamer]
|
238
309
|
attr_accessor :namer
|
239
310
|
|
240
|
-
# @return [String]
|
241
|
-
attr_accessor :processingMode
|
242
|
-
|
243
311
|
##
|
244
312
|
# Create a new context by parsing a context.
|
245
313
|
#
|
@@ -249,8 +317,8 @@ module JSON::LD
|
|
249
317
|
# @raise [JsonLdError]
|
250
318
|
# on a remote context load error, syntax error, or a reference to a term which is not defined.
|
251
319
|
# @return [Context]
|
252
|
-
def self.parse(local_context, **options)
|
253
|
-
self.new(options).parse(local_context)
|
320
|
+
def self.parse(local_context, protected: false, override_protected: false, propagate: true, **options)
|
321
|
+
self.new(**options).parse(local_context, protected: false, override_protected: override_protected, propagate: propagate)
|
254
322
|
end
|
255
323
|
|
256
324
|
##
|
@@ -274,8 +342,7 @@ module JSON::LD
|
|
274
342
|
@base = @doc_base = RDF::URI(options[:base]).dup
|
275
343
|
@doc_base.canonicalize! if options[:canonicalize]
|
276
344
|
end
|
277
|
-
options[:
|
278
|
-
@processingMode ||= options[:processingMode]
|
345
|
+
self.processingMode = options[:processingMode] if options.has_key?(:processingMode)
|
279
346
|
@term_definitions = {}
|
280
347
|
@iri_to_term = {
|
281
348
|
RDF.to_uri.to_s => "rdf",
|
@@ -293,11 +360,11 @@ module JSON::LD
|
|
293
360
|
end
|
294
361
|
|
295
362
|
self.vocab = options[:vocab] if options[:vocab]
|
296
|
-
self.default_language = options[:language] if options[:language]
|
363
|
+
self.default_language = options[:language] if options[:language] =~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
|
297
364
|
@term_definitions = options[:term_definitions] if options[:term_definitions]
|
298
365
|
|
299
366
|
#log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
|
300
|
-
|
367
|
+
|
301
368
|
yield(self) if block_given?
|
302
369
|
end
|
303
370
|
|
@@ -310,7 +377,7 @@ module JSON::LD
|
|
310
377
|
end
|
311
378
|
|
312
379
|
# @param [String] value must be an absolute IRI
|
313
|
-
def base=(value)
|
380
|
+
def base=(value, **options)
|
314
381
|
if value
|
315
382
|
raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
|
316
383
|
value = RDF::URI(value).dup
|
@@ -326,38 +393,91 @@ module JSON::LD
|
|
326
393
|
end
|
327
394
|
|
328
395
|
# @param [String] value
|
329
|
-
def default_language=(value)
|
330
|
-
@default_language =
|
331
|
-
|
332
|
-
|
396
|
+
def default_language=(value, **options)
|
397
|
+
@default_language = case value
|
398
|
+
when String
|
399
|
+
# Warn on an invalid language tag, unless :validate is true, in which case it's an error
|
400
|
+
if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
|
401
|
+
warn "@language must be valid BCP47: #{value.inspect}"
|
402
|
+
end
|
403
|
+
options[:lowercaseLanguage] ? value.downcase : value
|
404
|
+
when nil
|
405
|
+
nil
|
406
|
+
else
|
407
|
+
raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# @param [String] value
|
412
|
+
def default_direction=(value, **options)
|
413
|
+
@default_direction = if value
|
414
|
+
raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
|
415
|
+
value
|
333
416
|
else
|
334
417
|
nil
|
335
418
|
end
|
336
419
|
end
|
337
420
|
|
421
|
+
##
|
422
|
+
# Retrieve, or check processing mode.
|
423
|
+
#
|
424
|
+
# * With no arguments, retrieves the current set processingMode.
|
425
|
+
# * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
|
426
|
+
# * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
|
427
|
+
#
|
428
|
+
# @param [String, Number] expected (nil)
|
429
|
+
# @return [String]
|
430
|
+
def processingMode(expected = nil)
|
431
|
+
case expected
|
432
|
+
when 1.0, 'json-ld-1.0'
|
433
|
+
@processingMode == 'json-ld-1.0'
|
434
|
+
when 1.1, 'json-ld-1.1'
|
435
|
+
@processingMode ||= 'json-ld-1.1'
|
436
|
+
@processingMode == 'json-ld-1.1'
|
437
|
+
when nil
|
438
|
+
@processingMode
|
439
|
+
else
|
440
|
+
false
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
##
|
445
|
+
# Set processing mode.
|
446
|
+
#
|
447
|
+
# * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
|
448
|
+
#
|
338
449
|
# If contex has a @version member, it's value MUST be 1.1, otherwise an "invalid @version value" has been detected, and processing is aborted.
|
339
450
|
# If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
|
340
|
-
#
|
341
|
-
|
451
|
+
#
|
452
|
+
# @param [String, Number] expected
|
453
|
+
# @return [String]
|
454
|
+
# @raise [JsonLdError::ProcessingModeConflict]
|
455
|
+
def processingMode=(value = nil, **options)
|
456
|
+
value = "json-ld-1.1" if value == 1.1
|
342
457
|
case value
|
343
|
-
when 1.1
|
344
|
-
if processingMode && processingMode
|
345
|
-
raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{processingMode}"
|
458
|
+
when "json-ld-1.0", "json-ld-1.1"
|
459
|
+
if @processingMode && @processingMode != value
|
460
|
+
raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
|
346
461
|
end
|
347
|
-
@processingMode =
|
462
|
+
@processingMode = value
|
348
463
|
else
|
349
|
-
raise JsonLdError::InvalidVersionValue, value
|
464
|
+
raise JsonLdError::InvalidVersionValue, value.inspect
|
350
465
|
end
|
351
466
|
end
|
352
467
|
|
353
468
|
# If context has a @vocab member: if its value is not a valid absolute IRI or null trigger an INVALID_VOCAB_MAPPING error; otherwise set the active context's vocabulary mapping to its value and remove the @vocab member from context.
|
354
469
|
# @param [String] value must be an absolute IRI
|
355
|
-
def vocab=(value)
|
470
|
+
def vocab=(value, **options)
|
356
471
|
@vocab = case value
|
357
472
|
when /_:/
|
473
|
+
# BNode vocab is deprecated
|
474
|
+
warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
|
358
475
|
value
|
359
476
|
when String, RDF::URI
|
360
|
-
|
477
|
+
if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
|
478
|
+
raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
|
479
|
+
end
|
480
|
+
v = expand_iri(value.to_s, vocab: true, documentRelative: true)
|
361
481
|
raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}" if !v.valid? && @options[:validate]
|
362
482
|
v
|
363
483
|
when nil
|
@@ -367,31 +487,56 @@ module JSON::LD
|
|
367
487
|
end
|
368
488
|
end
|
369
489
|
|
490
|
+
# Set propagation
|
491
|
+
# @note: by the time this is called, the work has already been done.
|
492
|
+
#
|
493
|
+
# @param [Boolean] value
|
494
|
+
def propagate=(value, **options)
|
495
|
+
raise JsonLdError::InvalidContextMember, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
|
496
|
+
raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
497
|
+
value
|
498
|
+
end
|
499
|
+
|
370
500
|
# Create an Evaluation Context
|
371
501
|
#
|
372
502
|
# When processing a JSON-LD data structure, each processing rule is applied using information provided by the active context. This section describes how to produce an active context.
|
373
|
-
#
|
503
|
+
#
|
374
504
|
# The active context contains the active term definitions which specify how properties and values have to be interpreted as well as the current base IRI, the vocabulary mapping and the default language. Each term definition consists of an IRI mapping, a boolean flag reverse property, an optional type mapping or language mapping, and an optional container mapping. A term definition can not only be used to map a term to an IRI, but also to map a term to a keyword, in which case it is referred to as a keyword alias.
|
375
|
-
#
|
505
|
+
#
|
376
506
|
# When processing, the active context is initialized without any term definitions, vocabulary mapping, or default language. If a local context is encountered during processing, a new active context is created by cloning the existing active context. Then the information from the local context is merged into the new active context. Given that local contexts may contain references to remote contexts, this includes their retrieval.
|
377
|
-
#
|
507
|
+
#
|
378
508
|
#
|
379
509
|
# @param [String, #read, Array, Hash, Context] local_context
|
510
|
+
# @param [Array<String>] remote_contexts
|
511
|
+
# @param [Boolean] protected Make defined terms protected (as if `@protected` were used).
|
512
|
+
# @param [Boolean] override_protected Protected terms may be cleared.
|
513
|
+
# @param [Boolean] propagate
|
514
|
+
# If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
|
380
515
|
# @raise [JsonLdError]
|
381
516
|
# on a remote context load error, syntax error, or a reference to a term which is not defined.
|
382
517
|
# @return [Context]
|
383
|
-
# @see
|
384
|
-
def parse(local_context, remote_contexts
|
518
|
+
# @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
|
519
|
+
def parse(local_context, remote_contexts: [], protected: false, override_protected: false, propagate: true)
|
385
520
|
result = self.dup
|
386
521
|
result.provided_context = local_context if self.empty?
|
522
|
+
# Early check for @propagate, which can only appear in a local context
|
523
|
+
propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
|
524
|
+
result.previous_context ||= result.dup unless propagate
|
387
525
|
|
388
526
|
local_context = as_array(local_context)
|
389
527
|
|
390
528
|
local_context.each do |context|
|
391
529
|
case context
|
392
530
|
when nil
|
393
|
-
# 3.1 If
|
394
|
-
|
531
|
+
# 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
|
532
|
+
if override_protected || result.term_definitions.values.none?(&:protected?)
|
533
|
+
null_context = Context.new(**options)
|
534
|
+
null_context.previous_context = result unless propagate
|
535
|
+
result = null_context
|
536
|
+
else
|
537
|
+
raise JSON::LD::JsonLdError::InvalidContextNullification,
|
538
|
+
"Attempt to clear a context with protected terms"
|
539
|
+
end
|
395
540
|
when Context
|
396
541
|
#log_debug("parse") {"context: #{context.inspect}"}
|
397
542
|
result = context.dup
|
@@ -413,11 +558,10 @@ module JSON::LD
|
|
413
558
|
#log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
|
414
559
|
|
415
560
|
# 3.2.1) Set context to the result of resolving value against the base IRI which is established as specified in section 5.1 Establishing a Base URI of [RFC3986]. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
|
416
|
-
context = RDF::URI(result.context_base ||
|
417
|
-
context_canon =
|
418
|
-
context_canon.
|
561
|
+
context = RDF::URI(result.context_base || options[:base]).join(context)
|
562
|
+
context_canon = context.canonicalize
|
563
|
+
context_canon.scheme == 'http' if context_canon.scheme == 'https'
|
419
564
|
|
420
|
-
raise JsonLdError::RecursiveContextInclusion, "#{context}" if remote_contexts.include?(context.to_s)
|
421
565
|
remote_contexts << context.to_s
|
422
566
|
raise JsonLdError::ContextOverflow, "#{context}" if remote_contexts.length >= MAX_CONTEXTS_LOADED
|
423
567
|
|
@@ -436,19 +580,17 @@ module JSON::LD
|
|
436
580
|
end
|
437
581
|
context = context_no_base.merge!(PRELOADED[context_canon.to_s])
|
438
582
|
else
|
439
|
-
|
440
583
|
# Load context document, if it is a string
|
441
584
|
begin
|
442
|
-
context_opts = @options.
|
443
|
-
|
444
|
-
|
585
|
+
context_opts = @options.merge(
|
586
|
+
profile: 'http://www.w3.org/ns/json-ld#context',
|
587
|
+
requestProfile: 'http://www.w3.org/ns/json-ld#context',
|
588
|
+
base: nil)
|
589
|
+
#context_opts.delete(:headers)
|
590
|
+
JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
|
445
591
|
# 3.2.5) Dereference context. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
|
446
|
-
|
447
|
-
|
448
|
-
else remote_doc.document
|
449
|
-
end
|
450
|
-
raise JsonLdError::InvalidRemoteContext, "#{context}" unless jo.is_a?(Hash) && jo.has_key?('@context')
|
451
|
-
context = jo['@context']
|
592
|
+
raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
|
593
|
+
context = remote_doc.document['@context']
|
452
594
|
end
|
453
595
|
rescue JsonLdError::LoadingDocumentFailed => e
|
454
596
|
#log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
|
@@ -461,8 +603,8 @@ module JSON::LD
|
|
461
603
|
end
|
462
604
|
|
463
605
|
# 3.2.6) Set context to the result of recursively calling this algorithm, passing context no base for active context, context for local context, and remote contexts.
|
464
|
-
context = context_no_base.parse(context, remote_contexts.dup)
|
465
|
-
PRELOADED[context_canon.to_s] = context
|
606
|
+
context = context_no_base.parse(context, remote_contexts: remote_contexts.dup, protected: protected, override_protected: override_protected, propagate: propagate)
|
607
|
+
PRELOADED[context_canon.to_s] = context.dup
|
466
608
|
context.provided_context = result.provided_context
|
467
609
|
end
|
468
610
|
context.base ||= result.base
|
@@ -471,25 +613,58 @@ module JSON::LD
|
|
471
613
|
when Hash
|
472
614
|
context = context.dup # keep from modifying a hash passed as a param
|
473
615
|
|
616
|
+
# This counts on hash elements being processed in order
|
474
617
|
{
|
475
|
-
'@
|
476
|
-
'@
|
477
|
-
'@
|
478
|
-
'@
|
618
|
+
'@version' => :processingMode=,
|
619
|
+
'@import' => nil,
|
620
|
+
'@base' => :base=,
|
621
|
+
'@direction' => :default_direction=,
|
622
|
+
'@language' => :default_language=,
|
623
|
+
'@propagate' => :propagate=,
|
624
|
+
'@vocab' => :vocab=,
|
479
625
|
}.each do |key, setter|
|
480
|
-
|
481
|
-
|
482
|
-
context.
|
483
|
-
|
484
|
-
|
626
|
+
next unless context.has_key?(key)
|
627
|
+
if key == '@import'
|
628
|
+
# Retrieve remote context and merge the remaining context object into the result.
|
629
|
+
raise JsonLdError::InvalidContextMember, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
|
630
|
+
raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
|
631
|
+
source = RDF::URI(result.context_base || result.base).join(context['@import'])
|
632
|
+
begin
|
633
|
+
context_opts = @options.merge(
|
634
|
+
profile: 'http://www.w3.org/ns/json-ld#context',
|
635
|
+
requestProfile: 'http://www.w3.org/ns/json-ld#context',
|
636
|
+
base: nil)
|
637
|
+
context_opts.delete(:headers)
|
638
|
+
JSON::LD::API.loadRemoteDocument(source, **context_opts) do |remote_doc|
|
639
|
+
# Dereference source. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
|
640
|
+
raise JsonLdError::InvalidRemoteContext, "#{source}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
|
641
|
+
import_context = remote_doc.document['@context']
|
642
|
+
raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
|
643
|
+
raise JsonLdError::InvalidContextMember, "#{import_context.to_json} must not include @import entry" if import_context.has_key?('@import')
|
644
|
+
context.delete(key)
|
645
|
+
context = import_context.merge(context)
|
646
|
+
end
|
647
|
+
rescue JsonLdError::LoadingDocumentFailed => e
|
648
|
+
raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
|
649
|
+
rescue JsonLdError
|
650
|
+
raise
|
651
|
+
rescue StandardError => e
|
652
|
+
raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
|
653
|
+
end
|
654
|
+
else
|
655
|
+
result.send(setter, context[key], remote_contexts: remote_contexts, protected: context.fetch('@protected', protected))
|
485
656
|
end
|
657
|
+
context.delete(key)
|
486
658
|
end
|
487
659
|
|
488
660
|
defined = {}
|
489
661
|
|
490
662
|
# For each key-value pair in context invoke the Create Term Definition subalgorithm, passing result for active context, context for local context, key, and defined
|
491
663
|
context.each_key do |key|
|
492
|
-
|
664
|
+
# ... where key is not @base, @vocab, @language, or @version
|
665
|
+
result.create_term_definition(context, key, defined,
|
666
|
+
override_protected: override_protected,
|
667
|
+
protected: context.fetch('@protected', protected)) unless NON_TERMDEF_KEYS.include?(key)
|
493
668
|
end
|
494
669
|
else
|
495
670
|
# 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
|
@@ -529,23 +704,32 @@ module JSON::LD
|
|
529
704
|
|
530
705
|
# The following constants are used to reduce object allocations in #create_term_definition below
|
531
706
|
ID_NULL_OBJECT = { '@id' => nil }.freeze
|
707
|
+
NON_TERMDEF_KEYS = Set.new(%w(@base @direction @language @protected @version @vocab)).freeze
|
532
708
|
JSON_LD_10_EXPECTED_KEYS = Set.new(%w(@container @id @language @reverse @type)).freeze
|
533
|
-
|
709
|
+
JSON_LD_11_EXPECTED_KEYS = Set.new(%w(@context @direction @index @nest @prefix @protected)).freeze
|
710
|
+
JSON_LD_EXPECTED_KEYS = (JSON_LD_10_EXPECTED_KEYS + JSON_LD_11_EXPECTED_KEYS).freeze
|
711
|
+
JSON_LD_10_TYPE_VALUES = Set.new(%w(@id @vocab)).freeze
|
712
|
+
JSON_LD_11_TYPE_VALUES = Set.new(%w(@json @none)).freeze
|
713
|
+
PREFIX_URI_ENDINGS = Set.new(%w(: / ? # [ ] @)).freeze
|
534
714
|
|
535
715
|
##
|
536
716
|
# Create Term Definition
|
537
717
|
#
|
538
718
|
# Term definitions are created by parsing the information in the given local context for the given term. If the given term is a compact IRI, it may omit an IRI mapping by depending on its prefix having its own term definition. If the prefix is a key in the local context, then its term definition must first be created, through recursion, before continuing. Because a term definition can depend on other term definitions, a mechanism must be used to detect cyclical dependencies. The solution employed here uses a map, defined, that keeps track of whether or not a term has been defined or is currently in the process of being defined. This map is checked before any recursion is attempted.
|
539
|
-
#
|
719
|
+
#
|
540
720
|
# After all dependencies for a term have been defined, the rest of the information in the local context for the given term is taken into account, creating the appropriate IRI mapping, container mapping, and type mapping or language mapping for the term.
|
541
721
|
#
|
542
722
|
# @param [Hash] local_context
|
543
723
|
# @param [String] term
|
544
724
|
# @param [Hash] defined
|
725
|
+
# @param [Boolean] protected if true, causes all terms to be marked protected
|
726
|
+
# @param [Boolean] override_protected Protected terms may be cleared.
|
727
|
+
# @param [Boolean] propagate
|
728
|
+
# Context is propagated across node objects.
|
545
729
|
# @raise [JsonLdError]
|
546
730
|
# Represents a cyclical term dependency
|
547
|
-
# @see
|
548
|
-
def create_term_definition(local_context, term, defined)
|
731
|
+
# @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
|
732
|
+
def create_term_definition(local_context, term, defined, override_protected: false, protected: false)
|
549
733
|
# Expand a string value, unless it matches a keyword
|
550
734
|
#log_debug("create_term_definition") {"term = #{term.inspect}"}
|
551
735
|
|
@@ -558,174 +742,267 @@ module JSON::LD
|
|
558
742
|
raise JsonLdError::CyclicIRIMapping, "Cyclical term dependency found: #{term.inspect}"
|
559
743
|
end
|
560
744
|
|
745
|
+
# Initialize value to a the value associated with the key term in local context.
|
746
|
+
value = local_context.fetch(term, false)
|
747
|
+
simple_term = value.is_a?(String) || value.nil?
|
748
|
+
|
561
749
|
# Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
|
562
|
-
if
|
750
|
+
if term == '@type' &&
|
751
|
+
value.is_a?(Hash) &&
|
752
|
+
processingMode("json-ld-1.1") &&
|
753
|
+
(value.keys - %w(@container @protected)).empty? &&
|
754
|
+
value.fetch('@container', '@set') == '@set'
|
755
|
+
# thes are the only cases were redefining a keyword is allowed
|
756
|
+
elsif KEYWORDS.include?(term) # TODO anything that looks like a keyword
|
563
757
|
raise JsonLdError::KeywordRedefinition, "term must not be a keyword: #{term.inspect}" if
|
564
758
|
@options[:validate]
|
759
|
+
elsif term.to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
|
760
|
+
warn "Terms beginning with '@' are reserved for future use and ignored: #{term}."
|
761
|
+
return
|
565
762
|
elsif !term_valid?(term) && @options[:validate]
|
566
763
|
raise JsonLdError::InvalidTermDefinition, "term is invalid: #{term.inspect}"
|
567
764
|
end
|
568
765
|
|
766
|
+
value = {'@id' => value} if simple_term
|
767
|
+
|
569
768
|
# Remove any existing term definition for term in active context.
|
570
|
-
term_definitions
|
769
|
+
previous_definition = term_definitions[term]
|
770
|
+
if previous_definition && previous_definition.protected? && !override_protected
|
771
|
+
# Check later to detect identical redefinition
|
772
|
+
else
|
773
|
+
term_definitions.delete(term) if previous_definition
|
774
|
+
end
|
571
775
|
|
572
|
-
|
573
|
-
value = local_context.fetch(term, false)
|
574
|
-
simple_term = value.is_a?(String)
|
575
|
-
value = {'@id' => value} if simple_term
|
776
|
+
raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}" unless value.is_a?(Hash)
|
576
777
|
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
#log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
|
586
|
-
definition = TermDefinition.new(term)
|
587
|
-
definition.simple = simple_term
|
588
|
-
|
589
|
-
if options[:validate]
|
590
|
-
expected_keys = case processingMode
|
591
|
-
when "json-ld-1.0", nil then JSON_LD_10_EXPECTED_KEYS
|
592
|
-
else JSON_LD_EXPECTED_KEYS
|
593
|
-
end
|
778
|
+
#log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
|
779
|
+
definition = TermDefinition.new(term)
|
780
|
+
definition.simple = simple_term
|
781
|
+
|
782
|
+
expected_keys = case processingMode
|
783
|
+
when "json-ld-1.0" then JSON_LD_10_EXPECTED_KEYS
|
784
|
+
else JSON_LD_EXPECTED_KEYS
|
785
|
+
end
|
594
786
|
|
595
|
-
|
596
|
-
|
597
|
-
|
787
|
+
# Any of these keys cause us to process as json-ld-1.1, unless otherwise set
|
788
|
+
if processingMode.nil? && value.any? { |key, _| !JSON_LD_11_EXPECTED_KEYS.include?(key) }
|
789
|
+
processingMode('json-ld-11')
|
790
|
+
end
|
791
|
+
|
792
|
+
if value.any? { |key, _| !expected_keys.include?(key) }
|
793
|
+
extra_keys = value.keys - expected_keys.to_a
|
794
|
+
raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} has unexpected keys: #{extra_keys.join(', ')}"
|
795
|
+
end
|
796
|
+
|
797
|
+
# Potentially note that the term is protected
|
798
|
+
definition.protected = value.fetch('@protected', protected)
|
799
|
+
|
800
|
+
if value.has_key?('@type')
|
801
|
+
type = value['@type']
|
802
|
+
# SPEC FIXME: @type may be nil
|
803
|
+
type = case type
|
804
|
+
when nil
|
805
|
+
type
|
806
|
+
when String
|
807
|
+
begin
|
808
|
+
expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
|
809
|
+
rescue JsonLdError::InvalidIRIMapping
|
810
|
+
raise JsonLdError::InvalidTypeMapping, "invalid mapping for '@type': #{type.inspect} on term #{term.inspect}"
|
598
811
|
end
|
812
|
+
else
|
813
|
+
:error
|
814
|
+
end
|
815
|
+
if JSON_LD_11_TYPE_VALUES.include?(type) && processingMode('json-ld-1.1')
|
816
|
+
# This is okay and used in compaction in 1.1
|
817
|
+
elsif !JSON_LD_10_TYPE_VALUES.include?(type) && !(type.is_a?(RDF::URI) && type.absolute?)
|
818
|
+
raise JsonLdError::InvalidTypeMapping, "unknown mapping for '@type': #{type.inspect} on term #{term.inspect}"
|
599
819
|
end
|
820
|
+
#log_debug("") {"type_mapping: #{type.inspect}"}
|
821
|
+
definition.type_mapping = type
|
822
|
+
end
|
600
823
|
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
end
|
619
|
-
#log_debug("") {"type_mapping: #{type.inspect}"}
|
620
|
-
definition.type_mapping = type
|
824
|
+
if value.has_key?('@reverse')
|
825
|
+
raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
|
826
|
+
value.key?('@id') || value.key?('@nest')
|
827
|
+
raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
|
828
|
+
value['@reverse'].is_a?(String)
|
829
|
+
|
830
|
+
# Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm, passing active context, the value associated with the @reverse key for value, true for vocab, true for document relative, local context, and defined. If the result is not an absolute IRI, i.e., it contains no colon (:), an invalid IRI mapping error has been detected and processing is aborted.
|
831
|
+
definition.id = expand_iri(value['@reverse'],
|
832
|
+
vocab: true,
|
833
|
+
local_context: local_context,
|
834
|
+
defined: defined)
|
835
|
+
raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
|
836
|
+
definition.id.is_a?(RDF::Node) || definition.id.is_a?(RDF::URI) && definition.id.absolute?
|
837
|
+
|
838
|
+
if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
|
839
|
+
warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
|
840
|
+
return
|
621
841
|
end
|
622
842
|
|
623
|
-
if
|
624
|
-
raise JsonLdError::
|
625
|
-
value.key?('@id') || value.key?('@nest')
|
626
|
-
raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
|
627
|
-
value['@reverse'].is_a?(String)
|
628
|
-
|
629
|
-
# Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm, passing active context, the value associated with the @reverse key for value, true for vocab, true for document relative, local context, and defined. If the result is not an absolute IRI, i.e., it contains no colon (:), an invalid IRI mapping error has been detected and processing is aborted.
|
630
|
-
definition.id = expand_iri(value['@reverse'],
|
631
|
-
vocab: true,
|
632
|
-
documentRelative: true,
|
633
|
-
local_context: local_context,
|
634
|
-
defined: defined)
|
635
|
-
raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
|
636
|
-
definition.id.is_a?(RDF::URI) && definition.id.absolute?
|
637
|
-
|
638
|
-
# If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
|
639
|
-
if value.has_key?('@container')
|
640
|
-
container = value['@container']
|
641
|
-
raise JsonLdError::InvalidReverseProperty,
|
642
|
-
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
|
643
|
-
container.is_a?(String) && (container == '@set' || container == '@index')
|
644
|
-
definition.container_mapping = check_container(container, local_context, defined, term)
|
645
|
-
end
|
646
|
-
definition.reverse_property = true
|
647
|
-
elsif value.has_key?('@id') && value['@id'] != term
|
648
|
-
raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
|
649
|
-
value['@id'].is_a?(String)
|
650
|
-
definition.id = expand_iri(value['@id'],
|
651
|
-
vocab: true,
|
652
|
-
documentRelative: true,
|
653
|
-
local_context: local_context,
|
654
|
-
defined: defined)
|
655
|
-
raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
|
656
|
-
definition.id == '@context'
|
657
|
-
|
658
|
-
# If id ends with a gen-delim, it may be used as a prefix for simple terms
|
659
|
-
definition.prefix = true if !term.include?(':') &&
|
660
|
-
definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']', '@') &&
|
661
|
-
simple_term
|
662
|
-
elsif term.include?(':')
|
663
|
-
# If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined.
|
664
|
-
prefix, suffix = term.split(':', 2)
|
665
|
-
create_term_definition(local_context, prefix, defined) if local_context.has_key?(prefix)
|
666
|
-
|
667
|
-
definition.id = if td = term_definitions[prefix]
|
668
|
-
# If term's prefix has a term definition in active context, set the IRI mapping for definition to the result of concatenating the value associated with the prefix's IRI mapping and the term's suffix.
|
669
|
-
td.id + suffix
|
670
|
-
else
|
671
|
-
# Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
|
672
|
-
term
|
673
|
-
end
|
674
|
-
#log_debug("") {"=> #{definition.id}"}
|
675
|
-
else
|
676
|
-
# Otherwise, active context must have a vocabulary mapping, otherwise an invalid value has been detected, which is an error. Set the IRI mapping for definition to the result of concatenating the value associated with the vocabulary mapping and term.
|
677
|
-
raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab
|
678
|
-
definition.id = vocab + term
|
679
|
-
#log_debug("") {"=> #{definition.id}"}
|
843
|
+
if term[1..-1].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
|
844
|
+
raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
|
680
845
|
end
|
681
846
|
|
682
|
-
|
847
|
+
warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
|
683
848
|
|
849
|
+
# If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
|
684
850
|
if value.has_key?('@container')
|
685
|
-
|
686
|
-
|
851
|
+
container = value['@container']
|
852
|
+
raise JsonLdError::InvalidReverseProperty,
|
853
|
+
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
|
854
|
+
container.is_a?(String) && (container == '@set' || container == '@index')
|
855
|
+
definition.container_mapping = check_container(container, local_context, defined, term)
|
856
|
+
end
|
857
|
+
definition.reverse_property = true
|
858
|
+
elsif value.has_key?('@id') && value['@id'].nil?
|
859
|
+
# Allowed to reserve a null term, which may be protected
|
860
|
+
elsif value.has_key?('@id') && value['@id'] != term
|
861
|
+
raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
|
862
|
+
value['@id'].is_a?(String)
|
863
|
+
|
864
|
+
if !KEYWORDS.include?(value['@id'].to_s) && value['@id'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
|
865
|
+
warn "Values beginning with '@' are reserved for future use and ignored: #{value['@id']}."
|
866
|
+
return
|
687
867
|
end
|
688
868
|
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
869
|
+
definition.id = expand_iri(value['@id'],
|
870
|
+
vocab: true,
|
871
|
+
local_context: local_context,
|
872
|
+
defined: defined)
|
873
|
+
raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
|
874
|
+
definition.id == '@context'
|
875
|
+
|
876
|
+
if term.match?(/(?::[^:])|\//)
|
877
|
+
term_iri = expand_iri(term,
|
878
|
+
vocab: true,
|
879
|
+
local_context: local_context,
|
880
|
+
defined: defined.merge(term => true))
|
881
|
+
if term_iri != definition.id
|
882
|
+
raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
|
695
883
|
end
|
696
884
|
end
|
697
885
|
|
698
|
-
if
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
definition.
|
886
|
+
warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
|
887
|
+
|
888
|
+
# If id ends with a gen-delim, it may be used as a prefix for simple terms
|
889
|
+
definition.prefix = true if !term.include?(':') &&
|
890
|
+
simple_term &&
|
891
|
+
(definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']', '@') || definition.id.to_s.start_with?('_:'))
|
892
|
+
elsif term[1..-1].include?(':')
|
893
|
+
# If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined.
|
894
|
+
prefix, suffix = term.split(':', 2)
|
895
|
+
create_term_definition(local_context, prefix, defined, protected: protected) if local_context.has_key?(prefix)
|
896
|
+
|
897
|
+
definition.id = if td = term_definitions[prefix]
|
898
|
+
# If term's prefix has a term definition in active context, set the IRI mapping for definition to the result of concatenating the value associated with the prefix's IRI mapping and the term's suffix.
|
899
|
+
td.id + suffix
|
900
|
+
else
|
901
|
+
# Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
|
902
|
+
term
|
903
|
+
end
|
904
|
+
#log_debug("") {"=> #{definition.id}"}
|
905
|
+
elsif term.include?('/')
|
906
|
+
# If term is a relative IRI
|
907
|
+
definition.id = expand_iri(term, vocab: true)
|
908
|
+
raise JsonLdError::InvalidKeywordAlias, "expected term to expand to an absolute IRI #{term.inspect}" unless
|
909
|
+
definition.id.absolute?
|
910
|
+
elsif KEYWORDS.include?(term)
|
911
|
+
# This should only happen for @type when @container is @set
|
912
|
+
definition.id = term
|
913
|
+
else
|
914
|
+
# Otherwise, active context must have a vocabulary mapping, otherwise an invalid value has been detected, which is an error. Set the IRI mapping for definition to the result of concatenating the value associated with the vocabulary mapping and term.
|
915
|
+
raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab
|
916
|
+
definition.id = vocab + term
|
917
|
+
#log_debug("") {"=> #{definition.id}"}
|
918
|
+
end
|
919
|
+
|
920
|
+
@iri_to_term[definition.id] = term if simple_term && definition.id
|
921
|
+
|
922
|
+
if value.has_key?('@container')
|
923
|
+
#log_debug("") {"container_mapping: #{value['@container'].inspect}"}
|
924
|
+
definition.container_mapping = check_container(value['@container'], local_context, defined, term)
|
925
|
+
|
926
|
+
# If @container includes @type
|
927
|
+
if definition.container_mapping.include?('@type')
|
928
|
+
# If definition does not have @type, set @type to @id
|
929
|
+
definition.type_mapping ||= '@id'
|
930
|
+
# If definition includes @type with a value other than @id or @vocab, an illegal type mapping error has been detected
|
931
|
+
if !CONTEXT_TYPE_ID_VOCAB.include?(definition.type_mapping)
|
932
|
+
raise JsonLdError::InvalidTypeMapping, "@container: @type requires @type to be @id or @vocab"
|
933
|
+
end
|
704
934
|
end
|
935
|
+
end
|
705
936
|
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
937
|
+
if value.has_key?('@index')
|
938
|
+
# property-based indexing
|
939
|
+
raise JsonLdError::InvalidTermDefinition, "@index without @index in @container: #{value['@index']} on term #{term.inspect}" unless definition.container_mapping.include?('@index')
|
940
|
+
raise JsonLdError::InvalidTermDefinition, "@index must expand to an IRI: #{value['@index']} on term #{term.inspect}" unless value['@index'].is_a?(String) && !value['@index'].start_with?('@')
|
941
|
+
definition.index = value['@index'].to_s
|
942
|
+
end
|
943
|
+
|
944
|
+
if value.has_key?('@context')
|
945
|
+
begin
|
946
|
+
self.parse(value['@context'], override_protected: true)
|
947
|
+
# Record null context in array form
|
948
|
+
definition.context = value['@context'] ? value['@context'] : [nil]
|
949
|
+
rescue JsonLdError => e
|
950
|
+
raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
|
712
951
|
end
|
952
|
+
end
|
713
953
|
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
954
|
+
if value.has_key?('@language')
|
955
|
+
language = value['@language']
|
956
|
+
language = case value['@language']
|
957
|
+
when String
|
958
|
+
# Warn on an invalid language tag, unless :validate is true, in which case it's an error
|
959
|
+
if value['@language'] !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
|
960
|
+
warn "@language must be valid BCP47: #{value['@language'].inspect}"
|
721
961
|
end
|
962
|
+
options[:lowercaseLanguage] ? value['@language'].downcase : value['@language']
|
963
|
+
when nil
|
964
|
+
nil
|
965
|
+
else
|
966
|
+
raise JsonLdError::InvalidLanguageMapping, "language must be null or a string, was #{value['@language'].inspect}} on term #{term.inspect}"
|
722
967
|
end
|
968
|
+
#log_debug("") {"language_mapping: #{language.inspect}"}
|
969
|
+
definition.language_mapping = language || false
|
970
|
+
end
|
723
971
|
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
972
|
+
if value.has_key?('@direction')
|
973
|
+
direction = value['@direction']
|
974
|
+
raise JsonLdError::InvalidBaseDirection, "direction must be null, 'ltr', or 'rtl', was #{language.inspect}} on term #{term.inspect}" unless direction.nil? || %w(ltr rtl).include?(direction)
|
975
|
+
#log_debug("") {"direction_mapping: #{direction.inspect}"}
|
976
|
+
definition.direction_mapping = direction || false
|
977
|
+
end
|
978
|
+
|
979
|
+
if value.has_key?('@nest')
|
980
|
+
nest = value['@nest']
|
981
|
+
raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
|
982
|
+
raise JsonLdError::InvalidNestValue, "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}" if nest.match?(/^@[a-zA-Z]+$/) && nest != '@nest'
|
983
|
+
#log_debug("") {"nest: #{nest.inspect}"}
|
984
|
+
definition.nest = nest
|
985
|
+
end
|
986
|
+
|
987
|
+
if value.has_key?('@prefix')
|
988
|
+
raise JsonLdError::InvalidTermDefinition, "@prefix used on compact or relative IRI term #{term.inspect}" if term.match?(%r{:|/})
|
989
|
+
case pfx = value['@prefix']
|
990
|
+
when TrueClass, FalseClass
|
991
|
+
definition.prefix = pfx
|
992
|
+
else
|
993
|
+
raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
|
994
|
+
end
|
995
|
+
|
996
|
+
raise JsonLdError::InvalidTermDefinition, "keywords may not be used as prefixes" if pfx && KEYWORDS.include?(definition.id.to_s)
|
728
997
|
end
|
998
|
+
|
999
|
+
if previous_definition && previous_definition.protected? && definition != previous_definition && !override_protected
|
1000
|
+
definition = previous_definition
|
1001
|
+
raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
term_definitions[term] = definition
|
1005
|
+
defined[term] = true
|
729
1006
|
ensure
|
730
1007
|
# Re-build after term definitions set
|
731
1008
|
@inverse_context = nil
|
@@ -753,6 +1030,7 @@ module JSON::LD
|
|
753
1030
|
#log_debug("") {"=> context: #{inspect}"}
|
754
1031
|
ctx = {}
|
755
1032
|
ctx['@base'] = base.to_s if base && base != doc_base
|
1033
|
+
ctx['@direction'] = default_direction.to_s if default_direction
|
756
1034
|
ctx['@language'] = default_language.to_s if default_language
|
757
1035
|
ctx['@vocab'] = vocab.to_s if vocab
|
758
1036
|
|
@@ -808,7 +1086,7 @@ module JSON::LD
|
|
808
1086
|
statements.each do |subject, values|
|
809
1087
|
types = values.each_with_object([]) { |v, memo| memo << v.object if v.predicate == RDF.type }
|
810
1088
|
is_property = types.any? {|t| t.to_s.include?("Property")}
|
811
|
-
|
1089
|
+
|
812
1090
|
term = subject.to_s.split(/[\/\#]/).last
|
813
1091
|
|
814
1092
|
if !is_property
|
@@ -831,6 +1109,7 @@ module JSON::LD
|
|
831
1109
|
if self.default_language
|
832
1110
|
td.language_mapping = false
|
833
1111
|
end
|
1112
|
+
# FIXME: text direction
|
834
1113
|
when RDF::XSD.boolean, RDF::SCHEMA.Boolean, RDF::XSD.date, RDF::SCHEMA.Date,
|
835
1114
|
RDF::XSD.dateTime, RDF::SCHEMA.DateTime, RDF::XSD.time, RDF::SCHEMA.Time,
|
836
1115
|
RDF::XSD.duration, RDF::SCHEMA.Duration, RDF::XSD.decimal, RDF::SCHEMA.Number,
|
@@ -856,7 +1135,7 @@ module JSON::LD
|
|
856
1135
|
def set_mapping(term, value)
|
857
1136
|
#log_debug("") {"map #{term.inspect} to #{value.inspect}"}
|
858
1137
|
term = term.to_s
|
859
|
-
term_definitions[term] = TermDefinition.new(term, id: value, simple: true, prefix: (value.to_s.end_with?(
|
1138
|
+
term_definitions[term] = TermDefinition.new(term, id: value, simple: true, prefix: (value.to_s.end_with?(*PREFIX_URI_ENDINGS)))
|
860
1139
|
term_definitions[term].simple = true
|
861
1140
|
|
862
1141
|
term_sym = term.empty? ? "" : term.to_sym
|
@@ -881,11 +1160,24 @@ module JSON::LD
|
|
881
1160
|
# @param [Term, #to_s] term in unexpanded form
|
882
1161
|
# @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>]
|
883
1162
|
def container(term)
|
884
|
-
return [term] if
|
1163
|
+
return [term] if term == '@list'
|
885
1164
|
term = find_definition(term)
|
886
1165
|
term ? term.container_mapping : []
|
887
1166
|
end
|
888
1167
|
|
1168
|
+
##
|
1169
|
+
# Retrieve term coercion
|
1170
|
+
#
|
1171
|
+
# @param [Term, #to_s] term in unexpanded form
|
1172
|
+
# @return [RDF::URI, '@id']
|
1173
|
+
def coerce(term)
|
1174
|
+
# Map property, if it's not an RDF::Value
|
1175
|
+
# @type is always is an IRI
|
1176
|
+
return '@id' if term == RDF.type || term == '@type'
|
1177
|
+
term = find_definition(term)
|
1178
|
+
term && term.type_mapping
|
1179
|
+
end
|
1180
|
+
|
889
1181
|
##
|
890
1182
|
# Should values be represented using an array?
|
891
1183
|
#
|
@@ -935,7 +1227,17 @@ module JSON::LD
|
|
935
1227
|
def language(term)
|
936
1228
|
term = find_definition(term)
|
937
1229
|
lang = term && term.language_mapping
|
938
|
-
lang.nil? ? @default_language : lang
|
1230
|
+
lang.nil? ? @default_language : (lang == false ? nil : lang)
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
##
|
1234
|
+
# Retrieve the text direction associated with a term, or the default direction otherwise
|
1235
|
+
# @param [Term, #to_s] term in unexpanded form
|
1236
|
+
# @return [String]
|
1237
|
+
def direction(term)
|
1238
|
+
term = find_definition(term)
|
1239
|
+
dir = term && term.direction_mapping
|
1240
|
+
dir.nil? ? @default_direction : (dir == false ? nil : dir)
|
939
1241
|
end
|
940
1242
|
|
941
1243
|
##
|
@@ -984,11 +1286,12 @@ module JSON::LD
|
|
984
1286
|
# @return [RDF::URI, String]
|
985
1287
|
# IRI or String, if it's a keyword
|
986
1288
|
# @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
|
987
|
-
# @see
|
1289
|
+
# @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
|
988
1290
|
def expand_iri(value, documentRelative: false, vocab: false, local_context: nil, defined: nil, quiet: false, **options)
|
989
1291
|
return value unless value.is_a?(String)
|
990
1292
|
|
991
1293
|
return value if KEYWORDS.include?(value)
|
1294
|
+
return nil if value.match?(/^@[a-zA-Z]+$/)
|
992
1295
|
#log_debug("expand_iri") {"value: #{value.inspect}"} unless quiet
|
993
1296
|
|
994
1297
|
defined = defined || {} # if we initialized in the keyword arg we would allocate {} at each invokation, even in the 2 (common) early returns above.
|
@@ -1011,7 +1314,7 @@ module JSON::LD
|
|
1011
1314
|
end
|
1012
1315
|
|
1013
1316
|
# If value contains a colon (:), it is either an absolute IRI or a compact IRI:
|
1014
|
-
if value.include?(':')
|
1317
|
+
if value[1..-1].to_s.include?(':')
|
1015
1318
|
prefix, suffix = value.split(':', 2)
|
1016
1319
|
#log_debug("") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{self.vocab.inspect}"} unless quiet
|
1017
1320
|
|
@@ -1025,22 +1328,21 @@ module JSON::LD
|
|
1025
1328
|
end
|
1026
1329
|
|
1027
1330
|
# If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
|
1028
|
-
|
1029
|
-
|
1331
|
+
if (td = term_definitions[prefix]) && !td.id.nil? && td.prefix?
|
1332
|
+
return td.id + suffix
|
1333
|
+
elsif RDF::URI(value).absolute?
|
1334
|
+
# Otherwise, if the value has the form of an absolute IRI, return it
|
1335
|
+
return RDF::URI(value)
|
1030
1336
|
else
|
1031
|
-
#
|
1032
|
-
RDF::URI(value)
|
1337
|
+
# Otherwise, it is a relative IRI
|
1033
1338
|
end
|
1034
|
-
|
1035
|
-
#log_debug("") {"=> #{result.inspect}"} unless quiet
|
1036
|
-
return result
|
1037
1339
|
end
|
1038
1340
|
#log_debug("") {"=> #{result.inspect}"} unless quiet
|
1039
1341
|
|
1040
1342
|
result = if vocab && self.vocab
|
1041
1343
|
# If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
|
1042
1344
|
self.vocab + value
|
1043
|
-
elsif
|
1345
|
+
elsif documentRelative && (base ||= self.base)
|
1044
1346
|
# Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
|
1045
1347
|
value = RDF::URI(value)
|
1046
1348
|
value.absolute? ? value : RDF::URI(base).join(value)
|
@@ -1054,6 +1356,18 @@ module JSON::LD
|
|
1054
1356
|
result
|
1055
1357
|
end
|
1056
1358
|
|
1359
|
+
# The following constants are used to reduce object allocations in #compact_iri below
|
1360
|
+
CONTAINERS_GRAPH = %w(@graph@id @graph@id@set).freeze
|
1361
|
+
CONTAINERS_GRAPH_INDEX = %w(@graph@index @graph@index@set).freeze
|
1362
|
+
CONTAINERS_GRAPH_INDEX_INDEX = %w(@graph@index @graph@index@set @index @index@set).freeze
|
1363
|
+
CONTAINERS_GRAPH_SET = %w(@graph @graph@set @set).freeze
|
1364
|
+
CONTAINERS_ID_TYPE = %w(@id @id@set @type @set@type).freeze
|
1365
|
+
CONTAINERS_ID_VOCAB = %w(@id @vocab @none).freeze
|
1366
|
+
CONTAINERS_INDEX_SET = %w(@index @index@set).freeze
|
1367
|
+
CONTAINERS_LANGUAGE = %w(@language @language@set).freeze
|
1368
|
+
CONTAINERS_VALUE = %w(@value).freeze
|
1369
|
+
CONTAINERS_VOCAB_ID = %w(@vocab @id @none).freeze
|
1370
|
+
|
1057
1371
|
##
|
1058
1372
|
# Compacts an absolute IRI to the shortest matching term or compact IRI
|
1059
1373
|
#
|
@@ -1063,12 +1377,12 @@ module JSON::LD
|
|
1063
1377
|
# @param [Boolean] vocab
|
1064
1378
|
# specifies whether the passed iri should be compacted using the active context's vocabulary mapping
|
1065
1379
|
# @param [Boolean] reverse
|
1066
|
-
# specifies whether a reverse property is being compacted
|
1380
|
+
# specifies whether a reverse property is being compacted
|
1067
1381
|
# @param [Boolean] quiet (false)
|
1068
1382
|
# @param [Hash{Symbol => Object}] options ({})
|
1069
1383
|
#
|
1070
1384
|
# @return [String] compacted form of IRI
|
1071
|
-
# @see
|
1385
|
+
# @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
|
1072
1386
|
def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **options)
|
1073
1387
|
return if iri.nil?
|
1074
1388
|
iri = iri.to_s
|
@@ -1076,10 +1390,14 @@ module JSON::LD
|
|
1076
1390
|
|
1077
1391
|
if vocab && inverse_context.has_key?(iri)
|
1078
1392
|
#log_debug("") {"vocab and key in inverse context"} unless quiet
|
1079
|
-
default_language = self.
|
1393
|
+
default_language = if self.default_direction
|
1394
|
+
"#{self.default_language}_#{self.default_direction}".downcase
|
1395
|
+
else
|
1396
|
+
(self.default_language || "@none").downcase
|
1397
|
+
end
|
1080
1398
|
containers = []
|
1081
1399
|
tl, tl_value = "@language", "@null"
|
1082
|
-
containers.concat(
|
1400
|
+
containers.concat(CONTAINERS_INDEX_SET) if index?(value) && !graph?(value)
|
1083
1401
|
|
1084
1402
|
# If the value is a JSON Object with the key @preserve, use the value of @preserve.
|
1085
1403
|
value = value['@preserve'].first if value.is_a?(Hash) && value.has_key?('@preserve')
|
@@ -1097,8 +1415,10 @@ module JSON::LD
|
|
1097
1415
|
list.each do |item|
|
1098
1416
|
item_language, item_type = "@none", "@none"
|
1099
1417
|
if value?(item)
|
1100
|
-
if item.has_key?('@
|
1101
|
-
item_language = item['@language']
|
1418
|
+
if item.has_key?('@direction')
|
1419
|
+
item_language = "#{item['@language']}_#{item['@direction']}".downcase
|
1420
|
+
elsif item.has_key?('@language')
|
1421
|
+
item_language = item['@language'].downcase
|
1102
1422
|
elsif item.has_key?('@type')
|
1103
1423
|
item_type = item['@type']
|
1104
1424
|
else
|
@@ -1130,29 +1450,35 @@ module JSON::LD
|
|
1130
1450
|
#log_debug("") {"list: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
|
1131
1451
|
elsif graph?(value)
|
1132
1452
|
# Prefer @index and @id containers, then @graph, then @index
|
1133
|
-
containers.concat(
|
1134
|
-
containers.concat(
|
1453
|
+
containers.concat(CONTAINERS_GRAPH_INDEX_INDEX) if index?(value)
|
1454
|
+
containers.concat(CONTAINERS_GRAPH) if value.has_key?('@id')
|
1135
1455
|
|
1136
1456
|
# Prefer an @graph container next
|
1137
|
-
containers.concat(
|
1457
|
+
containers.concat(CONTAINERS_GRAPH_SET)
|
1138
1458
|
|
1139
1459
|
# Lastly, in 1.1, any graph can be indexed on @index or @id, so add if we haven't already
|
1140
|
-
containers.concat(
|
1141
|
-
containers.concat(
|
1142
|
-
containers.concat(
|
1460
|
+
containers.concat(CONTAINERS_GRAPH_INDEX) unless index?(value)
|
1461
|
+
containers.concat(CONTAINERS_GRAPH) unless value.has_key?('@id')
|
1462
|
+
containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
|
1463
|
+
containers << '@set'
|
1464
|
+
|
1465
|
+
tl, tl_value = '@type', '@id'
|
1143
1466
|
else
|
1144
1467
|
if value?(value)
|
1145
1468
|
# In 1.1, an language map can be used to index values using @none
|
1146
1469
|
if value.has_key?('@language') && !index?(value)
|
1147
|
-
tl_value = value['@language']
|
1148
|
-
|
1470
|
+
tl_value = value['@language'].downcase
|
1471
|
+
tl_value += "_#{value['@direction']}" if value['@direction']
|
1472
|
+
containers.concat(CONTAINERS_LANGUAGE)
|
1473
|
+
elsif value.has_key?('@direction') && !index?(value)
|
1474
|
+
tl_value = "_#{value['@direction']}"
|
1149
1475
|
elsif value.has_key?('@type')
|
1150
1476
|
tl_value = value['@type']
|
1151
1477
|
tl = '@type'
|
1152
1478
|
end
|
1153
1479
|
else
|
1154
1480
|
# In 1.1, an id or type map can be used to index values using @none
|
1155
|
-
containers.concat(
|
1481
|
+
containers.concat(CONTAINERS_ID_TYPE)
|
1156
1482
|
tl, tl_value = '@type', '@id'
|
1157
1483
|
end
|
1158
1484
|
containers << '@set'
|
@@ -1162,9 +1488,9 @@ module JSON::LD
|
|
1162
1488
|
containers << '@none'
|
1163
1489
|
|
1164
1490
|
# In 1.1, an index map can be used to index values using @none, so add as a low priority
|
1165
|
-
containers.concat(
|
1491
|
+
containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
|
1166
1492
|
# Values without type or language can use @language map
|
1167
|
-
containers.concat(
|
1493
|
+
containers.concat(CONTAINERS_LANGUAGE) if value?(value) && value.keys == CONTAINERS_VALUE
|
1168
1494
|
|
1169
1495
|
tl_value ||= '@null'
|
1170
1496
|
preferred_values = []
|
@@ -1172,15 +1498,22 @@ module JSON::LD
|
|
1172
1498
|
if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.has_key?('@id')
|
1173
1499
|
t_iri = compact_iri(value['@id'], vocab: true, document_relative: true)
|
1174
1500
|
if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
|
1175
|
-
preferred_values.concat(
|
1501
|
+
preferred_values.concat(CONTAINERS_VOCAB_ID)
|
1176
1502
|
else
|
1177
|
-
preferred_values.concat(
|
1503
|
+
preferred_values.concat(CONTAINERS_ID_VOCAB)
|
1178
1504
|
end
|
1179
1505
|
else
|
1180
1506
|
tl = '@any' if list?(value) && value['@list'].empty?
|
1181
1507
|
preferred_values.concat([tl_value, '@none'].compact)
|
1182
1508
|
end
|
1183
1509
|
#log_debug("") {"preferred_values: #{preferred_values.inspect}"} unless quiet
|
1510
|
+
preferred_values << '@any'
|
1511
|
+
|
1512
|
+
# if containers included `@language` and preferred_values includes something of the form language-tag_direction, add just the _direction part, to select terms that have that direction.
|
1513
|
+
if lang_dir = preferred_values.detect {|v| v.include?('_')}
|
1514
|
+
preferred_values << '_' + lang_dir.split('_').last
|
1515
|
+
end
|
1516
|
+
|
1184
1517
|
if p_term = select_term(iri, containers, tl, preferred_values)
|
1185
1518
|
#log_debug("") {"=> term: #{p_term.inspect}"} unless quiet
|
1186
1519
|
return p_term
|
@@ -1224,6 +1557,12 @@ module JSON::LD
|
|
1224
1557
|
return candidates.sort.first if !candidates.empty?
|
1225
1558
|
end
|
1226
1559
|
|
1560
|
+
# If iri could be confused with a compact IRI using a term in this context, signal an error
|
1561
|
+
term_definitions.each do |term, td|
|
1562
|
+
next unless iri.to_s.start_with?("#{term}:") && td.prefix?
|
1563
|
+
raise JSON::LD::JsonLdError:: IRIConfusedWithPrefix, "Absolute IRI '#{iri}' confused with prefix '#{term}'"
|
1564
|
+
end
|
1565
|
+
|
1227
1566
|
if !vocab
|
1228
1567
|
# transform iri to a relative IRI using the document's base IRI
|
1229
1568
|
iri = remove_base(iri)
|
@@ -1247,12 +1586,13 @@ module JSON::LD
|
|
1247
1586
|
# @param [Hash, String] value
|
1248
1587
|
# Value (literal or IRI) to be expanded
|
1249
1588
|
# @param [Boolean] useNativeTypes (false) use native representations
|
1589
|
+
# @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
|
1250
1590
|
# @param [Hash{Symbol => Object}] options
|
1251
1591
|
#
|
1252
1592
|
# @return [Hash] Object representation of value
|
1253
1593
|
# @raise [RDF::ReaderError] if the iri cannot be expanded
|
1254
|
-
# @see
|
1255
|
-
def expand_value(property, value, useNativeTypes: false, **options)
|
1594
|
+
# @see https://www.w3.org/TR/json-ld11-api/#value-expansion
|
1595
|
+
def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, **options)
|
1256
1596
|
#log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
|
1257
1597
|
|
1258
1598
|
td = term_definitions.fetch(property, TermDefinition.new(property))
|
@@ -1281,7 +1621,26 @@ module JSON::LD
|
|
1281
1621
|
when RDF::Literal
|
1282
1622
|
#log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
|
1283
1623
|
res = {}
|
1284
|
-
if
|
1624
|
+
if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
|
1625
|
+
# Value parsed as JSON
|
1626
|
+
# FIXME: MultiJson
|
1627
|
+
res['@value'] = ::JSON.parse(value.object)
|
1628
|
+
res['@type'] = '@json'
|
1629
|
+
elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
|
1630
|
+
lang, dir = value.datatype.fragment.split('_')
|
1631
|
+
res['@value'] = value.to_s
|
1632
|
+
unless lang.empty?
|
1633
|
+
if lang !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
|
1634
|
+
if options[:validate]
|
1635
|
+
raise JsonLdError::InvalidLanguageMapping, "rdf:language must be valid BCP47: #{lang.inspect}"
|
1636
|
+
else
|
1637
|
+
warn "rdf:language must be valid BCP47: #{lang.inspect}"
|
1638
|
+
end
|
1639
|
+
end
|
1640
|
+
res['@language'] = lang
|
1641
|
+
end
|
1642
|
+
res['@direction'] = dir
|
1643
|
+
elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
|
1285
1644
|
res['@value'] = value.object
|
1286
1645
|
res['@type'] = uri(coerce(property)) if coerce(property)
|
1287
1646
|
else
|
@@ -1293,6 +1652,7 @@ module JSON::LD
|
|
1293
1652
|
res['@type'] = uri(value.datatype).to_s
|
1294
1653
|
elsif value.has_language? || language(property)
|
1295
1654
|
res['@language'] = (value.language || language(property)).to_s
|
1655
|
+
# FIXME: direction
|
1296
1656
|
end
|
1297
1657
|
end
|
1298
1658
|
res
|
@@ -1300,12 +1660,13 @@ module JSON::LD
|
|
1300
1660
|
# Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
|
1301
1661
|
res = {'@value' => value}
|
1302
1662
|
|
1303
|
-
if td.type_mapping &&
|
1663
|
+
if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
|
1304
1664
|
res['@type'] = td.type_mapping.to_s
|
1305
|
-
elsif value.is_a?(String)
|
1306
|
-
|
1307
|
-
|
1308
|
-
res['@language'] =
|
1665
|
+
elsif value.is_a?(String)
|
1666
|
+
language = language(property)
|
1667
|
+
direction = direction(property)
|
1668
|
+
res['@language'] = language if language
|
1669
|
+
res['@direction'] = direction if direction
|
1309
1670
|
end
|
1310
1671
|
|
1311
1672
|
res
|
@@ -1313,6 +1674,8 @@ module JSON::LD
|
|
1313
1674
|
|
1314
1675
|
#log_debug("") {"=> #{result.inspect}"}
|
1315
1676
|
result
|
1677
|
+
rescue ::JSON::ParserError => e
|
1678
|
+
raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
|
1316
1679
|
end
|
1317
1680
|
|
1318
1681
|
##
|
@@ -1326,25 +1689,21 @@ module JSON::LD
|
|
1326
1689
|
#
|
1327
1690
|
# @return [Hash] Object representation of value
|
1328
1691
|
# @raise [JsonLdError] if the iri cannot be expanded
|
1329
|
-
# @see
|
1692
|
+
# @see https://www.w3.org/TR/json-ld11-api/#value-compaction
|
1330
1693
|
# FIXME: revisit the specification version of this.
|
1331
1694
|
def compact_value(property, value, **options)
|
1332
1695
|
#log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
|
1333
1696
|
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
if num_members > 2
|
1338
|
-
#log_debug("") {"can't compact value with # members > 2"}
|
1339
|
-
return value
|
1340
|
-
end
|
1697
|
+
indexing = index?(value) && container(property).include?('@index')
|
1698
|
+
language = language(property)
|
1699
|
+
direction = direction(property)
|
1341
1700
|
|
1342
1701
|
result = case
|
1343
|
-
when coerce(property) == '@id' && value.has_key?('@id') &&
|
1702
|
+
when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
|
1344
1703
|
# Compact an @id coercion
|
1345
1704
|
#log_debug("") {" (@id & coerce)"}
|
1346
1705
|
compact_iri(value['@id'])
|
1347
|
-
when coerce(property) == '@vocab' && value.has_key?('@id') &&
|
1706
|
+
when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
|
1348
1707
|
# Compact an @id coercion
|
1349
1708
|
#log_debug("") {" (@id & coerce & vocab)"}
|
1350
1709
|
compact_iri(value['@id'], vocab: true)
|
@@ -1352,25 +1711,32 @@ module JSON::LD
|
|
1352
1711
|
#log_debug("") {" (@id)"}
|
1353
1712
|
# return value as is
|
1354
1713
|
value
|
1355
|
-
when value['@type'] &&
|
1714
|
+
when value['@type'] && value['@type'] == coerce(property)
|
1356
1715
|
# Compact common datatype
|
1357
1716
|
#log_debug("") {" (@type & coerce) == #{coerce(property)}"}
|
1358
1717
|
value['@value']
|
1359
|
-
when
|
1360
|
-
#
|
1361
|
-
|
1362
|
-
|
1363
|
-
when num_members == 1 && !value['@value'].is_a?(String)
|
1718
|
+
when coerce(property) == '@none' || value['@type']
|
1719
|
+
# use original expanded value
|
1720
|
+
value
|
1721
|
+
when !value['@value'].is_a?(String)
|
1364
1722
|
#log_debug("") {" (native)"}
|
1365
|
-
value['@value']
|
1366
|
-
when
|
1367
|
-
#
|
1368
|
-
value['@value']
|
1723
|
+
indexing || !index?(value) ? value['@value'] : value
|
1724
|
+
when value['@language'].to_s.downcase == language.to_s.downcase && value['@direction'] == direction
|
1725
|
+
# Compact language and direction
|
1726
|
+
indexing || !index?(value) ? value['@value'] : value
|
1369
1727
|
else
|
1370
|
-
# Otherwise, use original value
|
1371
|
-
#log_debug("") {" (no change)"}
|
1372
1728
|
value
|
1373
1729
|
end
|
1730
|
+
|
1731
|
+
if result.is_a?(Hash) && result.has_key?('@type') && value['@type'] != '@json'
|
1732
|
+
# Compact values of @type
|
1733
|
+
c_type = if result['@type'].is_a?(Array)
|
1734
|
+
result['@type'].map {|t| compact_iri(t, vocab: true)}
|
1735
|
+
else
|
1736
|
+
compact_iri(result['@type'], vocab: true)
|
1737
|
+
end
|
1738
|
+
result = result.merge('@type' => c_type)
|
1739
|
+
end
|
1374
1740
|
|
1375
1741
|
# If the result is an object, tranform keys using any term keyword aliases
|
1376
1742
|
if result.is_a?(Hash) && result.keys.any? {|k| self.alias(k) != k}
|
@@ -1388,8 +1754,11 @@ module JSON::LD
|
|
1388
1754
|
|
1389
1755
|
##
|
1390
1756
|
# Turn this into a source for a new instantiation
|
1757
|
+
# @param [Array<String>] aliases
|
1758
|
+
# Other URLs to alias when preloading
|
1391
1759
|
# @return [String]
|
1392
|
-
def to_rb
|
1760
|
+
def to_rb(*aliases)
|
1761
|
+
canon_base = RDF::URI(context_base).canonicalize
|
1393
1762
|
defn = []
|
1394
1763
|
|
1395
1764
|
defn << "base: #{self.base.to_s.inspect}" if self.base
|
@@ -1406,7 +1775,9 @@ module JSON::LD
|
|
1406
1775
|
require 'json/ld'
|
1407
1776
|
class JSON::LD::Context
|
1408
1777
|
).gsub(/^ /, '') +
|
1409
|
-
|
1778
|
+
%[ add_preloaded("#{canon_base}") do\n new(] + defn.join(", ") + ")\n end\n" +
|
1779
|
+
aliases.map {|a| %[ alias_preloaded("#{a}", "#{canon_base}")\n]}.join("") +
|
1780
|
+
"end\n"
|
1410
1781
|
end
|
1411
1782
|
|
1412
1783
|
def inspect
|
@@ -1415,10 +1786,12 @@ module JSON::LD
|
|
1415
1786
|
v << "vocab=#{vocab}" if vocab
|
1416
1787
|
v << "processingMode=#{processingMode}" if processingMode
|
1417
1788
|
v << "default_language=#{default_language}" if default_language
|
1789
|
+
v << "default_direction=#{default_direction}" if default_direction
|
1790
|
+
v << "previous_context" if previous_context
|
1418
1791
|
v << "term_definitions[#{term_definitions.length}]=#{term_definitions}"
|
1419
1792
|
v.join(" ") + "]"
|
1420
1793
|
end
|
1421
|
-
|
1794
|
+
|
1422
1795
|
def dup
|
1423
1796
|
# Also duplicate mappings, coerce and list
|
1424
1797
|
that = self
|
@@ -1432,19 +1805,6 @@ module JSON::LD
|
|
1432
1805
|
|
1433
1806
|
protected
|
1434
1807
|
|
1435
|
-
##
|
1436
|
-
# Retrieve term coercion
|
1437
|
-
#
|
1438
|
-
# @param [String] property in unexpanded form
|
1439
|
-
#
|
1440
|
-
# @return [RDF::URI, '@id']
|
1441
|
-
def coerce(property)
|
1442
|
-
# Map property, if it's not an RDF::Value
|
1443
|
-
# @type is always is an IRI
|
1444
|
-
return '@id' if property == RDF.type || property == '@type'
|
1445
|
-
term_definitions[property] && term_definitions[property].type_mapping
|
1446
|
-
end
|
1447
|
-
|
1448
1808
|
##
|
1449
1809
|
# Determine if `term` is a suitable term.
|
1450
1810
|
# Term may be any valid JSON string.
|
@@ -1473,6 +1833,8 @@ module JSON::LD
|
|
1473
1833
|
CONTEXT_CONTAINER_ARRAY_TERMS = %w(@set @list @graph).freeze
|
1474
1834
|
CONTEXT_CONTAINER_ID_GRAPH = %w(@id @graph).freeze
|
1475
1835
|
CONTEXT_CONTAINER_INDEX_GRAPH = %w(@index @graph).freeze
|
1836
|
+
CONTEXT_BASE_FRAG_OR_QUERY = %w(? #).freeze
|
1837
|
+
CONTEXT_TYPE_ID_VOCAB = %w(@id @vocab).freeze
|
1476
1838
|
|
1477
1839
|
def uri(value)
|
1478
1840
|
case value.to_s
|
@@ -1520,7 +1882,8 @@ module JSON::LD
|
|
1520
1882
|
# "@language": {
|
1521
1883
|
# "@null": "term",
|
1522
1884
|
# "@none": "term",
|
1523
|
-
# "en": "term"
|
1885
|
+
# "en": "term",
|
1886
|
+
# "ar_rtl": "term"
|
1524
1887
|
# },
|
1525
1888
|
# "@type": {
|
1526
1889
|
# "@reverse": "term",
|
@@ -1537,7 +1900,7 @@ module JSON::LD
|
|
1537
1900
|
def inverse_context
|
1538
1901
|
@inverse_context ||= begin
|
1539
1902
|
result = {}
|
1540
|
-
default_language = self.default_language || '@none'
|
1903
|
+
default_language = (self.default_language || '@none').downcase
|
1541
1904
|
term_definitions.keys.sort do |a, b|
|
1542
1905
|
a.length == b.length ? (a <=> b) : (a.length <=> b.length)
|
1543
1906
|
end.each do |term|
|
@@ -1556,11 +1919,33 @@ module JSON::LD
|
|
1556
1919
|
any_map['@none'] ||= term
|
1557
1920
|
if td.reverse_property
|
1558
1921
|
type_map['@reverse'] ||= term
|
1922
|
+
elsif td.type_mapping == '@none'
|
1923
|
+
type_map['@any'] ||= term
|
1924
|
+
language_map['@any'] ||= term
|
1925
|
+
any_map['@any'] ||= term
|
1559
1926
|
elsif td.type_mapping
|
1560
1927
|
type_map[td.type_mapping.to_s] ||= term
|
1928
|
+
elsif !td.language_mapping.nil? && !td.direction_mapping.nil?
|
1929
|
+
lang_dir = if td.language_mapping && td.direction_mapping
|
1930
|
+
"#{td.language_mapping}_#{td.direction_mapping}".downcase
|
1931
|
+
elsif td.language_mapping
|
1932
|
+
td.language_mapping.downcase
|
1933
|
+
elsif td.direction_mapping
|
1934
|
+
"_#{td.direction_mapping}"
|
1935
|
+
else
|
1936
|
+
"@null"
|
1937
|
+
end
|
1938
|
+
language_map[lang_dir] ||= term
|
1561
1939
|
elsif !td.language_mapping.nil?
|
1562
|
-
|
1563
|
-
language_map[
|
1940
|
+
lang_dir = (td.language_mapping || '@null').downcase
|
1941
|
+
language_map[lang_dir] ||= term
|
1942
|
+
elsif !td.direction_mapping.nil?
|
1943
|
+
lang_dir = td.direction_mapping ? "_#{td.direction_mapping}" : '@none'
|
1944
|
+
language_map[lang_dir] ||= term
|
1945
|
+
elsif default_direction
|
1946
|
+
language_map[("#{td.language_mapping}_#{default_direction}").downcase] ||= term
|
1947
|
+
language_map['@none'] ||= term
|
1948
|
+
type_map['@none'] ||= term
|
1564
1949
|
else
|
1565
1950
|
language_map[default_language] ||= term
|
1566
1951
|
language_map['@none'] ||= term
|
@@ -1620,7 +2005,7 @@ module JSON::LD
|
|
1620
2005
|
iri_set
|
1621
2006
|
end
|
1622
2007
|
b = base.to_s
|
1623
|
-
return iri[b.length..-1] if iri.start_with?(b) &&
|
2008
|
+
return iri[b.length..-1] if iri.start_with?(b) && CONTEXT_BASE_FRAG_OR_QUERY.include?(iri[b.length, 1])
|
1624
2009
|
|
1625
2010
|
@base_and_parents.each_with_index do |bb, index|
|
1626
2011
|
next unless iri.start_with?(bb)
|
@@ -1668,7 +2053,7 @@ module JSON::LD
|
|
1668
2053
|
# Ensure @container mapping is appropriate
|
1669
2054
|
# The result is the original container definition. For IRI containers, this is necessary to be able to determine the @type mapping for string values
|
1670
2055
|
def check_container(container, local_context, defined, term)
|
1671
|
-
if container.is_a?(Array) && (
|
2056
|
+
if container.is_a?(Array) && processingMode('json-ld-1.0')
|
1672
2057
|
raise JsonLdError::InvalidContainerMapping,
|
1673
2058
|
"'@container' on term #{term.inspect} must be a string: #{container.inspect}"
|
1674
2059
|
end
|
@@ -1684,7 +2069,7 @@ module JSON::LD
|
|
1684
2069
|
elsif val.include?('@language')
|
1685
2070
|
raise JsonLdError::InvalidContainerMapping,
|
1686
2071
|
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
|
1687
|
-
has_set && (
|
2072
|
+
has_set && processingMode('json-ld-1.0')
|
1688
2073
|
raise JsonLdError::InvalidContainerMapping,
|
1689
2074
|
"'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
|
1690
2075
|
val.length == 1
|
@@ -1692,7 +2077,7 @@ module JSON::LD
|
|
1692
2077
|
elsif val.include?('@index')
|
1693
2078
|
raise JsonLdError::InvalidContainerMapping,
|
1694
2079
|
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
|
1695
|
-
has_set && (
|
2080
|
+
has_set && processingMode('json-ld-1.0')
|
1696
2081
|
raise JsonLdError::InvalidContainerMapping,
|
1697
2082
|
"'@container' on term #{term.inspect} using @index cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
|
1698
2083
|
(val - CONTEXT_CONTAINER_INDEX_GRAPH).empty?
|
@@ -1700,7 +2085,7 @@ module JSON::LD
|
|
1700
2085
|
elsif val.include?('@id')
|
1701
2086
|
raise JsonLdError::InvalidContainerMapping,
|
1702
2087
|
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
|
1703
|
-
(
|
2088
|
+
processingMode('json-ld-1.0')
|
1704
2089
|
raise JsonLdError::InvalidContainerMapping,
|
1705
2090
|
"'@container' on term #{term.inspect} using @id cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
|
1706
2091
|
(val - CONTEXT_CONTAINER_ID_GRAPH).empty?
|
@@ -1708,7 +2093,7 @@ module JSON::LD
|
|
1708
2093
|
elsif val.include?('@type') || val.include?('@graph')
|
1709
2094
|
raise JsonLdError::InvalidContainerMapping,
|
1710
2095
|
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
|
1711
|
-
(
|
2096
|
+
processingMode('json-ld-1.0')
|
1712
2097
|
raise JsonLdError::InvalidContainerMapping,
|
1713
2098
|
"'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
|
1714
2099
|
val.length == 1
|