json-ld 0.9.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/{README.markdown → README.md} +15 -3
- data/VERSION +1 -1
- data/lib/json/ld.rb +50 -87
- data/lib/json/ld/api.rb +85 -96
- data/lib/json/ld/compact.rb +103 -170
- data/lib/json/ld/context.rb +1137 -0
- data/lib/json/ld/expand.rb +212 -171
- data/lib/json/ld/extensions.rb +17 -1
- data/lib/json/ld/flatten.rb +145 -78
- data/lib/json/ld/frame.rb +1 -1
- data/lib/json/ld/from_rdf.rb +73 -103
- data/lib/json/ld/reader.rb +3 -1
- data/lib/json/ld/resource.rb +3 -3
- data/lib/json/ld/to_rdf.rb +98 -109
- data/lib/json/ld/utils.rb +54 -4
- data/lib/json/ld/writer.rb +5 -5
- data/spec/api_spec.rb +3 -28
- data/spec/compact_spec.rb +76 -113
- data/spec/{evaluation_context_spec.rb → context_spec.rb} +307 -563
- data/spec/expand_spec.rb +163 -187
- data/spec/flatten_spec.rb +119 -114
- data/spec/frame_spec.rb +5 -5
- data/spec/from_rdf_spec.rb +44 -24
- data/spec/suite_compact_spec.rb +11 -8
- data/spec/suite_error_expand_spec.rb +23 -0
- data/spec/suite_expand_spec.rb +3 -7
- data/spec/suite_flatten_spec.rb +3 -3
- data/spec/suite_frame_spec.rb +6 -6
- data/spec/suite_from_rdf_spec.rb +3 -3
- data/spec/suite_helper.rb +13 -6
- data/spec/suite_to_rdf_spec.rb +16 -10
- data/spec/test-files/test-1-rdf.ttl +4 -3
- data/spec/test-files/test-3-rdf.ttl +2 -1
- data/spec/test-files/test-4-compacted.json +1 -1
- data/spec/test-files/test-5-rdf.ttl +3 -2
- data/spec/test-files/test-6-rdf.ttl +3 -2
- data/spec/test-files/test-7-compacted.json +3 -3
- data/spec/test-files/test-7-expanded.json +3 -3
- data/spec/test-files/test-7-rdf.ttl +7 -6
- data/spec/test-files/test-9-compacted.json +1 -1
- data/spec/to_rdf_spec.rb +67 -75
- data/spec/writer_spec.rb +2 -0
- metadata +36 -24
- checksums.yaml +0 -15
- data/lib/json/ld/evaluation_context.rb +0 -984
data/lib/json/ld/compact.rb
CHANGED
@@ -3,7 +3,7 @@ module JSON::LD
|
|
3
3
|
include Utils
|
4
4
|
|
5
5
|
##
|
6
|
-
#
|
6
|
+
# 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.
|
7
7
|
#
|
8
8
|
# @param [Array, Hash] element
|
9
9
|
# @param [String] property (nil)
|
@@ -16,16 +16,13 @@ module JSON::LD
|
|
16
16
|
end
|
17
17
|
case element
|
18
18
|
when Array
|
19
|
-
|
20
|
-
|
21
|
-
# active property.
|
22
|
-
debug("compact") {"Array #{element.inspect}"}
|
23
|
-
result = depth {element.map {|v| compact(v, property)}}
|
19
|
+
debug("") {"Array #{element.inspect}"}
|
20
|
+
result = depth {element.map {|item| compact(item, property)}.compact}
|
24
21
|
|
25
22
|
# If element has a single member and the active property has no
|
26
23
|
# @container mapping to @list or @set, the compacted value is that
|
27
24
|
# member; otherwise the compacted value is element
|
28
|
-
if result.length == 1 && @options[:compactArrays]
|
25
|
+
if result.length == 1 && context.container(property).nil? && @options[:compactArrays]
|
29
26
|
debug("=> extract single element: #{result.first.inspect}")
|
30
27
|
result.first
|
31
28
|
else
|
@@ -33,160 +30,127 @@ module JSON::LD
|
|
33
30
|
result
|
34
31
|
end
|
35
32
|
when Hash
|
36
|
-
#
|
37
|
-
result = {}
|
33
|
+
# Otherwise element is a JSON object.
|
38
34
|
|
39
|
-
|
40
|
-
|
41
|
-
end
|
35
|
+
# @null objects are used in framing
|
36
|
+
return nil if element.has_key?('@null')
|
42
37
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
v = context.compact_value(property, element, :depth => @depth)
|
49
|
-
debug("compact") {"value optimization for #{property}, return as #{v.inspect}"}
|
50
|
-
return v
|
51
|
-
when '@list'
|
52
|
-
# Otherwise, if the active property has a @container mapping to @list and element has a corresponding @list property, recursively compact that property's value passing a copy of the active context and the active property ensuring that the result is an array with all null values removed.
|
53
|
-
|
54
|
-
# If there already exists a value for active property in element and the full IRI of property is also coerced to @list, return an error.
|
55
|
-
# FIXME: check for full-iri list coercion
|
56
|
-
|
57
|
-
# Otherwise store the resulting array as value of active property if empty or property otherwise.
|
58
|
-
compacted_key = context.compact_iri('@list', :position => :predicate, :depth => @depth)
|
59
|
-
v = depth { compact(element[k], property) }
|
60
|
-
|
61
|
-
# Return either the result as an array, as an object with a key of @list (or appropriate alias from active context
|
62
|
-
v = [v].compact unless v.is_a?(Array)
|
63
|
-
unless context.container(property) == '@list'
|
64
|
-
v = {compacted_key => v}
|
65
|
-
if element['@index']
|
66
|
-
compacted_key = context.compact_iri('@index', :position => :predicate, :depth => @depth)
|
67
|
-
v[compacted_key] = element['@index']
|
68
|
-
end
|
38
|
+
if element.keys.any? {|k| %w(@id @value).include?(k)}
|
39
|
+
result = context.compact_value(property, element, :depth => @depth)
|
40
|
+
unless result.is_a?(Hash)
|
41
|
+
debug("") {"=> scalar result: #{result.inspect}"}
|
42
|
+
return result
|
69
43
|
end
|
70
|
-
debug("compact") {"@list result, return as #{v.inspect}"}
|
71
|
-
return v
|
72
44
|
end
|
73
45
|
|
74
|
-
|
75
|
-
|
76
|
-
# Select property generator terms by shortest term
|
77
|
-
context.mappings.keys.sort.each do |term|
|
78
|
-
next unless context.mapping(term).is_a?(Array)
|
79
|
-
# Using the first expanded IRI p associated with the property generator
|
80
|
-
expanded_iris = context.mapping(term).map(&:to_s)
|
81
|
-
p = expanded_iris.first.to_s
|
82
|
-
|
83
|
-
# Skip to the next property generator term unless p is a property of element
|
84
|
-
next unless element.has_key?(p)
|
85
|
-
|
86
|
-
debug("compact") {"check pg #{term}: #{expanded_iris}"}
|
87
|
-
|
88
|
-
# For each node n which is a value of p in element
|
89
|
-
node_values = []
|
90
|
-
element[p].dup.each do |n|
|
91
|
-
# For each expanded IRI pi associated with the property generator other than p
|
92
|
-
next unless expanded_iris[1..-1].all? do |pi|
|
93
|
-
debug("compact") {"check #{pi} for (#{n.inspect})"}
|
94
|
-
element.has_key?(pi) && element[pi].any? do |ni|
|
95
|
-
nodesEquivalent?(n, ni)
|
96
|
-
end
|
97
|
-
end
|
46
|
+
inside_reverse = property == '@reverse'
|
47
|
+
result = {}
|
98
48
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
49
|
+
element.keys.each do |expanded_property|
|
50
|
+
expanded_value = element[expanded_property]
|
51
|
+
debug("") {"#{expanded_property}: #{expanded_value.inspect}"}
|
52
|
+
|
53
|
+
if %w(@id @type).include?(expanded_property)
|
54
|
+
compacted_value = [expanded_value].flatten.compact.map do |expanded_type|
|
55
|
+
depth {context.compact_iri(expanded_type, :vocab => (expanded_property == '@type'), :depth => @depth)}
|
104
56
|
end
|
57
|
+
compacted_value = compacted_value.first if compacted_value.length == 1
|
105
58
|
|
106
|
-
|
107
|
-
|
59
|
+
al = context.compact_iri(expanded_property, :vocab => true, :quiet => true)
|
60
|
+
debug(expanded_property) {"result[#{al}] = #{compacted_value.inspect}"}
|
61
|
+
result[al] = compacted_value
|
62
|
+
next
|
108
63
|
end
|
109
64
|
|
110
|
-
|
111
|
-
|
112
|
-
debug("
|
113
|
-
|
114
|
-
|
65
|
+
if expanded_property == '@reverse'
|
66
|
+
compacted_value = depth {compact(expanded_value, '@reverse')}
|
67
|
+
debug("@reverse") {"compacted_value: #{compacted_value.inspect}"}
|
68
|
+
compacted_value.each do |prop, value|
|
69
|
+
if context.reverse?(prop)
|
70
|
+
value = [value] unless value.is_a?(Array) || @options[:compactArrays]
|
71
|
+
debug("") {"merge #{prop} => #{value.inspect}"}
|
72
|
+
merge_compacted_value(result, prop, value)
|
73
|
+
compacted_value.delete(prop)
|
74
|
+
end
|
75
|
+
end
|
115
76
|
|
116
|
-
|
117
|
-
|
118
|
-
debug("
|
119
|
-
|
77
|
+
unless compacted_value.empty?
|
78
|
+
al = context.compact_iri('@reverse', :quiet => true)
|
79
|
+
debug("") {"remainder: #{al} => #{compacted_value.inspect}"}
|
80
|
+
result[al] = compacted_value
|
120
81
|
end
|
82
|
+
next
|
121
83
|
end
|
122
|
-
end
|
123
84
|
|
124
|
-
|
125
|
-
|
126
|
-
debug("compact") {"#{key}: #{value.inspect}"}
|
127
|
-
|
128
|
-
if %(@id @type).include?(key)
|
129
|
-
position = key == '@id' ? :subject : :type
|
130
|
-
compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
|
131
|
-
|
132
|
-
result[compacted_key] = case value
|
133
|
-
when String
|
134
|
-
# If value is a string, the compacted value is the result of performing IRI Compaction on value.
|
135
|
-
debug {" => compacted string for #{key}"}
|
136
|
-
context.compact_iri(value, :position => position, :depth => @depth)
|
137
|
-
when Array
|
138
|
-
# Otherwise, value must be an array. Perform IRI Compaction on every entry of value. If value contains just one entry, value is set to that entry
|
139
|
-
compacted_value = value.map {|v2| context.compact_iri(v2, :position => position, :depth => @depth)}
|
140
|
-
debug {" => compacted value(#{key}): #{compacted_value.inspect}"}
|
141
|
-
compacted_value = compacted_value.first if compacted_value.length == 1 && @options[:compactArrays]
|
142
|
-
compacted_value
|
143
|
-
end
|
144
|
-
elsif key == '@index' && context.container(property) == '@index'
|
145
|
-
# Skip the annotation key if annotations being applied
|
85
|
+
if expanded_property == '@index' && context.container(property) == '@index'
|
86
|
+
debug("@index") {"drop @index"}
|
146
87
|
next
|
147
|
-
|
148
|
-
if value.empty?
|
149
|
-
# Make sure that an empty array is preserved
|
150
|
-
compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
|
151
|
-
next if compacted_key.nil?
|
152
|
-
result[compacted_key] = value
|
153
|
-
next
|
154
|
-
end
|
88
|
+
end
|
155
89
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
end
|
90
|
+
# Otherwise, if expanded property is @index, @value, or @language:
|
91
|
+
if %w(@index @value @language).include?(expanded_property)
|
92
|
+
al = context.compact_iri(expanded_property, :vocab => true, :quiet => true)
|
93
|
+
debug(expanded_property) {"#{al} => #{expanded_value.inspect}"}
|
94
|
+
result[al] = expanded_value
|
95
|
+
next
|
96
|
+
end
|
97
|
+
|
98
|
+
if expanded_value == []
|
99
|
+
item_active_property = depth do
|
100
|
+
context.compact_iri(expanded_property,
|
101
|
+
:value => expanded_value,
|
102
|
+
:vocab => true,
|
103
|
+
:reverse => inside_reverse,
|
104
|
+
:depth => @depth)
|
105
|
+
end
|
173
106
|
|
174
|
-
|
175
|
-
|
107
|
+
iap = result[item_active_property] ||= []
|
108
|
+
result[item_active_property] = [iap] unless iap.is_a?(Array)
|
109
|
+
end
|
176
110
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
111
|
+
# At this point, expanded value must be an array due to the Expansion algorithm.
|
112
|
+
expanded_value.each do |expanded_item|
|
113
|
+
item_active_property = depth do
|
114
|
+
context.compact_iri(expanded_property,
|
115
|
+
:value => expanded_item,
|
116
|
+
:vocab => true,
|
117
|
+
:reverse => inside_reverse,
|
118
|
+
:depth => @depth)
|
119
|
+
end
|
120
|
+
container = context.container(item_active_property)
|
121
|
+
value = list?(expanded_item) ? expanded_item['@list'] : expanded_item
|
122
|
+
compacted_item = depth {compact(value, item_active_property)}
|
123
|
+
debug("") {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"}
|
124
|
+
|
125
|
+
if list?(expanded_item)
|
126
|
+
compacted_item = [compacted_item] unless compacted_item.is_a?(Array)
|
127
|
+
unless container == '@list'
|
128
|
+
al = context.compact_iri('@list', :vocab => true, :quiet => true)
|
129
|
+
compacted_item = {al => compacted_item}
|
130
|
+
if expanded_item.has_key?('@index')
|
131
|
+
key = context.compact_iri('@index', :vocab => true, :quiet => true)
|
132
|
+
compacted_item[key] = expanded_item['@index']
|
184
133
|
end
|
185
|
-
item_result[item_key] = compacted_item
|
186
134
|
else
|
187
|
-
|
135
|
+
raise ProcessingError::CompactionToListOfLists,
|
136
|
+
"key cannot have more than one list value" if result.has_key?(item_active_property)
|
188
137
|
end
|
189
138
|
end
|
139
|
+
|
140
|
+
if %w(@language @index).include?(container)
|
141
|
+
map_object = result[item_active_property] ||= {}
|
142
|
+
compacted_item = compacted_item['@value'] if container == '@language' && value?(compacted_item)
|
143
|
+
map_key = expanded_item[container]
|
144
|
+
merge_compacted_value(map_object, map_key, compacted_item)
|
145
|
+
else
|
146
|
+
compacted_item = [compacted_item] if
|
147
|
+
!compacted_item.is_a?(Array) && (
|
148
|
+
!@options[:compactArrays] ||
|
149
|
+
%w(@set @list).include?(container) ||
|
150
|
+
%w(@list @graph).include?(expanded_property)
|
151
|
+
)
|
152
|
+
merge_compacted_value(result, item_active_property, compacted_item)
|
153
|
+
end
|
190
154
|
end
|
191
155
|
end
|
192
156
|
|
@@ -200,36 +164,5 @@ module JSON::LD
|
|
200
164
|
element
|
201
165
|
end
|
202
166
|
end
|
203
|
-
|
204
|
-
private
|
205
|
-
|
206
|
-
# Determines if two nodes are equivalent.
|
207
|
-
# * Value nodes are equivalent using a deep comparison
|
208
|
-
# * Arrays are equivalent if they have the same number of elements and each element is equivalent to the matching element
|
209
|
-
# * Node Defintions/References are equivalent IFF the have the same @id
|
210
|
-
def nodesEquivalent?(n1, n2)
|
211
|
-
depth do
|
212
|
-
r = if n1.is_a?(Array) && n2.is_a?(Array) && n1.length == n2.length
|
213
|
-
equiv = true
|
214
|
-
n1.each_with_index do |v1, i|
|
215
|
-
equiv &&= nodesEquivalent?(v1, n2[i]) if equiv
|
216
|
-
end
|
217
|
-
equiv
|
218
|
-
elsif value?(n1) && value?(n2)
|
219
|
-
n1 == n2
|
220
|
-
elsif list?(n1)
|
221
|
-
list?(n2) &&
|
222
|
-
n1.fetch('@index', true) == n2.fetch('@index', true) &&
|
223
|
-
nodesEquivalent?(n1['@list'], n2['@list'])
|
224
|
-
elsif (node?(n1) || node_reference?(n2))
|
225
|
-
(node?(n2) || node_reference?(n2)) && n1['@id'] == n2['@id']
|
226
|
-
else
|
227
|
-
false
|
228
|
-
end
|
229
|
-
|
230
|
-
debug("nodesEquivalent?(#{n1.inspect}, #{n2.inspect}): #{r.inspect}")
|
231
|
-
r
|
232
|
-
end
|
233
|
-
end
|
234
167
|
end
|
235
168
|
end
|
@@ -0,0 +1,1137 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'json'
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module JSON::LD
|
6
|
+
class Context
|
7
|
+
include Utils
|
8
|
+
|
9
|
+
# Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
|
10
|
+
class TermDefinition
|
11
|
+
# @return [String] IRI map
|
12
|
+
attr_accessor :id
|
13
|
+
|
14
|
+
# @return [String] term name
|
15
|
+
attr_accessor :term
|
16
|
+
|
17
|
+
# @return [String] Type mapping
|
18
|
+
attr_accessor :type_mapping
|
19
|
+
|
20
|
+
# @return [String] Container mapping
|
21
|
+
attr_accessor :container_mapping
|
22
|
+
|
23
|
+
# Language mapping of term, `false` is used if there is explicitly no language mapping for this term.
|
24
|
+
# @return [String] Language mapping
|
25
|
+
attr_accessor :language_mapping
|
26
|
+
|
27
|
+
# @return [Boolean] Reverse Property
|
28
|
+
attr_accessor :reverse_property
|
29
|
+
|
30
|
+
# Create a new Term Mapping with an ID
|
31
|
+
# @param [String] term
|
32
|
+
# @param [String] id
|
33
|
+
def initialize(term, id = nil)
|
34
|
+
@term = term
|
35
|
+
@id = id.to_s if id
|
36
|
+
end
|
37
|
+
|
38
|
+
# Output Hash or String definition for this definition
|
39
|
+
# @param [Context] context
|
40
|
+
# @return [String, Hash{String => Array[String], String}]
|
41
|
+
def to_context_definition(context)
|
42
|
+
if language_mapping.nil? &&
|
43
|
+
container_mapping.nil? &&
|
44
|
+
type_mapping.nil? &&
|
45
|
+
reverse_property.nil?
|
46
|
+
cid = context.compact_iri(id)
|
47
|
+
cid == term ? id : cid
|
48
|
+
else
|
49
|
+
defn = Hash.ordered
|
50
|
+
cid = context.compact_iri(id)
|
51
|
+
defn[reverse_property ? '@reverse' : '@id'] = cid unless cid == term && !reverse_property
|
52
|
+
if type_mapping
|
53
|
+
defn['@type'] = if KEYWORDS.include?(type_mapping)
|
54
|
+
type_mapping
|
55
|
+
else
|
56
|
+
context.compact_iri(type_mapping, :vocab => true)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
defn['@container'] = container_mapping if container_mapping
|
60
|
+
# Language set as false to be output as null
|
61
|
+
defn['@language'] = (language_mapping ? language_mapping : nil) unless language_mapping.nil?
|
62
|
+
defn
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
v = %w([TD)
|
68
|
+
v << "id=#{@id}"
|
69
|
+
v << "rev" if reverse_property
|
70
|
+
v << "container=#{container_mapping}" if container_mapping
|
71
|
+
v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
|
72
|
+
v << "type=#{type_mapping}" unless type_mapping.nil?
|
73
|
+
v.join(" ") + "]"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# The base.
|
78
|
+
#
|
79
|
+
# @return [RDF::URI] Document base IRI, used for expanding relative IRIs.
|
80
|
+
attr_reader :base
|
81
|
+
|
82
|
+
# @return [RDF::URI] base IRI of the context, if loaded remotely. XXX
|
83
|
+
attr_accessor :context_base
|
84
|
+
|
85
|
+
# Term definitions
|
86
|
+
# @!attribute [r] term_definitions
|
87
|
+
# @return [Hash{String => TermDefinition}]
|
88
|
+
attr_reader :term_definitions
|
89
|
+
|
90
|
+
# @return [Hash{RDF::URI => String}] Reverse mappings from IRI to term only for terms, not CURIEs XXX
|
91
|
+
attr_accessor :iri_to_term
|
92
|
+
|
93
|
+
# Default language
|
94
|
+
#
|
95
|
+
#
|
96
|
+
# This adds a language to plain strings that aren't otherwise coerced
|
97
|
+
# @!attribute [rw] default_language
|
98
|
+
# @return [String]
|
99
|
+
attr_reader :default_language
|
100
|
+
|
101
|
+
# Default vocabulary
|
102
|
+
#
|
103
|
+
# Sets the default vocabulary used for expanding terms which
|
104
|
+
# aren't otherwise absolute IRIs
|
105
|
+
# @return [String]
|
106
|
+
attr_reader :vocab
|
107
|
+
|
108
|
+
# @return [Hash{Symbol => Object}] Global options used in generating IRIs
|
109
|
+
attr_accessor :options
|
110
|
+
|
111
|
+
# @return [Context] A context provided to us that we can use without re-serializing XXX
|
112
|
+
attr_accessor :provided_context
|
113
|
+
|
114
|
+
# @!attribute [r] remote_contexts
|
115
|
+
# @return [Array<String>] The list of remote contexts already processed
|
116
|
+
attr_accessor :remote_contexts
|
117
|
+
|
118
|
+
# @!attribute [r] namer
|
119
|
+
# @return [BlankNodeNamer]
|
120
|
+
attr_accessor :namer
|
121
|
+
|
122
|
+
##
|
123
|
+
# Create new evaluation context
|
124
|
+
# @yield [ec]
|
125
|
+
# @yieldparam [Context]
|
126
|
+
# @return [Context]
|
127
|
+
def initialize(options = {})
|
128
|
+
if options[:base]
|
129
|
+
@doc_base = RDF::URI(options[:base])
|
130
|
+
@doc_base.canonicalize!
|
131
|
+
@doc_base.fragment = nil
|
132
|
+
@doc_base.query = nil
|
133
|
+
end
|
134
|
+
@term_definitions = {}
|
135
|
+
@iri_to_term = {
|
136
|
+
RDF.to_uri.to_s => "rdf",
|
137
|
+
RDF::XSD.to_uri.to_s => "xsd"
|
138
|
+
}
|
139
|
+
@remote_contexts = []
|
140
|
+
@namer = BlankNodeMapper.new("t")
|
141
|
+
|
142
|
+
@options = options
|
143
|
+
|
144
|
+
# Load any defined prefixes
|
145
|
+
(options[:prefixes] || {}).each_pair do |k, v|
|
146
|
+
@iri_to_term[v.to_s] = k unless k.nil?
|
147
|
+
end
|
148
|
+
|
149
|
+
debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
|
150
|
+
|
151
|
+
yield(self) if block_given?
|
152
|
+
end
|
153
|
+
|
154
|
+
# @param [String] value must be an absolute IRI
|
155
|
+
def base=(value)
|
156
|
+
if value
|
157
|
+
raise InvalidContext::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String)
|
158
|
+
@base = RDF::URI(value)
|
159
|
+
@base.canonicalize!
|
160
|
+
@base.fragment = nil
|
161
|
+
@base.query = nil
|
162
|
+
raise InvalidContext::InvalidBaseIRI, "@base must be an absolute IRI: #{value.inspect}" unless @base.absolute?
|
163
|
+
@base
|
164
|
+
else
|
165
|
+
@base = nil
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
# @param [String] value
|
171
|
+
def default_language=(value)
|
172
|
+
@default_language = if value
|
173
|
+
raise InvalidContext::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}" unless value.is_a?(String)
|
174
|
+
value.downcase
|
175
|
+
else
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# @param [String] value must be an absolute IRI
|
181
|
+
def vocab=(value)
|
182
|
+
if value
|
183
|
+
raise InvalidContext::InvalidVocabMapping, "@value must be a string: #{value.inspect}" unless value.is_a?(String)
|
184
|
+
@vocab = RDF::URI(value)
|
185
|
+
raise InvalidContext::InvalidVocabMapping, "@value must be an absolute IRI: #{value.inspect}" unless @vocab.absolute?
|
186
|
+
@vocab
|
187
|
+
else
|
188
|
+
@vocab = nil
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Create an Evaluation Context
|
193
|
+
#
|
194
|
+
# 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.
|
195
|
+
#
|
196
|
+
# 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.
|
197
|
+
#
|
198
|
+
# 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.
|
199
|
+
#
|
200
|
+
#
|
201
|
+
# @param [String, #read, Array, Hash, Context] local_context
|
202
|
+
# @raise [InvalidContext]
|
203
|
+
# on a remote context load error, syntax error, or a reference to a term which is not defined.
|
204
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/index.html#context-processing-algorithm
|
205
|
+
def parse(local_context, remote_contexts = [])
|
206
|
+
result = self.dup
|
207
|
+
local_context = [local_context] unless local_context.is_a?(Array)
|
208
|
+
|
209
|
+
local_context.each do |context|
|
210
|
+
depth do
|
211
|
+
case context
|
212
|
+
when nil
|
213
|
+
# 3.1 If niil, set to a new empty context
|
214
|
+
result = Context.new(options)
|
215
|
+
when Context
|
216
|
+
debug("parse") {"context: #{context.inspect}"}
|
217
|
+
result = context.dup
|
218
|
+
when IO, StringIO
|
219
|
+
debug("parse") {"io: #{context}"}
|
220
|
+
# Load context document, if it is a string
|
221
|
+
begin
|
222
|
+
ctx = JSON.load(context)
|
223
|
+
raise JSON::LD::InvalidContext::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
|
224
|
+
result = parse(ctx["@context"] ? ctx["@context"].dup : {})
|
225
|
+
result.provided_context = ctx["@context"]
|
226
|
+
result
|
227
|
+
rescue JSON::ParserError => e
|
228
|
+
debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
|
229
|
+
raise JSON::LD::InvalidContext::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
|
230
|
+
self.dup
|
231
|
+
end
|
232
|
+
when String
|
233
|
+
debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
|
234
|
+
# Load context document, if it is a string
|
235
|
+
begin
|
236
|
+
# 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].
|
237
|
+
context = RDF::URI(result.context_base || result.base || @doc_base).join(context)
|
238
|
+
|
239
|
+
raise InvalidContext::RecursiveContextInclusion, "#{context}" if remote_contexts.include?(context)
|
240
|
+
@remote_contexts = @remote_contexts + [context]
|
241
|
+
|
242
|
+
context_no_base = self.dup
|
243
|
+
context_no_base.base = nil
|
244
|
+
context_no_base.provided_context = context
|
245
|
+
context_no_base.context_base = context
|
246
|
+
|
247
|
+
RDF::Util::File.open_file(context) do |f|
|
248
|
+
# 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.
|
249
|
+
jo = JSON.load(f)
|
250
|
+
raise InvalidContext::InvalidRemoteContext, "#{context}" unless jo.is_a?(Hash) && jo.has_key?('@context')
|
251
|
+
context = jo['@context']
|
252
|
+
end
|
253
|
+
|
254
|
+
# 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.
|
255
|
+
context = context_no_base.parse(context, remote_contexts.dup)
|
256
|
+
context.base = result.base unless result.base.nil?
|
257
|
+
result = context
|
258
|
+
debug("parse") {"=> provided_context: #{context.inspect}"}
|
259
|
+
rescue Exception => e
|
260
|
+
debug("parse") {"Failed to retrieve @context from remote document at #{context.inspect}: #{e.message}"}
|
261
|
+
raise InvalidContext::InvalidRemoteContext, "#{context}", e.backtrace if @options[:validate]
|
262
|
+
end
|
263
|
+
when Hash
|
264
|
+
# 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.
|
265
|
+
{
|
266
|
+
'@base' => :base=,
|
267
|
+
'@language' => :default_language=,
|
268
|
+
'@vocab' => :vocab=
|
269
|
+
}.each do |key, setter|
|
270
|
+
v = context.fetch(key, false)
|
271
|
+
unless v == false
|
272
|
+
context.delete(key)
|
273
|
+
debug("parse") {"Set #{key} to #{v.inspect}"}
|
274
|
+
result.send(setter, v)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
defined = {}
|
279
|
+
# 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
|
280
|
+
depth do
|
281
|
+
context.keys.each do |key|
|
282
|
+
result.create_term_definition(context, key, defined)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
else
|
286
|
+
# 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
|
287
|
+
raise InvalidContext::InvalidLocalContext
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
result
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
##
|
296
|
+
# Create Term Definition
|
297
|
+
#
|
298
|
+
# 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.
|
299
|
+
#
|
300
|
+
# 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.
|
301
|
+
#
|
302
|
+
# @param [Hash] local_context
|
303
|
+
# @param [String] term
|
304
|
+
# @param [Hash] defined
|
305
|
+
# @raise [InvalidContext]
|
306
|
+
# Represents a cyclical term dependency
|
307
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/index.html#create-term-definition
|
308
|
+
def create_term_definition(local_context, term, defined)
|
309
|
+
# Expand a string value, unless it matches a keyword
|
310
|
+
debug("create_term_definition") {"term = #{term.inspect}"}
|
311
|
+
|
312
|
+
# If defined contains the key term, then the associated value must be true, indicating that the term definition has already been created, so return. Otherwise, a cyclical term definition has been detected, which is an error.
|
313
|
+
case defined[term]
|
314
|
+
when TrueClass then return
|
315
|
+
when nil
|
316
|
+
defined[term] = false
|
317
|
+
else
|
318
|
+
raise InvalidContext::CyclicIRIMapping, "Cyclical term dependency found for #{term.inspect}"
|
319
|
+
end
|
320
|
+
|
321
|
+
# Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
|
322
|
+
if KEYWORDS.include?(term) && !%w(@vocab @language).include?(term)
|
323
|
+
raise InvalidContext::KeywordRedefinition, "term #{term.inspect} must not be a keyword" if
|
324
|
+
@options[:validate]
|
325
|
+
elsif !term_valid?(term) && @options[:validate]
|
326
|
+
raise InvalidContext::InvalidTermDefinition, "term #{term.inspect} is invalid"
|
327
|
+
end
|
328
|
+
|
329
|
+
# Remove any existing term definition for term in active context.
|
330
|
+
term_definitions.delete(term)
|
331
|
+
|
332
|
+
# Initialize value to a the value associated with the key term in local context.
|
333
|
+
value = local_context.fetch(term, false)
|
334
|
+
value = {'@id' => value} if value.is_a?(String)
|
335
|
+
|
336
|
+
case value
|
337
|
+
when nil, {'@id' => nil}
|
338
|
+
# If value equals null or value is a JSON object containing the key-value pair (@id-null), then set the term definition in active context to null, set the value associated with defined's key term to true, and return.
|
339
|
+
debug("") {"=> nil"}
|
340
|
+
term_definitions[term] = TermDefinition.new(term)
|
341
|
+
defined[term] = true
|
342
|
+
return
|
343
|
+
when Hash
|
344
|
+
debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
|
345
|
+
definition = TermDefinition.new(term)
|
346
|
+
|
347
|
+
if value.has_key?('@reverse')
|
348
|
+
raise InvalidContext::InvalidReverseProperty, "unexpected key in #{value.inspect}" if
|
349
|
+
value.keys.any? {|k| ['@id', '@type', '@language'].include?(k)}
|
350
|
+
raise InvalidContext::InvalidIRIMapping, "expected value of @reverse to be a string" unless
|
351
|
+
value['@reverse'].is_a?(String)
|
352
|
+
|
353
|
+
# 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.
|
354
|
+
definition.id = expand_iri(value['@reverse'],
|
355
|
+
:vocab => true,
|
356
|
+
:documentRelative => true,
|
357
|
+
:local_context => local_context,
|
358
|
+
:defined => defined)
|
359
|
+
raise InvalidContext::InvalidIRImapping, "non-absolute @reverse IRI: #{definition.id}" unless
|
360
|
+
definition.id.absolute?
|
361
|
+
definition.type_mapping = '@id'
|
362
|
+
|
363
|
+
# If value contains an @container member, set the container mapping of definition to @index if that is the value of the @container member; otherwise an invalid reverse property error has been detected (reverse properties only support index-containers) and processing is aborted.
|
364
|
+
if (container = value['@container']) && container != '@index'
|
365
|
+
raise InvalidContext::InvalidReverseProperty, "unknown mapping for '@container' to #{container.inspect}"
|
366
|
+
end
|
367
|
+
definition.reverse_property = true
|
368
|
+
elsif value.has_key?('@id') && value['@id'] != term
|
369
|
+
raise InvalidContext::InvalidIRIMapping, "expected value of @reverse to be a string" unless
|
370
|
+
value['@id'].is_a?(String)
|
371
|
+
definition.id = expand_iri(value['@id'],
|
372
|
+
:vocab => true,
|
373
|
+
:documentRelative => true,
|
374
|
+
:local_context => local_context,
|
375
|
+
:defined => defined)
|
376
|
+
elsif term.include?(':')
|
377
|
+
# 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.
|
378
|
+
prefix, suffix = term.split(':')
|
379
|
+
depth {create_term_definition(local_context, prefix, defined)} if local_context.has_key?(prefix)
|
380
|
+
|
381
|
+
definition.id = if td = term_definitions[prefix]
|
382
|
+
# 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.
|
383
|
+
td.id + suffix
|
384
|
+
else
|
385
|
+
# Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
|
386
|
+
term
|
387
|
+
end
|
388
|
+
debug("") {"=> #{definition.id}"}
|
389
|
+
else
|
390
|
+
# 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.
|
391
|
+
raise InvalidContext::InvalidIRIMapping, "relative term definition without vocab" unless vocab
|
392
|
+
definition.id = vocab + term
|
393
|
+
debug("") {"=> #{definition.id}"}
|
394
|
+
end
|
395
|
+
|
396
|
+
if value.has_key?('@type')
|
397
|
+
type = value['@type']
|
398
|
+
# SPEC FIXME: @type may be nil
|
399
|
+
raise InvalidContext::InvalidTypeMapping, "unknown mapping for '@type' to #{type.inspect}" unless type.is_a?(String) || type.nil?
|
400
|
+
type = expand_iri(type, :vocab => true, :documentRelative => true, :local_context => local_context, :defined => defined) if type.is_a?(String)
|
401
|
+
debug("") {"type_mapping: #{type.inspect}"}
|
402
|
+
definition.type_mapping = type
|
403
|
+
end
|
404
|
+
|
405
|
+
if value.has_key?('@container')
|
406
|
+
container = value['@container']
|
407
|
+
raise InvalidContext::InvalidContainerMapping, "unknown mapping for '@container' to #{container.inspect}" unless %w(@list @set @language @index).include?(container)
|
408
|
+
debug("") {"container_mapping: #{container.inspect}"}
|
409
|
+
definition.container_mapping = container
|
410
|
+
end
|
411
|
+
|
412
|
+
if value.has_key?('@language')
|
413
|
+
language = value['@language']
|
414
|
+
raise InvalidContext::InvalidLanguageMapping, "language must be null or a string, was #{language.inspect}}" unless language.nil? || (language || "").is_a?(String)
|
415
|
+
language = language.downcase if language.is_a?(String)
|
416
|
+
debug("") {"language_mapping: #{language.inspect}"}
|
417
|
+
definition.language_mapping = language || false
|
418
|
+
end
|
419
|
+
|
420
|
+
term_definitions[term] = definition
|
421
|
+
defined[term] = true
|
422
|
+
else
|
423
|
+
raise InvalidContext::InvalidTermDefinition, "Term definition for #{term.inspect} is an #{value.class}"
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
##
|
428
|
+
# Generate @context
|
429
|
+
#
|
430
|
+
# If a context was supplied in global options, use that, otherwise, generate one
|
431
|
+
# from this representation.
|
432
|
+
#
|
433
|
+
# @param [Hash{Symbol => Object}] options ({})
|
434
|
+
# @return [Hash]
|
435
|
+
def serialize(options = {})
|
436
|
+
depth(options) do
|
437
|
+
# FIXME: not setting provided_context now
|
438
|
+
use_context = case provided_context
|
439
|
+
when RDF::URI
|
440
|
+
debug "serlialize: reuse context: #{provided_context.inspect}"
|
441
|
+
provided_context.to_s
|
442
|
+
when Hash, Array
|
443
|
+
debug "serlialize: reuse context: #{provided_context.inspect}"
|
444
|
+
provided_context
|
445
|
+
else
|
446
|
+
debug("serlialize: generate context")
|
447
|
+
debug("") {"=> context: #{inspect}"}
|
448
|
+
ctx = Hash.ordered
|
449
|
+
ctx['@base'] = base.to_s if base
|
450
|
+
ctx['@language'] = default_language.to_s if default_language
|
451
|
+
ctx['@vocab'] = vocab.to_s if vocab
|
452
|
+
|
453
|
+
# Term Definitions
|
454
|
+
term_definitions.each do |term, definition|
|
455
|
+
ctx[term] = definition.to_context_definition(self)
|
456
|
+
end
|
457
|
+
|
458
|
+
debug("") {"start_doc: context=#{ctx.inspect}"}
|
459
|
+
ctx
|
460
|
+
end
|
461
|
+
|
462
|
+
# Return hash with @context, or empty
|
463
|
+
r = Hash.ordered
|
464
|
+
r['@context'] = use_context unless use_context.nil? || use_context.empty?
|
465
|
+
r
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
## FIXME: this should go away
|
470
|
+
# Retrieve term mappings
|
471
|
+
#
|
472
|
+
# @return [Array<String>]
|
473
|
+
# @deprecated
|
474
|
+
def mappings
|
475
|
+
term_definitions.inject({}) do |memo, (t,td)|
|
476
|
+
memo[t] = td ? td.id : nil
|
477
|
+
memo
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
## FIXME: this should go away
|
482
|
+
# Retrieve term mapping
|
483
|
+
#
|
484
|
+
# @param [String, #to_s] term
|
485
|
+
#
|
486
|
+
# @return [RDF::URI, String]
|
487
|
+
# @deprecated
|
488
|
+
def mapping(term)
|
489
|
+
term_definitions[term] ? term_definitions[term].id : nil
|
490
|
+
end
|
491
|
+
|
492
|
+
## FIXME: this should go away
|
493
|
+
# Set term mapping
|
494
|
+
#
|
495
|
+
# @param [#to_s] term
|
496
|
+
# @param [RDF::URI, String, nil] value
|
497
|
+
#
|
498
|
+
# @return [RDF::URI, String]
|
499
|
+
# @deprecated
|
500
|
+
def set_mapping(term, value)
|
501
|
+
debug("") {"map #{term.inspect} to #{value.inspect}"}
|
502
|
+
term = term.to_s
|
503
|
+
term_definitions[term] = TermDefinition.new(term, value)
|
504
|
+
|
505
|
+
term_sym = term.empty? ? "" : term.to_sym
|
506
|
+
iri_to_term.delete(term_definitions[term].id.to_s) if term_definitions[term].id.is_a?(String)
|
507
|
+
@options[:prefixes][term_sym] = value if @options.has_key?(:prefixes)
|
508
|
+
iri_to_term[value.to_s] = term
|
509
|
+
end
|
510
|
+
|
511
|
+
## FIXME: this should go away
|
512
|
+
# Reverse term mapping, typically used for finding aliases for keys.
|
513
|
+
#
|
514
|
+
# Returns either the original value, or a mapping for this value.
|
515
|
+
#
|
516
|
+
# @example
|
517
|
+
# {"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}
|
518
|
+
#
|
519
|
+
# @param [RDF::URI, String] value
|
520
|
+
# @return [String]
|
521
|
+
# @deprecated
|
522
|
+
def alias(value)
|
523
|
+
iri_to_term.fetch(value, value)
|
524
|
+
end
|
525
|
+
|
526
|
+
##
|
527
|
+
# Retrieve term coercion
|
528
|
+
#
|
529
|
+
# @param [String] property in unexpanded form
|
530
|
+
#
|
531
|
+
# @return [RDF::URI, '@id']
|
532
|
+
def coerce(property)
|
533
|
+
# Map property, if it's not an RDF::Value
|
534
|
+
# @type is always is an IRI
|
535
|
+
return '@id' if [RDF.type, '@type'].include?(property)
|
536
|
+
term_definitions[property] && term_definitions[property].type_mapping
|
537
|
+
end
|
538
|
+
protected :coerce
|
539
|
+
|
540
|
+
##
|
541
|
+
# Retrieve container mapping, add it if `value` is provided
|
542
|
+
#
|
543
|
+
# @param [String] property in unexpanded form
|
544
|
+
# @return [String]
|
545
|
+
def container(property)
|
546
|
+
return '@set' if property == '@graph'
|
547
|
+
return property if KEYWORDS.include?(property)
|
548
|
+
term_definitions[property] && term_definitions[property].container_mapping
|
549
|
+
end
|
550
|
+
|
551
|
+
## FIXME: this should go away
|
552
|
+
# Retrieve language mappings
|
553
|
+
#
|
554
|
+
# @return [Array<String>]
|
555
|
+
# @deprecated
|
556
|
+
def languages
|
557
|
+
term_definitions.inject({}) do |memo, (t,td)|
|
558
|
+
memo[t] = td.language_mapping
|
559
|
+
memo
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
##
|
564
|
+
# Retrieve the language associated with a property, or the default language otherwise
|
565
|
+
# @return [String]
|
566
|
+
def language(property)
|
567
|
+
lang = term_definitions[property] && term_definitions[property].language_mapping
|
568
|
+
lang.nil? ? @default_language : lang
|
569
|
+
end
|
570
|
+
|
571
|
+
##
|
572
|
+
# Is this a reverse term
|
573
|
+
# @return [Boolean]
|
574
|
+
def reverse?(property)
|
575
|
+
term_definitions[property] && term_definitions[property].reverse_property
|
576
|
+
end
|
577
|
+
|
578
|
+
##
|
579
|
+
# Determine if `term` is a suitable term.
|
580
|
+
# Term may be any valid JSON string.
|
581
|
+
#
|
582
|
+
# @param [String] term
|
583
|
+
# @return [Boolean]
|
584
|
+
def term_valid?(term)
|
585
|
+
term.is_a?(String)
|
586
|
+
end
|
587
|
+
protected :term_valid?
|
588
|
+
|
589
|
+
##
|
590
|
+
# Expand an IRI. Relative IRIs are expanded against any document base.
|
591
|
+
#
|
592
|
+
# @param [String] value
|
593
|
+
# A keyword, term, prefix:suffix or possibly relative IRI
|
594
|
+
# @param [Hash{Symbol => Object}] options
|
595
|
+
# @option options [Boolean] documentRelative (false)
|
596
|
+
# @option options [Boolean] vocab (false)
|
597
|
+
# @option options [Hash] local_context
|
598
|
+
# Used during Context Processing.
|
599
|
+
# @option options [Hash] defined
|
600
|
+
# Used during Context Processing.
|
601
|
+
# @return [RDF::URI, String]
|
602
|
+
# IRI or String, if it's a keyword
|
603
|
+
# @raise [JSON::LD::InvalidContext::InvalidIRIMapping] if the value cannot be expanded
|
604
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
|
605
|
+
def expand_iri(value, options = {})
|
606
|
+
return value unless value.is_a?(String)
|
607
|
+
|
608
|
+
return value if KEYWORDS.include?(value)
|
609
|
+
depth(options) do
|
610
|
+
debug("expand_iri") {"value: #{value.inspect}"} unless options[:quiet]
|
611
|
+
local_context = options[:local_context]
|
612
|
+
defined = options.fetch(:defined, {})
|
613
|
+
|
614
|
+
# If local context is not null, it contains a key that equals value, and the value associated with the key that equals value in defined is not true, then invoke the Create Term Definition subalgorithm, passing active context, local context, value as term, and defined. This will ensure that a term definition is created for value in active context during Context Processing.
|
615
|
+
if local_context && local_context.has_key?(value) && !defined[value]
|
616
|
+
depth {create_term_definition(local_context, value, defined)}
|
617
|
+
end
|
618
|
+
|
619
|
+
# If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
|
620
|
+
if options[:vocab] && (v_td = term_definitions[value])
|
621
|
+
debug("") {"match with #{v_td.id}"} unless options[:quiet]
|
622
|
+
return v_td.id
|
623
|
+
end
|
624
|
+
|
625
|
+
# If value contains a colon (:), it is either an absolute IRI or a compact IRI:
|
626
|
+
if value.include?(':')
|
627
|
+
prefix, suffix = value.split(':', 2)
|
628
|
+
debug("") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{vocab.inspect}"} unless options[:quiet]
|
629
|
+
|
630
|
+
# If prefix is underscore (_) or suffix begins with double-forward-slash (//), return value as it is already an absolute IRI or a blank node identifier.
|
631
|
+
return RDF::Node.new(namer.get_sym(suffix)) if prefix == '_'
|
632
|
+
return RDF::URI(value) if suffix[0,2] == '//'
|
633
|
+
|
634
|
+
# If local context is not null, it contains a key that equals prefix, and the value associated with the key that equals prefix in defined is not true, invoke the Create Term Definition algorithm, passing active context, local context, prefix as term, and defined. This will ensure that a term definition is created for prefix in active context during Context Processing.
|
635
|
+
if local_context && local_context.has_key?(prefix) && !defined[prefix]
|
636
|
+
create_term_definition(local_context, prefix, defined)
|
637
|
+
end
|
638
|
+
|
639
|
+
# If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
|
640
|
+
result = if (td = term_definitions[prefix])
|
641
|
+
result = td.id + suffix
|
642
|
+
else
|
643
|
+
# (Otherwise) Return value as it is already an absolute IRI.
|
644
|
+
RDF::URI(value)
|
645
|
+
end
|
646
|
+
|
647
|
+
debug("") {"=> #{result.inspect}"} unless options[:quiet]
|
648
|
+
return result
|
649
|
+
end
|
650
|
+
debug("") {"=> #{result.inspect}"} unless options[:quiet]
|
651
|
+
|
652
|
+
result = if options[:vocab] && vocab
|
653
|
+
# If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
|
654
|
+
vocab + value
|
655
|
+
elsif options[:documentRelative] && base = options.fetch(:base, self.base || @doc_base)
|
656
|
+
# 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].
|
657
|
+
RDF::URI(base).join(value).to_s
|
658
|
+
elsif local_context && RDF::URI(value).relative?
|
659
|
+
# If local context is not null and value is not an absolute IRI, an invalid IRI mapping error has been detected and processing is aborted.
|
660
|
+
raise JSON::LD::InvalidContext::InvalidIRIMapping, "not an absolute IRI: #{value}"
|
661
|
+
else
|
662
|
+
RDF::URI(value)
|
663
|
+
end
|
664
|
+
debug("") {"=> #{result}"} unless options[:quiet]
|
665
|
+
result
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
669
|
+
##
|
670
|
+
# Compacts an absolute IRI to the shortest matching term or compact IRI
|
671
|
+
#
|
672
|
+
# @param [RDF::URI] iri
|
673
|
+
# @param [Hash{Symbol => Object}] options ({})
|
674
|
+
# @option options [Object] :value
|
675
|
+
# Value, used to select among various maps for the same IRI
|
676
|
+
# @option options [Boolean] :vocab
|
677
|
+
# specifies whether the passed iri should be compacted using the active context's vocabulary mapping
|
678
|
+
# @option options [Boolean] :reverse
|
679
|
+
# specifies whether a reverse property is being compacted
|
680
|
+
#
|
681
|
+
# @return [String] compacted form of IRI
|
682
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#iri-compaction
|
683
|
+
def compact_iri(iri, options = {})
|
684
|
+
return if iri.nil?
|
685
|
+
iri = iri.to_s
|
686
|
+
debug("compact_iri(#{iri.inspect}", options) {options.inspect} unless options[:quiet]
|
687
|
+
depth(options) do
|
688
|
+
|
689
|
+
value = options.fetch(:value, nil)
|
690
|
+
|
691
|
+
if options[:vocab] && inverse_context.has_key?(iri)
|
692
|
+
debug("") {"vocab and key in inverse context"} unless options[:quiet]
|
693
|
+
default_language = self.default_language || @none
|
694
|
+
containers = []
|
695
|
+
tl, tl_value = "@language", "@null"
|
696
|
+
containers << '@index' if index?(value)
|
697
|
+
if options[:reverse]
|
698
|
+
tl, tl_value = "@type", "@reverse"
|
699
|
+
containers << '@set'
|
700
|
+
elsif list?(value)
|
701
|
+
debug("") {"list(#{value.inspect})"} unless options[:quiet]
|
702
|
+
# if value is a list object, then set type/language and type/language value to the most specific values that work for all items in the list as follows:
|
703
|
+
containers << "@list" unless index?(value)
|
704
|
+
list = value['@list']
|
705
|
+
common_type = nil
|
706
|
+
common_language = default_language if list.empty?
|
707
|
+
list.each do |item|
|
708
|
+
item_language, item_type = "@none", "@none"
|
709
|
+
if value?(item)
|
710
|
+
if item.has_key?('@language')
|
711
|
+
item_language = item['@language']
|
712
|
+
elsif item.has_key?('@type')
|
713
|
+
item_type = item['@type']
|
714
|
+
else
|
715
|
+
item_language = "@null"
|
716
|
+
end
|
717
|
+
else
|
718
|
+
item_type = '@id'
|
719
|
+
end
|
720
|
+
common_language ||= item_language
|
721
|
+
if item_language != common_language && value?(item)
|
722
|
+
debug("") {"-- #{item_language} conflicts with #{common_language}, use @none"} unless options[:quiet]
|
723
|
+
common_language = '@none'
|
724
|
+
end
|
725
|
+
common_type ||= item_type
|
726
|
+
if item_type != common_type
|
727
|
+
common_type = '@none'
|
728
|
+
debug("") {"#{item_type} conflicts with #{common_type}, use @none"} unless options[:quiet]
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
common_language ||= '@none'
|
733
|
+
common_type ||= '@none'
|
734
|
+
debug("") {"common type: #{common_type}, common language: #{common_language}"} unless options[:quiet]
|
735
|
+
if common_type != '@none'
|
736
|
+
tl, tl_value = '@type', common_type
|
737
|
+
else
|
738
|
+
tl_value = common_language
|
739
|
+
end
|
740
|
+
debug("") {"list: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless options[:quiet]
|
741
|
+
else
|
742
|
+
if value?(value)
|
743
|
+
if value.has_key?('@language') && !index?(value)
|
744
|
+
tl_value = value['@language']
|
745
|
+
containers << '@language'
|
746
|
+
elsif value.has_key?('@type')
|
747
|
+
tl_value = value['@type']
|
748
|
+
tl = '@type'
|
749
|
+
end
|
750
|
+
else
|
751
|
+
tl, tl_value = '@type', '@id'
|
752
|
+
end
|
753
|
+
containers << '@set'
|
754
|
+
debug("") {"value: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless options[:quiet]
|
755
|
+
end
|
756
|
+
|
757
|
+
containers << '@none'
|
758
|
+
tl_value ||= '@null'
|
759
|
+
preferred_values = []
|
760
|
+
preferred_values << '@reverse' if tl_value == '@reverse'
|
761
|
+
if %w(@id @reverse).include?(tl_value) && value.is_a?(Hash) && value.has_key?('@id')
|
762
|
+
t_iri = compact_iri(value['@id'], :vocab => true, :document_relative => true)
|
763
|
+
if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
|
764
|
+
preferred_values.concat(%w(@vocab @id @none))
|
765
|
+
else
|
766
|
+
preferred_values.concat(%w(@id @vocab @none))
|
767
|
+
end
|
768
|
+
else
|
769
|
+
preferred_values.concat([tl_value, '@none'])
|
770
|
+
end
|
771
|
+
debug("") {"preferred_values: #{preferred_values.inspect}"} unless options[:quiet]
|
772
|
+
if p_term = select_term(iri, containers, tl, preferred_values)
|
773
|
+
debug("") {"=> term: #{p_term.inspect}"} unless options[:quiet]
|
774
|
+
return p_term
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
# At this point, there is no simple term that iri can be compacted to. If vocab is true and active context has a vocabulary mapping:
|
779
|
+
if options[:vocab] && vocab && iri.start_with?(vocab) && iri.length > vocab.length
|
780
|
+
suffix = iri[vocab.length..-1]
|
781
|
+
debug("") {"=> vocab suffix: #{suffix.inspect}"} unless options[:quiet]
|
782
|
+
return suffix unless term_definitions.has_key?(suffix)
|
783
|
+
end
|
784
|
+
|
785
|
+
# The iri could not be compacted using the active context's vocabulary mapping. Try to create a compact IRI, starting by initializing compact IRI to null. This variable will be used to tore the created compact IRI, if any.
|
786
|
+
candidates = []
|
787
|
+
|
788
|
+
term_definitions.each do |term, td|
|
789
|
+
next if term.include?(":")
|
790
|
+
next if td.nil? || td.id.nil? || td.id == iri || !iri.start_with?(td.id)
|
791
|
+
suffix = iri[td.id.length..-1]
|
792
|
+
ciri = "#{term}:#{suffix}"
|
793
|
+
candidates << ciri unless value && term_definitions.has_key?(ciri)
|
794
|
+
end
|
795
|
+
|
796
|
+
if !candidates.empty?
|
797
|
+
debug("") {"=> compact iri: #{candidates.term_sort.first.inspect}"} unless options[:quiet]
|
798
|
+
return candidates.term_sort.first
|
799
|
+
end
|
800
|
+
|
801
|
+
# If we still don't have any terms and we're using standard_prefixes,
|
802
|
+
# try those, and add to mapping
|
803
|
+
if @options[:standard_prefixes]
|
804
|
+
candidates = RDF::Vocabulary.
|
805
|
+
select {|v| iri.start_with?(v.to_uri.to_s)}.
|
806
|
+
map do |v|
|
807
|
+
prefix = v.__name__.to_s.split('::').last.downcase
|
808
|
+
set_mapping(prefix, v.to_uri.to_s)
|
809
|
+
iri.sub(v.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
|
810
|
+
end
|
811
|
+
|
812
|
+
if !candidates.empty?
|
813
|
+
debug("") {"=> standard prefies: #{candidates.term_sort.first.inspect}"} unless options[:quiet]
|
814
|
+
return candidates.term_sort.first
|
815
|
+
end
|
816
|
+
end
|
817
|
+
|
818
|
+
if !options[:vocab]
|
819
|
+
# transform iri to a relative IRI using the document's base IRI
|
820
|
+
iri = remove_base(iri)
|
821
|
+
debug("") {"=> relative iri: #{iri.inspect}"} unless options[:quiet]
|
822
|
+
return iri
|
823
|
+
else
|
824
|
+
debug("") {"=> absolute iri: #{iri.inspect}"} unless options[:quiet]
|
825
|
+
return iri
|
826
|
+
end
|
827
|
+
end
|
828
|
+
end
|
829
|
+
|
830
|
+
##
|
831
|
+
# If active property has a type mapping in the active context set to @id or @vocab, a JSON object with a single member @id whose value is the result of using the IRI Expansion algorithm on value is returned.
|
832
|
+
#
|
833
|
+
# Otherwise, the result will be a JSON object containing an @value member whose value is the passed value. Additionally, an @type member will be included if there is a type mapping associated with the active property or an @language member if value is a string and there is language mapping associated with the active property.
|
834
|
+
#
|
835
|
+
# @param [String] property
|
836
|
+
# Associated property used to find coercion rules
|
837
|
+
# @param [Hash, String] value
|
838
|
+
# Value (literal or IRI) to be expanded
|
839
|
+
# @param [Hash{Symbol => Object}] options
|
840
|
+
# @option options [Boolean] :useNativeTypes (true) use native representations
|
841
|
+
#
|
842
|
+
# @return [Hash] Object representation of value
|
843
|
+
# @raise [RDF::ReaderError] if the iri cannot be expanded
|
844
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
|
845
|
+
def expand_value(property, value, options = {})
|
846
|
+
options = {:useNativeTypes => true}.merge(options)
|
847
|
+
depth(options) do
|
848
|
+
debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
|
849
|
+
|
850
|
+
# If the active property has a type mapping in active context that is @id, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, and true for document relative.
|
851
|
+
if (td = term_definitions.fetch(property, TermDefinition.new(property))) && td.type_mapping == '@id'
|
852
|
+
debug("") {"as relative IRI: #{value.inspect}"}
|
853
|
+
return {'@id' => expand_iri(value, :documentRelative => true).to_s}
|
854
|
+
end
|
855
|
+
|
856
|
+
# If active property has a type mapping in active context that is @vocab, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, true for vocab, and true for document relative.
|
857
|
+
if td.type_mapping == '@vocab'
|
858
|
+
debug("") {"as vocab IRI: #{value.inspect}"}
|
859
|
+
return {'@id' => expand_iri(value, :vocab => true, :documentRelative => true).to_s}
|
860
|
+
end
|
861
|
+
|
862
|
+
value = RDF::Literal(value) if
|
863
|
+
value.is_a?(Date) ||
|
864
|
+
value.is_a?(DateTime) ||
|
865
|
+
value.is_a?(Time)
|
866
|
+
|
867
|
+
result = case value
|
868
|
+
when RDF::URI, RDF::Node
|
869
|
+
debug("URI | BNode") { value.to_s }
|
870
|
+
{'@id' => value.to_s}
|
871
|
+
when RDF::Literal
|
872
|
+
debug("Literal") {"datatype: #{value.datatype.inspect}"}
|
873
|
+
res = Hash.ordered
|
874
|
+
if options[:useNativeTypes] && [RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double].include?(value.datatype)
|
875
|
+
res['@value'] = value.object
|
876
|
+
res['@type'] = uri(coerce(property)) if coerce(property)
|
877
|
+
else
|
878
|
+
value.canonicalize! if value.datatype == RDF::XSD.double
|
879
|
+
res['@value'] = value.to_s
|
880
|
+
if coerce(property)
|
881
|
+
res['@type'] = uri(coerce(property)).to_s
|
882
|
+
elsif value.has_datatype?
|
883
|
+
res['@type'] = uri(value.datatype).to_s
|
884
|
+
elsif value.has_language? || language(property)
|
885
|
+
res['@language'] = (value.language || language(property)).to_s
|
886
|
+
end
|
887
|
+
end
|
888
|
+
res
|
889
|
+
else
|
890
|
+
# Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
|
891
|
+
res = {'@value' => value}
|
892
|
+
|
893
|
+
if td.type_mapping
|
894
|
+
res['@type'] = td.type_mapping.to_s
|
895
|
+
elsif value.is_a?(String)
|
896
|
+
if td.language_mapping
|
897
|
+
res['@language'] = td.language_mapping
|
898
|
+
elsif default_language && td.language_mapping.nil?
|
899
|
+
res['@language'] = default_language
|
900
|
+
end
|
901
|
+
end
|
902
|
+
res
|
903
|
+
end
|
904
|
+
|
905
|
+
debug("") {"=> #{result.inspect}"}
|
906
|
+
result
|
907
|
+
end
|
908
|
+
end
|
909
|
+
|
910
|
+
##
|
911
|
+
# Compact a value
|
912
|
+
#
|
913
|
+
# @param [String] property
|
914
|
+
# Associated property used to find coercion rules
|
915
|
+
# @param [Hash] value
|
916
|
+
# Value (literal or IRI), in full object representation, to be compacted
|
917
|
+
# @param [Hash{Symbol => Object}] options
|
918
|
+
#
|
919
|
+
# @return [Hash] Object representation of value
|
920
|
+
# @raise [ProcessingError] if the iri cannot be expanded
|
921
|
+
# @see http://json-ld.org/spec/latest/json-ld-api/#value-compaction
|
922
|
+
# FIXME: revisit the specification version of this.
|
923
|
+
def compact_value(property, value, options = {})
|
924
|
+
|
925
|
+
depth(options) do
|
926
|
+
debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
|
927
|
+
|
928
|
+
num_members = value.keys.length
|
929
|
+
|
930
|
+
num_members -= 1 if index?(value) && container(property) == '@index'
|
931
|
+
if num_members > 2
|
932
|
+
debug("") {"can't compact value with # members > 2"}
|
933
|
+
return value
|
934
|
+
end
|
935
|
+
|
936
|
+
result = case
|
937
|
+
when coerce(property) == '@id' && value.has_key?('@id') && num_members == 1
|
938
|
+
# Compact an @id coercion
|
939
|
+
debug("") {" (@id & coerce)"}
|
940
|
+
compact_iri(value['@id'])
|
941
|
+
when coerce(property) == '@vocab' && value.has_key?('@id') && num_members == 1
|
942
|
+
# Compact an @id coercion
|
943
|
+
debug("") {" (@id & coerce & vocab)"}
|
944
|
+
compact_iri(value['@id'], :vocab => true)
|
945
|
+
when value.has_key?('@id')
|
946
|
+
debug("") {" (@id)"}
|
947
|
+
# return value as is
|
948
|
+
value
|
949
|
+
when value['@type'] && expand_iri(value['@type'], :vocab => true) == coerce(property)
|
950
|
+
# Compact common datatype
|
951
|
+
debug("") {" (@type & coerce) == #{coerce(property)}"}
|
952
|
+
value['@value']
|
953
|
+
when value['@language'] && (value['@language'] == language(property))
|
954
|
+
# Compact language
|
955
|
+
debug("") {" (@language) == #{language(property).inspect}"}
|
956
|
+
value['@value']
|
957
|
+
when num_members == 1 && !value['@value'].is_a?(String)
|
958
|
+
debug("") {" (native)"}
|
959
|
+
value['@value']
|
960
|
+
when num_members == 1 && default_language.nil? || language(property) == false
|
961
|
+
debug("") {" (!@language)"}
|
962
|
+
value['@value']
|
963
|
+
else
|
964
|
+
# Otherwise, use original value
|
965
|
+
debug("") {" (no change)"}
|
966
|
+
value
|
967
|
+
end
|
968
|
+
|
969
|
+
# If the result is an object, tranform keys using any term keyword aliases
|
970
|
+
if result.is_a?(Hash) && result.keys.any? {|k| self.alias(k) != k}
|
971
|
+
debug("") {" (map to key aliases)"}
|
972
|
+
new_element = {}
|
973
|
+
result.each do |k, v|
|
974
|
+
new_element[self.alias(k)] = v
|
975
|
+
end
|
976
|
+
result = new_element
|
977
|
+
end
|
978
|
+
|
979
|
+
debug("") {"=> #{result.inspect}"}
|
980
|
+
result
|
981
|
+
end
|
982
|
+
end
|
983
|
+
|
984
|
+
def inspect
|
985
|
+
v = %w([Context)
|
986
|
+
v << "vocab=#{vocab}" if vocab
|
987
|
+
v << "def_language=#{default_language}" if default_language
|
988
|
+
v << "term_definitions[#{term_definitions.length}]=#{term_definitions}"
|
989
|
+
v.join(" ") + "]"
|
990
|
+
end
|
991
|
+
|
992
|
+
def dup
|
993
|
+
# Also duplicate mappings, coerce and list
|
994
|
+
that = self
|
995
|
+
ec = super
|
996
|
+
ec.instance_eval do
|
997
|
+
@term_definitions = that.term_definitions.dup
|
998
|
+
@iri_to_term = that.iri_to_term.dup
|
999
|
+
end
|
1000
|
+
ec
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
private
|
1004
|
+
|
1005
|
+
def uri(value)
|
1006
|
+
case value.to_s
|
1007
|
+
when /^_:(.*)$/
|
1008
|
+
# Map BlankNodes if a namer is given
|
1009
|
+
debug "uri(bnode)#{value}: #{$1}"
|
1010
|
+
bnode(namer.get_sym($1))
|
1011
|
+
else
|
1012
|
+
value = RDF::URI.new(value)
|
1013
|
+
value.validate! if @options[:validate]
|
1014
|
+
value.canonicalize! if @options[:canonicalize]
|
1015
|
+
value = RDF::URI.intern(value) if @options[:intern]
|
1016
|
+
value
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
|
1020
|
+
# Clear the provided context, used for testing
|
1021
|
+
# @return [Context] self
|
1022
|
+
def clear_provided_context
|
1023
|
+
@provided_context = nil
|
1024
|
+
self
|
1025
|
+
end
|
1026
|
+
|
1027
|
+
# Keep track of allocated BNodes
|
1028
|
+
#
|
1029
|
+
# Don't actually use the name provided, to prevent name alias issues.
|
1030
|
+
# @return [RDF::Node]
|
1031
|
+
def bnode(value = nil)
|
1032
|
+
@@bnode_cache ||= {}
|
1033
|
+
@@bnode_cache[value.to_s] ||= RDF::Node.new(value)
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
##
|
1037
|
+
# Inverse Context creation
|
1038
|
+
#
|
1039
|
+
# When there is more than one term that could be chosen to compact an IRI, it has to be ensured that the term selection is both deterministic and represents the most context-appropriate choice whilst taking into consideration algorithmic complexity.
|
1040
|
+
#
|
1041
|
+
# In order to make term selections, the concept of an inverse context is introduced. An inverse context is essentially a reverse lookup table that maps container mappings, type mappings, and language mappings to a simple term for a given active context. A inverse context only needs to be generated for an active context if it is being used for compaction.
|
1042
|
+
#
|
1043
|
+
# To make use of an inverse context, a list of preferred container mappings and the type mapping or language mapping are gathered for a particular value associated with an IRI. These parameters are then fed to the Term Selection algorithm, which will find the term that most appropriately matches the value's mappings.
|
1044
|
+
#
|
1045
|
+
# @return [Hash{String => Hash{String => String}}]
|
1046
|
+
def inverse_context
|
1047
|
+
@inverse_context ||= begin
|
1048
|
+
result = {}
|
1049
|
+
default_language = self.default_language || '@none'
|
1050
|
+
term_definitions.keys.sort do |a, b|
|
1051
|
+
a.length == b.length ? (a <=> b) : (a.length <=> b.length)
|
1052
|
+
end.each do |term|
|
1053
|
+
next unless td = term_definitions[term]
|
1054
|
+
container = td.container_mapping || '@none'
|
1055
|
+
container_map = result[td.id.to_s] ||= {}
|
1056
|
+
tl_map = container_map[container] ||= {'@language' => {}, '@type' => {}}
|
1057
|
+
type_map = tl_map['@type']
|
1058
|
+
language_map = tl_map['@language']
|
1059
|
+
if td.reverse_property
|
1060
|
+
type_map['@reverse'] ||= term
|
1061
|
+
elsif td.type_mapping
|
1062
|
+
type_map[td.type_mapping.to_s] ||= term
|
1063
|
+
elsif !td.language_mapping.nil?
|
1064
|
+
language = td.language_mapping || '@null'
|
1065
|
+
language_map[language] ||= term
|
1066
|
+
else
|
1067
|
+
language_map[default_language] ||= term
|
1068
|
+
language_map['@none'] ||= term
|
1069
|
+
type_map['@none'] ||= term
|
1070
|
+
end
|
1071
|
+
end
|
1072
|
+
result
|
1073
|
+
end
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
##
|
1077
|
+
# This algorithm, invoked via the IRI Compaction algorithm, makes use of an active context's inverse context to find the term that is best used to compact an IRI. Other information about a value associated with the IRI is given, including which container mappings and which type mapping or language mapping would be best used to express the value.
|
1078
|
+
#
|
1079
|
+
# @param [String] iri
|
1080
|
+
# @param [Array<String>] containers
|
1081
|
+
# represents an ordered list of preferred container mappings
|
1082
|
+
# @param [String] type_language
|
1083
|
+
# indicates whether to look for a term with a matching type mapping or language mapping
|
1084
|
+
# @param [Array<String>] preferred_values
|
1085
|
+
# for the type mapping or language mapping
|
1086
|
+
# @return [String]
|
1087
|
+
def select_term(iri, containers, type_language, preferred_values)
|
1088
|
+
depth do
|
1089
|
+
debug("select_term") {
|
1090
|
+
"iri: #{iri.inspect}, " +
|
1091
|
+
"containers: #{containers.inspect}, " +
|
1092
|
+
"type_language: #{type_language.inspect}, " +
|
1093
|
+
"preferred_values: #{preferred_values.inspect}"
|
1094
|
+
}
|
1095
|
+
container_map = inverse_context[iri]
|
1096
|
+
debug(" ") {"container_map: #{container_map.inspect}"}
|
1097
|
+
containers.each do |container|
|
1098
|
+
next unless container_map.has_key?(container)
|
1099
|
+
tl_map = container_map[container]
|
1100
|
+
value_map = tl_map[type_language]
|
1101
|
+
preferred_values.each do |item|
|
1102
|
+
next unless value_map.has_key?(item)
|
1103
|
+
debug("=>") {value_map[item].inspect}
|
1104
|
+
return value_map[item]
|
1105
|
+
end
|
1106
|
+
end
|
1107
|
+
debug("=>") {"nil"}
|
1108
|
+
nil
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
##
|
1113
|
+
# Removes a base IRI from the given absolute IRI.
|
1114
|
+
#
|
1115
|
+
# @param [String] iri the absolute IRI
|
1116
|
+
# @return [String]
|
1117
|
+
# the relative IRI if relative to base, otherwise the absolute IRI.
|
1118
|
+
def remove_base(iri)
|
1119
|
+
return iri unless base || @doc_base
|
1120
|
+
@base_and_parents ||= begin
|
1121
|
+
u = base || @doc_base
|
1122
|
+
iri_set = u.to_s.end_with?('/') ? [u.to_s] : []
|
1123
|
+
iri_set << u.to_s while (u = u.parent)
|
1124
|
+
iri_set
|
1125
|
+
end
|
1126
|
+
b = (base || @doc_base).to_s
|
1127
|
+
return iri[b.length..-1] if iri.start_with?(b) && %w(? #).include?(iri[b.length, 1])
|
1128
|
+
|
1129
|
+
@base_and_parents.each_with_index do |b, index|
|
1130
|
+
next unless iri.start_with?(b)
|
1131
|
+
rel = "../" * index + iri[b.length..-1]
|
1132
|
+
return rel.empty? ? "./" : rel
|
1133
|
+
end
|
1134
|
+
iri
|
1135
|
+
end
|
1136
|
+
end
|
1137
|
+
end
|