json-ld 3.2.3 → 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 -764
  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 -1945
  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 +224 -166
  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 -167
  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 +78 -209
  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 -2036
  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 -2498
  33. data/spec/from_rdf_spec.rb +0 -1005
  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 -1551
  99. data/spec/writer_spec.rb +0 -427
@@ -1,2223 +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 = JSON.load(context)
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]
308
- else
309
- # Load context document, if it is a string
310
- Context.cache[context_canon.to_s] ||= begin
311
- context_opts = @options.merge(
312
- profile: 'http://www.w3.org/ns/json-ld#context',
313
- requestProfile: 'http://www.w3.org/ns/json-ld#context',
314
- base: nil)
315
- #context_opts.delete(:headers)
316
- JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
317
- # 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.
318
- raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
319
-
320
- # Parse stand-alone
321
- ctx = Context.new(unfrozen: true, **options).dup
322
- ctx.context_base = context.to_s
323
- ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
324
- ctx.context_base = context.to_s # In case it was altered
325
- ctx.instance_variable_set(:@base, nil)
326
- ctx
327
- end
328
- rescue JsonLdError::LoadingDocumentFailed => e
329
- log_info("parse") {"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"}
330
- raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
331
- rescue JsonLdError
332
- raise
333
- rescue StandardError => e
334
- log_info("parse") {"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"}
335
- raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
336
- end
337
- end
303
+ remote_contexts << context.to_s
304
+ raise JsonLdError::ContextOverflow, context.to_s if remote_contexts.length >= MAX_CONTEXTS_LOADED
338
305
 
339
- # Merge loaded context noting protected term overriding
340
- 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}"}
341
309
 
342
- context.previous_context = self unless propagate
343
- result = context
344
- when Hash
345
- context = context.dup # keep from modifying a hash passed as a param
346
-
347
- # This counts on hash elements being processed in order
348
- {
349
- '@version' => :processingMode=,
350
- '@import' => nil,
351
- '@base' => :base=,
352
- '@direction' => :default_direction=,
353
- '@language' => :default_language=,
354
- '@propagate' => :propagate=,
355
- '@vocab' => :vocab=,
356
- }.each do |key, setter|
357
- next unless context.key?(key)
358
- if key == '@import'
359
- # Retrieve remote context and merge the remaining context object into the result.
360
- raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
361
- raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
362
- import_loc = RDF::URI(result.context_base || base).join(context['@import'])
363
- begin
364
- context_opts = @options.merge(
365
- profile: 'http://www.w3.org/ns/json-ld#context',
366
- requestProfile: 'http://www.w3.org/ns/json-ld#context',
367
- base: nil)
368
- context_opts.delete(:headers)
369
- # FIXME: should cache this, but ContextCache is for parsed contexts
370
- JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc|
371
- # 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.
372
- raise JsonLdError::InvalidRemoteContext, "#{import_loc}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
373
- import_context = remote_doc.document['@context']
374
- import_context.delete('@base')
375
- raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
376
- raise JsonLdError::InvalidContextEntry, "#{import_context.to_json} must not include @import entry" if import_context.key?('@import')
377
- context.delete(key)
378
- 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
379
353
  end
380
- rescue JsonLdError::LoadingDocumentFailed => e
381
- raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
382
- rescue JsonLdError
383
- raise
384
- rescue StandardError => e
385
- raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
386
354
  end
387
- else
388
- result.send(setter, context[key], remote_contexts: remote_contexts)
389
- end
390
- context.delete(key)
391
- end
392
355
 
393
- 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 = {}
394
431
 
395
- # 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
396
- context.each_key do |key|
397
- # ... where key is not @base, @vocab, @language, or @version
398
- result.create_term_definition(context, key, defined,
399
- base: base,
400
- override_protected: override_protected,
401
- protected: context['@protected'],
402
- remote_contexts: remote_contexts.dup,
403
- validate_scoped: validate_scoped
404
- ) 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
405
448
  end
406
- else
407
- # 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
408
- raise JsonLdError::InvalidLocalContext, "must be a URL, JSON object or array of same: #{context.inspect}"
409
449
  end
450
+ result
410
451
  end
411
- end
412
- result
413
- end
414
452
 
415
- ##
416
- # Merge in a context, creating a new context with updates from `context`
417
- #
418
- # @param [Context] context
419
- # @param [Boolean] override_protected Allow or disallow protected terms to be changed
420
- # @return [Context]
421
- def merge(context, override_protected: false)
422
- ctx = Context.new(term_definitions: self.term_definitions, standard_prefixes: options[:standard_prefixes])
423
- ctx.context_base = context.context_base || self.context_base
424
- ctx.default_language = context.default_language || self.default_language
425
- ctx.default_direction = context.default_direction || self.default_direction
426
- ctx.vocab = context.vocab || self.vocab
427
- ctx.base = self.base unless self.base.nil?
428
- if !override_protected
429
- ctx.term_definitions.each do |term, definition|
430
- next unless definition.protected? && (other = context.term_definitions[term])
431
- unless definition == other
432
- 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
433
472
  end
434
473
  end
435
- end
436
474
 
437
- # Add term definitions
438
- context.term_definitions.each do |term, definition|
439
- 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
440
480
  end
441
- ctx
442
- end
443
481
 
