json-ld 3.2.4 → 3.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/json/ld/api.rb +807 -771
  4. data/lib/json/ld/compact.rb +304 -304
  5. data/lib/json/ld/conneg.rb +179 -161
  6. data/lib/json/ld/context.rb +2080 -1913
  7. data/lib/json/ld/expand.rb +745 -666
  8. data/lib/json/ld/extensions.rb +14 -13
  9. data/lib/json/ld/flatten.rb +257 -247
  10. data/lib/json/ld/format.rb +202 -194
  11. data/lib/json/ld/frame.rb +525 -502
  12. data/lib/json/ld/from_rdf.rb +223 -204
  13. data/lib/json/ld/html/nokogiri.rb +123 -121
  14. data/lib/json/ld/html/rexml.rb +151 -147
  15. data/lib/json/ld/reader.rb +107 -100
  16. data/lib/json/ld/resource.rb +224 -205
  17. data/lib/json/ld/streaming_reader.rb +574 -507
  18. data/lib/json/ld/streaming_writer.rb +93 -92
  19. data/lib/json/ld/to_rdf.rb +171 -169
  20. data/lib/json/ld/utils.rb +270 -264
  21. data/lib/json/ld/version.rb +24 -14
  22. data/lib/json/ld/writer.rb +334 -311
  23. data/lib/json/ld.rb +103 -96
  24. metadata +55 -198
  25. data/spec/api_spec.rb +0 -132
  26. data/spec/compact_spec.rb +0 -3482
  27. data/spec/conneg_spec.rb +0 -373
  28. data/spec/context_spec.rb +0 -2056
  29. data/spec/expand_spec.rb +0 -4496
  30. data/spec/flatten_spec.rb +0 -1203
  31. data/spec/format_spec.rb +0 -115
  32. data/spec/frame_spec.rb +0 -2541
  33. data/spec/from_rdf_spec.rb +0 -1072
  34. data/spec/matchers.rb +0 -20
  35. data/spec/rdfstar_spec.rb +0 -25
  36. data/spec/reader_spec.rb +0 -883
  37. data/spec/resource_spec.rb +0 -76
  38. data/spec/spec_helper.rb +0 -281
  39. data/spec/streaming_reader_spec.rb +0 -237
  40. data/spec/streaming_writer_spec.rb +0 -145
  41. data/spec/suite_compact_spec.rb +0 -22
  42. data/spec/suite_expand_spec.rb +0 -36
  43. data/spec/suite_flatten_spec.rb +0 -34
  44. data/spec/suite_frame_spec.rb +0 -29
  45. data/spec/suite_from_rdf_spec.rb +0 -22
  46. data/spec/suite_helper.rb +0 -411
  47. data/spec/suite_html_spec.rb +0 -22
  48. data/spec/suite_http_spec.rb +0 -35
  49. data/spec/suite_remote_doc_spec.rb +0 -22
  50. data/spec/suite_to_rdf_spec.rb +0 -30
  51. data/spec/support/extensions.rb +0 -44
  52. data/spec/test-files/test-1-compacted.jsonld +0 -10
  53. data/spec/test-files/test-1-context.jsonld +0 -7
  54. data/spec/test-files/test-1-expanded.jsonld +0 -5
  55. data/spec/test-files/test-1-input.jsonld +0 -10
  56. data/spec/test-files/test-1-rdf.ttl +0 -8
  57. data/spec/test-files/test-2-compacted.jsonld +0 -20
  58. data/spec/test-files/test-2-context.jsonld +0 -7
  59. data/spec/test-files/test-2-expanded.jsonld +0 -16
  60. data/spec/test-files/test-2-input.jsonld +0 -20
  61. data/spec/test-files/test-2-rdf.ttl +0 -14
  62. data/spec/test-files/test-3-compacted.jsonld +0 -11
  63. data/spec/test-files/test-3-context.jsonld +0 -8
  64. data/spec/test-files/test-3-expanded.jsonld +0 -10
  65. data/spec/test-files/test-3-input.jsonld +0 -11
  66. data/spec/test-files/test-3-rdf.ttl +0 -8
  67. data/spec/test-files/test-4-compacted.jsonld +0 -10
  68. data/spec/test-files/test-4-context.jsonld +0 -7
  69. data/spec/test-files/test-4-expanded.jsonld +0 -6
  70. data/spec/test-files/test-4-input.jsonld +0 -10
  71. data/spec/test-files/test-4-rdf.ttl +0 -5
  72. data/spec/test-files/test-5-compacted.jsonld +0 -13
  73. data/spec/test-files/test-5-context.jsonld +0 -7
  74. data/spec/test-files/test-5-expanded.jsonld +0 -9
  75. data/spec/test-files/test-5-input.jsonld +0 -13
  76. data/spec/test-files/test-5-rdf.ttl +0 -7
  77. data/spec/test-files/test-6-compacted.jsonld +0 -10
  78. data/spec/test-files/test-6-context.jsonld +0 -7
  79. data/spec/test-files/test-6-expanded.jsonld +0 -10
  80. data/spec/test-files/test-6-input.jsonld +0 -10
  81. data/spec/test-files/test-6-rdf.ttl +0 -6
  82. data/spec/test-files/test-7-compacted.jsonld +0 -23
  83. data/spec/test-files/test-7-context.jsonld +0 -4
  84. data/spec/test-files/test-7-expanded.jsonld +0 -20
  85. data/spec/test-files/test-7-input.jsonld +0 -23
  86. data/spec/test-files/test-7-rdf.ttl +0 -14
  87. data/spec/test-files/test-8-compacted.jsonld +0 -34
  88. data/spec/test-files/test-8-context.jsonld +0 -11
  89. data/spec/test-files/test-8-expanded.jsonld +0 -24
  90. data/spec/test-files/test-8-frame.jsonld +0 -18
  91. data/spec/test-files/test-8-framed.jsonld +0 -25
  92. data/spec/test-files/test-8-input.jsonld +0 -30
  93. data/spec/test-files/test-8-rdf.ttl +0 -15
  94. data/spec/test-files/test-9-compacted.jsonld +0 -20
  95. data/spec/test-files/test-9-context.jsonld +0 -13
  96. data/spec/test-files/test-9-expanded.jsonld +0 -14
  97. data/spec/test-files/test-9-input.jsonld +0 -12
  98. data/spec/to_rdf_spec.rb +0 -1684
  99. data/spec/writer_spec.rb +0 -427
@@ -1,2191 +1,2358 @@
1
- # -*- encoding: utf-8 -*-
2
1
  # frozen_string_literal: true
2
+
3
3
  require 'json'
4
4
  require 'bigdecimal'
5
5
  require 'set'
6
6
  require 'rdf/util/cache'
7
7
 
8
- module JSON::LD
9
- class Context
10
- include Utils
11
- include RDF::Util::Logger
12
-
13
- ##
14
- # Preloaded contexts.
15
- # To avoid runtime context parsing and downloading, contexts may be pre-loaded by implementations.
16
- # @return [Hash{Symbol => Context}]
17
- PRELOADED = {}
8
+ module JSON
9
+ module LD
10
+ class Context
11
+ include Utils
12
+ include RDF::Util::Logger
18
13
 
19
- # Initial contexts, defined on first access
20
- INITIAL_CONTEXTS = {}
14
+ ##
15
+ # Preloaded contexts.
16
+ # To avoid runtime context parsing and downloading, contexts may be pre-loaded by implementations.
17
+ # @return [Hash{Symbol => Context}]
18
+ PRELOADED = {}
21
19
 
22
- ##
23
- # Defines the maximum number of interned URI references that can be held
24
- # cached in memory at any one time.
25
- CACHE_SIZE = 100 # unlimited by default
20
+ # Initial contexts, defined on first access
21
+ INITIAL_CONTEXTS = {}
26
22
 
27
- class << self
28
23
  ##
29
- # Add preloaded context. In the block form, the context is lazy evaulated on first use.
30
- # @param [String, RDF::URI] url
31
- # @param [Context] context (nil)
32
- # @yieldreturn [Context]
33
- def add_preloaded(url, context = nil, &block)
34
- PRELOADED[url.to_s.freeze] = context || block
24
+ # Defines the maximum number of interned URI references that can be held
25
+ # cached in memory at any one time.
26
+ CACHE_SIZE = 100 # unlimited by default
27
+
28
+ class << self
29
+ ##
30
+ # Add preloaded context. In the block form, the context is lazy evaulated on first use.
31
+ # @param [String, RDF::URI] url
32
+ # @param [Context] context (nil)
33
+ # @yieldreturn [Context]
34
+ def add_preloaded(url, context = nil, &block)
35
+ PRELOADED[url.to_s.freeze] = context || block
36
+ end
37
+
38
+ ##
39
+ # Alias a previousliy loaded context
40
+ # @param [String, RDF::URI] a
41
+ # @param [String, RDF::URI] url
42
+ def alias_preloaded(a, url)
43
+ PRELOADED[a.to_s.freeze] = PRELOADED[url.to_s.freeze]
44
+ end
35
45
  end
36
46
 
37
- ##
38
- # Alias a previousliy loaded context
39
- # @param [String, RDF::URI] a
40
- # @param [String, RDF::URI] url
41
- def alias_preloaded(a, url)
42
- PRELOADED[a.to_s.freeze] = PRELOADED[url.to_s.freeze]
47
+ begin
48
+ # Attempt to load this to avoid unnecessary context fetches
49
+ require 'json/ld/preloaded'
50
+ rescue LoadError
51
+ # Silently allow this to fail
43
52
  end
44
- end
45
53
 
46
- begin
47
- # Attempt to load this to avoid unnecessary context fetches
48
- require 'json/ld/preloaded'
49
- rescue LoadError
50
- # Silently allow this to fail
51
- end
54
+ # The base.
55
+ #
56
+ # @return [RDF::URI] Current base IRI, used for expanding relative IRIs.
57
+ attr_reader :base
58
+
59
+ # @return [RDF::URI] base IRI of the context, if loaded remotely.
60
+ attr_accessor :context_base
61
+
62
+ # Term definitions
63
+ # @return [Hash{String => TermDefinition}]
64
+ attr_reader :term_definitions
65
+
66
+ # @return [Hash{RDF::URI => String}] Reverse mappings from IRI to term only for terms, not CURIEs XXX
67
+ attr_accessor :iri_to_term
68
+
69
+ # Previous definition for this context. This is used for rolling back type-scoped contexts.
70
+ # @return [Context]
71
+ attr_accessor :previous_context
72
+
73
+ # Context is property-scoped
74
+ # @return [Boolean]
75
+ attr_accessor :property_scoped
76
+
77
+ # Default language
78
+ #
79
+ # This adds a language to plain strings that aren't otherwise coerced
80
+ # @return [String]
81
+ attr_reader :default_language
52
82
 
53
- # The base.
54
- #
55
- # @return [RDF::URI] Current base IRI, used for expanding relative IRIs.
56
- attr_reader :base
57
-
58
- # @return [RDF::URI] base IRI of the context, if loaded remotely.
59
- attr_accessor :context_base
60
-
61
- # Term definitions
62
- # @return [Hash{String => TermDefinition}]
63
- attr_reader :term_definitions
64
-
65
- # @return [Hash{RDF::URI => String}] Reverse mappings from IRI to term only for terms, not CURIEs XXX
66
- attr_accessor :iri_to_term
67
-
68
- # Previous definition for this context. This is used for rolling back type-scoped contexts.
69
- # @return [Context]
70
- attr_accessor :previous_context
71
-
72
- # Context is property-scoped
73
- # @return [Boolean]
74
- attr_accessor :property_scoped
75
-
76
- # Default language
77
- #
78
- # This adds a language to plain strings that aren't otherwise coerced
79
- # @return [String]
80
- attr_reader :default_language
81
-
82
- # Default direction
83
- #
84
- # This adds a direction to plain strings that aren't otherwise coerced
85
- # @return ["lrt", "rtl"]
86
- attr_reader :default_direction
87
-
88
- # Default vocabulary
89
- #
90
- # Sets the default vocabulary used for expanding terms which
91
- # aren't otherwise absolute IRIs
92
- # @return [RDF::URI]
93
- attr_reader :vocab
94
-
95
- # @return [Hash{Symbol => Object}] Global options used in generating IRIs
96
- attr_accessor :options
97
-
98
- # @return [BlankNodeNamer]
99
- attr_accessor :namer
100
-
101
- ##
102
- # Create a new context by parsing a context.
103
- #
104
- # @see #initialize
105
- # @see #parse
106
- # @param [String, #read, Array, Hash, Context] local_context
107
- # @param [String, #to_s] base (nil)
108
- # The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context.
109
- # @param [Boolean] override_protected (false)
110
- # Protected terms may be cleared.
111
- # @param [Boolean] propagate (true)
112
- # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
113
- # @raise [JsonLdError]
114
- # on a remote context load error, syntax error, or a reference to a term which is not defined.
115
- # @return [Context]
116
- def self.parse(local_context,
117
- base: nil,
118
- override_protected: false,
119
- propagate: true,
120
- **options)
121
- c = self.new(**options)
122
- if local_context.respond_to?(:empty?) && local_context.empty?
123
- c
124
- else
125
- c.parse(local_context,
126
- base: base,
127
- override_protected: override_protected,
128
- propagate: propagate)
83
+ # Default direction
84
+ #
85
+ # This adds a direction to plain strings that aren't otherwise coerced
86
+ # @return ["lrt", "rtl"]
87
+ attr_reader :default_direction
88
+
89
+ # Default vocabulary
90
+ #
91
+ # Sets the default vocabulary used for expanding terms which
92
+ # aren't otherwise absolute IRIs
93
+ # @return [RDF::URI]
94
+ attr_reader :vocab
95
+
96
+ # @return [Hash{Symbol => Object}] Global options used in generating IRIs
97
+ attr_accessor :options
98
+
99
+ # @return [BlankNodeNamer]
100
+ attr_accessor :namer
101
+
102
+ ##
103
+ # Create a new context by parsing a context.
104
+ #
105
+ # @see #initialize
106
+ # @see #parse
107
+ # @param [String, #read, Array, Hash, Context] local_context
108
+ # @param [String, #to_s] base (nil)
109
+ # The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context.
110
+ # @param [Boolean] override_protected (false)
111
+ # Protected terms may be cleared.
112
+ # @param [Boolean] propagate (true)
113
+ # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
114
+ # @raise [JsonLdError]
115
+ # on a remote context load error, syntax error, or a reference to a term which is not defined.
116
+ # @return [Context]
117
+ def self.parse(local_context,
118
+ base: nil,
119
+ override_protected: false,
120
+ propagate: true,
121
+ **options)
122
+ c = new(**options)
123
+ if local_context.respond_to?(:empty?) && local_context.empty?
124
+ c
125
+ else
126
+ c.parse(local_context,
127
+ base: base,
128
+ override_protected: override_protected,
129
+ propagate: propagate)
130
+ end
129
131
  end
130
- end
131
132
 
132
- ##
133
- # Class-level cache used for retaining parsed remote contexts.
134
- #
135
- # @return [RDF::Util::Cache]
136
- # @private
137
- def self.cache
138
- @cache ||= RDF::Util::Cache.new(CACHE_SIZE)
139
- end
133
+ ##
134
+ # Class-level cache used for retaining parsed remote contexts.
135
+ #
136
+ # @return [RDF::Util::Cache]
137
+ # @private
138
+ def self.cache
139
+ @cache ||= RDF::Util::Cache.new(CACHE_SIZE)
140
+ end
140
141
 
141
- ##
142
- # Class-level cache inverse contexts.
143
- #
144
- # @return [RDF::Util::Cache]
145
- # @private
146
- def self.inverse_cache
147
- @inverse_cache ||= RDF::Util::Cache.new(CACHE_SIZE)
148
- end
142
+ ##
143
+ # Class-level cache inverse contexts.
144
+ #
145
+ # @return [RDF::Util::Cache]
146
+ # @private
147
+ def self.inverse_cache
148
+ @inverse_cache ||= RDF::Util::Cache.new(CACHE_SIZE)
149
+ end
149
150
 
150
- ##
151
- # @private
152
- # Allow caching of well-known contexts
153
- def self.new(**options)
154
- if (options.keys - [
155
- :compactArrays,
156
- :documentLoader,
157
- :extractAllScripts,
158
- :ordered,
159
- :processingMode,
160
- :validate
161
- ]).empty?
162
- # allow caching
163
- key = options.hash
164
- INITIAL_CONTEXTS[key] ||= begin
151
+ ##
152
+ # @private
153
+ # Allow caching of well-known contexts
154
+ def self.new(**options)
155
+ if (options.keys - %i[
156
+ compactArrays
157
+ documentLoader
158
+ extractAllScripts
159
+ ordered
160
+ processingMode
161
+ validate
162
+ ]).empty?
163
+ # allow caching
164
+ key = options.hash
165
+ INITIAL_CONTEXTS[key] ||= begin
166
+ context = JSON::LD::Context.allocate
167
+ context.send(:initialize, **options)
168
+ context.freeze
169
+ context.term_definitions.freeze
170
+ context
171
+ end
172
+ else
173
+ # Don't try to cache
165
174
  context = JSON::LD::Context.allocate
166
175
  context.send(:initialize, **options)
167
- context.freeze
168
- context.term_definitions.freeze
169
176
  context
170
177
  end
171
- else
172
- # Don't try to cache
173
- context = JSON::LD::Context.allocate
174
- context.send(:initialize, **options)
175
- context
176
178
  end
177
- end
178
179
 
179
- ##
180
- # Create new evaluation context
181
- # @param [Hash] options
182
- # @option options [Hash{Symbol => String}] :prefixes
183
- # See `RDF::Reader#initialize`
184
- # @option options [String, #to_s] :vocab
185
- # Initial value for @vocab
186
- # @option options [String, #to_s] :language
187
- # Initial value for @langauge
188
- # @yield [ec]
189
- # @yieldparam [Context]
190
- # @return [Context]
191
- def initialize(**options)
192
- if options[:processingMode] == 'json-ld-1.0'
193
- @processingMode = 'json-ld-1.0'
194
- end
195
- @term_definitions = {}
196
- @iri_to_term = {
197
- RDF.to_uri.to_s => "rdf",
198
- RDF::XSD.to_uri.to_s => "xsd"
199
- }
200
- @namer = BlankNodeMapper.new("t")
201
-
202
- @options = options
203
-
204
- # Load any defined prefixes
205
- (options[:prefixes] || {}).each_pair do |k, v|
206
- next if k.nil?
207
- @iri_to_term[v.to_s] = k
208
- @term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
209
- end
180
+ ##
181
+ # Create new evaluation context
182
+ # @param [Hash] options
183
+ # @option options [Hash{Symbol => String}] :prefixes
184
+ # See `RDF::Reader#initialize`
185
+ # @option options [String, #to_s] :vocab
186
+ # Initial value for @vocab
187
+ # @option options [String, #to_s] :language
188
+ # Initial value for @langauge
189
+ # @yield [ec]
190
+ # @yieldparam [Context]
191
+ # @return [Context]
192
+ def initialize(**options)
193
+ @processingMode = 'json-ld-1.0' if options[:processingMode] == 'json-ld-1.0'
194
+ @term_definitions = {}
195
+ @iri_to_term = {
196
+ RDF.to_uri.to_s => "rdf",
197
+ RDF::XSD.to_uri.to_s => "xsd"
198
+ }
199
+ @namer = BlankNodeMapper.new("t")
200
+
201
+ @options = options
202
+
203
+ # Load any defined prefixes
204
+ (options[:prefixes] || {}).each_pair do |k, v|
205
+ next if k.nil?
206
+
207
+ @iri_to_term[v.to_s] = k
208
+ @term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
209
+ end
210
210
 
211
- self.vocab = options[:vocab] if options[:vocab]
212
- self.default_language = options[:language] if options[:language] =~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
213
- @term_definitions = options[:term_definitions] if options[:term_definitions]
211
+ self.vocab = options[:vocab] if options[:vocab]
212
+ self.default_language = options[:language] if /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(options[:language])
213
+ @term_definitions = options[:term_definitions] if options[:term_definitions]
214
214
 
215
- # log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
215
+ # log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
216
216
 
217
- yield(self) if block_given?
218
- end
217
+ yield(self) if block_given?
218
+ end
219
219
 
220
- # Create an Evaluation Context
221
- #
222
- # 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.
223
- #
224
- # 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.
225
- #
226
- # 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.
227
- #
228
- #
229
- # @param [String, #read, Array, Hash, Context] local_context
230
- # @param [String, #to_s] base
231
- # The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context.
232
- # @param [Boolean] override_protected Protected terms may be cleared.
233
- # @param [Boolean] propagate (true)
234
- # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
235
- # @param [Array<String>] remote_contexts ([])
236
- # @param [Boolean] validate_scoped (true).
237
- # Validate scoped context, loading if necessary.
238
- # If false, do not load scoped contexts.
239
- # @raise [JsonLdError]
240
- # on a remote context load error, syntax error, or a reference to a term which is not defined.
241
- # @return [Context]
242
- # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
243
- def parse(local_context,
244
- base: nil,
245
- override_protected: false,
246
- propagate: true,
247
- remote_contexts: [],
248
- validate_scoped: true)
249
- result = self.dup
250
- # Early check for @propagate, which can only appear in a local context
251
- propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
252
- result.previous_context ||= result.dup unless propagate
253
-
254
- local_context = as_array(local_context)
255
-
256
- log_depth do
257
- local_context.each do |context|
258
- case context
259
- when nil,false
260
- # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
261
- if override_protected || result.term_definitions.values.none?(&:protected?)
262
- null_context = Context.new(**options)
263
- null_context.previous_context = result unless propagate
264
- result = null_context
265
- else
266
- raise JSON::LD::JsonLdError::InvalidContextNullification,
220
+ # Create an Evaluation Context
221
+ #
222
+ # 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.
223
+ #
224
+ # 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.
225
+ #
226
+ # 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.
227
+ #
228
+ #
229
+ # @param [String, #read, Array, Hash, Context] local_context
230
+ # @param [String, #to_s] base
231
+ # The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context.
232
+ # @param [Boolean] override_protected Protected terms may be cleared.
233
+ # @param [Boolean] propagate (true)
234
+ # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
235
+ # @param [Array<String>] remote_contexts ([])
236
+ # @param [Boolean] validate_scoped (true).
237
+ # Validate scoped context, loading if necessary.
238
+ # If false, do not load scoped contexts.
239
+ # @raise [JsonLdError]
240
+ # on a remote context load error, syntax error, or a reference to a term which is not defined.
241
+ # @return [Context]
242
+ # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
243
+ def parse(local_context,
244
+ base: nil,
245
+ override_protected: false,
246
+ propagate: true,
247
+ remote_contexts: [],
248
+ validate_scoped: true)
249
+ result = dup
250
+ # Early check for @propagate, which can only appear in a local context
251
+ propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
252
+ result.previous_context ||= result.dup unless propagate
253
+
254
+ local_context = as_array(local_context)
255
+
256
+ log_depth do
257
+ local_context.each do |context|
258
+ case context
259
+ when nil, false
260
+ # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
261
+ if override_protected || result.term_definitions.values.none?(&:protected?)
262
+ null_context = Context.new(**options)
263
+ null_context.previous_context = result unless propagate
264
+ result = null_context
265
+ else
266
+ raise JSON::LD::JsonLdError::InvalidContextNullification,
267
267
  "Attempt to clear a context with protected terms"
