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.
Files changed (45) hide show
  1. data/{README.markdown → README.md} +15 -3
  2. data/VERSION +1 -1
  3. data/lib/json/ld.rb +50 -87
  4. data/lib/json/ld/api.rb +85 -96
  5. data/lib/json/ld/compact.rb +103 -170
  6. data/lib/json/ld/context.rb +1137 -0
  7. data/lib/json/ld/expand.rb +212 -171
  8. data/lib/json/ld/extensions.rb +17 -1
  9. data/lib/json/ld/flatten.rb +145 -78
  10. data/lib/json/ld/frame.rb +1 -1
  11. data/lib/json/ld/from_rdf.rb +73 -103
  12. data/lib/json/ld/reader.rb +3 -1
  13. data/lib/json/ld/resource.rb +3 -3
  14. data/lib/json/ld/to_rdf.rb +98 -109
  15. data/lib/json/ld/utils.rb +54 -4
  16. data/lib/json/ld/writer.rb +5 -5
  17. data/spec/api_spec.rb +3 -28
  18. data/spec/compact_spec.rb +76 -113
  19. data/spec/{evaluation_context_spec.rb → context_spec.rb} +307 -563
  20. data/spec/expand_spec.rb +163 -187
  21. data/spec/flatten_spec.rb +119 -114
  22. data/spec/frame_spec.rb +5 -5
  23. data/spec/from_rdf_spec.rb +44 -24
  24. data/spec/suite_compact_spec.rb +11 -8
  25. data/spec/suite_error_expand_spec.rb +23 -0
  26. data/spec/suite_expand_spec.rb +3 -7
  27. data/spec/suite_flatten_spec.rb +3 -3
  28. data/spec/suite_frame_spec.rb +6 -6
  29. data/spec/suite_from_rdf_spec.rb +3 -3
  30. data/spec/suite_helper.rb +13 -6
  31. data/spec/suite_to_rdf_spec.rb +16 -10
  32. data/spec/test-files/test-1-rdf.ttl +4 -3
  33. data/spec/test-files/test-3-rdf.ttl +2 -1
  34. data/spec/test-files/test-4-compacted.json +1 -1
  35. data/spec/test-files/test-5-rdf.ttl +3 -2
  36. data/spec/test-files/test-6-rdf.ttl +3 -2
  37. data/spec/test-files/test-7-compacted.json +3 -3
  38. data/spec/test-files/test-7-expanded.json +3 -3
  39. data/spec/test-files/test-7-rdf.ttl +7 -6
  40. data/spec/test-files/test-9-compacted.json +1 -1
  41. data/spec/to_rdf_spec.rb +67 -75
  42. data/spec/writer_spec.rb +2 -0
  43. metadata +36 -24
  44. checksums.yaml +0 -15
  45. data/lib/json/ld/evaluation_context.rb +0 -984
@@ -3,7 +3,7 @@ module JSON::LD
3
3
  include Utils
4
4
 
5
5
  ##
6
- # Compact an expanded Array or Hash given an active property and a context.
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
- # 1) If value is an array, process each item in value recursively using
20
- # this algorithm, passing copies of the active context and the
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
- # 2) Otherwise, if element is an object:
37
- result = {}
33
+ # Otherwise element is a JSON object.
38
34
 
39
- if k = %w(@list @set @value).detect {|container| element.has_key?(container)}
40
- debug("compact") {"#{k}: container(#{property}) = #{context.container(property)}"}
41
- end
35
+ # @null objects are used in framing
36
+ return nil if element.has_key?('@null')
42
37
 
43
- k ||= '@id' if element.keys == ['@id']
44
-
45
- case k
46
- when '@value', '@id'
47
- # If element has an @value property or element is a node reference, return the result of performing Value Compaction on element using active property.
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
- # Check for property generators before continuing with other elements
75
- # For each term pg in the active context which is a property generator
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
- # Remove n as a value of all p and pi in element
100
- debug("compact") {"removed matched value #{n.inspect} from #{expanded_iris.inspect}"}
101
- expanded_iris.each do |pi|
102
- # FIXME: This removes all values equivalent to n, not just the first
103
- element[pi] = element[pi].reject {|ni| nodesEquivalent?(n, ni)}
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
- # Add the result of performing the compaction algorithm on n to pg to output
107
- node_values << n
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
- # If there are node_values, or all the values from expanded_iris are empty, add node_values to result, and remove the expanded_iris as keys from element
111
- if node_values.length > 0 || expanded_iris.all? {|pi| element.has_key?(pi) && element[pi].empty?}
112
- debug("compact") {"compact extracted pg values"}
113
- result[term] = depth { compact(node_values, term)}
114
- result[term] = [result[term]] if !result[term].is_a?(Array) && context.container(term) == '@set'
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
- debug("compact") {"remove empty pg keys from element"}
117
- expanded_iris.each do |pi|
118
- debug(" =>") {"#{pi}? #{element.fetch(pi, []).empty?}"}
119
- element.delete(pi) if element.fetch(pi, []).empty?
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
- # Otherwise, for each property and value in element:
125
- element.each do |key, value|
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
- else
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
- # For each item in value:
157
- value = [value] if key == '@index' && value.is_a?(String)
158
- raise ProcessingError, "found #{value.inspect} for #{key} of #{element.inspect}" unless value.is_a?(Array)
159
- value.each do |item|
160
- compacted_key = context.compact_iri(key, :position => :predicate, :value => item, :depth => @depth)
161
-
162
- # Result for this item, typically the output object itself
163
- item_result = result
164
- item_key = compacted_key
165
- debug {" => compacted key: #{compacted_key.inspect} for #{item.inspect}"}
166
- next if compacted_key.nil?
167
-
168
- # Language maps and annotations
169
- if field = %w(@language @index).detect {|kk| context.container(compacted_key) == kk}
170
- item_result = result[compacted_key] ||= Hash.new
171
- item_key = item[field]
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
- compacted_item = depth {self.compact(item, compacted_key)}
175
- debug {" => compacted value: #{compacted_value.inspect}"}
107
+ iap = result[item_active_property] ||= []
108
+ result[item_active_property] = [iap] unless iap.is_a?(Array)
109
+ end
176
110
 
177
- case item_result[item_key]
178
- when Array
179
- item_result[item_key] << compacted_item
180
- when nil
181
- if !compacted_value.is_a?(Array) && context.container(compacted_key) == '@set'
182
- compacted_item = [compacted_item].compact
183
- debug {" => as @set: #{compacted_item.inspect}"}
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
- item_result[item_key] = [item_result[item_key], compacted_item]
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