444
- # The following constants are used to reduce object allocations in #create_term_definition below
445
- ID_NULL_OBJECT = { '@id' => nil }.freeze
446
- NON_TERMDEF_KEYS = Set.new(%w(@base @direction @language @protected @version @vocab)).freeze
447
- JSON_LD_10_EXPECTED_KEYS = Set.new(%w(@container @id @language @reverse @type)).freeze
448
- JSON_LD_11_EXPECTED_KEYS = Set.new(%w(@context @direction @index @nest @prefix @protected)).freeze
449
- JSON_LD_EXPECTED_KEYS = (JSON_LD_10_EXPECTED_KEYS + JSON_LD_11_EXPECTED_KEYS).freeze
450
- JSON_LD_10_TYPE_VALUES = Set.new(%w(@id @vocab)).freeze
451
- JSON_LD_11_TYPE_VALUES = Set.new(%w(@json @none)).freeze
452
- PREFIX_URI_ENDINGS = Set.new(%w(: / ? # [ ] @)).freeze
453
-
454
- ##
455
- # Create Term Definition
456
- #
457
- # 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.
458
- #
459
- # 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.
460
- #
461
- # @param [Hash] local_context
462
- # @param [String] term
463
- # @param [Hash] defined
464
- # @param [String, RDF::URI] base for resolving document-relative IRIs
465
- # @param [Boolean] protected if true, causes all terms to be marked protected
466
- # @param [Boolean] override_protected Protected terms may be cleared.
467
- # @param [Array<String>] remote_contexts
468
- # @param [Boolean] validate_scoped (true).
469
- # Validate scoped context, loading if necessary.
470
- # If false, do not load scoped contexts.
471
- # @raise [JsonLdError]
472
- # Represents a cyclical term dependency
473
- # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
474
- def create_term_definition(local_context, term, defined,
475
- base: nil,
476
- override_protected: false,
477
- protected: nil,
478
- remote_contexts: [],
479
- validate_scoped: true)
480
- # Expand a string value, unless it matches a keyword
481
- log_debug("create_term_definition") {"term = #{term.inspect}"}
482
-
483
- # 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.
484
- case defined[term]
485
- when TrueClass then return
486
- when nil
487
- defined[term] = false
488
- else
489
- raise JsonLdError::CyclicIRIMapping, "Cyclical term dependency found: #{term.inspect}"
490
- 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
491
491
 
492
- # Initialize value to a the value associated with the key term in local context.
493
- value = local_context.fetch(term, false)
494
- simple_term = value.is_a?(String) || value.nil?
495
-
496
- # Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
497
- if term == '@type' &&
498
- value.is_a?(Hash) &&
499
- !value.empty? &&
500
- processingMode("json-ld-1.1") &&
501
- (value.keys - %w(@container @protected)).empty? &&
502
- value.fetch('@container', '@set') == '@set'
503
- # thes are the only cases were redefining a keyword is allowed
504
- elsif KEYWORDS.include?(term) # TODO anything that looks like a keyword
505
- raise JsonLdError::KeywordRedefinition, "term must not be a keyword: #{term.inspect}" if
506
- @options[:validate]
507
- elsif term.to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
508
- warn "Terms beginning with '@' are reserved for future use and ignored: #{term}."
509
- return
510
- elsif !term_valid?(term) && @options[:validate]
511
- raise JsonLdError::InvalidTermDefinition, "term is invalid: #{term.inspect}"
512
- 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
513
529
 
514
- 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
515
551
 
516
- # Remove any existing term definition for term in active context.
517
- previous_definition = term_definitions[term]
518
- if previous_definition && previous_definition.protected? && !override_protected
519
- # Check later to detect identical redefinition
520
- else
521
- term_definitions.delete(term) if previous_definition
522
- end
552
+ value = { '@id' => value } if simple_term
523
553
 
524
- 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
525
561
 
526
- #log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
527
- definition = TermDefinition.new(term)
528
- 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
529
566
 
530
- expected_keys = case processingMode
531
- when "json-ld-1.0" then JSON_LD_10_EXPECTED_KEYS
532
- else JSON_LD_EXPECTED_KEYS
533
- end
567
+ # log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
568
+ definition = TermDefinition.new(term)
569
+ definition.simple = simple_term
534
570
 
535
- # Any of these keys cause us to process as json-ld-1.1, unless otherwise set
536
- if processingMode.nil? && value.any? { |key, _| !JSON_LD_11_EXPECTED_KEYS.include?(key) }
537
- processingMode('json-ld-11')
538
- 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
539
575
 
540
- if value.any? { |key, _| !expected_keys.include?(key) }
541
- extra_keys = value.keys - expected_keys.to_a
542
- raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} has unexpected keys: #{extra_keys.join(', ')}"
543
- 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
544
580
 
545
- # Potentially note that the term is protected
546
- 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
547
586
 
548
- if value.key?('@type')
549
- type = value['@type']
550
- # SPEC FIXME: @type may be nil
551
- type = case type
552
- when nil
553
- type
554
- when String
555
- begin
556
- expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
557
- rescue JsonLdError::InvalidIRIMapping
558
- 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
559
605
  end
560
- else
561
- :error
562
- end
563
- if JSON_LD_11_TYPE_VALUES.include?(type) && processingMode('json-ld-1.1')
564
- # This is okay and used in compaction in 1.1
565
- elsif !JSON_LD_10_TYPE_VALUES.include?(type) && !(type.is_a?(RDF::URI) && type.absolute?)
566
- 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
567
614
  end
568
- #log_debug("") {"type_mapping: #{type.inspect}"}
569
- definition.type_mapping = type
570
- end
571
615
 
572
- if value.key?('@reverse')
573
- raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
574
- value.key?('@id') || value.key?('@nest')
575
- raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
576
- 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')
577
619
 
578
- if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
579
- warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
580
- return
581
- 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
624
+
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
582
629
 
583
- # 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.
584
- definition.id = expand_iri(value['@reverse'],
585
- vocab: true,
586
- local_context: local_context,
587
- defined: defined)
588
- raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
589
- definition.id.is_a?(RDF::Node) || definition.id.is_a?(RDF::URI) && definition.id.absolute?
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
590
724
 
591
- if term[1..-1].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
592
- raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
725
+ definition.id = vocab + term
726
+ # log_debug("") {"=> #{definition.id}"}
593
727
  end
594
728
 
595
- 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
596
730
 
597
- # 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.
598
731
  if value.key?('@container')
599
- container = value['@container']
600
- raise JsonLdError::InvalidReverseProperty,
601
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
602
- container.is_a?(String) && (container == '@set' || container == '@index')
603
- definition.container_mapping = check_container(container, local_context, defined, term)
604
- end
605
- definition.reverse_property = true
606
- elsif value.key?('@id') && value['@id'].nil?
607
- # Allowed to reserve a null term, which may be protected
608
- elsif value.key?('@id') && value['@id'] != term
609
- raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
610
- value['@id'].is_a?(String)
611
-
612
- if !KEYWORDS.include?(value['@id'].to_s) && value['@id'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
613
- warn "Values beginning with '@' are reserved for future use and ignored: #{value['@id']}."
614
- 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
615
744
  end
616
745
 
617
- definition.id = expand_iri(value['@id'],
618
- vocab: true,
619
- local_context: local_context,
620
- defined: defined)
621
- raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
622
- 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
623
756
 
624
- if term.match?(/(?::[^:])|\//)
625
- term_iri = expand_iri(term,
626
- vocab: true,
627
- local_context: local_context,
628
- defined: defined.merge(term => true))
629
- if term_iri != definition.id
630
- 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}"
631
777
  end
632
778
  end
633
779
 
634
- 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
635
798
 
636
- # If id ends with a gen-delim, it may be used as a prefix for simple terms
637
- definition.prefix = true if !term.include?(':') &&
638
- simple_term &&
639
- (definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']', '@') || definition.id.to_s.start_with?('_:'))
640
- elsif term[1..-1].include?(':')
641
- # 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.
642
- prefix, suffix = term.split(':', 2)
643
- 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
644
807
 
645
- definition.id = if td = term_definitions[prefix]
646
- # 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.
647
- td.id + suffix
648
- else
649
- # Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
650
- term
651
- end
652
- log_debug("") {"=> #{definition.id}"}
653
- elsif term.include?('/')
654
- # If term is a relative IRI
655
- definition.id = expand_iri(term, vocab: true)
656
- raise JsonLdError::InvalidKeywordAlias, "expected term to expand to an absolute IRI #{term.inspect}" unless
657
- definition.id.absolute?
658
- elsif KEYWORDS.include?(term)
659
- # This should only happen for @type when @container is @set
660
- definition.id = term
661
- else
662
- # 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.
663
- raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab
664
- definition.id = vocab + term
665
- log_debug("") {"=> #{definition.id}"}
666
- end
808
+ # log_debug("") {"direction_mapping: #{direction.inspect}"}
809
+ definition.direction_mapping = direction || false
810
+ end
811
+
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
667
822
 
668
- @iri_to_term[definition.id] = term if simple_term && definition.id
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
669
832
 
670
- if value.key?('@container')
671
- #log_debug("") {"container_mapping: #{value['@container'].inspect}"}
672
- 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
673
839
 
674
- # If @container includes @type
675
- if definition.container_mapping.include?('@type')
676
- # If definition does not have @type, set @type to @id
677
- definition.type_mapping ||= '@id'
678
- # If definition includes @type with a value other than @id or @vocab, an illegal type mapping error has been detected
679
- if !CONTEXT_TYPE_ID_VOCAB.include?(definition.type_mapping)
680
- 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"
681
843
  end
682
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
683
853
  end
684
854
 
685
- if value.key?('@index')
686
- # property-based indexing
687
- raise JsonLdError::InvalidTermDefinition, "@index without @index in @container: #{value['@index']} on term #{term.inspect}" unless definition.container_mapping.include?('@index')
688
- 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?('@')
689
- 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?
690
861
  end
691
862
 
692
- if value.key?('@context')
693
- begin
694
- new_ctx = self.parse(value['@context'],
695
- base: base,
696
- override_protected: true,
697
- remote_contexts: remote_contexts,
698
- validate_scoped: false)
699
- # Record null context in array form
700
- definition.context = case value['@context']
701
- when String then new_ctx.context_base
702
- when nil then [nil]
703
- else value['@context']
704
- end
705
- log_debug("") {"context: #{definition.context.inspect}"}
706
- rescue JsonLdError => e
707
- 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
708
877
  end
709
878
  end
710
879
 
711
- if value.key?('@language')
712
- language = value['@language']
713
- language = case value['@language']
880
+ # @param [String] value
881
+ def default_language=(value, **options)
882
+ @default_language = case value
714
883
  when String
715
884
  # Warn on an invalid language tag, unless :validate is true, in which case it's an error
716
- if value['@language'] !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
717
- 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}"
718
887
  end