268
- end
269
- when Context
270
- # log_debug("parse") {"context: #{context.inspect}"}
271
- result = result.merge(context)
272
- when IO, StringIO
273
- # log_debug("parse") {"io: #{context}"}
274
- # Load context document, if it is an open file
275
- begin
276
- ctx = load_context(context, **@options)
277
- raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
278
- result = result.parse(ctx["@context"] ? ctx["@context"] : {})
279
- rescue JSON::ParserError => e
280
- log_info("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
281
- raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
282
- self
283
- end
284
- when String, RDF::URI
285
- # log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
268
+ end
269
+ when Context
270
+ # log_debug("parse") {"context: #{context.inspect}"}
271
+ result = result.merge(context)
272
+ when IO, StringIO
273
+ # log_debug("parse") {"io: #{context}"}
274
+ # Load context document, if it is an open file
275
+ begin
276
+ ctx = load_context(context, **@options)
277
+ if @options[:validate] && ctx['@context'].nil?
278
+ raise JSON::LD::JsonLdError::InvalidRemoteContext,
279
+ "Context missing @context key"
280
+ end
286
281
 
287
- # 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].
288
- context = RDF::URI(result.context_base || base).join(context)
289
- context_canon = context.canonicalize
290
- context_canon.scheme = 'http' if context_canon.scheme == 'https'
282
+ result = result.parse(ctx["@context"] || {})
283
+ rescue JSON::ParserError => e
284
+ log_info("parse") { "Failed to parse @context from remote document at #{context}: #{e.message}" }
285
+ if @options[:validate]
286
+ raise JSON::LD::JsonLdError::InvalidRemoteContext,
287
+ "Failed to parse remote context at #{context}: #{e.message}"
288
+ end
291
289
 
292
- # If validating a scoped context which has already been loaded, skip to the next one
293
- next if !validate_scoped && remote_contexts.include?(context.to_s)
290
+ self
291
+ end
292
+ when String, RDF::URI
293
+ # log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
294
294
 
295
- remote_contexts << context.to_s
296
- raise JsonLdError::ContextOverflow, "#{context}" if remote_contexts.length >= MAX_CONTEXTS_LOADED
295
+ # 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].
296
+ context = RDF::URI(result.context_base || base).join(context)
297
+ context_canon = context.canonicalize
298
+ context_canon.scheme = 'http' if context_canon.scheme == 'https'
297
299
 
298
- cached_context = if PRELOADED[context_canon.to_s]
299
- # If we have a cached context, merge it into the current context (result) and use as the new context
300
- # log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
300
+ # If validating a scoped context which has already been loaded, skip to the next one
301
+ next if !validate_scoped && remote_contexts.include?(context.to_s)
301
302
 
302
- # If this is a Proc, then replace the entry with the result of running the Proc
303
- if PRELOADED[context_canon.to_s].respond_to?(:call)
304
- # log_debug("parse") {"=> (call)"}
305
- PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
306
- end
307
- PRELOADED[context_canon.to_s].context_base ||= context_canon.to_s
308
- PRELOADED[context_canon.to_s]
309
- else
310
- # Load context document, if it is a string
311
- Context.cache[context_canon.to_s] ||= begin
312
- context_opts = @options.merge(
313
- profile: 'http://www.w3.org/ns/json-ld#context',
314
- requestProfile: 'http://www.w3.org/ns/json-ld#context',
315
- base: nil)
316
- #context_opts.delete(:headers)
317
- JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
318
- # 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.
319
- raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
320
-
321
- # Parse stand-alone
322
- ctx = Context.new(unfrozen: true, **options).dup
323
- ctx.context_base = context.to_s
324
- ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
325
- ctx.context_base = context.to_s # In case it was altered
326
- ctx.instance_variable_set(:@base, nil)
327
- ctx
328
- end
329
- rescue JsonLdError::LoadingDocumentFailed => e
330
- log_info("parse") {"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"}
331
- raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
332
- rescue JsonLdError
333
- raise
334
- rescue StandardError => e
335
- log_info("parse") {"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"}
336
- raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
337
- end
338
- end
303
+ remote_contexts << context.to_s
304
+ raise JsonLdError::ContextOverflow, context.to_s if remote_contexts.length >= MAX_CONTEXTS_LOADED
339
305
 
340
- # Merge loaded context noting protected term overriding
341
- context = result.merge(cached_context, override_protected: override_protected)
306
+ cached_context = if PRELOADED[context_canon.to_s]
307
+ # If we have a cached context, merge it into the current context (result) and use as the new context
308
+ # log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
342
309
 
343
- context.previous_context = self unless propagate
344
- result = context
345
- when Hash
346
- context = context.dup # keep from modifying a hash passed as a param
347
-
348
- # This counts on hash elements being processed in order
349
- {
350
- '@version' => :processingMode=,
351
- '@import' => nil,
352
- '@base' => :base=,
353
- '@direction' => :default_direction=,
354
- '@language' => :default_language=,
355
- '@propagate' => :propagate=,
356
- '@vocab' => :vocab=,
357
- }.each do |key, setter|
358
- next unless context.key?(key)
359
- if key == '@import'
360
- # Retrieve remote context and merge the remaining context object into the result.
361
- raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
362
- raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
363
- import_loc = RDF::URI(result.context_base || base).join(context['@import'])
364
- begin
365
- context_opts = @options.merge(
366
- profile: 'http://www.w3.org/ns/json-ld#context',
367
- requestProfile: 'http://www.w3.org/ns/json-ld#context',
368
- base: nil)
369
- context_opts.delete(:headers)
370
- # FIXME: should cache this, but ContextCache is for parsed contexts
371
- JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc|
372
- # Dereference import_loc. 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.
373
- raise JsonLdError::InvalidRemoteContext, "#{import_loc}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
374
- import_context = remote_doc.document['@context']
375
- import_context.delete('@base')
376
- raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
377
- raise JsonLdError::InvalidContextEntry, "#{import_context.to_json} must not include @import entry" if import_context.key?('@import')
378
- context.delete(key)
379
- context = import_context.merge(context)
310
+ # If this is a Proc, then replace the entry with the result of running the Proc
311
+ if PRELOADED[context_canon.to_s].respond_to?(:call)
312
+ # log_debug("parse") {"=> (call)"}
313
+ PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
314
+ end
315
+ PRELOADED[context_canon.to_s].context_base ||= context_canon.to_s
316
+ PRELOADED[context_canon.to_s]
317
+ else
318
+ # Load context document, if it is a string
319
+ Context.cache[context_canon.to_s] ||= begin
320
+ context_opts = @options.merge(
321
+ profile: 'http://www.w3.org/ns/json-ld#context',
322
+ requestProfile: 'http://www.w3.org/ns/json-ld#context',
323
+ base: nil
324
+ )
325
+ # context_opts.delete(:headers)
326
+ JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
327
+ # 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.
328
+ unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
329
+ raise JsonLdError::InvalidRemoteContext,
330
+ context.to_s
331
+ end
332
+
333
+ # Parse stand-alone
334
+ ctx = Context.new(unfrozen: true, **options).dup
335
+ ctx.context_base = context.to_s
336
+ ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
337
+ ctx.context_base = context.to_s # In case it was altered
338
+ ctx.instance_variable_set(:@base, nil)
339
+ ctx
340
+ end
341
+ rescue JsonLdError::LoadingDocumentFailed => e
342
+ log_info("parse") do
343
+ "Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"
344
+ end
345
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
346
+ rescue JsonLdError
347
+ raise
348
+ rescue StandardError => e
349
+ log_info("parse") do
350
+ "Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"
351
+ end
352
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
380
353
  end
381
- rescue JsonLdError::LoadingDocumentFailed => e
382
- raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
383
- rescue JsonLdError
384
- raise
385
- rescue StandardError => e
386
- raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
387
354
  end
388
- else
389
- result.send(setter, context[key], remote_contexts: remote_contexts)
390
- end
391
- context.delete(key)
392
- end
393
355
 
394
- defined = {}
356
+ # Merge loaded context noting protected term overriding
357
+ context = result.merge(cached_context, override_protected: override_protected)
358
+
359
+ context.previous_context = self unless propagate
360
+ result = context
361
+ when Hash
362
+ context = context.dup # keep from modifying a hash passed as a param
363
+
364
+ # This counts on hash elements being processed in order
365
+ {
366
+ '@version' => :processingMode=,
367
+ '@import' => nil,
368
+ '@base' => :base=,
369
+ '@direction' => :default_direction=,
370
+ '@language' => :default_language=,
371
+ '@propagate' => :propagate=,
372
+ '@vocab' => :vocab=
373
+ }.each do |key, setter|
374
+ next unless context.key?(key)
375
+
376
+ if key == '@import'
377
+ # Retrieve remote context and merge the remaining context object into the result.
378
+ if result.processingMode("json-ld-1.0")
379
+ raise JsonLdError::InvalidContextEntry,
380
+ "@import may only be used in 1.1 mode}"
381
+ end
382
+ unless context['@import'].is_a?(String)
383
+ raise JsonLdError::InvalidImportValue,
384
+ "@import must be a string: #{context['@import'].inspect}"
385
+ end
386
+
387
+ import_loc = RDF::URI(result.context_base || base).join(context['@import'])
388
+ begin
389
+ context_opts = @options.merge(
390
+ profile: 'http://www.w3.org/ns/json-ld#context',
391
+ requestProfile: 'http://www.w3.org/ns/json-ld#context',
392
+ base: nil
393
+ )
394
+ context_opts.delete(:headers)
395
+ # FIXME: should cache this, but ContextCache is for parsed contexts
396
+ JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc|
397
+ # Dereference import_loc. 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.
398
+ unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
399
+ raise JsonLdError::InvalidRemoteContext,
400
+ import_loc.to_s
401
+ end
402
+
403
+ import_context = remote_doc.document['@context']
404
+ import_context.delete('@base')
405
+ unless import_context.is_a?(Hash)
406
+ raise JsonLdError::InvalidRemoteContext,
407
+ "#{import_context.to_json} must be an object"
408
+ end
409
+ if import_context.key?('@import')
410
+ raise JsonLdError::InvalidContextEntry,
411
+ "#{import_context.to_json} must not include @import entry"
412
+ end
413
+
414
+ context.delete(key)
415
+ context = import_context.merge(context)
416
+ end
417
+ rescue JsonLdError::LoadingDocumentFailed => e
418
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
419
+ rescue JsonLdError
420
+ raise
421
+ rescue StandardError => e
422
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
423
+ end
424
+ else
425
+ result.send(setter, context[key], remote_contexts: remote_contexts)
426
+ end
427
+ context.delete(key)
428
+ end
429
+
430
+ defined = {}
395
431
 
396
- # 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
397
- context.each_key do |key|
398
- # ... where key is not @base, @vocab, @language, or @version
399
- result.create_term_definition(context, key, defined,
400
- base: base,
401
- override_protected: override_protected,
402
- protected: context['@protected'],
403
- remote_contexts: remote_contexts.dup,
404
- validate_scoped: validate_scoped
405
- ) unless NON_TERMDEF_KEYS.include?(key)
432
+ # 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
433
+ context.each_key do |key|
434
+ # ... where key is not @base, @vocab, @language, or @version
435
+ next if NON_TERMDEF_KEYS.include?(key)
436
+
437
+ result.create_term_definition(context, key, defined,
438
+ base: base,
439
+ override_protected: override_protected,
440
+ protected: context['@protected'],
441
+ remote_contexts: remote_contexts.dup,
442
+ validate_scoped: validate_scoped)
443
+ end
444
+ else
445
+ # 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
446
+ raise JsonLdError::InvalidLocalContext, "must be a URL, JSON object or array of same: #{context.inspect}"
447
+ end
406
448
  end
407
- else
408
- # 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
409
- raise JsonLdError::InvalidLocalContext, "must be a URL, JSON object or array of same: #{context.inspect}"
410
449
  end
450
+ result
411
451
  end
412
- end
413
- result
414
- end
415
452
 
416
- ##
417
- # Merge in a context, creating a new context with updates from `context`
418
- #
419
- # @param [Context] context
420
- # @param [Boolean] override_protected Allow or disallow protected terms to be changed
421
- # @return [Context]
422
- def merge(context, override_protected: false)
423
- ctx = Context.new(term_definitions: self.term_definitions, standard_prefixes: options[:standard_prefixes])
424
- ctx.context_base = context.context_base || self.context_base
425
- ctx.default_language = context.default_language || self.default_language
426
- ctx.default_direction = context.default_direction || self.default_direction
427
- ctx.vocab = context.vocab || self.vocab
428
- ctx.base = self.base unless self.base.nil?
429
- if !override_protected
430
- ctx.term_definitions.each do |term, definition|
431
- next unless definition.protected? && (other = context.term_definitions[term])
432
- unless definition == other
433
- raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
453
+ ##
454
+ # Merge in a context, creating a new context with updates from `context`
455
+ #
456
+ # @param [Context] context
457
+ # @param [Boolean] override_protected Allow or disallow protected terms to be changed
458
+ # @return [Context]
459
+ def merge(context, override_protected: false)
460
+ ctx = Context.new(term_definitions: term_definitions, standard_prefixes: options[:standard_prefixes])
461
+ ctx.context_base = context.context_base || context_base
462
+ ctx.default_language = context.default_language || default_language
463
+ ctx.default_direction = context.default_direction || default_direction
464
+ ctx.vocab = context.vocab || vocab
465
+ ctx.base = base unless base.nil?
466
+ unless override_protected
467
+ ctx.term_definitions.each do |term, definition|
468
+ next unless definition.protected? && (other = context.term_definitions[term])
469
+ unless definition == other
470
+ raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
471
+ end
434
472
  end
435
473
  end
436
- end
437
474
 
438
- # Add term definitions
439
- context.term_definitions.each do |term, definition|
440
- ctx.term_definitions[term] = definition
475
+ # Add term definitions
476
+ context.term_definitions.each do |term, definition|
477
+ ctx.term_definitions[term] = definition
478
+ end
479
+ ctx
441
480
  end
442
- ctx
443
- end
444
481
 
445
- # The following constants are used to reduce object allocations in #create_term_definition below
446
- ID_NULL_OBJECT = { '@id' => nil }.freeze
447
- NON_TERMDEF_KEYS = Set.new(%w(@base @direction @language @protected @version @vocab)).freeze
448
- JSON_LD_10_EXPECTED_KEYS = Set.new(%w(@container @id @language @reverse @type)).freeze
449
- JSON_LD_11_EXPECTED_KEYS = Set.new(%w(@context @direction @index @nest @prefix @protected)).freeze
450
- JSON_LD_EXPECTED_KEYS = (JSON_LD_10_EXPECTED_KEYS + JSON_LD_11_EXPECTED_KEYS).freeze
451
- JSON_LD_10_TYPE_VALUES = Set.new(%w(@id @vocab)).freeze
452
- JSON_LD_11_TYPE_VALUES = Set.new(%w(@json @none)).freeze
453
- PREFIX_URI_ENDINGS = Set.new(%w(: / ? # [ ] @)).freeze
454
-
455
- ##
456
- # Create Term Definition
457
- #
458
- # 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.
459
- #
460
- # 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.
461
- #
462
- # @param [Hash] local_context
463
- # @param [String] term
464
- # @param [Hash] defined
465
- # @param [String, RDF::URI] base for resolving document-relative IRIs
466
- # @param [Boolean] protected if true, causes all terms to be marked protected
467
- # @param [Boolean] override_protected Protected terms may be cleared.
468
- # @param [Array<String>] remote_contexts
469
- # @param [Boolean] validate_scoped (true).
470
- # Validate scoped context, loading if necessary.
471
- # If false, do not load scoped contexts.
472
- # @raise [JsonLdError]
473
- # Represents a cyclical term dependency
474
- # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
475
- def create_term_definition(local_context, term, defined,
476
- base: nil,
477
- override_protected: false,
478
- protected: nil,
479
- remote_contexts: [],
480
- validate_scoped: true)
481
- # Expand a string value, unless it matches a keyword
482
- # log_debug("create_term_definition") {"term = #{term.inspect}"}
483
-
484
- # 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.
485
- case defined[term]
486
- when TrueClass then return
487
- when nil
488
- defined[term] = false
489
- else
490
- raise JsonLdError::CyclicIRIMapping, "Cyclical term dependency found: #{term.inspect}"
491
- end
482
+ # The following constants are used to reduce object allocations in #create_term_definition below
483
+ ID_NULL_OBJECT = { '@id' => nil }.freeze
484
+ NON_TERMDEF_KEYS = Set.new(%w[@base @direction @language @protected @version @vocab]).freeze
485
+ JSON_LD_10_EXPECTED_KEYS = Set.new(%w[@container @id @language @reverse @type]).freeze
486
+ JSON_LD_11_EXPECTED_KEYS = Set.new(%w[@context @direction @index @nest @prefix @protected]).freeze
487
+ JSON_LD_EXPECTED_KEYS = (JSON_LD_10_EXPECTED_KEYS + JSON_LD_11_EXPECTED_KEYS).freeze
488
+ JSON_LD_10_TYPE_VALUES = Set.new(%w[@id @vocab]).freeze
489
+ JSON_LD_11_TYPE_VALUES = Set.new(%w[@json @none]).freeze
490
+ PREFIX_URI_ENDINGS = Set.new(%w(: / ? # [ ] @)).freeze
492
491
 
493
- # Initialize value to a the value associated with the key term in local context.
494
- value = local_context.fetch(term, false)
495
- simple_term = value.is_a?(String) || value.nil?
496
-
497
- # Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
498
- if term == '@type' &&
499
- value.is_a?(Hash) &&
500
- !value.empty? &&
501
- processingMode("json-ld-1.1") &&
502
- (value.keys - %w(@container @protected)).empty? &&
503
- value.fetch('@container', '@set') == '@set'
504
- # thes are the only cases were redefining a keyword is allowed
505
- elsif KEYWORDS.include?(term) # TODO anything that looks like a keyword
506
- raise JsonLdError::KeywordRedefinition, "term must not be a keyword: #{term.inspect}" if
507
- @options[:validate]
508
- elsif term.to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
509
- warn "Terms beginning with '@' are reserved for future use and ignored: #{term}."
510
- return
511
- elsif !term_valid?(term) && @options[:validate]
512
- raise JsonLdError::InvalidTermDefinition, "term is invalid: #{term.inspect}"
513
- end
492
+ ##
493
+ # Create Term Definition
494
+ #
495
+ # 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.
496
+ #
497
+ # 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.
498
+ #
499
+ # @param [Hash] local_context
500
+ # @param [String] term
501
+ # @param [Hash] defined
502
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
503
+ # @param [Boolean] protected if true, causes all terms to be marked protected
504
+ # @param [Boolean] override_protected Protected terms may be cleared.
505
+ # @param [Array<String>] remote_contexts
506
+ # @param [Boolean] validate_scoped (true).
507
+ # Validate scoped context, loading if necessary.
508
+ # If false, do not load scoped contexts.
509
+ # @raise [JsonLdError]
510
+ # Represents a cyclical term dependency
511
+ # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
512
+ def create_term_definition(local_context, term, defined,
513
+ base: nil,
514
+ override_protected: false,
515
+ protected: nil,
516
+ remote_contexts: [],
517
+ validate_scoped: true)
518
+ # Expand a string value, unless it matches a keyword
519
+ # log_debug("create_term_definition") {"term = #{term.inspect}"}
520
+
521
+ # 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.
522
+ case defined[term]
523
+ when TrueClass then return
524
+ when nil
525
+ defined[term] = false
526
+ else
527
+ raise JsonLdError::CyclicIRIMapping, "Cyclical term dependency found: #{term.inspect}"
528
+ end
514
529
 
515
- value = {'@id' => value} if simple_term
530
+ # Initialize value to a the value associated with the key term in local context.
531
+ value = local_context.fetch(term, false)
532
+ simple_term = value.is_a?(String) || value.nil?
533
+
534
+ # Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
535
+ if term == '@type' &&
536
+ value.is_a?(Hash) &&
537
+ !value.empty? &&
538
+ processingMode("json-ld-1.1") &&
539
+ (value.keys - %w[@container @protected]).empty? &&
540
+ value.fetch('@container', '@set') == '@set'
541
+ # thes are the only cases were redefining a keyword is allowed
542
+ elsif KEYWORDS.include?(term) # TODO: anything that looks like a keyword
543
+ raise JsonLdError::KeywordRedefinition, "term must not be a keyword: #{term.inspect}" if
544
+ @options[:validate]
545
+ elsif term.to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
546
+ warn "Terms beginning with '@' are reserved for future use and ignored: #{term}."
547
+ return
548
+ elsif !term_valid?(term) && @options[:validate]
549
+ raise JsonLdError::InvalidTermDefinition, "term is invalid: #{term.inspect}"
550
+ end
516
551
 
517
- # Remove any existing term definition for term in active context.
518
- previous_definition = term_definitions[term]
519
- if previous_definition && previous_definition.protected? && !override_protected
520
- # Check later to detect identical redefinition
521
- else
522
- term_definitions.delete(term) if previous_definition
523
- end
552
+ value = { '@id' => value } if simple_term
524
553
 
525
- raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}" unless value.is_a?(Hash)
554
+ # Remove any existing term definition for term in active context.
555
+ previous_definition = term_definitions[term]
556
+ if previous_definition&.protected? && !override_protected
557
+ # Check later to detect identical redefinition
558
+ elsif previous_definition
559
+ term_definitions.delete(term)
560
+ end
526
561
 
527
- # log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
528
- definition = TermDefinition.new(term)
529
- definition.simple = simple_term
562
+ unless value.is_a?(Hash)
563
+ raise JsonLdError::InvalidTermDefinition,
564
+ "Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}"
565
+ end
530
566
 
531
- expected_keys = case processingMode
532
- when "json-ld-1.0" then JSON_LD_10_EXPECTED_KEYS
533
- else JSON_LD_EXPECTED_KEYS
534
- end
567
+ # log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
568
+ definition = TermDefinition.new(term)
569
+ definition.simple = simple_term
535
570
 
536
- # Any of these keys cause us to process as json-ld-1.1, unless otherwise set
537
- if processingMode.nil? && value.any? { |key, _| !JSON_LD_11_EXPECTED_KEYS.include?(key) }
538
- processingMode('json-ld-11')
539
- end
571
+ expected_keys = case processingMode
572
+ when "json-ld-1.0" then JSON_LD_10_EXPECTED_KEYS
573
+ else JSON_LD_EXPECTED_KEYS
574
+ end
540
575
 
541
- if value.any? { |key, _| !expected_keys.include?(key) }
542
- extra_keys = value.keys - expected_keys.to_a
543
- raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} has unexpected keys: #{extra_keys.join(', ')}"
544
- end
576
+ # Any of these keys cause us to process as json-ld-1.1, unless otherwise set
577
+ if processingMode.nil? && value.any? { |key, _| !JSON_LD_11_EXPECTED_KEYS.include?(key) }
578
+ processingMode('json-ld-11')
579
+ end
545
580
 
546
- # Potentially note that the term is protected
547
- definition.protected = value.fetch('@protected', protected)
581
+ if value.any? { |key, _| !expected_keys.include?(key) }
582
+ extra_keys = value.keys - expected_keys.to_a
583
+ raise JsonLdError::InvalidTermDefinition,
584
+ "Term definition for #{term.inspect} has unexpected keys: #{extra_keys.join(', ')}"
585
+ end
548
586
 
549
- if value.key?('@type')
550
- type = value['@type']
551
- # SPEC FIXME: @type may be nil
552
- type = case type
553
- when nil
554
- type
555
- when String
556
- begin
557
- expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
558
- rescue JsonLdError::InvalidIRIMapping
559
- raise JsonLdError::InvalidTypeMapping, "invalid mapping for '@type': #{type.inspect} on term #{term.inspect}"
587
+ # Potentially note that the term is protected
588
+ definition.protected = value.fetch('@protected', protected)
589
+
590
+ if value.key?('@type')
591
+ type = value['@type']
592
+ # SPEC FIXME: @type may be nil
593
+ type = case type
594
+ when nil
595
+ type
596
+ when String
597
+ begin
598
+ expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
599
+ rescue JsonLdError::InvalidIRIMapping
600
+ raise JsonLdError::InvalidTypeMapping,
601
+ "invalid mapping for '@type': #{type.inspect} on term #{term.inspect}"
602
+ end
603
+ else
604
+ :error
560
605
  end
561
- else
562
- :error
563
- end
564
- if JSON_LD_11_TYPE_VALUES.include?(type) && processingMode('json-ld-1.1')
565
- # This is okay and used in compaction in 1.1
566
- elsif !JSON_LD_10_TYPE_VALUES.include?(type) && !(type.is_a?(RDF::URI) && type.absolute?)
567
- raise JsonLdError::InvalidTypeMapping, "unknown mapping for '@type': #{type.inspect} on term #{term.inspect}"
606
+ if JSON_LD_11_TYPE_VALUES.include?(type) && processingMode('json-ld-1.1')
607
+ # This is okay and used in compaction in 1.1
608
+ elsif !JSON_LD_10_TYPE_VALUES.include?(type) && !(type.is_a?(RDF::URI) && type.absolute?)
609
+ raise JsonLdError::InvalidTypeMapping,
610
+ "unknown mapping for '@type': #{type.inspect} on term #{term.inspect}"
611
+ end
612
+ # log_debug("") {"type_mapping: #{type.inspect}"}
613
+ definition.type_mapping = type
568
614
  end
569
- # log_debug("") {"type_mapping: #{type.inspect}"}
570
- definition.type_mapping = type
571
- end
572
615
 
573
- if value.key?('@reverse')
574
- raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
575
- value.key?('@id') || value.key?('@nest')
576
- raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
577
- value['@reverse'].is_a?(String)
616
+ if value.key?('@reverse')
617
+ raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
618
+ value.key?('@id') || value.key?('@nest')
578
619
 
579
- if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
580
- warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
581
- return
582
- end
620
+ unless value['@reverse'].is_a?(String)
621
+ raise JsonLdError::InvalidIRIMapping,
622
+ "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}"
623
+ end
583
624
 
584
- # 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.
585
- definition.id = expand_iri(value['@reverse'],
586
- vocab: true,
587
- local_context: local_context,
588
- defined: defined)
589
- raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
590
- definition.id.is_a?(RDF::Node) || definition.id.is_a?(RDF::URI) && definition.id.absolute?
625
+ if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
626
+ warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
627
+ return
628
+ end
591
629
 
592
- if term[1..-1].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
593
- raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
630
+ # 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.
631
+ definition.id = expand_iri(value['@reverse'],
632
+ vocab: true,
633
+ local_context: local_context,
634
+ defined: defined)
635
+ unless definition.id.is_a?(RDF::Node) || (definition.id.is_a?(RDF::URI) && definition.id.absolute?)
636
+ raise JsonLdError::InvalidIRIMapping,
637
+ "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}"
638
+ end
639
+
640
+ if term[1..].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
641
+ raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
642
+ end
643
+
644
+ if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
645
+ warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1."
646
+ end
647
+
648
+ # If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
649
+ if value.key?('@container')
650
+ container = value['@container']
651
+ unless container.is_a?(String) && ['@set', '@index'].include?(container)
652
+ raise JsonLdError::InvalidReverseProperty,
653
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
654
+ end
655
+ definition.container_mapping = check_container(container, local_context, defined, term)
656
+ end
657
+ definition.reverse_property = true
658
+ elsif value.key?('@id') && value['@id'].nil?
659
+ # Allowed to reserve a null term, which may be protected
660
+ elsif value.key?('@id') && value['@id'] != term
661
+ unless value['@id'].is_a?(String)
662
+ raise JsonLdError::InvalidIRIMapping,
663
+ "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}"
664
+ end
665
+
666
+ if !KEYWORDS.include?(value['@id'].to_s) && value['@id'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
667
+ warn "Values beginning with '@' are reserved for future use and ignored: #{value['@id']}."
668
+ return
669
+ end
670
+
671
+ definition.id = expand_iri(value['@id'],
672
+ vocab: true,
673
+ local_context: local_context,
674
+ defined: defined)
675
+ raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
676
+ definition.id == '@context'
677
+
678
+ if term.match?(%r{(?::[^:])|/})
679
+ term_iri = expand_iri(term,
680
+ vocab: true,
681
+ local_context: local_context,
682
+ defined: defined.merge(term => true))
683
+ if term_iri != definition.id
684
+ raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
685
+ end
686
+ end
687
+
688
+ if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
689
+ warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1."
690
+ end
691
+
692
+ # If id ends with a gen-delim, it may be used as a prefix for simple terms
693
+ definition.prefix = true if !term.include?(':') &&
694
+ simple_term &&
695
+ (definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']',
696
+ '@') || definition.id.to_s.start_with?('_:'))
697
+ elsif term[1..].include?(':')
698
+ # 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.
699
+ prefix, suffix = term.split(':', 2)
700
+ create_term_definition(local_context, prefix, defined, protected: protected) if local_context.key?(prefix)
701
+
702
+ definition.id = if (td = term_definitions[prefix])
703
+ # 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.
704
+ td.id + suffix
705
+ else
706
+ # Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
707
+ term
708
+ end
709
+ # log_debug("") {"=> #{definition.id}"}
710
+ elsif term.include?('/')
711
+ # If term is a relative IRI
712
+ definition.id = expand_iri(term, vocab: true)
713
+ raise JsonLdError::InvalidKeywordAlias, "expected term to expand to an absolute IRI #{term.inspect}" unless
714
+ definition.id.absolute?
715
+ elsif KEYWORDS.include?(term)
716
+ # This should only happen for @type when @container is @set
717
+ definition.id = term
718
+ else
719
+ # 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.
720
+ unless vocab
721
+ raise JsonLdError::InvalidIRIMapping,
722
+ "relative term definition without vocab: #{term} on term #{term.inspect}"
723
+ end
724
+
725
+ definition.id = vocab + term
726
+ # log_debug("") {"=> #{definition.id}"}
594
727
  end
595
728
 
596
- warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
729
+ @iri_to_term[definition.id] = term if simple_term && definition.id
597
730
 
598
- # If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
599
731
  if value.key?('@container')
600
- container = value['@container']
601
- raise JsonLdError::InvalidReverseProperty,
602
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
603
- container.is_a?(String) && (container == '@set' || container == '@index')
604
- definition.container_mapping = check_container(container, local_context, defined, term)
605
- end
606
- definition.reverse_property = true
607
- elsif value.key?('@id') && value['@id'].nil?
608
- # Allowed to reserve a null term, which may be protected
609
- elsif value.key?('@id') && value['@id'] != term
610
- raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
611
- value['@id'].is_a?(String)
612
-
613
- if !KEYWORDS.include?(value['@id'].to_s) && value['@id'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
614
- warn "Values beginning with '@' are reserved for future use and ignored: #{value['@id']}."
615
- return
732
+ # log_debug("") {"container_mapping: #{value['@container'].inspect}"}
733
+ definition.container_mapping = check_container(value['@container'], local_context, defined, term)
734
+
735
+ # If @container includes @type
736
+ if definition.container_mapping.include?('@type')
737
+ # If definition does not have @type, set @type to @id
738
+ definition.type_mapping ||= '@id'
739
+ # If definition includes @type with a value other than @id or @vocab, an illegal type mapping error has been detected
740
+ unless CONTEXT_TYPE_ID_VOCAB.include?(definition.type_mapping)
741
+ raise JsonLdError::InvalidTypeMapping, "@container: @type requires @type to be @id or @vocab"
742
+ end
743
+ end
616
744
  end
617
745
 
618
- definition.id = expand_iri(value['@id'],
619
- vocab: true,
620
- local_context: local_context,
621
- defined: defined)
622
- raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
623
- definition.id == '@context'
746
+ if value.key?('@index')
747
+ # property-based indexing
748
+ unless definition.container_mapping.include?('@index')
749
+ raise JsonLdError::InvalidTermDefinition,
750
+ "@index without @index in @container: #{value['@index']} on term #{term.inspect}"
751
+ end
752
+ unless value['@index'].is_a?(String) && !value['@index'].start_with?('@')
753
+ raise JsonLdError::InvalidTermDefinition,
754
+ "@index must expand to an IRI: #{value['@index']} on term #{term.inspect}"
755
+ end
624
756
 
625
- if term.match?(/(?::[^:])|\//)
626
- term_iri = expand_iri(term,
627
- vocab: true,
628
- local_context: local_context,
629
- defined: defined.merge(term => true))
630
- if term_iri != definition.id
631
- raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
757
+ definition.index = value['@index'].to_s
758
+ end
759
+
760
+ if value.key?('@context')
761
+ begin
762
+ new_ctx = parse(value['@context'],
763
+ base: base,
764
+ override_protected: true,
765
+ remote_contexts: remote_contexts,
766
+ validate_scoped: false)
767
+ # Record null context in array form
768
+ definition.context = case value['@context']
769
+ when String then new_ctx.context_base
770
+ when nil then [nil]
771
+ else value['@context']
772
+ end
773
+ # log_debug("") {"context: #{definition.context.inspect}"}
774
+ rescue JsonLdError => e
775
+ raise JsonLdError::InvalidScopedContext,
776
+ "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
632
777
  end
633
778
  end
634
779
 
635
- warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
780
+ if value.key?('@language')
781
+ language = value['@language']
782
+ language = case value['@language']
783
+ when String
784
+ # Warn on an invalid language tag, unless :validate is true, in which case it's an error
785
+ unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(value['@language'])
786
+ warn "@language must be valid BCP47: #{value['@language'].inspect}"
787
+ end
788
+ options[:lowercaseLanguage] ? value['@language'].downcase : value['@language']
789
+ when nil
790
+ nil
791
+ else
792
+ raise JsonLdError::InvalidLanguageMapping,
793
+ "language must be null or a string, was #{value['@language'].inspect}} on term #{term.inspect}"
794
+ end
795
+ # log_debug("") {"language_mapping: #{language.inspect}"}
796
+ definition.language_mapping = language || false
797
+ end
636
798
 
637
- # If id ends with a gen-delim, it may be used as a prefix for simple terms
638
- definition.prefix = true if !term.include?(':') &&
639
- simple_term &&
640
- (definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']', '@') || definition.id.to_s.start_with?('_:'))
641
- elsif term[1..-1].include?(':')
642
- # 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.
643
- prefix, suffix = term.split(':', 2)
644
- create_term_definition(local_context, prefix, defined, protected: protected) if local_context.key?(prefix)
799
+ if value.key?('@direction')
800
+ direction = value['@direction']
801
+ unless direction.nil? || %w[
802
+ ltr rtl
803
+ ].include?(direction)
804
+ raise JsonLdError::InvalidBaseDirection,
805
+ "direction must be null, 'ltr', or 'rtl', was #{language.inspect}} on term #{term.inspect}"
806
+ end
645
807
 
646
- definition.id = if td = term_definitions[prefix]
647
- # 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.
648
- td.id + suffix
649
- else
650
- # Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
651
- term
652
- end
653
- # log_debug("") {"=> #{definition.id}"}
654
- elsif term.include?('/')
655
- # If term is a relative IRI
656
- definition.id = expand_iri(term, vocab: true)
657
- raise JsonLdError::InvalidKeywordAlias, "expected term to expand to an absolute IRI #{term.inspect}" unless
658
- definition.id.absolute?
659
- elsif KEYWORDS.include?(term)
660
- # This should only happen for @type when @container is @set
661
- definition.id = term
662
- else
663
- # 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.
664
- raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab
665
- definition.id = vocab + term
666
- # log_debug("") {"=> #{definition.id}"}
667
- end
808
+ # log_debug("") {"direction_mapping: #{direction.inspect}"}
809
+ definition.direction_mapping = direction || false
810
+ end
668
811
 
669
- @iri_to_term[definition.id] = term if simple_term && definition.id
812
+ if value.key?('@nest')
813
+ nest = value['@nest']
814
+ unless nest.is_a?(String)
815
+ raise JsonLdError::InvalidNestValue,
816
+ "nest must be a string, was #{nest.inspect}} on term #{term.inspect}"
817
+ end
818
+ if nest.match?(/^@[a-zA-Z]+$/) && nest != '@nest'
819
+ raise JsonLdError::InvalidNestValue,
820
+ "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}"
821
+ end
822
+
823
+ # log_debug("") {"nest: #{nest.inspect}"}
824
+ definition.nest = nest
825
+ end
826
+
827
+ if value.key?('@prefix')
828
+ if term.match?(%r{:|/})
829
+ raise JsonLdError::InvalidTermDefinition,
830
+ "@prefix used on compact or relative IRI term #{term.inspect}"
831
+ end
670
832
 
671
- if value.key?('@container')
672
- # log_debug("") {"container_mapping: #{value['@container'].inspect}"}
673
- definition.container_mapping = check_container(value['@container'], local_context, defined, term)
833
+ case pfx = value['@prefix']
834
+ when TrueClass, FalseClass
835
+ definition.prefix = pfx
836
+ else
837
+ raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
838
+ end
674
839
 
675
- # If @container includes @type
676
- if definition.container_mapping.include?('@type')
677
- # If definition does not have @type, set @type to @id
678
- definition.type_mapping ||= '@id'
679
- # If definition includes @type with a value other than @id or @vocab, an illegal type mapping error has been detected
680
- if !CONTEXT_TYPE_ID_VOCAB.include?(definition.type_mapping)
681
- raise JsonLdError::InvalidTypeMapping, "@container: @type requires @type to be @id or @vocab"
840
+ if pfx && KEYWORDS.include?(definition.id.to_s)
841
+ raise JsonLdError::InvalidTermDefinition,
842
+ "keywords may not be used as prefixes"
682
843
  end
683
844
  end
845
+
846
+ if previous_definition&.protected? && definition != previous_definition && !override_protected
847
+ definition = previous_definition
848
+ raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
849
+ end
850
+
851
+ term_definitions[term] = definition
852
+ defined[term] = true
684
853
  end
685
854
 
686
- if value.key?('@index')
687
- # property-based indexing
688
- raise JsonLdError::InvalidTermDefinition, "@index without @index in @container: #{value['@index']} on term #{term.inspect}" unless definition.container_mapping.include?('@index')
689
- raise JsonLdError::InvalidTermDefinition, "@index must expand to an IRI: #{value['@index']} on term #{term.inspect}" unless value['@index'].is_a?(String) && !value['@index'].start_with?('@')
690
- definition.index = value['@index'].to_s
855
+ ##
856
+ # Initial context, without mappings, vocab or default language
857
+ #
858
+ # @return [Boolean]
859
+ def empty?
860
+ @term_definitions.empty? && vocab.nil? && default_language.nil?
691
861
  end
692
862
 
693
- if value.key?('@context')
694
- begin
695
- new_ctx = self.parse(value['@context'],
696
- base: base,
697
- override_protected: true,
698
- remote_contexts: remote_contexts,
699
- validate_scoped: false)
700
- # Record null context in array form
701
- definition.context = case value['@context']
702
- when String then new_ctx.context_base
703
- when nil then [nil]
704
- else value['@context']
705
- end
706
- # log_debug("") {"context: #{definition.context.inspect}"}
707
- rescue JsonLdError => e
708
- raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
863
+ # @param [String] value must be an absolute IRI
864
+ def base=(value, **_options)
865
+ if value
866
+ unless value.is_a?(String) || value.is_a?(RDF::URI)
867
+ raise JsonLdError::InvalidBaseIRI,
868
+ "@base must be a string: #{value.inspect}"
869
+ end
870
+
871
+ value = RDF::URI(value)
872
+ value = @base.join(value) if @base && value.relative?
873
+ # still might be relative to document
874
+ @base = value
875
+ else
876
+ @base = false
709
877
  end
710
878
  end
711
879
 
712
- if value.key?('@language')
713
- language = value['@language']
714
- language = case value['@language']
880
+ # @param [String] value
881
+ def default_language=(value, **options)
882
+ @default_language = case value
715
883
  when String
716
884
  # Warn on an invalid language tag, unless :validate is true, in which case it's an error
717
- if value['@language'] !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
718
- warn "@language must be valid BCP47: #{value['@language'].inspect}"
885
+ unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(value)
886
+ warn "@language must be valid BCP47: #{value.inspect}"
719
887
  end
720
- options[:lowercaseLanguage] ? value['@language'].downcase : value['@language']
888
+ options[:lowercaseLanguage] ? value.downcase : value
721
889
  when nil
722
890
  nil
723
891
  else
724
- raise JsonLdError::InvalidLanguageMapping, "language must be null or a string, was #{value['@language'].inspect}} on term #{term.inspect}"
892
+ raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
725
893
  end
726
- # log_debug("") {"language_mapping: #{language.inspect}"}
727
- definition.language_mapping = language || false
728
894
  end
729
895
 
730
- if value.key?('@direction')
731
- direction = value['@direction']
732
- raise JsonLdError::InvalidBaseDirection, "direction must be null, 'ltr', or 'rtl', was #{language.inspect}} on term #{term.inspect}" unless direction.nil? || %w(ltr rtl).include?(direction)
733
- # log_debug("") {"direction_mapping: #{direction.inspect}"}
734
- definition.direction_mapping = direction || false
735
- end
896
+ # @param [String] value
897
+ def default_direction=(value, **_options)
898
+ @default_direction = if value
899
+ unless %w[
900
+ ltr rtl
901
+ ].include?(value)
902
+ raise JsonLdError::InvalidBaseDirection,
903
+ "@direction must be one or 'ltr', or 'rtl': #{value.inspect}"
904
+ end
736
905
 
737
- if value.key?('@nest')
738
- nest = value['@nest']
739
- raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
740
- raise JsonLdError::InvalidNestValue, "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}" if nest.match?(/^@[a-zA-Z]+$/) && nest != '@nest'
741
- # log_debug("") {"nest: #{nest.inspect}"}
742
- definition.nest = nest
906
+ value
907
+ end
743
908
  end
744
909
 
745
- if value.key?('@prefix')
746
- raise JsonLdError::InvalidTermDefinition, "@prefix used on compact or relative IRI term #{term.inspect}" if term.match?(%r{:|/})
747
- case pfx = value['@prefix']
748
- when TrueClass, FalseClass
749
- definition.prefix = pfx
910
+ ##
911
+ # Retrieve, or check processing mode.
912
+ #
913
+ # * With no arguments, retrieves the current set processingMode.
914
+ # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
915
+ # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
916
+ #
917
+ # @param [String, Number] expected (nil)
918
+ # @return [String]
919
+ def processingMode(expected = nil)
920
+ case expected
921
+ when 1.0, 'json-ld-1.0'
922
+ @processingMode == 'json-ld-1.0'
923
+ when 1.1, 'json-ld-1.1'
924
+ @processingMode.nil? || @processingMode == 'json-ld-1.1'
925
+ when nil
926
+ @processingMode || 'json-ld-1.1'
750
927
  else
751
- raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
928
+ false
752
929
  end
753
-
754
- raise JsonLdError::InvalidTermDefinition, "keywords may not be used as prefixes" if pfx && KEYWORDS.include?(definition.id.to_s)
755
930
  end
756
931
 
757
- if previous_definition && previous_definition.protected? && definition != previous_definition && !override_protected
758
- definition = previous_definition
759
- raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
760
- end
932
+ ##
933
+ # Set processing mode.
934
+ #
935
+ # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
936
+ #
937
+ # If contex has a @version member, it's value MUST be 1.1, otherwise an "invalid @version value" has been detected, and processing is aborted.
938
+ # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
939
+ #
940
+ # @param [String, Number] value
941
+ # @return [String]
942
+ # @raise [JsonLdError::ProcessingModeConflict]
943
+ def processingMode=(value = nil, **_options)
944
+ value = "json-ld-1.1" if value == 1.1
945
+ case value
946
+ when "json-ld-1.0", "json-ld-1.1"
947
+ if @processingMode && @processingMode != value
948
+ raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
949
+ end
761
950
 
762
- term_definitions[term] = definition
763
- defined[term] = true
764
- end
951
+ @processingMode = value
952
+ else
953
+ raise JsonLdError::InvalidVersionValue, value.inspect
954
+ end
955
+ end
765
956
 
766
- ##
767
- # Initial context, without mappings, vocab or default language
768
- #
769
- # @return [Boolean]
770
- def empty?
771
- @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
772
- end
957
+ # 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.
958
+ # @param [String] value must be an absolute IRI
959
+ def vocab=(value, **_options)
960
+ @vocab = case value
961
+ when /_:/
962
+ # BNode vocab is deprecated
963
+ if @options[:validate] && processingMode("json-ld-1.1")
964
+ warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1."
965
+ end
966
+ value
967
+ when String, RDF::URI
968
+ if RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0")
969
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
970
+ end
773
971
 
774
- # @param [String] value must be an absolute IRI
775
- def base=(value, **options)
776
- if value
777
- raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
778
- value = RDF::URI(value)
779
- value = @base.join(value) if @base && value.relative?
780
- # still might be relative to document
781
- @base = value
782
- else
783
- @base = false
972
+ expand_iri(value.to_s, vocab: true, documentRelative: true)
973
+ when nil
974
+ nil
975
+ else
976
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
977
+ end
784
978
  end
785
979
 
786
- end
980
+ # Set propagation
981
+ # @note: by the time this is called, the work has already been done.
982
+ #
983
+ # @param [Boolean] value
984
+ def propagate=(value, **_options)
985
+ if processingMode("json-ld-1.0")
986
+ raise JsonLdError::InvalidContextEntry,
987
+ "@propagate may only be set in 1.1 mode"
988
+ end
787
989
 
788
- # @param [String] value
789
- def default_language=(value, **options)
790
- @default_language = case value
791
- when String
792
- # Warn on an invalid language tag, unless :validate is true, in which case it's an error
793
- if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
794
- warn "@language must be valid BCP47: #{value.inspect}"
990
+ unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
991
+ raise JsonLdError::InvalidPropagateValue,
992
+ "@propagate must be boolean valued: #{value.inspect}"
795
993
  end
796
- options[:lowercaseLanguage] ? value.downcase : value
797
- when nil
798
- nil
799
- else
800
- raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
801
- end
802
- end
803
994
 
804
- # @param [String] value
805
- def default_direction=(value, **options)
806
- @default_direction = if value
807
- raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
808
995
  value
809
- else
810
- nil
811
996
  end
812
- end
813
997
 
814
- ##
815
- # Retrieve, or check processing mode.
816
- #
817
- # * With no arguments, retrieves the current set processingMode.
818
- # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
819
- # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
820
- #
821
- # @param [String, Number] expected (nil)
822
- # @return [String]
823
- def processingMode(expected = nil)
824
- case expected
825
- when 1.0, 'json-ld-1.0'
826
- @processingMode == 'json-ld-1.0'
827
- when 1.1, 'json-ld-1.1'
828
- @processingMode.nil? || @processingMode == 'json-ld-1.1'
829
- when nil
830
- @processingMode || 'json-ld-1.1'
831
- else
832
- false
833
- end
834
- end
998
+ ##
999
+ # Generate @context
1000
+ #
1001
+ # If a context was supplied in global options, use that, otherwise, generate one
1002
+ # from this representation.
1003
+ #
1004
+ # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
1005
+ # Original context to use, if available
1006
+ # @param [Hash{Symbol => Object}] options ({})
1007
+ # @return [Hash]
1008
+ def serialize(provided_context: nil, **_options)
1009
+ # log_debug("serlialize: generate context")
1010
+ # log_debug("") {"=> context: #{inspect}"}
1011
+ use_context = case provided_context
1012
+ when String, RDF::URI
1013
+ # log_debug "serlialize: reuse context: #{provided_context.inspect}"
1014
+ provided_context.to_s
1015
+ when Hash
1016
+ # log_debug "serlialize: reuse context: #{provided_context.inspect}"
1017
+ # If it has an @context entry use it, otherwise it is assumed to be the body of a context
1018
+ provided_context.fetch('@context', provided_context)
1019
+ when Array
1020
+ # log_debug "serlialize: reuse context: #{provided_context.inspect}"
1021
+ provided_context
1022
+ when IO, StringIO
1023
+ load_context(provided_context, **@options).fetch('@context', {})
1024
+ else
1025
+ ctx = {}
1026
+ ctx['@version'] = 1.1 if @processingMode == 'json-ld-1.1'
1027
+ ctx['@base'] = base.to_s if base
1028
+ ctx['@direction'] = default_direction.to_s if default_direction
1029
+ ctx['@language'] = default_language.to_s if default_language
1030
+ ctx['@vocab'] = vocab.to_s if vocab
1031
+
1032
+ # Term Definitions
1033
+ term_definitions.each do |term, defn|
1034
+ ctx[term] = defn.to_context_definition(self)
1035
+ end
1036
+ ctx
1037
+ end
835
1038
 
836
- ##
837
- # Set processing mode.
838
- #
839
- # * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
840
- #
841
- # If contex has a @version member, it's value MUST be 1.1, otherwise an "invalid @version value" has been detected, and processing is aborted.
842
- # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
843
- #
844
- # @param [String, Number] value
845
- # @return [String]
846
- # @raise [JsonLdError::ProcessingModeConflict]
847
- def processingMode=(value = nil, **options)
848
- value = "json-ld-1.1" if value == 1.1
849
- case value
850
- when "json-ld-1.0", "json-ld-1.1"
851
- if @processingMode && @processingMode != value
852
- raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
853
- end
854
- @processingMode = value
855
- else
856
- raise JsonLdError::InvalidVersionValue, value.inspect
1039
+ # Return hash with @context, or empty
1040
+ use_context.nil? || use_context.empty? ? {} : { '@context' => use_context }
857
1041
  end
858
- end
859
1042
 
860
- # 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.
861
- # @param [String] value must be an absolute IRI
862
- def vocab=(value, **options)
863
- @vocab = case value
864
- when /_:/
865
- # BNode vocab is deprecated
866
- warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
867
- value
868
- when String, RDF::URI
869
- if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
870
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
1043
+ ##
1044
+ # Build a context from an RDF::Vocabulary definition.
1045
+ #
1046
+ # @example building from an external vocabulary definition
1047
+ #
1048
+ # g = RDF::Graph.load("http://schema.org/docs/schema_org_rdfa.html")
1049
+ #
1050
+ # context = JSON::LD::Context.new.from_vocabulary(g,
1051
+ # vocab: "http://schema.org/",
1052
+ # prefixes: {schema: "http://schema.org/"},
1053
+ # language: "en")
1054
+ #
1055
+ # @param [RDF::Queryable] graph
1056
+ #
1057
+ # @note requires rdf/vocab gem.
1058
+ #
1059
+ # @return [self]
1060
+ def from_vocabulary(graph)
1061
+ require 'rdf/vocab' unless RDF.const_defined?(:Vocab)
1062
+ statements = {}
1063
+ ranges = {}
1064
+
1065
+ # Add term definitions for each class and property not in schema:, and
1066
+ # for those properties having an object range
1067
+ graph.each do |statement|
1068
+ next if statement.subject.node?
1069
+
1070
+ (statements[statement.subject] ||= []) << statement
1071
+
1072
+ # Keep track of predicate ranges
1073
+ if [RDF::RDFS.range, RDF::Vocab::SCHEMA.rangeIncludes].include?(statement.predicate)
1074
+ (ranges[statement.subject] ||= []) << statement.object
1075
+ end
871
1076
  end
872
- expand_iri(value.to_s, vocab: true, documentRelative: true)
873
- when nil
874
- nil
875
- else
876
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
877
- end
878
- end
879
1077
 
880
- # Set propagation
881
- # @note: by the time this is called, the work has already been done.
882
- #
883
- # @param [Boolean] value
884
- def propagate=(value, **options)
885
- raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
886
- raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
887
- value
888
- end
1078
+ # Add term definitions for each class and property not in vocab, and
1079
+ # for those properties having an object range
1080
+ statements.each do |subject, values|
1081
+ types = values.each_with_object([]) { |v, memo| memo << v.object if v.predicate == RDF.type }
1082
+ is_property = types.any? { |t| t.to_s.include?("Property") }
1083
+
1084
+ term = subject.to_s.split(%r{[/\#]}).last
1085
+
1086
+ if is_property
1087
+ prop_ranges = ranges.fetch(subject, [])
1088
+ # If any range is empty or member of range includes rdfs:Literal or schema:Text
1089
+ next if (vocab && prop_ranges.empty?) ||
1090
+ prop_ranges.include?(RDF::Vocab::SCHEMA.Text) ||
1091
+ prop_ranges.include?(RDF::RDFS.Literal)
1092
+
1093
+ td = term_definitions[term] = TermDefinition.new(term, id: subject.to_s)
1094
+
1095
+ # Set context typing based on first element in range
1096
+ case r = prop_ranges.first
1097
+ when RDF::XSD.string
1098
+ td.language_mapping = false if default_language
1099
+ # FIXME: text direction
1100
+ when RDF::XSD.boolean, RDF::Vocab::SCHEMA.Boolean, RDF::XSD.date, RDF::Vocab::SCHEMA.Date,
1101
+ RDF::XSD.dateTime, RDF::Vocab::SCHEMA.DateTime, RDF::XSD.time, RDF::Vocab::SCHEMA.Time,
1102
+ RDF::XSD.duration, RDF::Vocab::SCHEMA.Duration, RDF::XSD.decimal, RDF::Vocab::SCHEMA.Number,
1103
+ RDF::XSD.float, RDF::Vocab::SCHEMA.Float, RDF::XSD.integer, RDF::Vocab::SCHEMA.Integer
1104
+ td.type_mapping = r
1105
+ td.simple = false
1106
+ else
1107
+ # It's an object range (includes schema:URL)
1108
+ td.type_mapping = '@id'
1109
+ end
1110
+ else
1111
+ # Ignore if there's a default voabulary and this is not a property
1112
+ next if vocab && subject.to_s.start_with?(vocab)
889
1113
 
890
- ##
891
- # Generate @context
892
- #
893
- # If a context was supplied in global options, use that, otherwise, generate one
894
- # from this representation.
895
- #
896
- # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
897
- # Original context to use, if available
898
- # @param [Hash{Symbol => Object}] options ({})
899
- # @return [Hash]
900
- def serialize(provided_context: nil, **options)
901
- # log_debug("serlialize: generate context")
902
- # log_debug("") {"=> context: #{inspect}"}
903
- use_context = case provided_context
904
- when String, RDF::URI
905
- # log_debug "serlialize: reuse context: #{provided_context.inspect}"
906
- provided_context.to_s
907
- when Hash
908
- # log_debug "serlialize: reuse context: #{provided_context.inspect}"
909
- # If it has an @context entry use it, otherwise it is assumed to be the body of a context
910
- provided_context.fetch('@context', provided_context)
911
- when Array
912
- # log_debug "serlialize: reuse context: #{provided_context.inspect}"
913
- provided_context
914
- when IO, StringIO
915
- load_context(provided_context, **@options).fetch('@context', {})
916
- else
917
- ctx = {}
918
- ctx['@version'] = 1.1 if @processingMode == 'json-ld-1.1'
919
- ctx['@base'] = base.to_s if base
920
- ctx['@direction'] = default_direction.to_s if default_direction
921
- ctx['@language'] = default_language.to_s if default_language
922
- ctx['@vocab'] = vocab.to_s if vocab
923
-
924
- # Term Definitions
925
- term_definitions.each do |term, defn|
926
- ctx[term] = defn.to_context_definition(self)
1114
+ # otherwise, create a term definition
1115
+ td = term_definitions[term] = TermDefinition.new(term, id: subject.to_s)
1116
+ end
927
1117
  end
928
- ctx
1118
+
1119
+ self
929
1120
  end
930
1121
 
931
- # Return hash with @context, or empty
932
- use_context.nil? || use_context.empty? ? {} : {'@context' => use_context}
933
- end
1122
+ # Set term mapping
1123
+ #
1124
+ # @param [#to_s] term
1125
+ # @param [RDF::URI, String, nil] value
1126
+ #
1127
+ # @return [TermDefinition]
1128
+ def set_mapping(term, value)
1129
+ # log_debug("") {"map #{term.inspect} to #{value.inspect}"}
1130
+ term = term.to_s
1131
+ term_definitions[term] =
1132
+ TermDefinition.new(term, id: value, simple: true, prefix: value.to_s.end_with?(*PREFIX_URI_ENDINGS))
1133
+ term_definitions[term].simple = true
934
1134
 
935
- ##
936
- # Build a context from an RDF::Vocabulary definition.
937
- #
938
- # @example building from an external vocabulary definition
939
- #
940
- # g = RDF::Graph.load("http://schema.org/docs/schema_org_rdfa.html")
941
- #
942
- # context = JSON::LD::Context.new.from_vocabulary(g,
943
- # vocab: "http://schema.org/",
944
- # prefixes: {schema: "http://schema.org/"},
945
- # language: "en")
946
- #
947
- # @param [RDF::Queryable] graph
948
- #
949
- # @return [self]
950
- def from_vocabulary(graph)
951
- statements = {}
952
- ranges = {}
953
-
954
- # Add term definitions for each class and property not in schema:, and
955
- # for those properties having an object range
956
- graph.each do |statement|
957
- next if statement.subject.node?
958
- (statements[statement.subject] ||= []) << statement
959
-
960
- # Keep track of predicate ranges
961
- if [RDF::RDFS.range, RDF::SCHEMA.rangeIncludes].include?(statement.predicate)
962
- (ranges[statement.subject] ||= []) << statement.object
963
- end
1135
+ term_sym = term.empty? ? "" : term.to_sym
1136
+ iri_to_term.delete(term_definitions[term].id.to_s) if term_definitions[term].id.is_a?(String)
1137
+ @options[:prefixes][term_sym] = value if @options.key?(:prefixes)
1138
+ iri_to_term[value.to_s] = term
1139
+ term_definitions[term]
964
1140
  end
965
1141
 
966
- # Add term definitions for each class and property not in vocab, and
967
- # for those properties having an object range
968
- statements.each do |subject, values|
969
- types = values.each_with_object([]) { |v, memo| memo << v.object if v.predicate == RDF.type }
970
- is_property = types.any? {|t| t.to_s.include?("Property")}
971
-
972
- term = subject.to_s.split(/[\/\#]/).last
1142
+ ##
1143
+ # Find a term definition
1144
+ #
1145
+ # @param [Term, #to_s] term in unexpanded form
1146
+ # @return [Term]
1147
+ def find_definition(term)
1148
+ term.is_a?(TermDefinition) ? term : term_definitions[term.to_s]
1149
+ end
973
1150
 
974
- if !is_property
975
- # Ignore if there's a default voabulary and this is not a property
976
- next if vocab && subject.to_s.start_with?(vocab)
1151
+ ##
1152
+ # Retrieve container mapping, add it if `value` is provided
1153
+ #
1154
+ # @param [Term, #to_s] term in unexpanded form
1155
+ # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>]
1156
+ def container(term)
1157
+ return Set[term] if term == '@list'
977
1158
 
978
- # otherwise, create a term definition
979
- td = term_definitions[term] = TermDefinition.new(term, id: subject.to_s)
980
- else
981
- prop_ranges = ranges.fetch(subject, [])
982
- # If any range is empty or member of range includes rdfs:Literal or schema:Text
983
- next if vocab && prop_ranges.empty? ||
984
- prop_ranges.include?(RDF::SCHEMA.Text) ||
985
- prop_ranges.include?(RDF::RDFS.Literal)
986
- td = term_definitions[term] = TermDefinition.new(term, id: subject.to_s)
987
-
988
- # Set context typing based on first element in range
989
- case r = prop_ranges.first
990
- when RDF::XSD.string
991
- if self.default_language
992
- td.language_mapping = false
993
- end
994
- # FIXME: text direction
995
- when RDF::XSD.boolean, RDF::SCHEMA.Boolean, RDF::XSD.date, RDF::SCHEMA.Date,
996
- RDF::XSD.dateTime, RDF::SCHEMA.DateTime, RDF::XSD.time, RDF::SCHEMA.Time,
997
- RDF::XSD.duration, RDF::SCHEMA.Duration, RDF::XSD.decimal, RDF::SCHEMA.Number,
998
- RDF::XSD.float, RDF::SCHEMA.Float, RDF::XSD.integer, RDF::SCHEMA.Integer
999
- td.type_mapping = r
1000
- td.simple = false
1001
- else
1002
- # It's an object range (includes schema:URL)
1003
- td.type_mapping = '@id'
1004
- end
1005
- end
1159
+ term = find_definition(term)
1160
+ term ? term.container_mapping : Set.new
1006
1161
  end
1007
1162
 
1008
- self
1009
- end
1010
-
1011
- # Set term mapping
1012
- #
1013
- # @param [#to_s] term
1014
- # @param [RDF::URI, String, nil] value
1015
- #
1016
- # @return [TermDefinition]
1017
- def set_mapping(term, value)
1018
- # log_debug("") {"map #{term.inspect} to #{value.inspect}"}
1019
- term = term.to_s
1020
- term_definitions[term] = TermDefinition.new(term, id: value, simple: true, prefix: (value.to_s.end_with?(*PREFIX_URI_ENDINGS)))
1021
- term_definitions[term].simple = true
1022
-
1023
- term_sym = term.empty? ? "" : term.to_sym
1024
- iri_to_term.delete(term_definitions[term].id.to_s) if term_definitions[term].id.is_a?(String)
1025
- @options[:prefixes][term_sym] = value if @options.key?(:prefixes)
1026
- iri_to_term[value.to_s] = term
1027
- term_definitions[term]
1028
- end
1163
+ ##
1164
+ # Retrieve term coercion
1165
+ #
1166
+ # @param [Term, #to_s] term in unexpanded form
1167
+ # @return [RDF::URI, '@id']
1168
+ def coerce(term)
1169
+ # Map property, if it's not an RDF::Value
1170
+ # @type is always is an IRI
1171
+ return '@id' if term == RDF.type || term == '@type'
1029
1172
 
1030
- ##
1031
- # Find a term definition
1032
- #
1033
- # @param [Term, #to_s] term in unexpanded form
1034
- # @return [Term]
1035
- def find_definition(term)
1036
- term.is_a?(TermDefinition) ? term : term_definitions[term.to_s]
1037
- end
1173
+ term = find_definition(term)
1174
+ term&.type_mapping
1175
+ end
1038
1176
 
1039
- ##
1040
- # Retrieve container mapping, add it if `value` is provided
1041
- #
1042
- # @param [Term, #to_s] term in unexpanded form
1043
- # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>]
1044
- def container(term)
1045
- return Set[term] if term == '@list'
1046
- term = find_definition(term)
1047
- term ? term.container_mapping : Set.new
1048
- end
1177
+ ##
1178
+ # Should values be represented using an array?
1179
+ #
1180
+ # @param [Term, #to_s] term in unexpanded form
1181
+ # @return [Boolean]
1182
+ def as_array?(term)
1183
+ return true if CONTEXT_CONTAINER_ARRAY_TERMS.include?(term)
1049
1184
 
1050
- ##
1051
- # Retrieve term coercion
1052
- #
1053
- # @param [Term, #to_s] term in unexpanded form
1054
- # @return [RDF::URI, '@id']
1055
- def coerce(term)
1056
- # Map property, if it's not an RDF::Value
1057
- # @type is always is an IRI
1058
- return '@id' if term == RDF.type || term == '@type'
1059
- term = find_definition(term)
1060
- term && term.type_mapping
1061
- end
1185
+ term = find_definition(term)
1186
+ term && (term.as_set? || term.container_mapping.include?('@list'))
1187
+ end
1062
1188
 
1063
- ##
1064
- # Should values be represented using an array?
1065
- #
1066
- # @param [Term, #to_s] term in unexpanded form
1067
- # @return [Boolean]
1068
- def as_array?(term)
1069
- return true if CONTEXT_CONTAINER_ARRAY_TERMS.include?(term)
1070
- term = find_definition(term)
1071
- term && (term.as_set? || term.container_mapping.include?('@list'))
1072
- end
1189
+ ##
1190
+ # Retrieve content of a term
1191
+ #
1192
+ # @param [Term, #to_s] term in unexpanded form
1193
+ # @return [Hash]
1194
+ def content(term)
1195
+ term = find_definition(term)
1196
+ term&.content
1197
+ end
1073
1198
 
1074
- ##
1075
- # Retrieve content of a term
1076
- #
1077
- # @param [Term, #to_s] term in unexpanded form
1078
- # @return [Hash]
1079
- def content(term)
1080
- term = find_definition(term)
1081
- term && term.content
1082
- end
1199
+ ##
1200
+ # Retrieve nest of a term.
1201
+ # value of nest must be @nest or a term that resolves to @nest
1202
+ #
1203
+ # @param [Term, #to_s] term in unexpanded form
1204
+ # @return [String] Nesting term
1205
+ # @raise JsonLdError::InvalidNestValue if nesting term exists and is not a term resolving to `@nest` in the current context.
1206
+ def nest(term)
1207
+ term = find_definition(term)
1208
+ return unless term
1083
1209
 
1084
- ##
1085
- # Retrieve nest of a term.
1086
- # value of nest must be @nest or a term that resolves to @nest
1087
- #
1088
- # @param [Term, #to_s] term in unexpanded form
1089
- # @return [String] Nesting term
1090
- # @raise JsonLdError::InvalidNestValue if nesting term exists and is not a term resolving to `@nest` in the current context.
1091
- def nest(term)
1092
- term = find_definition(term)
1093
- if term
1094
1210
  case term.nest
1095
1211
  when '@nest', nil
1096
- term.nest
1097
1212
  else
1098
- nest_term = find_definition(term.nest)
1099
- raise JsonLdError::InvalidNestValue, "nest must a term resolving to @nest, was #{nest_term.inspect}" unless nest_term && nest_term.id == '@nest'
1100
- term.nest
1213
+ nest_term = find_definition(term.nest)
1214
+ unless nest_term && nest_term.id == '@nest'
1215
+ raise JsonLdError::InvalidNestValue,
1216
+ "nest must a term resolving to @nest, was #{nest_term.inspect}"
1217
+ end
1218
+
1219
+ end
1220
+ term.nest
1221
+ end
1222
+
1223
+ ##
1224
+ # Retrieve the language associated with a term, or the default language otherwise
1225
+ # @param [Term, #to_s] term in unexpanded form
1226
+ # @return [String]
1227
+ def language(term)
1228
+ term = find_definition(term)
1229
+ lang = term&.language_mapping
1230
+ if lang.nil?
1231
+ @default_language
1232
+ else
1233
+ (lang == false ? nil : lang)
1101
1234
  end
1102
1235
  end
1103
- end
1104
-
1105
- ##
1106
- # Retrieve the language associated with a term, or the default language otherwise
1107
- # @param [Term, #to_s] term in unexpanded form
1108
- # @return [String]
1109
- def language(term)
1110
- term = find_definition(term)
1111
- lang = term && term.language_mapping
1112
- lang.nil? ? @default_language : (lang == false ? nil : lang)
1113
- end
1114
-
1115
- ##
1116
- # Retrieve the text direction associated with a term, or the default direction otherwise
1117
- # @param [Term, #to_s] term in unexpanded form
1118
- # @return [String]
1119
- def direction(term)
1120
- term = find_definition(term)
1121
- dir = term && term.direction_mapping
1122
- dir.nil? ? @default_direction : (dir == false ? nil : dir)
1123
- end
1124
1236
 
1125
- ##
1126
- # Is this a reverse term
1127
- # @param [Term, #to_s] term in unexpanded form
1128
- # @return [Boolean]
1129
- def reverse?(term)
1130
- term = find_definition(term)
1131
- term && term.reverse_property
1132
- end
1133
-
1134
- ##
1135
- # Given a term or IRI, find a reverse term definition matching that term. If the term is already reversed, find a non-reversed version.
1136
- #
1137
- # @param [Term, #to_s] term
1138
- # @return [Term] related term definition
1139
- def reverse_term(term)
1140
- # Direct lookup of term
1141
- term = term_definitions[term.to_s] if term_definitions.key?(term.to_s) && !term.is_a?(TermDefinition)
1142
-
1143
- # Lookup term, assuming term is an IRI
1144
- unless term.is_a?(TermDefinition)
1145
- td = term_definitions.values.detect {|t| t.id == term.to_s}
1146
-
1147
- # Otherwise create a temporary term definition
1148
- term = td || TermDefinition.new(term.to_s, id: expand_iri(term, vocab:true))
1237
+ ##
1238
+ # Retrieve the text direction associated with a term, or the default direction otherwise
1239
+ # @param [Term, #to_s] term in unexpanded form
1240
+ # @return [String]
1241
+ def direction(term)
1242
+ term = find_definition(term)
1243
+ dir = term&.direction_mapping
1244
+ if dir.nil?
1245
+ @default_direction
1246
+ else
1247
+ (dir == false ? nil : dir)
1248
+ end
1149
1249
  end
1150
1250
 
1151
- # Now, return a term, which reverses this term
1152
- term_definitions.values.detect {|t| t.id == term.id && t.reverse_property != term.reverse_property}
1153
- end
1154
-
1155
- ##
1156
- # Expand an IRI. Relative IRIs are expanded against any document base.
1157
- #
1158
- # @param [String] value
1159
- # A keyword, term, prefix:suffix or possibly relative IRI
1160
- # @param [Boolean] as_string (false) transform RDF::Resource values to string
1161
- # @param [String, RDF::URI] base for resolving document-relative IRIs
1162
- # @param [Hash] defined
1163
- # Used during Context Processing.
1164
- # @param [Boolean] documentRelative (false)
1165
- # @param [Hash] local_context
1166
- # Used during Context Processing.
1167
- # @param [Boolean] vocab (false)
1168
- # @param [Hash{Symbol => Object}] options
1169
- # @return [RDF::Resource, String]
1170
- # IRI or String, if it's a keyword
1171
- # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
1172
- # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
1173
- def expand_iri(value,
1174
- as_string: false,
1175
- base: nil,
1176
- defined: nil,
1177
- documentRelative: false,
1178
- local_context: nil,
1179
- vocab: false,
1180
- **options)
1181
- return (value && as_string ? value.to_s : value) unless value.is_a?(String)
1182
-
1183
- return value if KEYWORDS.include?(value)
1184
- return nil if value.match?(/^@[a-zA-Z]+$/)
1185
-
1186
- defined = defined || {} # if we initialized in the keyword arg we would allocate {} at each invokation, even in the 2 (common) early returns above.
1187
-
1188
- # 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.
1189
- if local_context && local_context.key?(value) && !defined[value]
1190
- create_term_definition(local_context, value, defined)
1251
+ ##
1252
+ # Is this a reverse term
1253
+ # @param [Term, #to_s] term in unexpanded form
1254
+ # @return [Boolean]
1255
+ def reverse?(term)
1256
+ term = find_definition(term)
1257
+ term&.reverse_property
1191
1258
  end
1192
1259
 
1193
- if (v_td = term_definitions[value]) && KEYWORDS.include?(v_td.id)
1194
- return (as_string ? v_td.id.to_s : v_td.id)
1195
- end
1260
+ ##
1261
+ # Given a term or IRI, find a reverse term definition matching that term. If the term is already reversed, find a non-reversed version.
1262
+ #
1263
+ # @param [Term, #to_s] term
1264
+ # @return [Term] related term definition
1265
+ def reverse_term(term)
1266
+ # Direct lookup of term
1267
+ term = term_definitions[term.to_s] if term_definitions.key?(term.to_s) && !term.is_a?(TermDefinition)
1268
+
1269
+ # Lookup term, assuming term is an IRI
1270
+ unless term.is_a?(TermDefinition)
1271
+ td = term_definitions.values.detect { |t| t.id == term.to_s }
1272
+
1273
+ # Otherwise create a temporary term definition
1274
+ term = td || TermDefinition.new(term.to_s, id: expand_iri(term, vocab: true))
1275
+ end
1196
1276
 
1197
- # If active context has a term definition for value, and the associated mapping is a keyword, return that keyword.
1198
- # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
1199
- if (v_td = term_definitions[value]) && (vocab || KEYWORDS.include?(v_td.id))
1200
- iri = base && v_td.id ? base.join(v_td.id) : v_td.id # vocab might be doc relative
1201
- return (as_string ? iri.to_s : iri)
1277
+ # Now, return a term, which reverses this term
1278
+ term_definitions.values.detect { |t| t.id == term.id && t.reverse_property != term.reverse_property }
1202
1279
  end
1203
1280
 
1204
- # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
1205
- if value[1..-1].to_s.include?(':')
1206
- prefix, suffix = value.split(':', 2)
1207
-
1208
- # 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.
1209
- if prefix == '_'
1210
- v = RDF::Node.new(namer.get_sym(suffix))
1211
- return (as_string ? v.to_s : v)
1212
- end
1213
- if suffix.start_with?('//')
1214
- v = RDF::URI(value)
1215
- return (as_string ? v.to_s : v)
1281
+ ##
1282
+ # Expand an IRI. Relative IRIs are expanded against any document base.
1283
+ #
1284
+ # @param [String] value
1285
+ # A keyword, term, prefix:suffix or possibly relative IRI
1286
+ # @param [Boolean] as_string (false) transform RDF::Resource values to string
1287
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1288
+ # @param [Hash] defined
1289
+ # Used during Context Processing.
1290
+ # @param [Boolean] documentRelative (false)
1291
+ # @param [Hash] local_context
1292
+ # Used during Context Processing.
1293
+ # @param [Boolean] vocab (false)
1294
+ # @param [Hash{Symbol => Object}] options
1295
+ # @return [RDF::Resource, String]
1296
+ # IRI or String, if it's a keyword
1297
+ # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
1298
+ # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
1299
+ def expand_iri(value,
1300
+ as_string: false,
1301
+ base: nil,
1302
+ defined: nil,
1303
+ documentRelative: false,
1304
+ local_context: nil,
1305
+ vocab: false,
1306
+ **_options)
1307
+ return (value && as_string ? value.to_s : value) unless value.is_a?(String)
1308
+
1309
+ return value if KEYWORDS.include?(value)
1310
+ return nil if value.match?(/^@[a-zA-Z]+$/)
1311
+
1312
+ defined ||= {} # if we initialized in the keyword arg we would allocate {} at each invokation, even in the 2 (common) early returns above.
1313
+
1314
+ # 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.
1315
+ create_term_definition(local_context, value, defined) if local_context&.key?(value) && !defined[value]
1316
+
1317
+ if (v_td = term_definitions[value]) && KEYWORDS.include?(v_td.id)
1318
+ return (as_string ? v_td.id.to_s : v_td.id)
1216
1319
  end
1217
1320
 
1218
- # 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.
1219
- if local_context && local_context.key?(prefix) && !defined[prefix]
1220
- create_term_definition(local_context, prefix, defined)
1321
+ # If active context has a term definition for value, and the associated mapping is a keyword, return that keyword.
1322
+ # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
1323
+ if (v_td = term_definitions[value]) && (vocab || KEYWORDS.include?(v_td.id))
1324
+ iri = base && v_td.id ? base.join(v_td.id) : v_td.id # vocab might be doc relative
1325
+ return (as_string ? iri.to_s : iri)
1221
1326
  end
1222
1327
 
1223
- # If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
1224
- if (td = term_definitions[prefix]) && !td.id.nil? && td.prefix?
1225
- return (as_string ? td.id.to_s : td.id) + suffix
1226
- elsif RDF::URI(value).absolute?
1227
- # Otherwise, if the value has the form of an absolute IRI, return it
1228
- return (as_string ? value.to_s : RDF::URI(value))
1229
- else
1230
- # Otherwise, it is a relative IRI
1328
+ # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
1329
+ if value[1..].to_s.include?(':')
1330
+ prefix, suffix = value.split(':', 2)
1331
+
1332
+ # 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.
1333
+ if prefix == '_'
1334
+ v = RDF::Node.new(namer.get_sym(suffix))
1335
+ return (as_string ? v.to_s : v)
1336
+ end
1337
+ if suffix.start_with?('//')
1338
+ v = RDF::URI(value)
1339
+ return (as_string ? v.to_s : v)
1340
+ end
1341
+
1342
+ # 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.
1343
+ create_term_definition(local_context, prefix, defined) if local_context&.key?(prefix) && !defined[prefix]
1344
+
1345
+ # If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
1346
+ if (td = term_definitions[prefix]) && !td.id.nil? && td.prefix?
1347
+ return (as_string ? td.id.to_s : td.id) + suffix
1348
+ elsif RDF::URI(value).absolute?
1349
+ # Otherwise, if the value has the form of an absolute IRI, return it
1350
+ return (as_string ? value.to_s : RDF::URI(value))
1351
+ end
1231
1352
  end
1232
- end
1233
1353
 
1234
- iri = value.is_a?(RDF::URI) ? value : RDF::URI(value)
1235
- result = if vocab && self.vocab
1236
- # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
1237
- # Note that @vocab could still be relative to a document base
1238
- (base && self.vocab.is_a?(RDF::URI) && self.vocab.relative? ? base.join(self.vocab) : self.vocab) + value
1239
- elsif documentRelative
1240
- if iri.absolute?
1241
- iri
1242
- elsif self.base.is_a?(RDF::URI) && self.base.absolute?
1243
- self.base.join(iri)
1244
- elsif self.base == false
1245
- # No resollution of `@base: null`
1246
- iri
1247
- elsif base && self.base
1248
- base.join(self.base).join(iri)
1249
- elsif base
1250
- base.join(iri)
1354
+ iri = value.is_a?(RDF::URI) ? value : RDF::URI(value)
1355
+ result = if vocab && self.vocab
1356
+ # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
1357
+ # Note that @vocab could still be relative to a document base
1358
+ (base && self.vocab.is_a?(RDF::URI) && self.vocab.relative? ? base.join(self.vocab) : self.vocab) + value
1359
+ elsif documentRelative
1360
+ if iri.absolute?
1361
+ iri
1362
+ elsif self.base.is_a?(RDF::URI) && self.base.absolute?
1363
+ self.base.join(iri)
1364
+ elsif self.base == false
1365
+ # No resollution of `@base: null`
1366
+ iri
1367
+ elsif base && self.base
1368
+ base.join(self.base).join(iri)
1369
+ elsif base
1370
+ base.join(iri)
1371
+ else
1372
+ # Returns a relative IRI in an odd case.
1373
+ iri
1374
+ end
1375
+ elsif local_context && iri.relative?
1376
+ # 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.
1377
+ raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
1251
1378
  else
1252
- # Returns a relative IRI in an odd case.
1253
1379
  iri
1254
1380
  end
1255
- elsif local_context && iri.relative?
1256
- # 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.
1257
- raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
1258
- else
1259
- iri
1381
+ result && as_string ? result.to_s : result
1260
1382
  end
1261
- result && as_string ? result.to_s : result
1262
- end
1263
1383
 
1264
- # The following constants are used to reduce object allocations in #compact_iri below
1265
- CONTAINERS_GRAPH = %w(@graph@id @graph@id@set).freeze
1266
- CONTAINERS_GRAPH_INDEX = %w(@graph@index @graph@index@set).freeze
1267
- CONTAINERS_GRAPH_INDEX_INDEX = %w(@graph@index @graph@index@set @index @index@set).freeze
1268
- CONTAINERS_GRAPH_SET = %w(@graph @graph@set @set).freeze
1269
- CONTAINERS_ID_TYPE = %w(@id @id@set @type @set@type).freeze
1270
- CONTAINERS_ID_VOCAB = %w(@id @vocab @none).freeze
1271
- CONTAINERS_INDEX_SET = %w(@index @index@set).freeze
1272
- CONTAINERS_LANGUAGE = %w(@language @language@set).freeze
1273
- CONTAINERS_VALUE = %w(@value).freeze
1274
- CONTAINERS_VOCAB_ID = %w(@vocab @id @none).freeze
1275
-
1276
- ##
1277
- # Compacts an absolute IRI to the shortest matching term or compact IRI
1278
- #
1279
- # @param [RDF::URI] iri
1280
- # @param [String, RDF::URI] base for resolving document-relative IRIs
1281
- # @param [Object] value
1282
- # Value, used to select among various maps for the same IRI
1283
- # @param [Boolean] reverse
1284
- # specifies whether a reverse property is being compacted
1285
- # @param [Boolean] vocab
1286
- # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1287
- #
1288
- # @return [String] compacted form of IRI
1289
- # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1290
- def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
1291
- return if iri.nil?
1292
- iri = iri.to_s
1293
-
1294
- if vocab && inverse_context.key?(iri)
1295
- default_language = if self.default_direction
1296
- "#{self.default_language}_#{self.default_direction}".downcase
1297
- else
1298
- (self.default_language || "@none").downcase
1299
- end
1300
- containers = []
1301
- tl, tl_value = "@language", "@null"
1302
- containers.concat(CONTAINERS_INDEX_SET) if index?(value) && !graph?(value)
1303
-
1304
- # If the value is a JSON Object with the key @preserve, use the value of @preserve.
1305
- value = value['@preserve'].first if value.is_a?(Hash) && value.key?('@preserve')
1306
-
1307
- if reverse
1308
- tl, tl_value = "@type", "@reverse"
1309
- containers << '@set'
1310
- elsif list?(value)
1311
- # 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:
1312
- containers << "@list" unless index?(value)
1313
- list = value['@list']
1314
- common_type = nil
1315
- common_language = default_language if list.empty?
1316
- list.each do |item|
1317
- item_language, item_type = "@none", "@none"
1318
- if value?(item)
1319
- if item.key?('@direction')
1320
- item_language = "#{item['@language']}_#{item['@direction']}".downcase
1321
- elsif item.key?('@language')
1322
- item_language = item['@language'].downcase
1323
- elsif item.key?('@type')
1324
- item_type = item['@type']
1384
+ # The following constants are used to reduce object allocations in #compact_iri below
1385
+ CONTAINERS_GRAPH = %w[@graph@id @graph@id@set].freeze
1386
+ CONTAINERS_GRAPH_INDEX = %w[@graph@index @graph@index@set].freeze
1387
+ CONTAINERS_GRAPH_INDEX_INDEX = %w[@graph@index @graph@index@set @index @index@set].freeze
1388
+ CONTAINERS_GRAPH_SET = %w[@graph @graph@set @set].freeze
1389
+ CONTAINERS_ID_TYPE = %w[@id @id@set @type @set@type].freeze
1390
+ CONTAINERS_ID_VOCAB = %w[@id @vocab @none].freeze
1391
+ CONTAINERS_INDEX_SET = %w[@index @index@set].freeze
1392
+ CONTAINERS_LANGUAGE = %w[@language @language@set].freeze
1393
+ CONTAINERS_VALUE = %w[@value].freeze
1394
+ CONTAINERS_VOCAB_ID = %w[@vocab @id @none].freeze
1395
+
1396
+ ##
1397
+ # Compacts an absolute IRI to the shortest matching term or compact IRI
1398
+ #
1399
+ # @param [RDF::URI] iri
1400
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1401
+ # @param [Object] value
1402
+ # Value, used to select among various maps for the same IRI
1403
+ # @param [Boolean] reverse
1404
+ # specifies whether a reverse property is being compacted
1405
+ # @param [Boolean] vocab
1406
+ # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1407
+ #
1408
+ # @return [String] compacted form of IRI
1409
+ # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1410
+ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
1411
+ return if iri.nil?
1412
+
1413
+ iri = iri.to_s
1414
+
1415
+ if vocab && inverse_context.key?(iri)
1416
+ default_language = if default_direction
1417
+ "#{self.default_language}_#{default_direction}".downcase
1418
+ else
1419
+ (self.default_language || "@none").downcase
1420
+ end
1421
+ containers = []
1422
+ tl = "@language"
1423
+ tl_value = "@null"
1424
+ containers.concat(CONTAINERS_INDEX_SET) if index?(value) && !graph?(value)
1425
+
1426
+ # If the value is a JSON Object with the key @preserve, use the value of @preserve.
1427
+ value = value['@preserve'].first if value.is_a?(Hash) && value.key?('@preserve')
1428
+
1429
+ if reverse
1430
+ tl = "@type"
1431
+ tl_value = "@reverse"
1432
+ containers << '@set'
1433
+ elsif list?(value)
1434
+ # 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:
1435
+ containers << "@list" unless index?(value)
1436
+ list = value['@list']
1437
+ common_type = nil
1438
+ common_language = default_language if list.empty?
1439
+ list.each do |item|
1440
+ item_language = "@none"
1441
+ item_type = "@none"
1442
+ if value?(item)
1443
+ if item.key?('@direction')
1444
+ item_language = "#{item['@language']}_#{item['@direction']}".downcase
1445
+ elsif item.key?('@language')
1446
+ item_language = item['@language'].downcase
1447
+ elsif item.key?('@type')
1448
+ item_type = item['@type']
1449
+ else
1450
+ item_language = "@null"
1451
+ end
1325
1452
  else
1326
- item_language = "@null"
1453
+ item_type = '@id'
1327
1454
  end
1328
- else
1329
- item_type = '@id'
1330
- end
1331
- common_language ||= item_language
1332
- if item_language != common_language && value?(item)
1333
- common_language = '@none'
1455
+ common_language ||= item_language
1456
+ common_language = '@none' if item_language != common_language && value?(item)
1457
+ common_type ||= item_type
1458
+ common_type = '@none' if item_type != common_type
1334
1459
  end
1335
- common_type ||= item_type
1336
- if item_type != common_type
1337
- common_type = '@none'
1338
- end
1339
- end
1340
1460
 
1341
- common_language ||= '@none'
1342
- common_type ||= '@none'
1343
- if common_type != '@none'
1344
- tl, tl_value = '@type', common_type
1461
+ common_language ||= '@none'
1462
+ common_type ||= '@none'
1463
+ if common_type == '@none'
1464
+ tl_value = common_language
1465
+ else
1466
+ tl = '@type'
1467
+ tl_value = common_type
1468
+ end
1469
+ elsif graph?(value)
1470
+ # Prefer @index and @id containers, then @graph, then @index
1471
+ containers.concat(CONTAINERS_GRAPH_INDEX_INDEX) if index?(value)
1472
+ containers.concat(CONTAINERS_GRAPH) if value.key?('@id')
1473
+
1474
+ # Prefer an @graph container next
1475
+ containers.concat(CONTAINERS_GRAPH_SET)
1476
+
1477
+ # Lastly, in 1.1, any graph can be indexed on @index or @id, so add if we haven't already
1478
+ containers.concat(CONTAINERS_GRAPH_INDEX) unless index?(value)
1479
+ containers.concat(CONTAINERS_GRAPH) unless value.key?('@id')
1480
+ containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
1481
+ containers << '@set'
1482
+
1483
+ tl = '@type'
1484
+ tl_value = '@id'
1345
1485
  else
1346
- tl_value = common_language
1486
+ if value?(value)
1487
+ # In 1.1, an language map can be used to index values using @none
1488
+ if value.key?('@language') && !index?(value)
1489
+ tl_value = value['@language'].downcase
1490
+ tl_value += "_#{value['@direction']}" if value['@direction']
1491
+ containers.concat(CONTAINERS_LANGUAGE)
1492
+ elsif value.key?('@direction') && !index?(value)
1493
+ tl_value = "_#{value['@direction']}"
1494
+ elsif value.key?('@type')
1495
+ tl_value = value['@type']
1496
+ tl = '@type'
1497
+ end
1498
+ else
1499
+ # In 1.1, an id or type map can be used to index values using @none
1500
+ containers.concat(CONTAINERS_ID_TYPE)
1501
+ tl = '@type'
1502
+ tl_value = '@id'
1503
+ end
1504
+ containers << '@set'
1347
1505
  end
1348
- elsif graph?(value)
1349
- # Prefer @index and @id containers, then @graph, then @index
1350
- containers.concat(CONTAINERS_GRAPH_INDEX_INDEX) if index?(value)
1351
- containers.concat(CONTAINERS_GRAPH) if value.key?('@id')
1352
1506
 
1353
- # Prefer an @graph container next
1354
- containers.concat(CONTAINERS_GRAPH_SET)
1507
+ containers << '@none'
1355
1508
 
1356
- # Lastly, in 1.1, any graph can be indexed on @index or @id, so add if we haven't already
1357
- containers.concat(CONTAINERS_GRAPH_INDEX) unless index?(value)
1358
- containers.concat(CONTAINERS_GRAPH) unless value.key?('@id')
1509
+ # In 1.1, an index map can be used to index values using @none, so add as a low priority
1359
1510
  containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
1360
- containers << '@set'
1361
-
1362
- tl, tl_value = '@type', '@id'
1363
- else
1364
- if value?(value)
1365
- # In 1.1, an language map can be used to index values using @none
1366
- if value.key?('@language') && !index?(value)
1367
- tl_value = value['@language'].downcase
1368
- tl_value += "_#{value['@direction']}" if value['@direction']
1369
- containers.concat(CONTAINERS_LANGUAGE)
1370
- elsif value.key?('@direction') && !index?(value)
1371
- tl_value = "_#{value['@direction']}"
1372
- elsif value.key?('@type')
1373
- tl_value = value['@type']
1374
- tl = '@type'
1511
+ # Values without type or language can use @language map
1512
+ containers.concat(CONTAINERS_LANGUAGE) if value?(value) && value.keys == CONTAINERS_VALUE
1513
+
1514
+ tl_value ||= '@null'
1515
+ preferred_values = []
1516
+ preferred_values << '@reverse' if tl_value == '@reverse'
1517
+ if ['@id', '@reverse'].include?(tl_value) && value.is_a?(Hash) && value.key?('@id')
1518
+ t_iri = compact_iri(value['@id'], vocab: true, base: base)
1519
+ if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1520
+ preferred_values.concat(CONTAINERS_VOCAB_ID)
1521
+ else
1522
+ preferred_values.concat(CONTAINERS_ID_VOCAB)
1375
1523
  end
1376
1524
  else
1377
- # In 1.1, an id or type map can be used to index values using @none
1378
- containers.concat(CONTAINERS_ID_TYPE)
1379
- tl, tl_value = '@type', '@id'
1525
+ tl = '@any' if list?(value) && value['@list'].empty?
1526
+ preferred_values.concat([tl_value, '@none'].compact)
1380
1527
  end
1381
- containers << '@set'
1382
- end
1383
-
1384
- containers << '@none'
1385
-
1386
- # In 1.1, an index map can be used to index values using @none, so add as a low priority
1387
- containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
1388
- # Values without type or language can use @language map
1389
- containers.concat(CONTAINERS_LANGUAGE) if value?(value) && value.keys == CONTAINERS_VALUE
1528
+ preferred_values << '@any'
1390
1529
 
1391
- tl_value ||= '@null'
1392
- preferred_values = []
1393
- preferred_values << '@reverse' if tl_value == '@reverse'
1394
- if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.key?('@id')
1395
- t_iri = compact_iri(value['@id'], vocab: true, base: base)
1396
- if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1397
- preferred_values.concat(CONTAINERS_VOCAB_ID)
1398
- else
1399
- preferred_values.concat(CONTAINERS_ID_VOCAB)
1530
+ # if containers included `@language` and preferred_values includes something of the form language-tag_direction, add just the _direction part, to select terms that have that direction.
1531
+ if (lang_dir = preferred_values.detect { |v| v.include?('_') })
1532
+ preferred_values << ('_' + lang_dir.split('_').last)
1400
1533
  end
1401
- else
1402
- tl = '@any' if list?(value) && value['@list'].empty?
1403
- preferred_values.concat([tl_value, '@none'].compact)
1404
- end
1405
- preferred_values << '@any'
1406
1534
 
1407
- # if containers included `@language` and preferred_values includes something of the form language-tag_direction, add just the _direction part, to select terms that have that direction.
1408
- if lang_dir = preferred_values.detect {|v| v.include?('_')}
1409
- preferred_values << '_' + lang_dir.split('_').last
1535
+ if (p_term = select_term(iri, containers, tl, preferred_values))
1536
+ return p_term
1537
+ end
1410
1538
  end
1411
1539
 
1412
- if p_term = select_term(iri, containers, tl, preferred_values)
1413
- return p_term
1540
+ # 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:
1541
+ if vocab && self.vocab && iri.start_with?(self.vocab) && iri.length > self.vocab.length
1542
+ suffix = iri[self.vocab.length..]
1543
+ return suffix unless term_definitions.key?(suffix)
1414
1544
  end
1415
- end
1416
1545
 
1417
- # 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:
1418
- if vocab && self.vocab && iri.start_with?(self.vocab) && iri.length > self.vocab.length
1419
- suffix = iri[self.vocab.length..-1]
1420
- return suffix unless term_definitions.key?(suffix)
1421
- end
1546
+ # 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.
1547
+ candidates = []
1422
1548
 
1423
- # 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.
1424
- candidates = []
1549
+ term_definitions.each do |term, td|
1550
+ # Skip term if `@prefix` is not true in term definition
1551
+ next unless td&.prefix?
1425
1552
 
1426
- term_definitions.each do |term, td|
1427
- next if td.nil? || td.id.nil? || td.id == iri || !iri.start_with?(td.id)
1553
+ next if td&.id.nil? || td.id == iri || !td.match_iri?(iri)
1428
1554
 
1429
- # Skip term if `@prefix` is not true in term definition
1430
- next unless td.prefix?
1555
+ suffix = iri[td.id.length..]
1556
+ ciri = "#{term}:#{suffix}"
1557
+ candidates << ciri unless value && term_definitions.key?(ciri)
1558
+ end
1431
1559
 
1432
- suffix = iri[td.id.length..-1]
1433
- ciri = "#{term}:#{suffix}"
1434
- candidates << ciri unless value && term_definitions.key?(ciri)
1435
- end
1560
+ return candidates.min unless candidates.empty?
1561
+
1562
+ # If we still don't have any terms and we're using standard_prefixes,
1563
+ # try those, and add to mapping
1564
+ if @options[:standard_prefixes]
1565
+ candidates = RDF::Vocabulary
1566
+ .select { |v| iri.start_with?(v.to_uri.to_s) && iri != v.to_uri.to_s }
1567
+ .map do |v|
1568
+ prefix = v.__name__.to_s.split('::').last.downcase
1569
+ set_mapping(prefix, v.to_uri.to_s)
1570
+ iri.sub(v.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
1571
+ end
1436
1572
 
1437
- return candidates.sort.first if !candidates.empty?
1573
+ return candidates.min unless candidates.empty?
1574
+ end
1438
1575
 
1439
- # If we still don't have any terms and we're using standard_prefixes,
1440
- # try those, and add to mapping
1441
- if @options[:standard_prefixes]
1442
- candidates = RDF::Vocabulary.
1443
- select {|v| iri.start_with?(v.to_uri.to_s) && iri != v.to_uri.to_s}.
1444
- map do |v|
1445
- prefix = v.__name__.to_s.split('::').last.downcase
1446
- set_mapping(prefix, v.to_uri.to_s)
1447
- iri.sub(v.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
1448
- end
1576
+ # If iri could be confused with a compact IRI using a term in this context, signal an error
1577
+ term_definitions.each do |term, td|
1578
+ next unless td.prefix? && td.match_compact_iri?(iri)
1449
1579
 
1450
- return candidates.sort.first if !candidates.empty?
1451
- end
1580
+ raise JSON::LD::JsonLdError::IRIConfusedWithPrefix, "Absolute IRI '#{iri}' confused with prefix '#{term}'"
1581
+ end
1452
1582
 
1453
- # If iri could be confused with a compact IRI using a term in this context, signal an error
1454
- term_definitions.each do |term, td|
1455
- next unless iri.to_s.start_with?("#{term}:") && td.prefix?
1456
- raise JSON::LD::JsonLdError:: IRIConfusedWithPrefix, "Absolute IRI '#{iri}' confused with prefix '#{term}'"
1457
- end
1583
+ return iri if vocab
1458
1584
 
1459
- if !vocab
1460
1585
  # transform iri to a relative IRI using the document's base IRI
1461
1586
  iri = remove_base(self.base || base, iri)
1462
1587
  # Make . relative if it has the form of a keyword.
1463
1588
  iri = "./#{iri}" if iri.match?(/^@[a-zA-Z]+$/)
1464
- return iri
1465
- else
1466
- return iri
1467
- end
1468
- end
1469
1589
 
1470
- ##
1471
- # 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.
1472
- #
1473
- # 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.
1474
- #
1475
- # @param [String] property
1476
- # Associated property used to find coercion rules
1477
- # @param [Hash, String] value
1478
- # Value (literal or IRI) to be expanded
1479
- # @param [Boolean] useNativeTypes (false) use native representations
1480
- # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1481
- # @param [String, RDF::URI] base for resolving document-relative IRIs
1482
- # @param [Hash{Symbol => Object}] options
1483
- #
1484
- # @return [Hash] Object representation of value
1485
- # @raise [RDF::ReaderError] if the iri cannot be expanded
1486
- # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1487
- def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **options)
1488
- td = term_definitions.fetch(property, TermDefinition.new(property))
1489
-
1490
- # 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.
1491
- if value.is_a?(String) && td.type_mapping == '@id'
1492
- # log_debug("") {"as relative IRI: #{value.inspect}"}
1493
- return {'@id' => expand_iri(value, documentRelative: true, base: base).to_s}
1590
+ iri
1494
1591
  end
1495
1592
 
1496
- # 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.
1497
- if value.is_a?(String) && td.type_mapping == '@vocab'
1498
- return {'@id' => expand_iri(value, vocab: true, documentRelative: true, base: base).to_s}
1499
- end
1593
+ ##
1594
+ # 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.
1595
+ #
1596
+ # 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.
1597
+ #
1598
+ # @param [String] property
1599
+ # Associated property used to find coercion rules
1600
+ # @param [Hash, String] value
1601
+ # Value (literal or IRI) to be expanded
1602
+ # @param [Boolean] useNativeTypes (false) use native representations
1603
+ # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1604
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1605
+ # @param [Hash{Symbol => Object}] options
1606
+ #
1607
+ # @return [Hash] Object representation of value
1608
+ # @raise [RDF::ReaderError] if the iri cannot be expanded
1609
+ # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1610
+ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **_options)
1611
+ td = term_definitions.fetch(property, TermDefinition.new(property))
1612
+
1613
+ # 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.
1614
+ if value.is_a?(String) && td.type_mapping == '@id'
1615
+ # log_debug("") {"as relative IRI: #{value.inspect}"}
1616
+ return { '@id' => expand_iri(value, documentRelative: true, base: base).to_s }
1617
+ end
1500
1618
 
1501
- result = case value
1502
- when RDF::URI, RDF::Node
1503
- {'@id' => value.to_s}
1504
- when Date, DateTime, Time
1505
- lit = RDF::Literal.new(value)
1506
- {'@value' => lit.to_s, '@type' => lit.datatype.to_s}
1507
- else
1508
- # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1509
- res = {}
1510
-
1511
- if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1512
- res['@type'] = td.type_mapping.to_s
1513
- elsif value.is_a?(String)
1514
- language = language(property)
1515
- direction = direction(property)
1516
- res['@language'] = language if language
1517
- res['@direction'] = direction if direction
1518
- end
1519
-
1520
- res.merge('@value' => value)
1521
- end
1619
+ # 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.
1620
+ if value.is_a?(String) && td.type_mapping == '@vocab'
1621
+ return { '@id' => expand_iri(value, vocab: true, documentRelative: true, base: base).to_s }
1622
+ end
1522
1623
 
1523
- result
1524
- end
1624
+ case value
1625
+ when RDF::URI, RDF::Node
1626
+ { '@id' => value.to_s }
1627
+ when Date, DateTime, Time
1628
+ lit = RDF::Literal.new(value)
1629
+ { '@value' => lit.to_s, '@type' => lit.datatype.to_s }
1630
+ else
1631
+ # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1632
+ res = {}
1633
+
1634
+ if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1635
+ res['@type'] = td.type_mapping.to_s
1636
+ elsif value.is_a?(String)
1637
+ language = language(property)
1638
+ direction = direction(property)
1639
+ res['@language'] = language if language
1640
+ res['@direction'] = direction if direction
1641
+ end
1525
1642
 
1526
- ##
1527
- # Compact a value
1528
- #
1529
- # @param [String] property
1530
- # Associated property used to find coercion rules
1531
- # @param [Hash] value
1532
- # Value (literal or IRI), in full object representation, to be compacted
1533
- # @param [String, RDF::URI] base for resolving document-relative IRIs
1534
- #
1535
- # @return [Hash] Object representation of value
1536
- # @raise [JsonLdError] if the iri cannot be expanded
1537
- # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1538
- # FIXME: revisit the specification version of this.
1539
- def compact_value(property, value, base: nil)
1540
- # log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1541
-
1542
- indexing = index?(value) && container(property).include?('@index')
1543
- language = language(property)
1544
- direction = direction(property)
1545
-
1546
- result = case
1547
- when coerce(property) == '@id' && value.key?('@id') && (value.keys - %w(@id @index)).empty?
1548
- # Compact an @id coercion
1549
- # log_debug("") {" (@id & coerce)"}
1550
- compact_iri(value['@id'], base: base)
1551
- when coerce(property) == '@vocab' && value.key?('@id') && (value.keys - %w(@id @index)).empty?
1552
- # Compact an @id coercion
1553
- # log_debug("") {" (@id & coerce & vocab)"}
1554
- compact_iri(value['@id'], vocab: true)
1555
- when value.key?('@id')
1556
- # log_debug("") {" (@id)"}
1557
- # return value as is
1558
- value
1559
- when value['@type'] && value['@type'] == coerce(property)
1560
- # Compact common datatype
1561
- # log_debug("") {" (@type & coerce) == #{coerce(property)}"}
1562
- value['@value']
1563
- when coerce(property) == '@none' || value['@type']
1564
- # use original expanded value
1565
- value
1566
- when !value['@value'].is_a?(String)
1567
- # log_debug("") {" (native)"}
1568
- indexing || !index?(value) ? value['@value'] : value
1569
- when value['@language'].to_s.downcase == language.to_s.downcase && value['@direction'] == direction
1570
- # Compact language and direction
1571
- indexing || !index?(value) ? value['@value'] : value
1572
- else
1573
- value
1643
+ res.merge('@value' => value)
1644
+ end
1574
1645
  end
1575
1646
 
1576
- if result.is_a?(Hash) && result.key?('@type') && value['@type'] != '@json'
1577
- # Compact values of @type
1578
- c_type = if result['@type'].is_a?(Array)
1579
- result['@type'].map {|t| compact_iri(t, vocab: true)}
1647
+ ##
1648
+ # Compact a value
1649
+ #
1650
+ # @param [String] property
1651
+ # Associated property used to find coercion rules
1652
+ # @param [Hash] value
1653
+ # Value (literal or IRI), in full object representation, to be compacted
1654
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1655
+ #
1656
+ # @return [Hash] Object representation of value
1657
+ # @raise [JsonLdError] if the iri cannot be expanded
1658
+ # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1659
+ # FIXME: revisit the specification version of this.
1660
+ def compact_value(property, value, base: nil)
1661
+ # log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1662
+
1663
+ indexing = index?(value) && container(property).include?('@index')
1664
+ language = language(property)
1665
+ direction = direction(property)
1666
+
1667
+ result = if coerce(property) == '@id' && value.key?('@id') && (value.keys - %w[@id @index]).empty?
1668
+ # Compact an @id coercion
1669
+ # log_debug("") {" (@id & coerce)"}
1670
+ compact_iri(value['@id'], base: base)
1671
+ elsif coerce(property) == '@vocab' && value.key?('@id') && (value.keys - %w[@id @index]).empty?
1672
+ # Compact an @id coercion
1673
+ # log_debug("") {" (@id & coerce & vocab)"}
1674
+ compact_iri(value['@id'], vocab: true)
1675
+ elsif value.key?('@id')
1676
+ # log_debug("") {" (@id)"}
1677
+ # return value as is
1678
+ value
1679
+ elsif value['@type'] && value['@type'] == coerce(property)
1680
+ # Compact common datatype
1681
+ # log_debug("") {" (@type & coerce) == #{coerce(property)}"}
1682
+ value['@value']
1683
+ elsif coerce(property) == '@none' || value['@type']
1684
+ # use original expanded value
1685
+ value
1686
+ elsif !value['@value'].is_a?(String)
1687
+ # log_debug("") {" (native)"}
1688
+ indexing || !index?(value) ? value['@value'] : value
1689
+ elsif value['@language'].to_s.casecmp(language.to_s).zero? && value['@direction'] == direction
1690
+ # Compact language and direction
1691
+ indexing || !index?(value) ? value['@value'] : value
1580
1692
  else
1581
- compact_iri(result['@type'], vocab: true)
1693
+ value
1582
1694
  end
1583
- result = result.merge('@type' => c_type)
1584
- end
1585
-
1586
- # If the result is an object, tranform keys using any term keyword aliases
1587
- if result.is_a?(Hash) && result.keys.any? {|k| self.alias(k) != k}
1588
- # log_debug("") {" (map to key aliases)"}
1589
- new_element = {}
1590
- result.each do |k, v|
1591
- new_element[self.alias(k)] = v
1592
- end
1593
- result = new_element
1594
- end
1595
1695
 
1596
- # log_debug("") {"=> #{result.inspect}"}
1597
- result
1598
- end
1696
+ if result.is_a?(Hash) && result.key?('@type') && value['@type'] != '@json'
1697
+ # Compact values of @type
1698
+ c_type = if result['@type'].is_a?(Array)
1699
+ result['@type'].map { |t| compact_iri(t, vocab: true) }
1700
+ else
1701
+ compact_iri(result['@type'], vocab: true)
1702
+ end
1703
+ result = result.merge('@type' => c_type)
1704
+ end
1705
+
1706
+ # If the result is an object, tranform keys using any term keyword aliases
1707
+ if result.is_a?(Hash) && result.keys.any? { |k| self.alias(k) != k }
1708
+ # log_debug("") {" (map to key aliases)"}
1709
+ new_element = {}
1710
+ result.each do |k, v|
1711
+ new_element[self.alias(k)] = v
1712
+ end
1713
+ result = new_element
1714
+ end
1599
1715
 
1600
- ##
1601
- # Turn this into a source for a new instantiation
1602
- # @param [Array<String>] aliases
1603
- # Other URLs to alias when preloading
1604
- # @return [String]
1605
- def to_rb(*aliases)
1606
- canon_base = RDF::URI(context_base).canonicalize
1607
- defn = []
1608
-
1609
- defn << "base: #{self.base.to_s.inspect}" if self.base
1610
- defn << "language: #{self.default_language.inspect}" if self.default_language
1611
- defn << "vocab: #{self.vocab.to_s.inspect}" if self.vocab
1612
- defn << "processingMode: #{self.processingMode.inspect}" if self.processingMode
1613
- term_defs = term_definitions.map do |term, td|
1614
- " " + term.inspect + " => " + td.to_rb
1615
- end.sort
1616
- defn << "term_definitions: {\n#{term_defs.join(",\n") }\n }" unless term_defs.empty?
1617
- %(# -*- encoding: utf-8 -*-
1716
+ # log_debug("") {"=> #{result.inspect}"}
1717
+ result
1718
+ end
1719
+
1720
+ ##
1721
+ # Turn this into a source for a new instantiation
1722
+ # @param [Array<String>] aliases
1723
+ # Other URLs to alias when preloading
1724
+ # @return [String]
1725
+ def to_rb(*aliases)
1726
+ canon_base = RDF::URI(context_base).canonicalize
1727
+ defn = []
1728
+
1729
+ defn << "base: #{base.to_s.inspect}" if base
1730
+ defn << "language: #{default_language.inspect}" if default_language
1731
+ defn << "vocab: #{vocab.to_s.inspect}" if vocab
1732
+ defn << "processingMode: #{processingMode.inspect}" if processingMode
1733
+ term_defs = term_definitions.map do |term, td|
1734
+ " " + term.inspect + " => " + td.to_rb
1735
+ end.sort
1736
+ defn << "term_definitions: {\n#{term_defs.join(",\n")}\n }" unless term_defs.empty?
1737
+ %(# -*- encoding: utf-8 -*-
1618
1738
  # frozen_string_literal: true
1619
1739
  # This file generated automatically from #{context_base}
1620
1740
  require 'json/ld'
1621
1741
  class JSON::LD::Context
1622
1742
  ).gsub(/^ /, '') +
1623
- %[ add_preloaded("#{canon_base}") do\n new(] + defn.join(", ") + ")\n end\n" +
1624
- aliases.map {|a| %[ alias_preloaded("#{a}", "#{canon_base}")\n]}.join("") +
1625
- "end\n"
1626
- end
1743
+ %[ add_preloaded("#{canon_base}") do\n new(] + defn.join(", ") + ")\n end\n" +
1744
+ aliases.map { |a| %[ alias_preloaded("#{a}", "#{canon_base}")\n] }.join +
1745
+ "end\n"
1746
+ end
1627
1747
 
1628
- def inspect
1629
- v = %w([Context)
1630
- v << "base=#{base}" if base
1631
- v << "vocab=#{vocab}" if vocab
1632
- v << "processingMode=#{processingMode}" if processingMode
1633
- v << "default_language=#{default_language}" if default_language
1634
- v << "default_direction=#{default_direction}" if default_direction
1635
- v << "previous_context" if previous_context
1636
- v << "term_definitions[#{term_definitions.length}]=#{term_definitions}"
1637
- v.join(" ") + "]"
1638
- end
1748
+ def inspect
1749
+ v = %w([Context)
1750
+ v << "base=#{base}" if base
1751
+ v << "vocab=#{vocab}" if vocab
1752
+ v << "processingMode=#{processingMode}" if processingMode
1753
+ v << "default_language=#{default_language}" if default_language
1754
+ v << "default_direction=#{default_direction}" if default_direction
1755
+ v << "previous_context" if previous_context
1756
+ v << "term_definitions[#{term_definitions.length}]=#{term_definitions}"
1757
+ v.join(" ") + "]"
1758
+ end
1639
1759
 
1640
- # Duplicate an active context, allowing it to be modified.
1641
- def dup
1642
- that = self
1643
- ec = Context.new(unfrozen: true, **@options)
1644
- ec.context_base = that.context_base
1645
- ec.base = that.base unless that.base.nil?
1646
- ec.default_direction = that.default_direction
1647
- ec.default_language = that.default_language
1648
- ec.previous_context = that.previous_context
1649
- ec.processingMode = that.processingMode if that.instance_variable_get(:@processingMode)
1650
- ec.vocab = that.vocab if that.vocab
1651
-
1652
- ec.instance_eval do
1653
- @term_definitions = that.term_definitions.dup
1654
- @iri_to_term = that.iri_to_term
1760
+ # Duplicate an active context, allowing it to be modified.
1761
+ def dup
1762
+ that = self
1763
+ ec = Context.new(unfrozen: true, **@options)
1764
+ ec.context_base = that.context_base
1765
+ ec.base = that.base unless that.base.nil?
1766
+ ec.default_direction = that.default_direction
1767
+ ec.default_language = that.default_language
1768
+ ec.previous_context = that.previous_context
1769
+ ec.processingMode = that.processingMode if that.instance_variable_get(:@processingMode)
1770
+ ec.vocab = that.vocab if that.vocab
1771
+
1772
+ ec.instance_eval do
1773
+ @term_definitions = that.term_definitions.dup
1774
+ @iri_to_term = that.iri_to_term
1775
+ end
1776
+ ec
1655
1777
  end
1656
- ec
1657
- end
1658
1778
 
1659
- protected
1779
+ protected
1660
1780
 
1661
- ##
1662
- # Determine if `term` is a suitable term.
1663
- # Term may be any valid JSON string.
1664
- #
1665
- # @param [String] term
1666
- # @return [Boolean]
1667
- def term_valid?(term)
1668
- term.is_a?(String) && !term.empty?
1669
- end
1781
+ ##
1782
+ # Determine if `term` is a suitable term.
1783
+ # Term may be any valid JSON string.
1784
+ #
1785
+ # @param [String] term
1786
+ # @return [Boolean]
1787
+ def term_valid?(term)
1788
+ term.is_a?(String) && !term.empty?
1789
+ end
1670
1790
 
1671
- # Reverse term mapping, typically used for finding aliases for keys.
1672
- #
1673
- # Returns either the original value, or a mapping for this value.
1674
- #
1675
- # @example
1676
- # {"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}
1677
- #
1678
- # @param [RDF::URI, String] value
1679
- # @return [String]
1680
- def alias(value)
1681
- iri_to_term.fetch(value, value)
1682
- end
1791
+ # Reverse term mapping, typically used for finding aliases for keys.
1792
+ #
1793
+ # Returns either the original value, or a mapping for this value.
1794
+ #
1795
+ # @example
1796
+ # {"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}
1797
+ #
1798
+ # @param [RDF::URI, String] value
1799
+ # @return [String]
1800
+ def alias(value)
1801
+ iri_to_term.fetch(value, value)
1802
+ end
1683
1803
 
1684
- private
1685
-
1686
- CONTEXT_CONTAINER_ARRAY_TERMS = Set.new(%w(@set @list @graph)).freeze
1687
- CONTEXT_CONTAINER_ID_GRAPH = Set.new(%w(@id @graph)).freeze
1688
- CONTEXT_CONTAINER_INDEX_GRAPH = Set.new(%w(@index @graph)).freeze
1689
- CONTEXT_BASE_FRAG_OR_QUERY = %w(? #).freeze
1690
- CONTEXT_TYPE_ID_VOCAB = %w(@id @vocab).freeze
1691
-
1692
- ##
1693
- # Reads the `@context` from an IO
1694
- def load_context(io, **options)
1695
- io.rewind
1696
- remote_doc = API.loadRemoteDocument(io, **options)
1697
- remote_doc.document.is_a?(String) ?
1698
- MultiJson.load(remote_doc.document) :
1699
- remote_doc.document
1700
- end
1804
+ private
1701
1805
 
1702
- def uri(value)
1703
- case value.to_s
1704
- when /^_:(.*)$/
1705
- # Map BlankNodes if a namer is given
1706
- # log_debug "uri(bnode)#{value}: #{$1}"
1707
- bnode(namer.get_sym($1))
1708
- else
1709
- value = RDF::URI(value)
1710
- #value.validate! if options[:validate]
1711
- value
1806
+ CONTEXT_CONTAINER_ARRAY_TERMS = Set.new(%w[@set @list @graph]).freeze
1807
+ CONTEXT_CONTAINER_ID_GRAPH = Set.new(%w[@id @graph]).freeze
1808
+ CONTEXT_CONTAINER_INDEX_GRAPH = Set.new(%w[@index @graph]).freeze
1809
+ CONTEXT_BASE_FRAG_OR_QUERY = %w[? #].freeze
1810
+ CONTEXT_TYPE_ID_VOCAB = %w[@id @vocab].freeze
1811
+
1812
+ ##
1813
+ # Reads the `@context` from an IO
1814
+ def load_context(io, **options)
1815
+ io.rewind
1816
+ remote_doc = API.loadRemoteDocument(io, **options)
1817
+ if remote_doc.document.is_a?(String)
1818
+ MultiJson.load(remote_doc.document)
1819
+ else
1820
+ remote_doc.document
1821
+ end
1712
1822
  end
1713
- end
1714
1823
 
1715
- # Keep track of allocated BNodes
1716
- #
1717
- # Don't actually use the name provided, to prevent name alias issues.
1718
- # @return [RDF::Node]
1719
- def bnode(value = nil)
1720
- @@bnode_cache ||= {}
1721
- @@bnode_cache[value.to_s] ||= RDF::Node.new(value)
1722
- end
1824
+ def uri(value)
1825
+ case value.to_s
1826
+ when /^_:(.*)$/
1827
+ # Map BlankNodes if a namer is given
1828
+ # log_debug "uri(bnode)#{value}: #{$1}"
1829
+ bnode(namer.get_sym(::Regexp.last_match(1)))
1830
+ else
1831
+ RDF::URI(value)
1832
+ # value.validate! if options[:validate]
1833
+
1834
+ end
1835
+ end
1836
+
1837
+ # Keep track of allocated BNodes
1838
+ #
1839
+ # Don't actually use the name provided, to prevent name alias issues.
1840
+ # @return [RDF::Node]
1841
+ def bnode(value = nil)
1842
+ @@bnode_cache ||= {}
1843
+ @@bnode_cache[value.to_s] ||= RDF::Node.new(value)
1844
+ end
1845
+
1846
+ ##
1847
+ # Inverse Context creation
1848
+ #
1849
+ # 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.
1850
+ #
1851
+ # 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.
1852
+ #
1853
+ # 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.
1854
+ #
1855
+ # @example Basic structure of resulting inverse context
1856
+ # {
1857
+ # "http://example.com/term": {
1858
+ # "@language": {
1859
+ # "@null": "term",
1860
+ # "@none": "term",
1861
+ # "en": "term",
1862
+ # "ar_rtl": "term"
1863
+ # },
1864
+ # "@type": {
1865
+ # "@reverse": "term",
1866
+ # "@none": "term",
1867
+ # "http://datatype": "term"
1868
+ # },
1869
+ # "@any": {
1870
+ # "@none": "term",
1871
+ # }
1872
+ # }
1873
+ # }
1874
+ # @return [Hash{String => Hash{String => String}}]
1875
+ # @todo May want to include @set along with container to allow selecting terms using @set over those without @set. May require adding some notion of value cardinality to compact_iri
1876
+ def inverse_context
1877
+ Context.inverse_cache[hash] ||= begin
1878
+ result = {}
1879
+ default_language = (self.default_language || '@none').downcase
1880
+ term_definitions.keys.sort do |a, b|
1881
+ a.length == b.length ? (a <=> b) : (a.length <=> b.length)
1882
+ end.each do |term|
1883
+ next unless (td = term_definitions[term])
1884
+
1885
+ container = td.container_mapping.to_a.join
1886
+ if container.empty?
1887
+ container = td.as_set? ? %(@set) : %(@none)
1888
+ end
1723
1889
 
1724
- ##
1725
- # Inverse Context creation
1726
- #
1727
- # 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.
1728
- #
1729
- # 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.
1730
- #
1731
- # 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.
1732
- #
1733
- # @example Basic structure of resulting inverse context
1734
- # {
1735
- # "http://example.com/term": {
1736
- # "@language": {
1737
- # "@null": "term",
1738
- # "@none": "term",
1739
- # "en": "term",
1740
- # "ar_rtl": "term"
1741
- # },
1742
- # "@type": {
1743
- # "@reverse": "term",
1744
- # "@none": "term",
1745
- # "http://datatype": "term"
1746
- # },
1747
- # "@any": {
1748
- # "@none": "term",
1749
- # }
1750
- # }
1751
- # }
1752
- # @return [Hash{String => Hash{String => String}}]
1753
- # @todo May want to include @set along with container to allow selecting terms using @set over those without @set. May require adding some notion of value cardinality to compact_iri
1754
- def inverse_context
1755
- Context.inverse_cache[self.hash] ||= begin
1756
- result = {}
1757
- default_language = (self.default_language || '@none').downcase
1758
- term_definitions.keys.sort do |a, b|
1759
- a.length == b.length ? (a <=> b) : (a.length <=> b.length)
1760
- end.each do |term|
1761
- next unless td = term_definitions[term]
1762
-
1763
- container = td.container_mapping.to_a.join('')
1764
- if container.empty?
1765
- container = td.as_set? ? %(@set) : %(@none)
1766
- end
1767
-
1768
- container_map = result[td.id.to_s] ||= {}
1769
- tl_map = container_map[container] ||= {'@language' => {}, '@type' => {}, '@any' => {}}
1770
- type_map = tl_map['@type']
1771
- language_map = tl_map['@language']
1772
- any_map = tl_map['@any']
1773
- any_map['@none'] ||= term
1774
- if td.reverse_property
1775
- type_map['@reverse'] ||= term
1776
- elsif td.type_mapping == '@none'
1777
- type_map['@any'] ||= term
1778
- language_map['@any'] ||= term
1779
- any_map['@any'] ||= term
1780
- elsif td.type_mapping
1781
- type_map[td.type_mapping.to_s] ||= term
1782
- elsif !td.language_mapping.nil? && !td.direction_mapping.nil?
1783
- lang_dir = if td.language_mapping && td.direction_mapping
1784
- "#{td.language_mapping}_#{td.direction_mapping}".downcase
1785
- elsif td.language_mapping
1786
- td.language_mapping.downcase
1787
- elsif td.direction_mapping
1788
- "_#{td.direction_mapping}"
1890
+ container_map = result[td.id.to_s] ||= {}
1891
+ tl_map = container_map[container] ||= { '@language' => {}, '@type' => {}, '@any' => {} }
1892
+ type_map = tl_map['@type']
1893
+ language_map = tl_map['@language']
1894
+ any_map = tl_map['@any']
1895
+ any_map['@none'] ||= term
1896
+ if td.reverse_property
1897
+ type_map['@reverse'] ||= term
1898
+ elsif td.type_mapping == '@none'
1899
+ type_map['@any'] ||= term
1900
+ language_map['@any'] ||= term
1901
+ any_map['@any'] ||= term
1902
+ elsif td.type_mapping
1903
+ type_map[td.type_mapping.to_s] ||= term
1904
+ elsif !td.language_mapping.nil? && !td.direction_mapping.nil?
1905
+ lang_dir = if td.language_mapping && td.direction_mapping
1906
+ "#{td.language_mapping}_#{td.direction_mapping}".downcase
1907
+ elsif td.language_mapping
1908
+ td.language_mapping.downcase
1909
+ elsif td.direction_mapping
1910
+ "_#{td.direction_mapping}"
1911
+ else
1912
+ "@null"
1913
+ end
1914
+ language_map[lang_dir] ||= term
1915
+ elsif !td.language_mapping.nil?
1916
+ lang_dir = (td.language_mapping || '@null').downcase
1917
+ language_map[lang_dir] ||= term
1918
+ elsif !td.direction_mapping.nil?
1919
+ lang_dir = td.direction_mapping ? "_#{td.direction_mapping}" : '@none'
1920
+ language_map[lang_dir] ||= term
1921
+ elsif default_direction
1922
+ language_map["_#{default_direction}"] ||= term
1923
+ language_map['@none'] ||= term
1924
+ type_map['@none'] ||= term
1789
1925
  else
1790
- "@null"
1926
+ language_map[default_language] ||= term
1927
+ language_map['@none'] ||= term
1928
+ type_map['@none'] ||= term
1791
1929
  end
1792
- language_map[lang_dir] ||= term
1793
- elsif !td.language_mapping.nil?
1794
- lang_dir = (td.language_mapping || '@null').downcase
1795
- language_map[lang_dir] ||= term
1796
- elsif !td.direction_mapping.nil?
1797
- lang_dir = td.direction_mapping ? "_#{td.direction_mapping}" : '@none'
1798
- language_map[lang_dir] ||= term
1799
- elsif default_direction
1800
- language_map["_#{default_direction}"] ||= term
1801
- language_map['@none'] ||= term
1802
- type_map['@none'] ||= term
1803
- else
1804
- language_map[default_language] ||= term
1805
- language_map['@none'] ||= term
1806
- type_map['@none'] ||= term
1807
1930
  end
1931
+ result
1808
1932
  end
1809
- result
1810
1933
  end
1811
- end
1812
1934
 
1813
- ##
1814
- # 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.
1815
- #
1816
- # @param [String] iri
1817
- # @param [Array<String>] containers
1818
- # represents an ordered list of preferred container mappings
1819
- # @param [String] type_language
1820
- # indicates whether to look for a term with a matching type mapping or language mapping
1821
- # @param [Array<String>] preferred_values
1822
- # for the type mapping or language mapping
1823
- # @return [String]
1824
- def select_term(iri, containers, type_language, preferred_values)
1825
- # log_debug("select_term") {
1826
- # "iri: #{iri.inspect}, " +
1827
- # "containers: #{containers.inspect}, " +
1828
- # "type_language: #{type_language.inspect}, " +
1829
- # "preferred_values: #{preferred_values.inspect}"
1830
- #}
1831
- container_map = inverse_context[iri]
1832
- # log_debug(" ") {"container_map: #{container_map.inspect}"}
1833
- containers.each do |container|
1834
- next unless container_map.key?(container)
1835
- tl_map = container_map[container]
1836
- value_map = tl_map[type_language]
1837
- preferred_values.each do |item|
1838
- next unless value_map.key?(item)
1839
- # log_debug("=>") {value_map[item].inspect}
1840
- return value_map[item]
1935
+ ##
1936
+ # 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.
1937
+ #
1938
+ # @param [String] iri
1939
+ # @param [Array<String>] containers
1940
+ # represents an ordered list of preferred container mappings
1941
+ # @param [String] type_language
1942
+ # indicates whether to look for a term with a matching type mapping or language mapping
1943
+ # @param [Array<String>] preferred_values
1944
+ # for the type mapping or language mapping
1945
+ # @return [String]
1946
+ def select_term(iri, containers, type_language, preferred_values)
1947
+ # log_debug("select_term") {
1948
+ # "iri: #{iri.inspect}, " +
1949
+ # "containers: #{containers.inspect}, " +
1950
+ # "type_language: #{type_language.inspect}, " +
1951
+ # "preferred_values: #{preferred_values.inspect}"
1952
+ # }
1953
+ container_map = inverse_context[iri]
1954
+ # log_debug(" ") {"container_map: #{container_map.inspect}"}
1955
+ containers.each do |container|
1956
+ next unless container_map.key?(container)
1957
+
1958
+ tl_map = container_map[container]
1959
+ value_map = tl_map[type_language]
1960
+ preferred_values.each do |item|
1961
+ next unless value_map.key?(item)
1962
+
1963
+ # log_debug("=>") {value_map[item].inspect}
1964
+ return value_map[item]
1965
+ end
1841
1966
  end
1967
+ # log_debug("=>") {"nil"}
1968
+ nil
1842
1969
  end
1843
- # log_debug("=>") {"nil"}
1844
- nil
1845
- end
1846
1970
 
1847
- ##
1848
- # Removes a base IRI from the given absolute IRI.
1849
- #
1850
- # @param [String] base the base used for making `iri` relative
1851
- # @param [String] iri the absolute IRI
1852
- # @return [String]
1853
- # the relative IRI if relative to base, otherwise the absolute IRI.
1854
- def remove_base(base, iri)
1855
- return iri unless base
1856
- @base_and_parents ||= begin
1857
- u = base
1858
- iri_set = u.to_s.end_with?('/') ? [u.to_s] : []
1859
- iri_set << u.to_s while (u != './' && u = u.parent)
1860
- iri_set
1861
- end
1862
- b = base.to_s
1863
- return iri[b.length..-1] if iri.start_with?(b) && CONTEXT_BASE_FRAG_OR_QUERY.include?(iri[b.length, 1])
1971
+ ##
1972
+ # Removes a base IRI from the given absolute IRI.
1973
+ #
1974
+ # @param [String] base the base used for making `iri` relative
1975
+ # @param [String] iri the absolute IRI
1976
+ # @return [String]
1977
+ # the relative IRI if relative to base, otherwise the absolute IRI.
1978
+ def remove_base(base, iri)
1979
+ return iri unless base
1980
+
1981
+ @base_and_parents ||= begin
1982
+ u = base
1983
+ iri_set = u.to_s.end_with?('/') ? [u.to_s] : []
1984
+ iri_set << u.to_s while u != './' && (u = u.parent)
1985
+ iri_set
1986
+ end
1987
+ b = base.to_s
1988
+ return iri[b.length..] if iri.start_with?(b) && CONTEXT_BASE_FRAG_OR_QUERY.include?(iri[b.length, 1])
1864
1989
 
1865
- @base_and_parents.each_with_index do |bb, index|
1866
- next unless iri.start_with?(bb)
1867
- rel = "../" * index + iri[bb.length..-1]
1868
- return rel.empty? ? "./" : rel
1990
+ @base_and_parents.each_with_index do |bb, index|
1991
+ next unless iri.start_with?(bb)
1992
+
1993
+ rel = ("../" * index) + iri[bb.length..]
1994
+ return rel.empty? ? "./" : rel
1995
+ end
1996
+ iri
1869
1997
  end
1870
- iri
1871
- end
1872
1998
 
1873
- ## Used for testing
1874
- # Retrieve term mappings
1875
- #
1876
- # @return [Array<RDF::URI>]
1877
- def mappings
1878
- {}.tap do |memo|
1879
- term_definitions.each_pair do |t,td|
1880
- memo[t] = td ? td.id : nil
1999
+ ## Used for testing
2000
+ # Retrieve term mappings
2001
+ #
2002
+ # @return [Array<RDF::URI>]
2003
+ def mappings
2004
+ {}.tap do |memo|
2005
+ term_definitions.each_pair do |t, td|
2006
+ memo[t] = td ? td.id : nil
2007
+ end
1881
2008
  end
1882
2009
  end
1883
- end
1884
2010
 
1885
- ## Used for testing
1886
- # Retrieve term mapping
1887
- #
1888
- # @param [String, #to_s] term
1889
- #
1890
- # @return [RDF::URI, String]
1891
- def mapping(term)
1892
- term_definitions[term] ? term_definitions[term].id : nil
1893
- end
2011
+ ## Used for testing
2012
+ # Retrieve term mapping
2013
+ #
2014
+ # @param [String, #to_s] term
2015
+ #
2016
+ # @return [RDF::URI, String]
2017
+ def mapping(term)
2018
+ term_definitions[term]&.id
2019
+ end
1894
2020
 
1895
- ## Used for testing
1896
- # Retrieve language mappings
1897
- #
1898
- # @return [Array<String>]
1899
- # @deprecated
1900
- def languages
1901
- {}.tap do |memo|
1902
- term_definitions.each_pair do |t,td|
1903
- memo[t] = td.language_mapping
2021
+ ## Used for testing
2022
+ # Retrieve language mappings
2023
+ #
2024
+ # @return [Array<String>]
2025
+ # @deprecated
2026
+ def languages
2027
+ {}.tap do |memo|
2028
+ term_definitions.each_pair do |t, td|
2029
+ memo[t] = td.language_mapping
2030
+ end
1904
2031
  end
1905
2032
  end
1906
- end
1907
2033
 
1908
- # Ensure @container mapping is appropriate
1909
- # The result is the original container definition. For IRI containers, this is necessary to be able to determine the @type mapping for string values
1910
- def check_container(container, local_context, defined, term)
1911
- if container.is_a?(Array) && processingMode('json-ld-1.0')
1912
- raise JsonLdError::InvalidContainerMapping,
1913
- "'@container' on term #{term.inspect} must be a string: #{container.inspect}"
1914
- end
2034
+ # Ensure @container mapping is appropriate
2035
+ # The result is the original container definition. For IRI containers, this is necessary to be able to determine the @type mapping for string values
2036
+ def check_container(container, _local_context, _defined, term)
2037
+ if container.is_a?(Array) && processingMode('json-ld-1.0')
2038
+ raise JsonLdError::InvalidContainerMapping,
2039
+ "'@container' on term #{term.inspect} must be a string: #{container.inspect}"
2040
+ end
1915
2041
 
1916
- val = Set.new(Array(container))
1917
- val.delete('@set') if has_set = val.include?('@set')
1918
-
1919
- if val.include?('@list')
1920
- raise JsonLdError::InvalidContainerMapping,
1921
- "'@container' on term #{term.inspect} using @list cannot have any other values" unless
1922
- !has_set && val.length == 1
1923
- # Okay
1924
- elsif val.include?('@language')
1925
- raise JsonLdError::InvalidContainerMapping,
1926
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1927
- has_set && processingMode('json-ld-1.0')
1928
- raise JsonLdError::InvalidContainerMapping,
1929
- "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1930
- val.length == 1
1931
- # Okay
1932
- elsif val.include?('@index')
1933
- raise JsonLdError::InvalidContainerMapping,
1934
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1935
- has_set && processingMode('json-ld-1.0')
1936
- raise JsonLdError::InvalidContainerMapping,
1937
- "'@container' on term #{term.inspect} using @index cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1938
- (val - CONTEXT_CONTAINER_INDEX_GRAPH).empty?
1939
- # Okay
1940
- elsif val.include?('@id')
1941
- raise JsonLdError::InvalidContainerMapping,
1942
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1943
- processingMode('json-ld-1.0')
1944
- raise JsonLdError::InvalidContainerMapping,
1945
- "'@container' on term #{term.inspect} using @id cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1946
- val.subset?(CONTEXT_CONTAINER_ID_GRAPH)
1947
- # Okay
1948
- elsif val.include?('@type') || val.include?('@graph')
1949
- raise JsonLdError::InvalidContainerMapping,
1950
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1951
- processingMode('json-ld-1.0')
1952
- raise JsonLdError::InvalidContainerMapping,
1953
- "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1954
- val.length == 1
1955
- # Okay
1956
- elsif val.empty?
1957
- # Okay
1958
- else
1959
- raise JsonLdError::InvalidContainerMapping,
2042
+ val = Set.new(Array(container))
2043
+ val.delete('@set') if (has_set = val.include?('@set'))
2044
+
2045
+ if val.include?('@list')
2046
+ unless !has_set && val.length == 1
2047
+ raise JsonLdError::InvalidContainerMapping,
2048
+ "'@container' on term #{term.inspect} using @list cannot have any other values"
2049
+ end
2050
+ # Okay
2051
+ elsif val.include?('@language')
2052
+ if has_set && processingMode('json-ld-1.0')
2053
+ raise JsonLdError::InvalidContainerMapping,
2054
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
2055
+ end
2056
+ unless val.length == 1
2057
+ raise JsonLdError::InvalidContainerMapping,
2058
+ "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}"
2059
+ end
2060
+ # Okay
2061
+ elsif val.include?('@index')
2062
+ if has_set && processingMode('json-ld-1.0')
2063
+ raise JsonLdError::InvalidContainerMapping,
2064
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
2065
+ end
2066
+ unless (val - CONTEXT_CONTAINER_INDEX_GRAPH).empty?
2067
+ raise JsonLdError::InvalidContainerMapping,
2068
+ "'@container' on term #{term.inspect} using @index cannot have any values other than @set and/or @graph, found #{container.inspect}"
2069
+ end
2070
+ # Okay
2071
+ elsif val.include?('@id')
2072
+ if processingMode('json-ld-1.0')
2073
+ raise JsonLdError::InvalidContainerMapping,
1960
2074
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
2075
+ end
2076
+ unless val.subset?(CONTEXT_CONTAINER_ID_GRAPH)
2077
+ raise JsonLdError::InvalidContainerMapping,
2078
+ "'@container' on term #{term.inspect} using @id cannot have any values other than @set and/or @graph, found #{container.inspect}"
2079
+ end
2080
+ # Okay
2081
+ elsif val.include?('@type') || val.include?('@graph')
2082
+ if processingMode('json-ld-1.0')
2083
+ raise JsonLdError::InvalidContainerMapping,
2084
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
2085
+ end
2086
+ unless val.length == 1
2087
+ raise JsonLdError::InvalidContainerMapping,
2088
+ "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}"
2089
+ end
2090
+ # Okay
2091
+ elsif val.empty?
2092
+ # Okay
2093
+ else
2094
+ raise JsonLdError::InvalidContainerMapping,
2095
+ "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
2096
+ end
2097
+ Array(container)
1961
2098
  end
1962
- Array(container)
1963
- end
1964
2099
 
1965
- # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
1966
- class TermDefinition
1967
- # @return [RDF::URI] IRI map
1968
- attr_accessor :id
2100
+ # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
2101
+ class TermDefinition
2102
+ # @return [RDF::URI] IRI map
2103
+ attr_accessor :id
1969
2104
 
1970
- # @return [String] term name
1971
- attr_accessor :term
2105
+ # @return [String] term name
2106
+ attr_accessor :term
1972
2107
 
1973
- # @return [String] Type mapping
1974
- attr_accessor :type_mapping
2108
+ # @return [String] Type mapping
2109
+ attr_accessor :type_mapping
1975
2110
 
1976
- # Base container mapping, without @set
1977
- # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
1978
- attr_reader :container_mapping
2111
+ # Base container mapping, without @set
2112
+ # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
2113
+ attr_reader :container_mapping
1979
2114
 
1980
- # @return [String] Term used for nest properties
1981
- attr_accessor :nest
2115
+ # @return [String] Term used for nest properties
2116
+ attr_accessor :nest
1982
2117
 
1983
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
1984
- # @return [String] Language mapping
1985
- attr_accessor :language_mapping
2118
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
2119
+ # @return [String] Language mapping
2120
+ attr_accessor :language_mapping
1986
2121
 
1987
- # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
1988
- # @return ["ltr", "rtl"] direction_mapping
1989
- attr_accessor :direction_mapping
2122
+ # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
2123
+ # @return ["ltr", "rtl"] direction_mapping
2124
+ attr_accessor :direction_mapping
1990
2125
 
1991
- # @return [Boolean] Reverse Property
1992
- attr_accessor :reverse_property
2126
+ # @return [Boolean] Reverse Property
2127
+ attr_accessor :reverse_property
1993
2128
 
1994
- # This is a simple term definition, not an expanded term definition
1995
- # @return [Boolean]
1996
- attr_accessor :simple
2129
+ # This is a simple term definition, not an expanded term definition
2130
+ # @return [Boolean]
2131
+ attr_accessor :simple
1997
2132
 
1998
- # Property used for data indexing; defaults to @index
1999
- # @return [Boolean]
2000
- attr_accessor :index
2133
+ # Property used for data indexing; defaults to @index
2134
+ # @return [Boolean]
2135
+ attr_accessor :index
2001
2136
 
2002
- # Indicate that term may be used as a prefix
2003
- attr_writer :prefix
2137
+ # Indicate that term may be used as a prefix
2138
+ attr_writer :prefix
2004
2139
 
2005
- # Term-specific context
2006
- # @return [Hash{String => Object}]
2007
- attr_accessor :context
2140
+ # Term-specific context
2141
+ # @return [Hash{String => Object}]
2142
+ attr_accessor :context
2008
2143
 
2009
- # Term is protected.
2010
- # @return [Boolean]
2011
- attr_writer :protected
2144
+ # Term is protected.
2145
+ # @return [Boolean]
2146
+ attr_writer :protected
2012
2147
 
2013
- # This is a simple term definition, not an expanded term definition
2014
- # @return [Boolean] simple
2015
- def simple?; simple; end
2148
+ # This is a simple term definition, not an expanded term definition
2149
+ # @return [Boolean] simple
2150
+ def simple?
2151
+ simple
2152
+ end
2016
2153
 
2017
- # This is an appropriate term to use as the prefix of a compact IRI
2018
- # @return [Boolean] simple
2019
- def prefix?; @prefix; end
2154
+ # This is an appropriate term to use as the prefix of a compact IRI
2155
+ # @return [Boolean] simple
2156
+ def prefix?
2157
+ @prefix
2158
+ end
2020
2159
 
2021
- # Create a new Term Mapping with an ID
2022
- # @param [String] term
2023
- # @param [String] id
2024
- # @param [String] type_mapping Type mapping
2025
- # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
2026
- # @param [String] language_mapping
2027
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term
2028
- # @param ["ltr", "rtl"] direction_mapping
2029
- # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
2030
- # @param [Boolean] reverse_property
2031
- # @param [Boolean] protected mark resulting context as protected
2032
- # @param [String] nest term used for nest properties
2033
- # @param [Boolean] simple
2034
- # This is a simple term definition, not an expanded term definition
2035
- # @param [Boolean] prefix
2036
- # Term may be used as a prefix
2037
- def initialize(term,
2038
- id: nil,
2039
- index: nil,
2040
- type_mapping: nil,
2041
- container_mapping: nil,
2042
- language_mapping: nil,
2043
- direction_mapping: nil,
2044
- reverse_property: false,
2045
- nest: nil,
2046
- protected: nil,
2047
- simple: false,
2048
- prefix: nil,
2049
- context: nil)
2050
- @term = term
2051
- @id = id.to_s unless id.nil?
2052
- @index = index.to_s unless index.nil?
2053
- @type_mapping = type_mapping.to_s unless type_mapping.nil?
2054
- self.container_mapping = container_mapping
2055
- @language_mapping = language_mapping unless language_mapping.nil?
2056
- @direction_mapping = direction_mapping unless direction_mapping.nil?
2057
- @reverse_property = reverse_property
2058
- @protected = protected
2059
- @nest = nest unless nest.nil?
2060
- @simple = simple
2061
- @prefix = prefix unless prefix.nil?
2062
- @context = context unless context.nil?
2063
- end
2160
+ # Create a new Term Mapping with an ID
2161
+ # @param [String] term
2162
+ # @param [String] id
2163
+ # @param [String] type_mapping Type mapping
2164
+ # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
2165
+ # @param [String] language_mapping
2166
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term
2167
+ # @param ["ltr", "rtl"] direction_mapping
2168
+ # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
2169
+ # @param [Boolean] reverse_property
2170
+ # @param [Boolean] protected mark resulting context as protected
2171
+ # @param [String] nest term used for nest properties
2172
+ # @param [Boolean] simple
2173
+ # This is a simple term definition, not an expanded term definition
2174
+ # @param [Boolean] prefix
2175
+ # Term may be used as a prefix
2176
+ def initialize(term,
2177
+ id: nil,
2178
+ index: nil,
2179
+ type_mapping: nil,
2180
+ container_mapping: nil,
2181
+ language_mapping: nil,
2182
+ direction_mapping: nil,
2183
+ reverse_property: false,
2184
+ nest: nil,
2185
+ protected: nil,
2186
+ simple: false,
2187
+ prefix: nil,
2188
+ context: nil)
2189
+ @term = term
2190
+ @id = id.to_s unless id.nil?
2191
+ @index = index.to_s unless index.nil?
2192
+ @type_mapping = type_mapping.to_s unless type_mapping.nil?
2193
+ self.container_mapping = container_mapping
2194
+ @language_mapping = language_mapping unless language_mapping.nil?
2195
+ @direction_mapping = direction_mapping unless direction_mapping.nil?
2196
+ @reverse_property = reverse_property
2197
+ @protected = protected
2198
+ @nest = nest unless nest.nil?
2199
+ @simple = simple
2200
+ @prefix = prefix unless prefix.nil?
2201
+ @context = context unless context.nil?
2202
+ end
2064
2203
 
2065
- # Term is protected.
2066
- # @return [Boolean]
2067
- def protected?; !!@protected; end
2068
-
2069
- # Set container mapping, from an array which may include @set
2070
- def container_mapping=(mapping)
2071
- mapping = case mapping
2072
- when Set then mapping
2073
- when Array then Set.new(mapping)
2074
- when String then Set[mapping]
2075
- when nil then Set.new
2076
- else
2077
- raise "Shouldn't happen with #{mapping.inspect}"
2204
+ # Term is protected.
2205
+ # @return [Boolean]
2206
+ def protected?
2207
+ !!@protected
2078
2208
  end
2079
- if @as_set = mapping.include?('@set')
2080
- mapping = mapping.dup
2081
- mapping.delete('@set')
2209
+
2210
+ # Returns true if the term matches a IRI
2211
+ #
2212
+ # @param iri [String] the IRI
2213
+ # @return [Boolean]
2214
+ def match_iri?(iri)
2215
+ iri.start_with?(id)
2082
2216
  end
2083
- @container_mapping = mapping
2084
- @index ||= '@index' if mapping.include?('@index')
2085
- end
2086
2217
 
2087
- ##
2088
- # Output Hash or String definition for this definition considering @language and @vocab
2089
- #
2090
- # @param [Context] context
2091
- # @return [String, Hash{String => Array[String], String}]
2092
- def to_context_definition(context)
2093
- cid = if context.vocab && id.start_with?(context.vocab)
2094
- # Nothing to return unless it's the same as the vocab
2095
- id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
2096
- else
2097
- # Find a term to act as a prefix
2098
- iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
2099
- iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
2218
+ # Returns true if the term matches a compact IRI
2219
+ #
2220
+ # @param iri [String] the compact IRI
2221
+ # @return [Boolean]
2222
+ def match_compact_iri?(iri)
2223
+ iri.start_with?(prefix_colon)
2100
2224
  end
2101
2225
 
2102
- if simple?
2103
- cid.to_s unless cid == term && context.vocab
2104
- else
2105
- defn = {}
2106
- defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
2107
- if type_mapping
2108
- defn['@type'] = if KEYWORDS.include?(type_mapping)
2109
- type_mapping
2110
- else
2111
- context.compact_iri(type_mapping, vocab: true)
2226
+ # Set container mapping, from an array which may include @set
2227
+ def container_mapping=(mapping)
2228
+ mapping = case mapping
2229
+ when Set then mapping
2230
+ when Array then Set.new(mapping)
2231
+ when String then Set[mapping]
2232
+ when nil then Set.new
2233
+ else
2234
+ raise "Shouldn't happen with #{mapping.inspect}"
2235
+ end
2236
+ if (@as_set = mapping.include?('@set'))
2237
+ mapping = mapping.dup
2238
+ mapping.delete('@set')
2239
+ end
2240
+ @container_mapping = mapping
2241
+ @index ||= '@index' if mapping.include?('@index')
2242
+ end
2243
+
2244
+ ##
2245
+ # Output Hash or String definition for this definition considering @language and @vocab
2246
+ #
2247
+ # @param [Context] context
2248
+ # @return [String, Hash{String => Array[String], String}]
2249
+ def to_context_definition(context)
2250
+ cid = if context.vocab && id.start_with?(context.vocab)
2251
+ # Nothing to return unless it's the same as the vocab
2252
+ id == context.vocab ? context.vocab : id.to_s[context.vocab.length..]
2253
+ else
2254
+ # Find a term to act as a prefix
2255
+ iri, prefix = context.iri_to_term.detect { |i, _p| id.to_s.start_with?(i.to_s) }
2256
+ iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..]}" : id
2257
+ end
2258
+
2259
+ if simple?
2260
+ cid.to_s unless cid == term && context.vocab
2261
+ else
2262
+ defn = {}
2263
+ defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
2264
+ if type_mapping
2265
+ defn['@type'] = if KEYWORDS.include?(type_mapping)
2266
+ type_mapping
2267
+ else
2268
+ context.compact_iri(type_mapping, vocab: true)
2269
+ end
2270
+ end
2271
+
2272
+ cm = Array(container_mapping)
2273
+ cm << "@set" if as_set? && !cm.include?("@set")
2274
+ cm = cm.first if cm.length == 1
2275
+ defn['@container'] = cm unless cm.empty?
2276
+ # Language set as false to be output as null
2277
+ defn['@language'] = (@language_mapping || nil) unless @language_mapping.nil?
2278
+ defn['@direction'] = (@direction_mapping || nil) unless @direction_mapping.nil?
2279
+ defn['@context'] = @context if @context
2280
+ defn['@nest'] = @nest if @nest
2281
+ defn['@index'] = @index if @index
2282
+ defn['@prefix'] = @prefix unless @prefix.nil?
2283
+ defn
2284
+ end
2285
+ end
2286
+
2287
+ ##
2288
+ # Turn this into a source for a new instantiation
2289
+ # FIXME: context serialization
2290
+ # @return [String]
2291
+ def to_rb
2292
+ defn = [%(TermDefinition.new\(#{term.inspect})]
2293
+ %w[id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple
2294
+ prefix context protected].each do |acc|
2295
+ v = instance_variable_get("@#{acc}".to_sym)
2296
+ v = v.to_s if v.is_a?(RDF::Term)
2297
+ if acc == 'container_mapping'
2298
+ v = v.to_a
2299
+ v << '@set' if as_set?
2300
+ v = v.first if v.length <= 1
2112
2301
  end
2302
+ defn << "#{acc}: #{v.inspect}" if v
2113
2303
  end
2304
+ defn.join(', ') + ")"
2305
+ end
2114
2306
 
2115
- cm = Array(container_mapping)
2116
- cm << "@set" if as_set? && !cm.include?("@set")
2117
- cm = cm.first if cm.length == 1
2118
- defn['@container'] = cm unless cm.empty?
2119
- # Language set as false to be output as null
2120
- defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
2121
- defn['@direction'] = (@direction_mapping ? @direction_mapping : nil) unless @direction_mapping.nil?
2122
- defn['@context'] = @context if @context
2123
- defn['@nest'] = @nest if @nest
2124
- defn['@index'] = @index if @index
2125
- defn['@prefix'] = @prefix unless @prefix.nil?
2126
- defn
2307
+ # If container mapping was defined along with @set
2308
+ # @return [Boolean]
2309
+ def as_set?
2310
+ @as_set || false
2127
2311
  end
2128
- end
2129
2312
 
2130
- ##
2131
- # Turn this into a source for a new instantiation
2132
- # FIXME: context serialization
2133
- # @return [String]
2134
- def to_rb
2135
- defn = [%(TermDefinition.new\(#{term.inspect})]
2136
- %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
2137
- v = instance_variable_get("@#{acc}".to_sym)
2138
- v = v.to_s if v.is_a?(RDF::Term)
2139
- if acc == 'container_mapping'
2140
- v = v.to_a
2141
- v << '@set' if as_set?
2142
- v = v.first if v.length <= 1
2143
- end
2144
- defn << "#{acc}: #{v.inspect}" if v
2145
- end
2146
- defn.join(', ') + ")"
2147
- end
2313
+ # Check if term definitions are identical, modulo @protected
2314
+ # @return [Boolean]
2315
+ def ==(other)
2316
+ other.is_a?(TermDefinition) &&
2317
+ id == other.id &&
2318
+ term == other.term &&
2319
+ type_mapping == other.type_mapping &&
2320
+ container_mapping == other.container_mapping &&
2321
+ nest == other.nest &&
2322
+ language_mapping == other.language_mapping &&
2323
+ direction_mapping == other.direction_mapping &&
2324
+ reverse_property == other.reverse_property &&
2325
+ simple == other.simple &&
2326
+ index == other.index &&
2327
+ context == other.context &&
2328
+ prefix? == other.prefix? &&
2329
+ as_set? == other.as_set?
2330
+ end
2148
2331
 
2149
- # If container mapping was defined along with @set
2150
- # @return [Boolean]
2151
- def as_set?; @as_set || false; end
2332
+ def inspect
2333
+ v = %w([TD)
2334
+ v << "id=#{@id}"
2335
+ v << "index=#{index.inspect}" unless index.nil?
2336
+ v << "term=#{@term}"
2337
+ v << "rev" if reverse_property
2338
+ v << "container=#{container_mapping}" if container_mapping
2339
+ v << "as_set=#{as_set?.inspect}"
2340
+ v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
2341
+ v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
2342
+ v << "type=#{type_mapping}" unless type_mapping.nil?
2343
+ v << "nest=#{nest.inspect}" unless nest.nil?
2344
+ v << "simple=true" if @simple
2345
+ v << "protected=true" if @protected
2346
+ v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
2347
+ v << "has-context" unless context.nil?
2348
+ v.join(" ") + "]"
2349
+ end
2152
2350
 
2153
- # Check if term definitions are identical, modulo @protected
2154
- # @return [Boolean]
2155
- def ==(other)
2156
- other.is_a?(TermDefinition) &&
2157
- id == other.id &&
2158
- term == other.term &&
2159
- type_mapping == other.type_mapping &&
2160
- container_mapping == other.container_mapping &&
2161
- nest == other.nest &&
2162
- language_mapping == other.language_mapping &&
2163
- direction_mapping == other.direction_mapping &&
2164
- reverse_property == other.reverse_property &&
2165
- simple == other.simple &&
2166
- index == other.index &&
2167
- context == other.context &&
2168
- prefix? == other.prefix? &&
2169
- as_set? == other.as_set?
2170
- end
2351
+ private
2171
2352
 
2172
- def inspect
2173
- v = %w([TD)
2174
- v << "id=#{@id}"
2175
- v << "index=#{index.inspect}" unless index.nil?
2176
- v << "term=#{@term}"
2177
- v << "rev" if reverse_property
2178
- v << "container=#{container_mapping}" if container_mapping
2179
- v << "as_set=#{as_set?.inspect}"
2180
- v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
2181
- v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
2182
- v << "type=#{type_mapping}" unless type_mapping.nil?
2183
- v << "nest=#{nest.inspect}" unless nest.nil?
2184
- v << "simple=true" if @simple
2185
- v << "protected=true" if @protected
2186
- v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
2187
- v << "has-context" unless context.nil?
2188
- v.join(" ") + "]"
2353
+ def prefix_colon
2354
+ @prefix_colon ||= "#{term}:".freeze
2355
+ end
2189
2356
  end
2190
2357
  end
2191
2358
  end