719
- options[:lowercaseLanguage] ? value['@language'].downcase : value['@language']
888
+ options[:lowercaseLanguage] ? value.downcase : value
720
889
  when nil
721
890
  nil
722
891
  else
723
- 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}"
724
893
  end
725
- #log_debug("") {"language_mapping: #{language.inspect}"}
726
- definition.language_mapping = language || false
727
894
  end
728
895
 
729
- if value.key?('@direction')
730
- direction = value['@direction']
731
- 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)
732
- #log_debug("") {"direction_mapping: #{direction.inspect}"}
733
- definition.direction_mapping = direction || false
734
- 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
735
905
 
736
- if value.key?('@nest')
737
- nest = value['@nest']
738
- raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
739
- 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'
740
- #log_debug("") {"nest: #{nest.inspect}"}
741
- definition.nest = nest
906
+ value
907
+ end
742
908
  end
743
909
 
744
- if value.key?('@prefix')
745
- raise JsonLdError::InvalidTermDefinition, "@prefix used on compact or relative IRI term #{term.inspect}" if term.match?(%r{:|/})
746
- case pfx = value['@prefix']
747
- when TrueClass, FalseClass
748
- 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'
749
927
  else
750
- raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
928
+ false
751
929
  end
752
-
753
- raise JsonLdError::InvalidTermDefinition, "keywords may not be used as prefixes" if pfx && KEYWORDS.include?(definition.id.to_s)
754
930
  end
755
931
 
756
- if previous_definition && previous_definition.protected? && definition != previous_definition && !override_protected
757
- definition = previous_definition
758
- raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
759
- 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
760
950
 
761
- term_definitions[term] = definition
762
- defined[term] = true
763
- end
951
+ @processingMode = value
952
+ else
953
+ raise JsonLdError::InvalidVersionValue, value.inspect
954
+ end
955
+ end
764
956
 
765
- ##
766
- # Initial context, without mappings, vocab or default language
767
- #
768
- # @return [Boolean]
769
- def empty?
770
- @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
771
- 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
772
971
 
773
- # @param [String] value must be an absolute IRI
774
- def base=(value, **options)
775
- if value
776
- raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
777
- value = RDF::URI(value)
778
- value = @base.join(value) if @base && value.relative?
779
- # still might be relative to document
780
- @base = value
781
- else
782
- @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
783
978
  end
784
979
 
785
- 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
786
989
 
787
- # @param [String] value
788
- def default_language=(value, **options)
789
- @default_language = case value
790
- when String
791
- # Warn on an invalid language tag, unless :validate is true, in which case it's an error
792
- if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
793
- 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}"
794
993
  end
795
- options[:lowercaseLanguage] ? value.downcase : value
796
- when nil
797
- nil
798
- else
799
- raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
800
- end
801
- end
802
994
 
803
- # @param [String] value
804
- def default_direction=(value, **options)
805
- @default_direction = if value
806
- raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
807
995
  value
808
- else
809
- nil
810
996
  end
811
- end
812
997
 
813
- ##
814
- # Retrieve, or check processing mode.
815
- #
816
- # * With no arguments, retrieves the current set processingMode.
817
- # * 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"
818
- # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
819
- #
820
- # @param [String, Number] expected (nil)
821
- # @return [String]
822
- def processingMode(expected = nil)
823
- case expected
824
- when 1.0, 'json-ld-1.0'
825
- @processingMode == 'json-ld-1.0'
826
- when 1.1, 'json-ld-1.1'
827
- @processingMode.nil? || @processingMode == 'json-ld-1.1'
828
- when nil
829
- @processingMode || 'json-ld-1.1'
830
- else
831
- false
832
- end
833
- 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
834
1038
 
835
- ##
836
- # Set processing mode.
837
- #
838
- # * 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"
839
- #
840
- # 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.
841
- # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
842
- #
843
- # @param [String, Number] value
844
- # @return [String]
845
- # @raise [JsonLdError::ProcessingModeConflict]
846
- def processingMode=(value = nil, **options)
847
- value = "json-ld-1.1" if value == 1.1
848
- case value
849
- when "json-ld-1.0", "json-ld-1.1"
850
- if @processingMode && @processingMode != value
851
- raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
852
- end
853
- @processingMode = value
854
- else
855
- raise JsonLdError::InvalidVersionValue, value.inspect
1039
+ # Return hash with @context, or empty
1040
+ use_context.nil? || use_context.empty? ? {} : { '@context' => use_context }
856
1041
  end
857
- end
858
1042
 
859
- # 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.
860
- # @param [String] value must be an absolute IRI
861
- def vocab=(value, **options)
862
- @vocab = case value
863
- when /_:/
864
- # BNode vocab is deprecated
865
- warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
866
- value
867
- when String, RDF::URI
868
- if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
869
- 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
870
1076
  end
871
- expand_iri(value.to_s, vocab: true, documentRelative: true)
872
- when nil
873
- nil
874
- else
875
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
876
- end
877
- end
878
1077
 
879
- # Set propagation
880
- # @note: by the time this is called, the work has already been done.
881
- #
882
- # @param [Boolean] value
883
- def propagate=(value, **options)
884
- raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
885
- raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
886
- value
887
- 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)
888
1113
 
889
- ##
890
- # Generate @context
891
- #
892
- # If a context was supplied in global options, use that, otherwise, generate one
893
- # from this representation.
894
- #
895
- # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
896
- # Original context to use, if available
897
- # @param [Hash{Symbol => Object}] options ({})
898
- # @return [Hash]
899
- def serialize(provided_context: nil, **options)
900
- #log_debug("serlialize: generate context")
901
- #log_debug("") {"=> context: #{inspect}"}
902
- use_context = case provided_context
903
- when String, RDF::URI
904
- #log_debug "serlialize: reuse context: #{provided_context.inspect}"
905
- provided_context.to_s
906
- when Hash
907
- #log_debug "serlialize: reuse context: #{provided_context.inspect}"
908
- # If it has an @context entry use it, otherwise it is assumed to be the body of a context
909
- provided_context.fetch('@context', provided_context)
910
- when Array
911
- #log_debug "serlialize: reuse context: #{provided_context.inspect}"
912
- provided_context
913
- when IO, StringIO
914
- provided_context.rewind
915
- JSON.load(provided_context).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
1213
  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
1101
- end
1102
- 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
-
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
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
1133
1218
 
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))
1219
+ end
1220
+ term.nest
1149
1221
  end
1150
1222
 
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)
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)
1234
+ end
1191
1235
  end
1192
1236
 
1193
- if (v_td = term_definitions[value]) && KEYWORDS.include?(v_td.id)
1194
- return (as_string ? v_td.id.to_s : v_td.id)
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
1195
1249
  end
1196
1250
 
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)
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
1202
1258
  end
1203
1259
 
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)
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))
1212
1275
  end
1213
- if suffix.start_with?('//')
1214
- v = RDF::URI(value)
1215
- return (as_string ? v.to_s : v)
1276
+
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 }
1279
+ end
1280
+
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'
1334
- end
1335
- common_type ||= item_type
1336
- if item_type != common_type
1337
- common_type = '@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
1338
1459
  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'
1528
+ preferred_values << '@any'
1385
1529
 
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
1390
-
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
- RDF_LITERAL_NATIVE_TYPES = Set.new([RDF::XSD.boolean, RDF::XSD.integer, RDF::XSD.double]).freeze
1471
-
1472
- ##
1473
- # 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.
1474
- #
1475
- # 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.
1476
- #
1477
- # @param [String] property
1478
- # Associated property used to find coercion rules
1479
- # @param [Hash, String] value
1480
- # Value (literal or IRI) to be expanded
1481
- # @param [Boolean] useNativeTypes (false) use native representations
1482
- # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1483
- # @param [String, RDF::URI] base for resolving document-relative IRIs
1484
- # @param [Hash{Symbol => Object}] options
1485
- #
1486
- # @return [Hash] Object representation of value
1487
- # @raise [RDF::ReaderError] if the iri cannot be expanded
1488
- # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1489
- def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **options)
1490
- td = term_definitions.fetch(property, TermDefinition.new(property))
1491
-
1492
- # 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.
1493
- if value.is_a?(String) && td.type_mapping == '@id'
1494
- #log_debug("") {"as relative IRI: #{value.inspect}"}
1495
- return {'@id' => expand_iri(value, documentRelative: true, base: base).to_s}
1590
+ iri
1496
1591
  end
1497
1592
 
1498
- # 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.
1499
- if value.is_a?(String) && td.type_mapping == '@vocab'
1500
- return {'@id' => expand_iri(value, vocab: true, documentRelative: true, base: base).to_s}
1501
- 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
1502
1618
 
1503
- value = RDF::Literal(value) if
1504
- value.is_a?(Date) ||
1505
- value.is_a?(DateTime) ||
1506
- value.is_a?(Time)
1507
-
1508
- result = case value
1509
- when RDF::URI, RDF::Node
1510
- {'@id' => value.to_s}
1511
- when RDF::Literal
1512
- res = {}
1513
- if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
1514
- # Value parsed as JSON
1515
- # FIXME: MultiJson
1516
- res['@type'] = '@json'
1517
- res['@value'] = ::JSON.parse(value.object)
1518
- elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
1519
- lang, dir = value.datatype.fragment.split('_')
1520
- res['@value'] = value.to_s
1521
- unless lang.empty?
1522
- if lang !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
1523
- if options[:validate]
1524
- raise JsonLdError::InvalidLanguageMapping, "rdf:language must be valid BCP47: #{lang.inspect}"
1525
- else
1526
- warn "rdf:language must be valid BCP47: #{lang.inspect}"
1527
- end
1528
- end
1529
- res['@language'] = lang
1530
- end
1531
- res['@direction'] = dir
1532
- elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype) && value.valid?
1533
- res['@type'] = uri(coerce(property)) if coerce(property)
1534
- res['@value'] = value.object
1535
- else
1536
- value.canonicalize! if value.valid? && value.datatype == RDF::XSD.double
1537
- if coerce(property)
1538
- res['@type'] = uri(coerce(property)).to_s
1539
- elsif value.datatype?
1540
- res['@type'] = uri(value.datatype).to_s
1541
- elsif value.language? || language(property)
1542
- res['@language'] = (value.language || language(property)).to_s
1543
- end
1544
- res['@value'] = value.to_s
1545
- end
1546
- res
1547
- else
1548
- # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1549
- res = {}
1550
-
1551
- if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1552
- res['@type'] = td.type_mapping.to_s
1553
- elsif value.is_a?(String)
1554
- language = language(property)
1555
- direction = direction(property)
1556
- res['@language'] = language if language
1557
- res['@direction'] = direction if direction
1558
- end
1559
-
1560
- res.merge('@value' => value)
1561
- 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
1562
1623
 
1563
- result
1564
- rescue ::JSON::ParserError => e
1565
- raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
1566
- 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
1567
1642
 
1568
- ##
1569
- # Compact a value
1570
- #
1571
- # @param [String] property
1572
- # Associated property used to find coercion rules
1573
- # @param [Hash] value
1574
- # Value (literal or IRI), in full object representation, to be compacted
1575
- # @param [String, RDF::URI] base for resolving document-relative IRIs
1576
- #
1577
- # @return [Hash] Object representation of value
1578
- # @raise [JsonLdError] if the iri cannot be expanded
1579
- # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1580
- # FIXME: revisit the specification version of this.
1581
- def compact_value(property, value, base: nil)
1582
- #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1583
-
1584
- indexing = index?(value) && container(property).include?('@index')
1585
- language = language(property)
1586
- direction = direction(property)
1587
-
1588
- result = case
1589
- when coerce(property) == '@id' && value.key?('@id') && (value.keys - %w(@id @index)).empty?
1590
- # Compact an @id coercion
1591
- #log_debug("") {" (@id & coerce)"}
1592
- compact_iri(value['@id'], base: base)
1593
- when coerce(property) == '@vocab' && value.key?('@id') && (value.keys - %w(@id @index)).empty?
1594
- # Compact an @id coercion
1595
- #log_debug("") {" (@id & coerce & vocab)"}
1596
- compact_iri(value['@id'], vocab: true)
1597
- when value.key?('@id')
1598
- #log_debug("") {" (@id)"}
1599
- # return value as is
1600
- value
1601
- when value['@type'] && value['@type'] == coerce(property)
1602
- # Compact common datatype
1603
- #log_debug("") {" (@type & coerce) == #{coerce(property)}"}
1604
- value['@value']
1605
- when coerce(property) == '@none' || value['@type']
1606
- # use original expanded value
1607
- value
1608
- when !value['@value'].is_a?(String)
1609
- #log_debug("") {" (native)"}
1610
- indexing || !index?(value) ? value['@value'] : value
1611
- when value['@language'].to_s.downcase == language.to_s.downcase && value['@direction'] == direction
1612
- # Compact language and direction
1613
- indexing || !index?(value) ? value['@value'] : value
1614
- else
1615
- value
1643
+ res.merge('@value' => value)
1644
+ end
1616
1645
  end
1617
1646
 
1618
- if result.is_a?(Hash) && result.key?('@type') && value['@type'] != '@json'
1619
- # Compact values of @type
1620
- c_type = if result['@type'].is_a?(Array)
1621
- 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
1622
1692
  else
1623
- compact_iri(result['@type'], vocab: true)
1693
+ value
1624
1694
  end
1625
- result = result.merge('@type' => c_type)
1626
- end
1627
-
1628
- # If the result is an object, tranform keys using any term keyword aliases
1629
- if result.is_a?(Hash) && result.keys.any? {|k| self.alias(k) != k}
1630
- #log_debug("") {" (map to key aliases)"}
1631
- new_element = {}
1632
- result.each do |k, v|
1633
- new_element[self.alias(k)] = v
1634
- end
1635
- result = new_element
1636
- end
1637
1695
 
1638
- #log_debug("") {"=> #{result.inspect}"}
1639
- result
1640
- 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
1641
1715
 
1642
- ##
1643
- # Turn this into a source for a new instantiation
1644
- # @param [Array<String>] aliases
1645
- # Other URLs to alias when preloading
1646
- # @return [String]
1647
- def to_rb(*aliases)
1648
- canon_base = RDF::URI(context_base).canonicalize
1649
- defn = []
1650
-
1651
- defn << "base: #{self.base.to_s.inspect}" if self.base
1652
- defn << "language: #{self.default_language.inspect}" if self.default_language
1653
- defn << "vocab: #{self.vocab.to_s.inspect}" if self.vocab
1654
- defn << "processingMode: #{self.processingMode.inspect}" if self.processingMode
1655
- term_defs = term_definitions.map do |term, td|
1656
- " " + term.inspect + " => " + td.to_rb
1657
- end.sort
1658
- defn << "term_definitions: {\n#{term_defs.join(",\n") }\n }" unless term_defs.empty?
1659
- %(# -*- 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 -*-
1660
1738
  # frozen_string_literal: true
1661
1739
  # This file generated automatically from #{context_base}
1662
1740
  require 'json/ld'
1663
1741
  class JSON::LD::Context
1664
1742
  ).gsub(/^ /, '') +
1665
- %[ add_preloaded("#{canon_base}") do\n new(] + defn.join(", ") + ")\n end\n" +
1666
- aliases.map {|a| %[ alias_preloaded("#{a}", "#{canon_base}")\n]}.join("") +
1667
- "end\n"
1668
- 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
1669
1747
 
1670
- def inspect
1671
- v = %w([Context)
1672
- v << "base=#{base}" if base
1673
- v << "vocab=#{vocab}" if vocab
1674
- v << "processingMode=#{processingMode}" if processingMode
1675
- v << "default_language=#{default_language}" if default_language
1676
- v << "default_direction=#{default_direction}" if default_direction
1677
- v << "previous_context" if previous_context
1678
- v << "term_definitions[#{term_definitions.length}]=#{term_definitions}"
1679
- v.join(" ") + "]"
1680
- 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
1681
1759
 
1682
- # Duplicate an active context, allowing it to be modified.
1683
- def dup
1684
- that = self
1685
- ec = Context.new(unfrozen: true, **@options)
1686
- ec.context_base = that.context_base
1687
- ec.base = that.base unless that.base.nil?
1688
- ec.default_direction = that.default_direction
1689
- ec.default_language = that.default_language
1690
- ec.previous_context = that.previous_context
1691
- ec.processingMode = that.processingMode if that.instance_variable_get(:@processingMode)
1692
- ec.vocab = that.vocab if that.vocab
1693
-
1694
- ec.instance_eval do
1695
- @term_definitions = that.term_definitions.dup
1696
- @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
1697
1777
  end
1698
- ec
1699
- end
1700
1778
 
1701
- protected
1779
+ protected
1702
1780
 
1703
- ##
1704
- # Determine if `term` is a suitable term.
1705
- # Term may be any valid JSON string.
1706
- #
1707
- # @param [String] term
1708
- # @return [Boolean]
1709
- def term_valid?(term)
1710
- term.is_a?(String) && !term.empty?
1711
- 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
1712
1790
 
1713
- # Reverse term mapping, typically used for finding aliases for keys.
1714
- #
1715
- # Returns either the original value, or a mapping for this value.
1716
- #
1717
- # @example
1718
- # {"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}
1719
- #
1720
- # @param [RDF::URI, String] value
1721
- # @return [String]
1722
- def alias(value)
1723
- iri_to_term.fetch(value, value)
1724
- 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
1725
1803
 
1726
- private
1727
-
1728
- CONTEXT_CONTAINER_ARRAY_TERMS = Set.new(%w(@set @list @graph)).freeze
1729
- CONTEXT_CONTAINER_ID_GRAPH = Set.new(%w(@id @graph)).freeze
1730
- CONTEXT_CONTAINER_INDEX_GRAPH = Set.new(%w(@index @graph)).freeze
1731
- CONTEXT_BASE_FRAG_OR_QUERY = %w(? #).freeze
1732
- CONTEXT_TYPE_ID_VOCAB = %w(@id @vocab).freeze
1733
-
1734
- def uri(value)
1735
- case value.to_s
1736
- when /^_:(.*)$/
1737
- # Map BlankNodes if a namer is given
1738
- #log_debug "uri(bnode)#{value}: #{$1}"
1739
- bnode(namer.get_sym($1))
1740
- else
1741
- value = RDF::URI(value)
1742
- #value.validate! if options[:validate]
1743
- value
1804
+ private
1805
+
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
1744
1822
  end
1745
- end
1746
1823
 
1747
- # Keep track of allocated BNodes
1748
- #
1749
- # Don't actually use the name provided, to prevent name alias issues.
1750
- # @return [RDF::Node]
1751
- def bnode(value = nil)
1752
- @@bnode_cache ||= {}
1753
- @@bnode_cache[value.to_s] ||= RDF::Node.new(value)
1754
- 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
1755
1889
 
1756
- ##
1757
- # Inverse Context creation
1758
- #
1759
- # 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.
1760
- #
1761
- # 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.
1762
- #
1763
- # 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.
1764
- #
1765
- # @example Basic structure of resulting inverse context
1766
- # {
1767
- # "http://example.com/term": {
1768
- # "@language": {
1769
- # "@null": "term",
1770
- # "@none": "term",
1771
- # "en": "term",
1772
- # "ar_rtl": "term"
1773
- # },
1774
- # "@type": {
1775
- # "@reverse": "term",
1776
- # "@none": "term",
1777
- # "http://datatype": "term"
1778
- # },
1779
- # "@any": {
1780
- # "@none": "term",
1781
- # }
1782
- # }
1783
- # }
1784
- # @return [Hash{String => Hash{String => String}}]
1785
- # @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
1786
- def inverse_context
1787
- Context.inverse_cache[self.hash] ||= begin
1788
- result = {}
1789
- default_language = (self.default_language || '@none').downcase
1790
- term_definitions.keys.sort do |a, b|
1791
- a.length == b.length ? (a <=> b) : (a.length <=> b.length)
1792
- end.each do |term|
1793
- next unless td = term_definitions[term]
1794
-
1795
- container = td.container_mapping.to_a.join('')
1796
- if container.empty?
1797
- container = td.as_set? ? %(@set) : %(@none)
1798
- end
1799
-
1800
- container_map = result[td.id.to_s] ||= {}
1801
- tl_map = container_map[container] ||= {'@language' => {}, '@type' => {}, '@any' => {}}
1802
- type_map = tl_map['@type']
1803
- language_map = tl_map['@language']
1804
- any_map = tl_map['@any']
1805
- any_map['@none'] ||= term
1806
- if td.reverse_property
1807
- type_map['@reverse'] ||= term
1808
- elsif td.type_mapping == '@none'
1809
- type_map['@any'] ||= term
1810
- language_map['@any'] ||= term
1811
- any_map['@any'] ||= term
1812
- elsif td.type_mapping
1813
- type_map[td.type_mapping.to_s] ||= term
1814
- elsif !td.language_mapping.nil? && !td.direction_mapping.nil?
1815
- lang_dir = if td.language_mapping && td.direction_mapping
1816
- "#{td.language_mapping}_#{td.direction_mapping}".downcase
1817
- elsif td.language_mapping
1818
- td.language_mapping.downcase
1819
- elsif td.direction_mapping
1820
- "_#{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
1821
1925
  else
1822
- "@null"
1926
+ language_map[default_language] ||= term
1927
+ language_map['@none'] ||= term
1928
+ type_map['@none'] ||= term
1823
1929
  end
1824
- language_map[lang_dir] ||= term
1825
- elsif !td.language_mapping.nil?
1826
- lang_dir = (td.language_mapping || '@null').downcase
1827
- language_map[lang_dir] ||= term
1828
- elsif !td.direction_mapping.nil?
1829
- lang_dir = td.direction_mapping ? "_#{td.direction_mapping}" : '@none'
1830
- language_map[lang_dir] ||= term
1831
- elsif default_direction
1832
- language_map["_#{default_direction}"] ||= term
1833
- language_map['@none'] ||= term
1834
- type_map['@none'] ||= term
1835
- else
1836
- language_map[default_language] ||= term
1837
- language_map['@none'] ||= term
1838
- type_map['@none'] ||= term
1839
1930
  end
1931
+ result
1840
1932
  end
1841
- result
1842
1933
  end
1843
- end
1844
1934
 
1845
- ##
1846
- # 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.
1847
- #
1848
- # @param [String] iri
1849
- # @param [Array<String>] containers
1850
- # represents an ordered list of preferred container mappings
1851
- # @param [String] type_language
1852
- # indicates whether to look for a term with a matching type mapping or language mapping
1853
- # @param [Array<String>] preferred_values
1854
- # for the type mapping or language mapping
1855
- # @return [String]
1856
- def select_term(iri, containers, type_language, preferred_values)
1857
- #log_debug("select_term") {
1858
- # "iri: #{iri.inspect}, " +
1859
- # "containers: #{containers.inspect}, " +
1860
- # "type_language: #{type_language.inspect}, " +
1861
- # "preferred_values: #{preferred_values.inspect}"
1862
- #}
1863
- container_map = inverse_context[iri]
1864
- #log_debug(" ") {"container_map: #{container_map.inspect}"}
1865
- containers.each do |container|
1866
- next unless container_map.key?(container)
1867
- tl_map = container_map[container]
1868
- value_map = tl_map[type_language]
1869
- preferred_values.each do |item|
1870
- next unless value_map.key?(item)
1871
- #log_debug("=>") {value_map[item].inspect}
1872
- 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
1873
1966
  end
1967
+ # log_debug("=>") {"nil"}
1968
+ nil
1874
1969
  end
1875
- #log_debug("=>") {"nil"}
1876
- nil
1877
- end
1878
1970
 
1879
- ##
1880
- # Removes a base IRI from the given absolute IRI.
1881
- #
1882
- # @param [String] base the base used for making `iri` relative
1883
- # @param [String] iri the absolute IRI
1884
- # @return [String]
1885
- # the relative IRI if relative to base, otherwise the absolute IRI.
1886
- def remove_base(base, iri)
1887
- return iri unless base
1888
- @base_and_parents ||= begin
1889
- u = base
1890
- iri_set = u.to_s.end_with?('/') ? [u.to_s] : []
1891
- iri_set << u.to_s while (u != './' && u = u.parent)
1892
- iri_set
1893
- end
1894
- b = base.to_s
1895
- 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])
1896
1989
 
1897
- @base_and_parents.each_with_index do |bb, index|
1898
- next unless iri.start_with?(bb)
1899
- rel = "../" * index + iri[bb.length..-1]
1900
- 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
1901
1997
  end
1902
- iri
1903
- end
1904
1998
 
1905
- ## Used for testing
1906
- # Retrieve term mappings
1907
- #
1908
- # @return [Array<RDF::URI>]
1909
- def mappings
1910
- {}.tap do |memo|
1911
- term_definitions.each_pair do |t,td|
1912
- 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
1913
2008
  end
1914
2009
  end
1915
- end
1916
2010
 
1917
- ## Used for testing
1918
- # Retrieve term mapping
1919
- #
1920
- # @param [String, #to_s] term
1921
- #
1922
- # @return [RDF::URI, String]
1923
- def mapping(term)
1924
- term_definitions[term] ? term_definitions[term].id : nil
1925
- 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
1926
2020
 
1927
- ## Used for testing
1928
- # Retrieve language mappings
1929
- #
1930
- # @return [Array<String>]
1931
- # @deprecated
1932
- def languages
1933
- {}.tap do |memo|
1934
- term_definitions.each_pair do |t,td|
1935
- 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
1936
2031
  end
1937
2032
  end
1938
- end
1939
2033
 
1940
- # Ensure @container mapping is appropriate
1941
- # The result is the original container definition. For IRI containers, this is necessary to be able to determine the @type mapping for string values
1942
- def check_container(container, local_context, defined, term)
1943
- if container.is_a?(Array) && processingMode('json-ld-1.0')
1944
- raise JsonLdError::InvalidContainerMapping,
1945
- "'@container' on term #{term.inspect} must be a string: #{container.inspect}"
1946
- 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
1947
2041
 
1948
- val = Set.new(Array(container))
1949
- val.delete('@set') if has_set = val.include?('@set')
1950
-
1951
- if val.include?('@list')
1952
- raise JsonLdError::InvalidContainerMapping,
1953
- "'@container' on term #{term.inspect} using @list cannot have any other values" unless
1954
- !has_set && val.length == 1
1955
- # Okay
1956
- elsif val.include?('@language')
1957
- raise JsonLdError::InvalidContainerMapping,
1958
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1959
- has_set && processingMode('json-ld-1.0')
1960
- raise JsonLdError::InvalidContainerMapping,
1961
- "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1962
- val.length == 1
1963
- # Okay
1964
- elsif val.include?('@index')
1965
- raise JsonLdError::InvalidContainerMapping,
1966
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1967
- has_set && processingMode('json-ld-1.0')
1968
- raise JsonLdError::InvalidContainerMapping,
1969
- "'@container' on term #{term.inspect} using @index cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1970
- (val - CONTEXT_CONTAINER_INDEX_GRAPH).empty?
1971
- # Okay
1972
- elsif val.include?('@id')
1973
- raise JsonLdError::InvalidContainerMapping,
1974
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1975
- processingMode('json-ld-1.0')
1976
- raise JsonLdError::InvalidContainerMapping,
1977
- "'@container' on term #{term.inspect} using @id cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
1978
- val.subset?(CONTEXT_CONTAINER_ID_GRAPH)
1979
- # Okay
1980
- elsif val.include?('@type') || val.include?('@graph')
1981
- raise JsonLdError::InvalidContainerMapping,
1982
- "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
1983
- processingMode('json-ld-1.0')
1984
- raise JsonLdError::InvalidContainerMapping,
1985
- "'@container' on term #{term.inspect} using @language cannot have any values other than @set, found #{container.inspect}" unless
1986
- val.length == 1
1987
- # Okay
1988
- elsif val.empty?
1989
- # Okay
1990
- else
1991
- 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,
1992
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)
1993
2098
  end
1994
- Array(container)
1995
- end
1996
2099
 
1997
- # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
1998
- class TermDefinition
1999
- # @return [RDF::URI] IRI map
2000
- 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
2001
2104
 
2002
- # @return [String] term name
2003
- attr_accessor :term
2105
+ # @return [String] term name
2106
+ attr_accessor :term
2004
2107
 
2005
- # @return [String] Type mapping
2006
- attr_accessor :type_mapping
2108
+ # @return [String] Type mapping
2109
+ attr_accessor :type_mapping
2007
2110
 
2008
- # Base container mapping, without @set
2009
- # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
2010
- 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
2011
2114
 
2012
- # @return [String] Term used for nest properties
2013
- attr_accessor :nest
2115
+ # @return [String] Term used for nest properties
2116
+ attr_accessor :nest
2014
2117
 
2015
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
2016
- # @return [String] Language mapping
2017
- 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
2018
2121
 
2019
- # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
2020
- # @return ["ltr", "rtl"] direction_mapping
2021
- 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
2022
2125
 
2023
- # @return [Boolean] Reverse Property
2024
- attr_accessor :reverse_property
2126
+ # @return [Boolean] Reverse Property
2127
+ attr_accessor :reverse_property
2025
2128
 
2026
- # This is a simple term definition, not an expanded term definition
2027
- # @return [Boolean]
2028
- attr_accessor :simple
2129
+ # This is a simple term definition, not an expanded term definition
2130
+ # @return [Boolean]
2131
+ attr_accessor :simple
2029
2132
 
2030
- # Property used for data indexing; defaults to @index
2031
- # @return [Boolean]
2032
- attr_accessor :index
2133
+ # Property used for data indexing; defaults to @index
2134
+ # @return [Boolean]
2135
+ attr_accessor :index
2033
2136
 
2034
- # Indicate that term may be used as a prefix
2035
- attr_writer :prefix
2137
+ # Indicate that term may be used as a prefix
2138
+ attr_writer :prefix
2036
2139
 
2037
- # Term-specific context
2038
- # @return [Hash{String => Object}]
2039
- attr_accessor :context
2140
+ # Term-specific context
2141
+ # @return [Hash{String => Object}]
2142
+ attr_accessor :context
2040
2143
 
2041
- # Term is protected.
2042
- # @return [Boolean]
2043
- attr_writer :protected
2144
+ # Term is protected.
2145
+ # @return [Boolean]
2146
+ attr_writer :protected
2044
2147
 
2045
- # This is a simple term definition, not an expanded term definition
2046
- # @return [Boolean] simple
2047
- 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
2048
2153
 
2049
- # This is an appropriate term to use as the prefix of a compact IRI
2050
- # @return [Boolean] simple
2051
- 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
2052
2159
 
2053
- # Create a new Term Mapping with an ID
2054
- # @param [String] term
2055
- # @param [String] id
2056
- # @param [String] type_mapping Type mapping
2057
- # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
2058
- # @param [String] language_mapping
2059
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term
2060
- # @param ["ltr", "rtl"] direction_mapping
2061
- # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
2062
- # @param [Boolean] reverse_property
2063
- # @param [Boolean] protected mark resulting context as protected
2064
- # @param [String] nest term used for nest properties
2065
- # @param [Boolean] simple
2066
- # This is a simple term definition, not an expanded term definition
2067
- # @param [Boolean] prefix
2068
- # Term may be used as a prefix
2069
- def initialize(term,
2070
- id: nil,
2071
- index: nil,
2072
- type_mapping: nil,
2073
- container_mapping: nil,
2074
- language_mapping: nil,
2075
- direction_mapping: nil,
2076
- reverse_property: false,
2077
- nest: nil,
2078
- protected: nil,
2079
- simple: false,
2080
- prefix: nil,
2081
- context: nil)
2082
- @term = term
2083
- @id = id.to_s unless id.nil?
2084
- @index = index.to_s unless index.nil?
2085
- @type_mapping = type_mapping.to_s unless type_mapping.nil?
2086
- self.container_mapping = container_mapping
2087
- @language_mapping = language_mapping unless language_mapping.nil?
2088
- @direction_mapping = direction_mapping unless direction_mapping.nil?
2089
- @reverse_property = reverse_property
2090
- @protected = protected
2091
- @nest = nest unless nest.nil?
2092
- @simple = simple
2093
- @prefix = prefix unless prefix.nil?
2094
- @context = context unless context.nil?
2095
- 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
2096
2203
 
2097
- # Term is protected.
2098
- # @return [Boolean]
2099
- def protected?; !!@protected; end
2100
-
2101
- # Set container mapping, from an array which may include @set
2102
- def container_mapping=(mapping)
2103
- mapping = case mapping
2104
- when Set then mapping
2105
- when Array then Set.new(mapping)
2106
- when String then Set[mapping]
2107
- when nil then Set.new
2108
- else
2109
- raise "Shouldn't happen with #{mapping.inspect}"
2204
+ # Term is protected.
2205
+ # @return [Boolean]
2206
+ def protected?
2207
+ !!@protected
2110
2208
  end
2111
- if @as_set = mapping.include?('@set')
2112
- mapping = mapping.dup
2113
- 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)
2114
2216
  end
2115
- @container_mapping = mapping
2116
- @index ||= '@index' if mapping.include?('@index')
2117
- end
2118
2217
 
2119
- ##
2120
- # Output Hash or String definition for this definition considering @language and @vocab
2121
- #
2122
- # @param [Context] context
2123
- # @return [String, Hash{String => Array[String], String}]
2124
- def to_context_definition(context)
2125
- cid = if context.vocab && id.start_with?(context.vocab)
2126
- # Nothing to return unless it's the same as the vocab
2127
- id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
2128
- else
2129
- # Find a term to act as a prefix
2130
- iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
2131
- 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)
2132
2224
  end
2133
2225
 
2134
- if simple?
2135
- cid.to_s unless cid == term && context.vocab
2136
- else
2137
- defn = {}
2138
- defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
2139
- if type_mapping
2140
- defn['@type'] = if KEYWORDS.include?(type_mapping)
2141
- type_mapping
2142
- else
2143
- 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
2144
2301
  end
2302
+ defn << "#{acc}: #{v.inspect}" if v
2145
2303
  end
2304
+ defn.join(', ') + ")"
2305
+ end
2146
2306
 
2147
- cm = Array(container_mapping)
2148
- cm << "@set" if as_set? && !cm.include?("@set")
2149
- cm = cm.first if cm.length == 1
2150
- defn['@container'] = cm unless cm.empty?
2151
- # Language set as false to be output as null
2152
- defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
2153
- defn['@direction'] = (@direction_mapping ? @direction_mapping : nil) unless @direction_mapping.nil?
2154
- defn['@context'] = @context if @context
2155
- defn['@nest'] = @nest if @nest
2156
- defn['@index'] = @index if @index
2157
- defn['@prefix'] = @prefix unless @prefix.nil?
2158
- defn
2307
+ # If container mapping was defined along with @set
2308
+ # @return [Boolean]
2309
+ def as_set?
2310
+ @as_set || false
2159
2311
  end
2160
- end
2161
2312
 
2162
- ##
2163
- # Turn this into a source for a new instantiation
2164
- # FIXME: context serialization
2165
- # @return [String]
2166
- def to_rb
2167
- defn = [%(TermDefinition.new\(#{term.inspect})]
2168
- %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
2169
- v = instance_variable_get("@#{acc}".to_sym)
2170
- v = v.to_s if v.is_a?(RDF::Term)
2171
- if acc == 'container_mapping'
2172
- v = v.to_a
2173
- v << '@set' if as_set?
2174
- v = v.first if v.length <= 1
2175
- end
2176
- defn << "#{acc}: #{v.inspect}" if v
2177
- end
2178
- defn.join(', ') + ")"
2179
- 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
2180
2331
 
2181
- # If container mapping was defined along with @set
2182
- # @return [Boolean]
2183
- 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
2184
2350
 
2185
- # Check if term definitions are identical, modulo @protected
2186
- # @return [Boolean]
2187
- def ==(other)
2188
- other.is_a?(TermDefinition) &&
2189
- id == other.id &&
2190
- term == other.term &&
2191
- type_mapping == other.type_mapping &&
2192
- container_mapping == other.container_mapping &&
2193
- nest == other.nest &&
2194
- language_mapping == other.language_mapping &&
2195
- direction_mapping == other.direction_mapping &&
2196
- reverse_property == other.reverse_property &&
2197
- simple == other.simple &&
2198
- index == other.index &&
2199
- context == other.context &&
2200
- prefix? == other.prefix? &&
2201
- as_set? == other.as_set?
2202
- end
2351
+ private
2203
2352
 
2204
- def inspect
2205
- v = %w([TD)
2206
- v << "id=#{@id}"
2207
- v << "index=#{index.inspect}" unless index.nil?
2208
- v << "term=#{@term}"
2209
- v << "rev" if reverse_property
2210
- v << "container=#{container_mapping}" if container_mapping
2211
- v << "as_set=#{as_set?.inspect}"
2212
- v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
2213
- v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
2214
- v << "type=#{type_mapping}" unless type_mapping.nil?
2215
- v << "nest=#{nest.inspect}" unless nest.nil?
2216
- v << "simple=true" if @simple
2217
- v << "protected=true" if @protected
2218
- v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
2219
- v << "has-context" unless context.nil?
2220
- v.join(" ") + "]"
2353
+ def prefix_colon
2354
+ @prefix_colon ||= "#{term}:".freeze
2355
+ end
2221
2356
  end
2222
2357
  end
2223
2358
  end