json-ld 3.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +1 -1
  3. data/README.md +90 -53
  4. data/UNLICENSE +1 -1
  5. data/VERSION +1 -1
  6. data/bin/jsonld +4 -4
  7. data/lib/json/ld.rb +27 -10
  8. data/lib/json/ld/api.rb +325 -96
  9. data/lib/json/ld/compact.rb +75 -27
  10. data/lib/json/ld/conneg.rb +188 -0
  11. data/lib/json/ld/context.rb +677 -292
  12. data/lib/json/ld/expand.rb +240 -75
  13. data/lib/json/ld/flatten.rb +5 -3
  14. data/lib/json/ld/format.rb +19 -19
  15. data/lib/json/ld/frame.rb +135 -85
  16. data/lib/json/ld/from_rdf.rb +44 -17
  17. data/lib/json/ld/html/nokogiri.rb +151 -0
  18. data/lib/json/ld/html/rexml.rb +186 -0
  19. data/lib/json/ld/reader.rb +25 -5
  20. data/lib/json/ld/resource.rb +2 -2
  21. data/lib/json/ld/streaming_writer.rb +3 -1
  22. data/lib/json/ld/to_rdf.rb +47 -17
  23. data/lib/json/ld/utils.rb +4 -2
  24. data/lib/json/ld/writer.rb +75 -14
  25. data/spec/api_spec.rb +13 -34
  26. data/spec/compact_spec.rb +968 -9
  27. data/spec/conneg_spec.rb +373 -0
  28. data/spec/context_spec.rb +447 -53
  29. data/spec/expand_spec.rb +1872 -416
  30. data/spec/flatten_spec.rb +434 -47
  31. data/spec/frame_spec.rb +979 -344
  32. data/spec/from_rdf_spec.rb +305 -5
  33. data/spec/spec_helper.rb +177 -0
  34. data/spec/streaming_writer_spec.rb +4 -4
  35. data/spec/suite_compact_spec.rb +2 -2
  36. data/spec/suite_expand_spec.rb +14 -2
  37. data/spec/suite_flatten_spec.rb +10 -2
  38. data/spec/suite_frame_spec.rb +3 -2
  39. data/spec/suite_from_rdf_spec.rb +2 -2
  40. data/spec/suite_helper.rb +55 -20
  41. data/spec/suite_html_spec.rb +22 -0
  42. data/spec/suite_http_spec.rb +35 -0
  43. data/spec/suite_remote_doc_spec.rb +2 -2
  44. data/spec/suite_to_rdf_spec.rb +14 -3
  45. data/spec/support/extensions.rb +5 -1
  46. data/spec/test-files/test-4-input.json +3 -3
  47. data/spec/test-files/test-5-input.json +2 -2
  48. data/spec/test-files/test-8-framed.json +14 -18
  49. data/spec/to_rdf_spec.rb +606 -16
  50. data/spec/writer_spec.rb +5 -5
  51. metadata +144 -88
@@ -142,8 +142,8 @@ module JSON::LD
142
142
  #
143
143
  # @param [Hash] options
144
144
  # @return [String] serizlied JSON representation of resource
145
- def to_json(options = nil)
146
- deresolve.to_json(options)
145
+ def to_json(**options)
146
+ deresolve.to_json(**options)
147
147
  end
148
148
 
149
149
  ##
@@ -60,6 +60,8 @@ module JSON::LD
60
60
 
61
61
  pd << if statement.object.resource?
62
62
  {'@id' => statement.object.to_s}
63
+ elsif statement.object.datatype == RDF::URI(RDF.to_uri + "JSON")
64
+ {"@value" => MultiJson.load(statement.object.to_s), "@type" => "@json"}
63
65
  else
64
66
  lit = {"@value" => statement.object.to_s}
65
67
  lit["@type"] = statement.object.datatype.to_s if statement.object.has_datatype?
@@ -110,7 +112,7 @@ module JSON::LD
110
112
  @output.puts(",") if [:wrote_node, :wrote_graph].include?(@state)
111
113
  if @current_node_def
112
114
  node_def = if context
113
- compacted = JSON::LD::API.compact(@current_node_def, context, rename_bnodes: false)
115
+ compacted = JSON::LD::API.compact(@current_node_def, context, rename_bnodes: false, **@options)
114
116
  compacted.delete('@context')
115
117
  compacted
116
118
  else
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
  require 'rdf'
4
4
  require 'rdf/nquads'
5
+ require 'json/canonicalization'
5
6
 
6
7
  module JSON::LD
7
8
  module ToRDF
@@ -18,6 +19,8 @@ module JSON::LD
18
19
  if value?(item)
19
20
  value, datatype = item.fetch('@value'), item.fetch('@type', nil)
20
21
 
22
+ datatype = RDF::URI(RDF.to_uri + "JSON") if datatype == '@json'
23
+
21
24
  case value
22
25
  when TrueClass, FalseClass
23
26
  # If value is true or false, then set value its canonical lexical form as defined in the section Data Round Tripping. If datatype is null, set it to xsd:boolean.
@@ -25,17 +28,48 @@ module JSON::LD
25
28
  datatype ||= RDF::XSD.boolean.to_s
26
29
  when Numeric
27
30
  # Otherwise, if value is a number, then set value to its canonical lexical form as defined in the section Data Round Tripping. If datatype is null, set it to either xsd:integer or xsd:double, depending on if the value contains a fractional and/or an exponential component.
28
- lit = RDF::Literal.new(value, canonicalize: true)
29
- value = lit.to_s
30
- datatype ||= lit.datatype
31
+ value = if datatype == RDF::URI(RDF.to_uri + "JSON")
32
+ value.to_json_c14n
33
+ else
34
+ # Don't serialize as double if there are no fractional bits
35
+ as_double = value.ceil != value || value >= 1e21 || datatype == RDF::XSD.double
36
+ lit = if as_double
37
+ RDF::Literal::Double.new(value, canonicalize: true)
38
+ else
39
+ RDF::Literal.new(value.numerator, canonicalize: true)
40
+ end
41
+
42
+ datatype ||= lit.datatype
43
+ lit.to_s.sub("E+", "E")
44
+ end
45
+ when Array, Hash
46
+ # Only valid for rdf:JSON
47
+ value = value.to_json_c14n
31
48
  else
49
+ if item.has_key?('@direction') && @options[:rdfDirection]
50
+ # Either serialize using a datatype, or a compound-literal
51
+ case @options[:rdfDirection]
52
+ when 'i18n-datatype'
53
+ datatype = RDF::URI("https://www.w3.org/ns/i18n##{item.fetch('@language', '')}_#{item['@direction']}")
54
+ when 'compound-literal'
55
+ cl = RDF::Node.new
56
+ yield RDF::Statement(cl, RDF.value, item['@value'].to_s)
57
+ yield RDF::Statement(cl, RDF.to_uri + 'language', item['@language']) if item['@language']
58
+ yield RDF::Statement(cl, RDF.to_uri + 'direction', item['@direction'])
59
+ return cl
60
+ end
61
+ end
62
+
32
63
  # Otherwise, if datatype is null, set it to xsd:string or xsd:langString, depending on if item has a @language key.
33
64
  datatype ||= item.has_key?('@language') ? RDF.langString : RDF::XSD.string
65
+ if datatype == RDF::URI(RDF.to_uri + "JSON")
66
+ value = value.to_json_c14n
67
+ end
34
68
  end
35
69
  datatype = RDF::URI(datatype) if datatype && !datatype.is_a?(RDF::URI)
36
70
 
37
71
  # Initialize literal as an RDF literal using value and datatype. If element has the key @language and datatype is xsd:string, then add the value associated with the @language key as the language of the object.
38
- language = item.fetch('@language', nil)
72
+ language = item.fetch('@language', nil) if datatype == RDF.langString
39
73
  return RDF::Literal.new(value, datatype: datatype, language: language)
40
74
  elsif list?(item)
41
75
  # If item is a list object, initialize list_results as an empty array, and object to the result of the List Conversion algorithm, passing the value associated with the @list key from item and list_results.
@@ -65,21 +99,17 @@ module JSON::LD
65
99
  #log_debug("item_to_rdf") {"@reverse predicate: #{predicate.to_ntriples rescue 'malformed rdf'}"}
66
100
  # For each item in values
67
101
  vv.each do |v|
68
- if list?(v)
69
- #log_debug("item_to_rdf") {"list: #{v.inspect}"}
70
- object = item_to_rdf(v, graph_name: graph_name, &block)
71
-
72
- # Append a triple composed of object, prediate, and object to results and add all triples from list_results to results.
73
- yield RDF::Statement(object, predicate, subject, graph_name: graph_name)
74
- else
75
- # Otherwise, item is a value object or a node definition. Generate object as the result of the Object Converstion algorithm passing item.
76
- object = item_to_rdf(v, graph_name: graph_name, &block)
77
- #log_debug("item_to_rdf") {"subject: #{object.to_ntriples rescue 'malformed rdf'}"}
78
- # yield subject, prediate, and literal to results.
79
- yield RDF::Statement(object, predicate, subject, graph_name: graph_name)
80
- end
102
+ # Item is a node definition. Generate object as the result of the Object Converstion algorithm passing item.
103
+ object = item_to_rdf(v, graph_name: graph_name, &block)
104
+ #log_debug("item_to_rdf") {"subject: #{object.to_ntriples rescue 'malformed rdf'}"}
105
+ # yield subject, prediate, and literal to results.
106
+ yield RDF::Statement(object, predicate, subject, graph_name: graph_name)
81
107
  end
82
108
  end
109
+ when '@included'
110
+ values.each do |v|
111
+ item_to_rdf(v, graph_name: graph_name, &block)
112
+ end
83
113
  when /^@/
84
114
  # Otherwise, if @type is any other keyword, skip to the next property-values pair
85
115
  else
@@ -49,7 +49,7 @@ module JSON::LD
49
49
  ##
50
50
  # Is value an expaned @graph?
51
51
  #
52
- # Note: A value is a simple graph if all of these hold true:
52
+ # Note: A value is a graph if all of these hold true:
53
53
  # 1. It is an object.
54
54
  # 2. It has an `@graph` key.
55
55
  # 3. It may have '@context', '@id' or '@index'
@@ -59,8 +59,10 @@ module JSON::LD
59
59
  def graph?(value)
60
60
  value.is_a?(Hash) && (value.keys - UTIL_GRAPH_KEYS) == ['@graph']
61
61
  end
62
+
62
63
  ##
63
- # Is value a simple @graph (lacking @id)?
64
+ # Is value a simple graph (lacking @id)?
65
+ #
64
66
  # @param [Object] value
65
67
  # @return [Boolean]
66
68
  def simple_graph?(value)
@@ -1,6 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # frozen_string_literal: true
3
3
  require 'json/ld/streaming_writer'
4
+ require 'link_header'
5
+
4
6
  module JSON::LD
5
7
  ##
6
8
  # A JSON-LD parser in Ruby.
@@ -50,8 +52,8 @@ module JSON::LD
50
52
  #
51
53
  # Select the :expand option to output JSON-LD in expanded form
52
54
  #
53
- # @see http://json-ld.org/spec/ED/20110507/
54
- # @see http://json-ld.org/spec/ED/20110507/#the-normalization-algorithm
55
+ # @see https://www.w3.org/TR/json-ld11-api/
56
+ # @see https://www.w3.org/TR/json-ld11-api/#the-normalization-algorithm
55
57
  # @author [Gregg Kellogg](http://greggkellogg.net/)
56
58
  class Writer < RDF::Writer
57
59
  include StreamingWriter
@@ -94,17 +96,23 @@ module JSON::LD
94
96
  description: "Context to use when compacting.") {|arg| RDF::URI(arg)},
95
97
  RDF::CLI::Option.new(
96
98
  symbol: :embed,
97
- datatype: %w(@always @last @never),
98
- default: '@last',
99
+ datatype: %w(@always @once @never),
100
+ default: '@once',
99
101
  control: :select,
100
102
  on: ["--embed EMBED"],
101
- description: "How to embed matched objects (@last).") {|arg| RDF::URI(arg)},
103
+ description: "How to embed matched objects (@once).") {|arg| RDF::URI(arg)},
102
104
  RDF::CLI::Option.new(
103
105
  symbol: :explicit,
104
106
  datatype: TrueClass,
105
107
  control: :checkbox,
106
108
  on: ["--[no-]explicit"],
107
109
  description: "Only include explicitly declared properties in output (false)") {|arg| arg},
110
+ RDF::CLI::Option.new(
111
+ symbol: :lowercaseLanguage,
112
+ datatype: TrueClass,
113
+ control: :checkbox,
114
+ on: ["--[no-]lowercase-language"],
115
+ description: "By default, language tags are left as is. To normalize to lowercase, set this option to `true`."),
108
116
  RDF::CLI::Option.new(
109
117
  symbol: :omitDefault,
110
118
  datatype: TrueClass,
@@ -123,6 +131,13 @@ module JSON::LD
123
131
  control: :radio,
124
132
  on: ["--processingMode MODE", %w(json-ld-1.0 json-ld-1.1)],
125
133
  description: "Set Processing Mode (json-ld-1.0 or json-ld-1.1)"),
134
+ RDF::CLI::Option.new(
135
+ symbol: :rdfDirection,
136
+ datatype: %w(i18n-datatype compound-literal),
137
+ default: 'null',
138
+ control: :select,
139
+ on: ["--rdf-direction DIR", %w(i18n-datatype compound-literal)],
140
+ description: "How to serialize literal direction (i18n-datatype compound-literal)") {|arg| RDF::URI(arg)},
126
141
  RDF::CLI::Option.new(
127
142
  symbol: :requireAll,
128
143
  datatype: TrueClass,
@@ -136,6 +151,12 @@ module JSON::LD
136
151
  control: :checkbox,
137
152
  on: ["--[no-]stream"],
138
153
  description: "Do not attempt to optimize graph presentation, suitable for streaming large graphs.") {|arg| arg},
154
+ RDF::CLI::Option.new(
155
+ symbol: :useNativeTypes,
156
+ datatype: TrueClass,
157
+ control: :checkbox,
158
+ on: ["--[no-]use-native-types"],
159
+ description: "Use native JSON values in value objects.") {|arg| arg},
139
160
  RDF::CLI::Option.new(
140
161
  symbol: :useRdfType,
141
162
  datatype: TrueClass,
@@ -145,6 +166,41 @@ module JSON::LD
145
166
  ]
146
167
  end
147
168
 
169
+ class << self
170
+ attr_reader :white_list
171
+ attr_reader :black_list
172
+
173
+ ##
174
+ # Use parameters from accept-params to determine if the parameters are acceptable to invoke this writer. The `accept_params` will subsequently be provided to the writer instance.
175
+ #
176
+ # @param [Hash{Symbol => String}] accept_params
177
+ # @yield [accept_params] if a block is given, returns the result of evaluating that block
178
+ # @yieldparam [Hash{Symbol => String}] accept_params
179
+ # @return [Boolean]
180
+ # @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
181
+ def accept?(accept_params)
182
+ # Profiles that aren't specific IANA relations represent the URL
183
+ # of a context or frame that may be subject to black- or white-listing
184
+ profile = accept_params[:profile].to_s.split(/\s+/)
185
+
186
+ if block_given?
187
+ yield(accept_params)
188
+ else
189
+ true
190
+ end
191
+ end
192
+
193
+ ##
194
+ # Returns default context used for compacted profile without an explicit context URL
195
+ # @return [String]
196
+ def default_context; @default_context || JSON::LD::DEFAULT_CONTEXT; end
197
+
198
+ ##
199
+ # Sets default context used for compacted profile without an explicit context URL
200
+ # @param [String] url
201
+ def default_context=(url); @default_context = url; end
202
+ end
203
+
148
204
  ##
149
205
  # Initializes the RDF-LD writer instance.
150
206
  #
@@ -237,13 +293,19 @@ module JSON::LD
237
293
  else
238
294
 
239
295
  log_debug("writer") { "serialize #{@repo.count} statements, #{@options.inspect}"}
240
- result = API.fromRdf(@repo, @options)
296
+ result = API.fromRdf(@repo, **@options)
297
+
298
+ # Some options may be indicated from accept parameters
299
+ profile = @options.fetch(:accept_params, {}).fetch(:profile, "").split(' ')
300
+ links = LinkHeader.parse(@options[:link])
301
+ @options[:context] ||= links.find_link(['rel', JSON_LD_NS+"context"]).href rescue nil
302
+ @options[:context] ||= Writer.default_context if profile.include?(JSON_LD_NS+"compacted")
303
+ @options[:frame] ||= links.find_link(['rel', JSON_LD_NS+"frame"]).href rescue nil
241
304
 
242
305
  # If we were provided a context, or prefixes, use them to compact the output
243
- context = RDF::Util::File.open_file(@options[:context]) if @options[:context].is_a?(String)
244
- context ||= @options[:context]
306
+ context = @options[:context]
245
307
  context ||= if @options[:prefixes] || @options[:language] || @options[:standard_prefixes]
246
- ctx = Context.new(@options)
308
+ ctx = Context.new(**@options)
247
309
  ctx.language = @options[:language] if @options[:language]
248
310
  @options[:prefixes].each do |prefix, iri|
249
311
  ctx.set_mapping(prefix, iri) if prefix && iri
@@ -253,18 +315,17 @@ module JSON::LD
253
315
 
254
316
  # Rename BNodes to uniquify them, if necessary
255
317
  if options[:unique_bnodes]
256
- result = API.flatten(result, context, @options)
318
+ result = API.flatten(result, context, **@options)
257
319
  end
258
320
 
259
- frame = RDF::Util::File.open_file(@options[:frame]) if @options[:frame].is_a?(String)
260
- if frame ||= @options[:frame]
321
+ if frame = @options[:frame]
261
322
  # Perform framing, if given a frame
262
323
  log_debug("writer") { "frame result"}
263
- result = API.frame(result, frame, @options)
324
+ result = API.frame(result, frame, **@options)
264
325
  elsif context
265
326
  # Perform compaction, if we have a context
266
327
  log_debug("writer") { "compact result"}
267
- result = API.compact(result, context, @options)
328
+ result = API.compact(result, context, **@options)
268
329
  end
269
330
 
270
331
  @output.write(result.to_json(JSON_STATE))
@@ -1,3 +1,4 @@
1
+
1
2
  # coding: utf-8
2
3
  require_relative 'spec_helper'
3
4
 
@@ -8,54 +9,32 @@ describe JSON::LD::API do
8
9
  describe "#initialize" do
9
10
  context "with string input" do
10
11
  let(:context) do
11
- JSON::LD::API::RemoteDocument.new("http://example.com/context", %q({
12
+ JSON::LD::API::RemoteDocument.new(%q({
12
13
  "@context": {
13
14
  "xsd": "http://www.w3.org/2001/XMLSchema#",
14
15
  "name": "http://xmlns.com/foaf/0.1/name",
15
16
  "homepage": {"@id": "http://xmlns.com/foaf/0.1/homepage", "@type": "@id"},
16
17
  "avatar": {"@id": "http://xmlns.com/foaf/0.1/avatar", "@type": "@id"}
17
18
  }
18
- }))
19
+ }),
20
+ documentUrl: "http://example.com/context",
21
+ contentType: 'application/ld+json'
22
+ )
19
23
  end
20
24
  let(:remote_doc) do
21
- JSON::LD::API::RemoteDocument.new("http://example.com/foo", %q({
22
- "@id": "",
23
- "name": "foo"
24
- }), "http://example.com/context")
25
+ JSON::LD::API::RemoteDocument.new(%q({"@id": "", "name": "foo"}),
26
+ documentUrl: "http://example.com/foo",
27
+ contentType: 'application/ld+json',
28
+ contextUrl: "http://example.com/context"
29
+ )
25
30
  end
26
31
 
27
32
  it "loads document with loader and loads context" do
28
- expect(described_class).to receive(:documentLoader).with("http://example.com/foo", anything).and_return(remote_doc)
33
+ expect(described_class).to receive(:documentLoader).with("http://example.com/foo", anything).and_yield(remote_doc)
29
34
  expect(described_class).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(context)
30
35
  described_class.new("http://example.com/foo", nil)
31
36
  end
32
37
  end
33
-
34
- context "with RDF::Util::File::RemoteDoc input" do
35
- let(:context) do
36
- JSON::LD::API::RemoteDocument.new("http://example.com/context", %q({
37
- "@context": {
38
- "xsd": "http://www.w3.org/2001/XMLSchema#",
39
- "name": "http://xmlns.com/foaf/0.1/name",
40
- "homepage": {"@id": "http://xmlns.com/foaf/0.1/homepage", "@type": "@id"},
41
- "avatar": {"@id": "http://xmlns.com/foaf/0.1/avatar", "@type": "@id"}
42
- }
43
- }))
44
- end
45
- let(:remote_doc) do
46
- RDF::Util::File::RemoteDocument.new(%q({"@id": "", "name": "foo"}),
47
- headers: {
48
- content_type: 'application/json',
49
- link: %(<http://example.com/context>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json")
50
- }
51
- )
52
- end
53
-
54
- it "processes document and retrieves linked context" do
55
- expect(described_class).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(context)
56
- described_class.new(remote_doc, nil)
57
- end
58
- end
59
38
  end
60
39
 
61
40
  context "when validating", pending: ("JRuby support for jsonlint" if RUBY_ENGINE == "jruby") do
@@ -80,7 +59,7 @@ describe JSON::LD::API do
80
59
  it "expands" do
81
60
  options = {logger: logger, adapter: adapter}
82
61
  options[:expandContext] = File.open(context) if context
83
- jld = described_class.expand(File.open(filename), options)
62
+ jld = described_class.expand(File.open(filename), **options)
84
63
  expect(jld).to produce_jsonld(JSON.load(File.open(expanded)), logger)
85
64
  end if File.exist?(expanded)
86
65
 
@@ -125,6 +125,18 @@ describe JSON::LD::API do
125
125
  "b": ["c"]
126
126
  })
127
127
  },
128
+ "@set coercion on @type" => {
129
+ input: %({
130
+ "@type": "http://www.w3.org/2000/01/rdf-schema#Resource",
131
+ "http://example.org/foo": {"@value": "bar", "@type": "http://example.com/type"}
132
+ }),
133
+ context: %({"@version": 1.1, "@type": {"@container": "@set"}}),
134
+ output: %({
135
+ "@context": {"@version": 1.1, "@type": {"@container": "@set"}},
136
+ "@type": ["http://www.w3.org/2000/01/rdf-schema#Resource"],
137
+ "http://example.org/foo": {"@value": "bar", "@type": "http://example.com/type"}
138
+ })
139
+ },
128
140
  "empty @set coercion" => {
129
141
  input: %({
130
142
  "http://example.com/b": []
@@ -176,6 +188,25 @@ describe JSON::LD::API do
176
188
  "term5": [ "v5", "plain literal" ]
177
189
  })
178
190
  },
191
+ "default direction" => {
192
+ input: %({
193
+ "http://example.com/term": [
194
+ "v5",
195
+ {"@value": "plain literal"}
196
+ ]
197
+ }),
198
+ context: %({
199
+ "term5": {"@id": "http://example.com/term", "@direction": null},
200
+ "@direction": "ltr"
201
+ }),
202
+ output: %({
203
+ "@context": {
204
+ "term5": {"@id": "http://example.com/term", "@direction": null},
205
+ "@direction": "ltr"
206
+ },
207
+ "term5": [ "v5", "plain literal" ]
208
+ })
209
+ },
179
210
  }.each_pair do |title, params|
180
211
  it(title) {run_compact(params)}
181
212
  end
@@ -206,6 +237,19 @@ describe JSON::LD::API do
206
237
  "http://example.org/foo": {"@value": "bar", "type": "http://example.com/type"}
207
238
  })
208
239
  },
240
+ "@type with @container: @set": {
241
+ input: %({
242
+ "@type": "http://www.w3.org/2000/01/rdf-schema#Resource",
243
+ "http://example.org/foo": {"@value": "bar", "@type": "http://example.com/type"}
244
+ }),
245
+ context: %({"type": {"@id": "@type", "@container": "@set"}}),
246
+ output: %({
247
+ "@context": {"type": {"@id": "@type", "@container": "@set"}},
248
+ "type": ["http://www.w3.org/2000/01/rdf-schema#Resource"],
249
+ "http://example.org/foo": {"@value": "bar", "type": "http://example.com/type"}
250
+ }),
251
+ processingMode: 'json-ld-1.1'
252
+ },
209
253
  "@language" => {
210
254
  input: %({
211
255
  "http://example.org/foo": {"@value": "bar", "@language": "baz"}
@@ -216,6 +260,16 @@ describe JSON::LD::API do
216
260
  "http://example.org/foo": {"@value": "bar", "language": "baz"}
217
261
  })
218
262
  },
263
+ "@direction" => {
264
+ input: %({
265
+ "http://example.org/foo": {"@value": "bar", "@direction": "ltr"}
266
+ }),
267
+ context: %({"direction": "@direction"}),
268
+ output: %({
269
+ "@context": {"direction": "@direction"},
270
+ "http://example.org/foo": {"@value": "bar", "direction": "ltr"}
271
+ })
272
+ },
219
273
  "@value" => {
220
274
  input: %({
221
275
  "http://example.org/foo": {"@value": "bar", "@language": "baz"}
@@ -259,6 +313,22 @@ describe JSON::LD::API do
259
313
  "term5": "v1"
260
314
  })
261
315
  },
316
+ "Uses term with null direction when two terms conflict on direction" => {
317
+ input: %([{
318
+ "http://example.com/term": {"@value": "v1"}
319
+ }]),
320
+ context: %({
321
+ "term5": {"@id": "http://example.com/term","@direction": null},
322
+ "@direction": "ltr"
323
+ }),
324
+ output: %({
325
+ "@context": {
326
+ "term5": {"@id": "http://example.com/term","@direction": null},
327
+ "@direction": "ltr"
328
+ },
329
+ "term5": "v1"
330
+ })
331
+ },
262
332
  "Uses subject alias" => {
263
333
  input: %([{
264
334
  "@id": "http://example.com/id1",
@@ -489,7 +559,9 @@ describe JSON::LD::API do
489
559
 
490
560
  context "context as reference" do
491
561
  let(:remote_doc) do
492
- JSON::LD::API::RemoteDocument.new("http://example.com/context", %q({"@context": {"b": "http://example.com/b"}}))
562
+ JSON::LD::API::RemoteDocument.new(
563
+ %q({"@context": {"b": "http://example.com/b"}}),
564
+ documentUrl: "http://example.com/context")
493
565
  end
494
566
  it "uses referenced context" do
495
567
  input = ::JSON.parse %({
@@ -527,6 +599,26 @@ describe JSON::LD::API do
527
599
  "foo_de": ["de"]
528
600
  })
529
601
  },
602
+ "1 term 2 lists 2 directions" => {
603
+ input: %([{
604
+ "http://example.com/foo": [
605
+ {"@list": [{"@value": "en", "@direction": "ltr"}]},
606
+ {"@list": [{"@value": "ar", "@direction": "rtl"}]}
607
+ ]
608
+ }]),
609
+ context: %({
610
+ "foo_ltr": {"@id": "http://example.com/foo", "@container": "@list", "@direction": "ltr"},
611
+ "foo_rtl": {"@id": "http://example.com/foo", "@container": "@list", "@direction": "rtl"}
612
+ }),
613
+ output: %({
614
+ "@context": {
615
+ "foo_ltr": {"@id": "http://example.com/foo", "@container": "@list", "@direction": "ltr"},
616
+ "foo_rtl": {"@id": "http://example.com/foo", "@container": "@list", "@direction": "rtl"}
617
+ },
618
+ "foo_ltr": ["en"],
619
+ "foo_rtl": ["ar"]
620
+ })
621
+ },
530
622
  "coerced @list containing an empty list" => {
531
623
  input: %([{
532
624
  "http://example.com/foo": [{"@list": [{"@list": []}]}]
@@ -598,6 +690,145 @@ describe JSON::LD::API do
598
690
  end
599
691
  end
600
692
 
693
+ context "with @type: @json" do
694
+ {
695
+ "true": {
696
+ output: %({
697
+ "@context": {
698
+ "@version": 1.1,
699
+ "e": {"@id": "http://example.org/vocab#bool", "@type": "@json"}
700
+ },
701
+ "e": true
702
+ }),
703
+ input:%( [{
704
+ "http://example.org/vocab#bool": [{"@value": true, "@type": "@json"}]
705
+ }]),
706
+ },
707
+ "false": {
708
+ output: %({
709
+ "@context": {
710
+ "@version": 1.1,
711
+ "e": {"@id": "http://example.org/vocab#bool", "@type": "@json"}
712
+ },
713
+ "e": false
714
+ }),
715
+ input: %([{
716
+ "http://example.org/vocab#bool": [{"@value": false, "@type": "@json"}]
717
+ }]),
718
+ },
719
+ "double": {
720
+ output: %({
721
+ "@context": {
722
+ "@version": 1.1,
723
+ "e": {"@id": "http://example.org/vocab#double", "@type": "@json"}
724
+ },
725
+ "e": 1.23
726
+ }),
727
+ input: %([{
728
+ "http://example.org/vocab#double": [{"@value": 1.23, "@type": "@json"}]
729
+ }]),
730
+ },
731
+ "double-zero": {
732
+ output: %({
733
+ "@context": {
734
+ "@version": 1.1,
735
+ "e": {"@id": "http://example.org/vocab#double", "@type": "@json"}
736
+ },
737
+ "e": 0.0e0
738
+ }),
739
+ input: %([{
740
+ "http://example.org/vocab#double": [{"@value": 0.0e0, "@type": "@json"}]
741
+ }]),
742
+ },
743
+ "integer": {
744
+ output: %({
745
+ "@context": {
746
+ "@version": 1.1,
747
+ "e": {"@id": "http://example.org/vocab#integer", "@type": "@json"}
748
+ },
749
+ "e": 123
750
+ }),
751
+ input: %([{
752
+ "http://example.org/vocab#integer": [{"@value": 123, "@type": "@json"}]
753
+ }]),
754
+ },
755
+ "string": {
756
+ input: %([{
757
+ "http://example.org/vocab#string": [{
758
+ "@value": "string",
759
+ "@type": "@json"
760
+ }]
761
+ }]),
762
+ output: %({
763
+ "@context": {
764
+ "@version": 1.1,
765
+ "e": {"@id": "http://example.org/vocab#string", "@type": "@json"}
766
+ },
767
+ "e": "string"
768
+ })
769
+ },
770
+ "null": {
771
+ input: %([{
772
+ "http://example.org/vocab#null": [{
773
+ "@value": null,
774
+ "@type": "@json"
775
+ }]
776
+ }]),
777
+ output: %({
778
+ "@context": {
779
+ "@version": 1.1,
780
+ "e": {"@id": "http://example.org/vocab#null", "@type": "@json"}
781
+ },
782
+ "e": null
783
+ })
784
+ },
785
+ "object": {
786
+ output: %({
787
+ "@context": {
788
+ "@version": 1.1,
789
+ "e": {"@id": "http://example.org/vocab#object", "@type": "@json"}
790
+ },
791
+ "e": {"foo": "bar"}
792
+ }),
793
+ input: %([{
794
+ "http://example.org/vocab#object": [{"@value": {"foo": "bar"}, "@type": "@json"}]
795
+ }]),
796
+ },
797
+ "array": {
798
+ output: %({
799
+ "@context": {
800
+ "@version": 1.1,
801
+ "e": {"@id": "http://example.org/vocab#array", "@type": "@json", "@container": "@set"}
802
+ },
803
+ "e": [{"foo": "bar"}]
804
+ }),
805
+ input: %([{
806
+ "http://example.org/vocab#array": [{"@value": [{"foo": "bar"}], "@type": "@json"}]
807
+ }]),
808
+ },
809
+ "Already expanded object": {
810
+ output: %({
811
+ "@context": {"@version": 1.1},
812
+ "http://example.org/vocab#object": {"@value": {"foo": "bar"}, "@type": "@json"}
813
+ }),
814
+ input: %([{
815
+ "http://example.org/vocab#object": [{"@value": {"foo": "bar"}, "@type": "@json"}]
816
+ }]),
817
+ },
818
+ "Already expanded object with aliased keys": {
819
+ output: %({
820
+ "@context": {"@version": 1.1, "value": "@value", "type": "@type", "json": "@json"},
821
+ "http://example.org/vocab#object": {"value": {"foo": "bar"}, "type": "json"}
822
+ }),
823
+ input: %([{
824
+ "http://example.org/vocab#object": [{"@value": {"foo": "bar"}, "@type": "@json"}]
825
+ }])
826
+ },
827
+ }.each do |title, params|
828
+ it(title) {run_compact(processingMode: 'json-ld-1.1', **params)}
829
+ end
830
+ end
831
+
601
832
  context "@container: @index" do
602
833
  {
603
834
  "compact-0029" => {
@@ -725,6 +956,164 @@ describe JSON::LD::API do
725
956
  }.each_pair do |title, params|
726
957
  it(title) {run_compact(params)}
727
958
  end
959
+
960
+ context "@index: property" do
961
+ {
962
+ "property-valued index indexes property value, instead of property (value)": {
963
+ output: %({
964
+ "@context": {
965
+ "@version": 1.1,
966
+ "@base": "http://example.com/",
967
+ "@vocab": "http://example.com/",
968
+ "author": {"@type": "@id", "@container": "@index", "@index": "prop"}
969
+ },
970
+ "@id": "article",
971
+ "author": {
972
+ "regular": {"@id": "person/1"},
973
+ "guest": [{"@id": "person/2"}, {"@id": "person/3"}]
974
+ }
975
+ }),
976
+ input: %([{
977
+ "@id": "http://example.com/article",
978
+ "http://example.com/author": [
979
+ {"@id": "http://example.com/person/1", "http://example.com/prop": [{"@value": "regular"}]},
980
+ {"@id": "http://example.com/person/2", "http://example.com/prop": [{"@value": "guest"}]},
981
+ {"@id": "http://example.com/person/3", "http://example.com/prop": [{"@value": "guest"}]}
982
+ ]
983
+ }])
984
+ },
985
+ "property-valued index indexes property value, instead of @index (multiple values)": {
986
+ output: %({
987
+ "@context": {
988
+ "@version": 1.1,
989
+ "@base": "http://example.com/",
990
+ "@vocab": "http://example.com/",
991
+ "author": {"@type": "@id", "@container": "@index", "@index": "prop"}
992
+ },
993
+ "@id": "article",
994
+ "author": {
995
+ "regular": {"@id": "person/1", "prop": "foo"},
996
+ "guest": [
997
+ {"@id": "person/2", "prop": "foo"},
998
+ {"@id": "person/3", "prop": "foo"}
999
+ ]
1000
+ }
1001
+ }),
1002
+ input: %([{
1003
+ "@id": "http://example.com/article",
1004
+ "http://example.com/author": [
1005
+ {"@id": "http://example.com/person/1", "http://example.com/prop": [{"@value": "regular"}, {"@value": "foo"}]},
1006
+ {"@id": "http://example.com/person/2", "http://example.com/prop": [{"@value": "guest"}, {"@value": "foo"}]},
1007
+ {"@id": "http://example.com/person/3", "http://example.com/prop": [{"@value": "guest"}, {"@value": "foo"}]}
1008
+ ]
1009
+ }])
1010
+ },
1011
+ "property-valued index extracts property value, instead of @index (node)": {
1012
+ output: %({
1013
+ "@context": {
1014
+ "@version": 1.1,
1015
+ "@base": "http://example.com/",
1016
+ "@vocab": "http://example.com/",
1017
+ "author": {"@type": "@vocab", "@container": "@index", "@index": "prop"},
1018
+ "prop": {"@type": "@id"}
1019
+ },
1020
+ "@id": "article",
1021
+ "author": {
1022
+ "regular": {"@id": "person/1"},
1023
+ "guest": [
1024
+ {"@id": "person/2"},
1025
+ {"@id": "person/3"}
1026
+ ]
1027
+ }
1028
+ }),
1029
+ input: %([{
1030
+ "@id": "http://example.com/article",
1031
+ "http://example.com/author": [
1032
+ {"@id": "http://example.com/person/1", "http://example.com/prop": [{"@id": "http://example.com/regular"}]},
1033
+ {"@id": "http://example.com/person/2", "http://example.com/prop": [{"@id": "http://example.com/guest"}]},
1034
+ {"@id": "http://example.com/person/3", "http://example.com/prop": [{"@id": "http://example.com/guest"}]}
1035
+ ]
1036
+ }])
1037
+ },
1038
+ "property-valued index indexes property value, instead of property (multimple nodes)": {
1039
+ output: %({
1040
+ "@context": {
1041
+ "@version": 1.1,
1042
+ "@base": "http://example.com/",
1043
+ "@vocab": "http://example.com/",
1044
+ "author": {"@type": "@vocab", "@container": "@index", "@index": "prop"},
1045
+ "prop": {"@type": "@id"}
1046
+ },
1047
+ "@id": "article",
1048
+ "author": {
1049
+ "regular": {"@id": "person/1", "prop": "foo"},
1050
+ "guest": [
1051
+ {"@id": "person/2", "prop": "foo"},
1052
+ {"@id": "person/3", "prop": "foo"}
1053
+ ]
1054
+ }
1055
+ }),
1056
+ input: %([{
1057
+ "@id": "http://example.com/article",
1058
+ "http://example.com/author": [
1059
+ {"@id": "http://example.com/person/1", "http://example.com/prop": [{"@id": "http://example.com/regular"}, {"@id": "http://example.com/foo"}]},
1060
+ {"@id": "http://example.com/person/2", "http://example.com/prop": [{"@id": "http://example.com/guest"}, {"@id": "http://example.com/foo"}]},
1061
+ {"@id": "http://example.com/person/3", "http://example.com/prop": [{"@id": "http://example.com/guest"}, {"@id": "http://example.com/foo"}]}
1062
+ ]
1063
+ }])
1064
+ },
1065
+ "property-valued index indexes using @none if no property value exists": {
1066
+ output: %({
1067
+ "@context": {
1068
+ "@version": 1.1,
1069
+ "@base": "http://example.com/",
1070
+ "@vocab": "http://example.com/",
1071
+ "author": {"@type": "@id", "@container": "@index", "@index": "prop"}
1072
+ },
1073
+ "@id": "article",
1074
+ "author": {
1075
+ "@none": ["person/1", "person/2", "person/3"]
1076
+ }
1077
+ }),
1078
+ input: %([{
1079
+ "@id": "http://example.com/article",
1080
+ "http://example.com/author": [
1081
+ {"@id": "http://example.com/person/1"},
1082
+ {"@id": "http://example.com/person/2"},
1083
+ {"@id": "http://example.com/person/3"}
1084
+ ]
1085
+ }])
1086
+ },
1087
+ "property-valued index indexes using @none if no property value does not compact to string": {
1088
+ output: %({
1089
+ "@context": {
1090
+ "@version": 1.1,
1091
+ "@base": "http://example.com/",
1092
+ "@vocab": "http://example.com/",
1093
+ "author": {"@type": "@id", "@container": "@index", "@index": "prop"}
1094
+ },
1095
+ "@id": "article",
1096
+ "author": {
1097
+ "@none": [
1098
+ {"@id": "person/1", "prop": {"@id": "regular"}},
1099
+ {"@id": "person/2", "prop": {"@id": "guest"}},
1100
+ {"@id": "person/3", "prop": {"@id": "guest"}}
1101
+ ]
1102
+ }
1103
+ }),
1104
+ input: %([{
1105
+ "@id": "http://example.com/article",
1106
+ "http://example.com/author": [
1107
+ {"@id": "http://example.com/person/1", "http://example.com/prop": [{"@id": "http://example.com/regular"}]},
1108
+ {"@id": "http://example.com/person/2", "http://example.com/prop": [{"@id": "http://example.com/guest"}]},
1109
+ {"@id": "http://example.com/person/3", "http://example.com/prop": [{"@id": "http://example.com/guest"}]}
1110
+ ]
1111
+ }])
1112
+ }
1113
+ }.each do |title, params|
1114
+ it(title) {run_compact(**params)}
1115
+ end
1116
+ end
728
1117
  end
729
1118
 
730
1119
  context "@container: @language" do
@@ -816,6 +1205,173 @@ describe JSON::LD::API do
816
1205
  }),
817
1206
  processingMode: "json-ld-1.1"
818
1207
  },
1208
+ "simple map with term direction": {
1209
+ input: %([
1210
+ {
1211
+ "@id": "http://example.com/queen",
1212
+ "http://example.com/vocab/label": [
1213
+ {"@value": "Die Königin", "@language": "de", "@direction": "ltr"},
1214
+ {"@value": "Ihre Majestät", "@language": "de", "@direction": "ltr"},
1215
+ {"@value": "The Queen", "@language": "en", "@direction": "ltr"}
1216
+ ]
1217
+ }
1218
+ ]),
1219
+ context: %({
1220
+ "@context": {
1221
+ "@version": 1.1,
1222
+ "vocab": "http://example.com/vocab/",
1223
+ "label": {
1224
+ "@id": "vocab:label",
1225
+ "@direction": "ltr",
1226
+ "@container": "@language"
1227
+ }
1228
+ }
1229
+ }),
1230
+ output: %({
1231
+ "@context": {
1232
+ "@version": 1.1,
1233
+ "vocab": "http://example.com/vocab/",
1234
+ "label": {
1235
+ "@id": "vocab:label",
1236
+ "@direction": "ltr",
1237
+ "@container": "@language"
1238
+ }
1239
+ },
1240
+ "@id": "http://example.com/queen",
1241
+ "label": {
1242
+ "en": "The Queen",
1243
+ "de": [ "Die Königin", "Ihre Majestät" ]
1244
+ }
1245
+ }),
1246
+ processingMode: "json-ld-1.1"
1247
+ },
1248
+ "simple map with overriding term direction": {
1249
+ input: %([
1250
+ {
1251
+ "@id": "http://example.com/queen",
1252
+ "http://example.com/vocab/label": [
1253
+ {"@value": "Die Königin", "@language": "de", "@direction": "ltr"},
1254
+ {"@value": "Ihre Majestät", "@language": "de", "@direction": "ltr"},
1255
+ {"@value": "The Queen", "@language": "en", "@direction": "ltr"}
1256
+ ]
1257
+ }
1258
+ ]),
1259
+ context: %({
1260
+ "@context": {
1261
+ "@version": 1.1,
1262
+ "@direction": "rtl",
1263
+ "vocab": "http://example.com/vocab/",
1264
+ "label": {
1265
+ "@id": "vocab:label",
1266
+ "@direction": "ltr",
1267
+ "@container": "@language"
1268
+ }
1269
+ }
1270
+ }),
1271
+ output: %({
1272
+ "@context": {
1273
+ "@version": 1.1,
1274
+ "@direction": "rtl",
1275
+ "vocab": "http://example.com/vocab/",
1276
+ "label": {
1277
+ "@id": "vocab:label",
1278
+ "@direction": "ltr",
1279
+ "@container": "@language"
1280
+ }
1281
+ },
1282
+ "@id": "http://example.com/queen",
1283
+ "label": {
1284
+ "en": "The Queen",
1285
+ "de": [ "Die Königin", "Ihre Majestät" ]
1286
+ }
1287
+ }),
1288
+ processingMode: "json-ld-1.1"
1289
+ },
1290
+ "simple map with overriding null direction": {
1291
+ input: %([
1292
+ {
1293
+ "@id": "http://example.com/queen",
1294
+ "http://example.com/vocab/label": [
1295
+ {"@value": "Die Königin", "@language": "de"},
1296
+ {"@value": "Ihre Majestät", "@language": "de"},
1297
+ {"@value": "The Queen", "@language": "en"}
1298
+ ]
1299
+ }
1300
+ ]),
1301
+ context: %({
1302
+ "@context": {
1303
+ "@version": 1.1,
1304
+ "@direction": "rtl",
1305
+ "vocab": "http://example.com/vocab/",
1306
+ "label": {
1307
+ "@id": "vocab:label",
1308
+ "@direction": null,
1309
+ "@container": "@language"
1310
+ }
1311
+ }
1312
+ }),
1313
+ output: %({
1314
+ "@context": {
1315
+ "@version": 1.1,
1316
+ "@direction": "rtl",
1317
+ "vocab": "http://example.com/vocab/",
1318
+ "label": {
1319
+ "@id": "vocab:label",
1320
+ "@direction": null,
1321
+ "@container": "@language"
1322
+ }
1323
+ },
1324
+ "@id": "http://example.com/queen",
1325
+ "label": {
1326
+ "en": "The Queen",
1327
+ "de": [ "Die Königin", "Ihre Majestät" ]
1328
+ }
1329
+ }),
1330
+ processingMode: "json-ld-1.1"
1331
+ },
1332
+ "simple map with mismatching term direction": {
1333
+ input: %([
1334
+ {
1335
+ "@id": "http://example.com/queen",
1336
+ "http://example.com/vocab/label": [
1337
+ {"@value": "Die Königin", "@language": "de"},
1338
+ {"@value": "Ihre Majestät", "@language": "de", "@direction": "ltr"},
1339
+ {"@value": "The Queen", "@language": "en", "@direction": "rtl"}
1340
+ ]
1341
+ }
1342
+ ]),
1343
+ context: %({
1344
+ "@context": {
1345
+ "@version": 1.1,
1346
+ "vocab": "http://example.com/vocab/",
1347
+ "label": {
1348
+ "@id": "vocab:label",
1349
+ "@direction": "rtl",
1350
+ "@container": "@language"
1351
+ }
1352
+ }
1353
+ }),
1354
+ output: %({
1355
+ "@context": {
1356
+ "@version": 1.1,
1357
+ "vocab": "http://example.com/vocab/",
1358
+ "label": {
1359
+ "@id": "vocab:label",
1360
+ "@direction": "rtl",
1361
+ "@container": "@language"
1362
+ }
1363
+ },
1364
+ "@id": "http://example.com/queen",
1365
+ "label": {
1366
+ "en": "The Queen"
1367
+ },
1368
+ "vocab:label": [
1369
+ {"@value": "Die Königin", "@language": "de"},
1370
+ {"@value": "Ihre Majestät", "@language": "de", "@direction": "ltr"}
1371
+ ]
1372
+ }),
1373
+ processingMode: "json-ld-1.1"
1374
+ },
819
1375
  }.each_pair do |title, params|
820
1376
  it(title) {run_compact(params)}
821
1377
  end
@@ -1160,6 +1716,35 @@ describe JSON::LD::API do
1160
1716
  }
1161
1717
  })
1162
1718
  },
1719
+ "Compacts simple graph with @index and multiple nodes" => {
1720
+ input: %([{
1721
+ "http://example.org/input": [{
1722
+ "@graph": [{
1723
+ "http://example.org/value": [{"@value": "x"}]
1724
+ }, {
1725
+ "http://example.org/value": [{"@value": "y"}]
1726
+ }],
1727
+ "@index": "ndx"
1728
+ }]
1729
+ }]),
1730
+ context: %({
1731
+ "@vocab": "http://example.org/",
1732
+ "input": {"@container": "@graph"}
1733
+ }),
1734
+ output: %({
1735
+ "@context": {
1736
+ "@vocab": "http://example.org/",
1737
+ "input": {"@container": "@graph"}
1738
+ },
1739
+ "input": {
1740
+ "@included": [{
1741
+ "value": "x"
1742
+ }, {
1743
+ "value": "y"
1744
+ }]
1745
+ }
1746
+ })
1747
+ },
1163
1748
  "Does not compact graph with @id" => {
1164
1749
  input: %([{
1165
1750
  "http://example.org/input": [{
@@ -1501,6 +2086,116 @@ describe JSON::LD::API do
1501
2086
  end
1502
2087
  end
1503
2088
 
2089
+ context "@included" do
2090
+ {
2091
+ "Basic Included array": {
2092
+ output: %({
2093
+ "@context": {
2094
+ "@version": 1.1,
2095
+ "@vocab": "http://example.org/",
2096
+ "included": {"@id": "@included", "@container": "@set"}
2097
+ },
2098
+ "prop": "value",
2099
+ "included": [{
2100
+ "prop": "value2"
2101
+ }]
2102
+ }),
2103
+ input: %([{
2104
+ "http://example.org/prop": [{"@value": "value"}],
2105
+ "@included": [{
2106
+ "http://example.org/prop": [{"@value": "value2"}]
2107
+ }]
2108
+ }])
2109
+ },
2110
+ "Basic Included object": {
2111
+ output: %({
2112
+ "@context": {
2113
+ "@version": 1.1,
2114
+ "@vocab": "http://example.org/"
2115
+ },
2116
+ "prop": "value",
2117
+ "@included": {
2118
+ "prop": "value2"
2119
+ }
2120
+ }),
2121
+ input: %([{
2122
+ "http://example.org/prop": [{"@value": "value"}],
2123
+ "@included": [{
2124
+ "http://example.org/prop": [{"@value": "value2"}]
2125
+ }]
2126
+ }])
2127
+ },
2128
+ "Multiple properties mapping to @included are folded together": {
2129
+ output: %({
2130
+ "@context": {
2131
+ "@version": 1.1,
2132
+ "@vocab": "http://example.org/",
2133
+ "included1": "@included",
2134
+ "included2": "@included"
2135
+ },
2136
+ "included1": [
2137
+ {"prop": "value1"},
2138
+ {"prop": "value2"}
2139
+ ]
2140
+ }),
2141
+ input: %([{
2142
+ "@included": [
2143
+ {"http://example.org/prop": [{"@value": "value1"}]},
2144
+ {"http://example.org/prop": [{"@value": "value2"}]}
2145
+ ]
2146
+ }])
2147
+ },
2148
+ "Included containing @included": {
2149
+ output: %({
2150
+ "@context": {
2151
+ "@version": 1.1,
2152
+ "@vocab": "http://example.org/"
2153
+ },
2154
+ "prop": "value",
2155
+ "@included": {
2156
+ "prop": "value2",
2157
+ "@included": {
2158
+ "prop": "value3"
2159
+ }
2160
+ }
2161
+ }),
2162
+ input: %([{
2163
+ "http://example.org/prop": [{"@value": "value"}],
2164
+ "@included": [{
2165
+ "http://example.org/prop": [{"@value": "value2"}],
2166
+ "@included": [{
2167
+ "http://example.org/prop": [{"@value": "value3"}]
2168
+ }]
2169
+ }]
2170
+ }])
2171
+ },
2172
+ "Property value with @included": {
2173
+ output: %({
2174
+ "@context": {
2175
+ "@version": 1.1,
2176
+ "@vocab": "http://example.org/"
2177
+ },
2178
+ "prop": {
2179
+ "@type": "Foo",
2180
+ "@included": {
2181
+ "@type": "Bar"
2182
+ }
2183
+ }
2184
+ }),
2185
+ input: %([{
2186
+ "http://example.org/prop": [{
2187
+ "@type": ["http://example.org/Foo"],
2188
+ "@included": [{
2189
+ "@type": ["http://example.org/Bar"]
2190
+ }]
2191
+ }]
2192
+ }])
2193
+ },
2194
+ }.each do |title, params|
2195
+ it(title) {run_compact(params)}
2196
+ end
2197
+ end
2198
+
1504
2199
  context "@nest" do
1505
2200
  {
1506
2201
  "Indexes to @nest for property with @nest" => {
@@ -1895,7 +2590,7 @@ describe JSON::LD::API do
1895
2590
  "c": "C in example"
1896
2591
  }),
1897
2592
  },
1898
- "Raises InvalidTermDefinition if processingMode is not specified" => {
2593
+ "Raises InvalidTermDefinition if processingMode is 1.0" => {
1899
2594
  input: %([{
1900
2595
  "http://example/foo": [{"http://example.org/bar": [{"@value": "baz"}]}]
1901
2596
  }]),
@@ -1903,10 +2598,59 @@ describe JSON::LD::API do
1903
2598
  "@vocab": "http://example/",
1904
2599
  "foo": {"@context": {"bar": "http://example.org/bar"}}
1905
2600
  }),
1906
- processingMode: nil,
2601
+ processingMode: 'json-ld-1.0',
1907
2602
  validate: true,
1908
2603
  exception: JSON::LD::JsonLdError::InvalidTermDefinition
1909
2604
  },
2605
+ "Scoped on id map": {
2606
+ output: %({
2607
+ "@context": {
2608
+ "@version": 1.1,
2609
+ "schema": "http://schema.org/",
2610
+ "name": "schema:name",
2611
+ "body": "schema:articleBody",
2612
+ "words": "schema:wordCount",
2613
+ "post": {
2614
+ "@id": "schema:blogPost",
2615
+ "@container": "@id",
2616
+ "@context": {
2617
+ "@base": "http://example.com/posts/"
2618
+ }
2619
+ }
2620
+ },
2621
+ "@id": "http://example.com/",
2622
+ "@type": "schema:Blog",
2623
+ "name": "World Financial News",
2624
+ "post": {
2625
+ "1/en": {
2626
+ "body": "World commodities were up today with heavy trading of crude oil...",
2627
+ "words": 1539
2628
+ },
2629
+ "1/de": {
2630
+ "body": "Die Werte an Warenbörsen stiegen im Sog eines starken Handels von Rohöl...",
2631
+ "words": 1204
2632
+ }
2633
+ }
2634
+ }),
2635
+ input: %([{
2636
+ "@id": "http://example.com/",
2637
+ "@type": ["http://schema.org/Blog"],
2638
+ "http://schema.org/name": [{"@value": "World Financial News"}],
2639
+ "http://schema.org/blogPost": [{
2640
+ "@id": "http://example.com/posts/1/en",
2641
+ "http://schema.org/articleBody": [
2642
+ {"@value": "World commodities were up today with heavy trading of crude oil..."}
2643
+ ],
2644
+ "http://schema.org/wordCount": [{"@value": 1539}]
2645
+ }, {
2646
+ "@id": "http://example.com/posts/1/de",
2647
+ "http://schema.org/articleBody": [
2648
+ {"@value": "Die Werte an Warenbörsen stiegen im Sog eines starken Handels von Rohöl..."}
2649
+ ],
2650
+ "http://schema.org/wordCount": [{"@value": 1204}]
2651
+ }]
2652
+ }])
2653
+ },
1910
2654
  }.each_pair do |title, params|
1911
2655
  it(title) {run_compact({processingMode: "json-ld-1.1"}.merge(params))}
1912
2656
  end
@@ -1981,7 +2725,7 @@ describe JSON::LD::API do
1981
2725
  "a": {"type": "Foo", "bar": "baz"}
1982
2726
  }),
1983
2727
  },
1984
- "deep @context affects nested nodes" => {
2728
+ "deep @context does not affect nested nodes" => {
1985
2729
  input: %([
1986
2730
  {
1987
2731
  "@type": ["http://example/Foo"],
@@ -2000,7 +2744,7 @@ describe JSON::LD::API do
2000
2744
  "Foo": {"@context": {"baz": {"@type": "@vocab"}}}
2001
2745
  },
2002
2746
  "@type": "Foo",
2003
- "bar": {"baz": "buzz"}
2747
+ "bar": {"baz": {"@id": "http://example/buzz"}}
2004
2748
  }),
2005
2749
  },
2006
2750
  "scoped context layers on intemediate contexts" => {
@@ -2073,7 +2817,7 @@ describe JSON::LD::API do
2073
2817
  }
2074
2818
  })
2075
2819
  },
2076
- "Raises InvalidTermDefinition if processingMode is not specified" => {
2820
+ "Raises InvalidTermDefinition if processingMode is 1.0" => {
2077
2821
  input: %([
2078
2822
  {
2079
2823
  "http://example/a": [{
@@ -2086,7 +2830,7 @@ describe JSON::LD::API do
2086
2830
  "@vocab": "http://example/",
2087
2831
  "Foo": {"@context": {"bar": "http://example.org/bar"}}
2088
2832
  }),
2089
- processingMode: nil,
2833
+ processingMode: 'json-ld-1.0',
2090
2834
  validate: true,
2091
2835
  exception: JSON::LD::JsonLdError::InvalidTermDefinition
2092
2836
  },
@@ -2227,16 +2971,231 @@ describe JSON::LD::API do
2227
2971
  end
2228
2972
  end
2229
2973
 
2974
+ context "html" do
2975
+ {
2976
+ "Compacts embedded JSON-LD script element": {
2977
+ input: %(
2978
+ <html>
2979
+ <head>
2980
+ <script type="application/ld+json">
2981
+ {
2982
+ "@context": {
2983
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
2984
+ },
2985
+ "foo": [{"@value": "bar"}]
2986
+ }
2987
+ </script>
2988
+ </head>
2989
+ </html>),
2990
+ context: %({"foo": {"@id": "http://example.com/foo", "@container": "@list"}}),
2991
+ output: %({
2992
+ "@context": {
2993
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
2994
+ },
2995
+ "foo": ["bar"]
2996
+ })
2997
+ },
2998
+ "Compacts first script element": {
2999
+ input: %(
3000
+ <html>
3001
+ <head>
3002
+ <script type="application/ld+json">
3003
+ {
3004
+ "@context": {
3005
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
3006
+ },
3007
+ "foo": [{"@value": "bar"}]
3008
+ }
3009
+ </script>
3010
+ <script type="application/ld+json">
3011
+ {
3012
+ "@context": {"ex": "http://example.com/"},
3013
+ "@graph": [
3014
+ {"ex:foo": {"@value": "foo"}},
3015
+ {"ex:bar": {"@value": "bar"}}
3016
+ ]
3017
+ }
3018
+ </script>
3019
+ </head>
3020
+ </html>),
3021
+ context: %({"foo": {"@id": "http://example.com/foo", "@container": "@list"}}),
3022
+ output: %({
3023
+ "@context": {
3024
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
3025
+ },
3026
+ "foo": ["bar"]
3027
+ })
3028
+ },
3029
+ "Compacts targeted script element": {
3030
+ input: %(
3031
+ <html>
3032
+ <head>
3033
+ <script id="first" type="application/ld+json">
3034
+ {
3035
+ "@context": {
3036
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
3037
+ },
3038
+ "foo": [{"@value": "bar"}]
3039
+ }
3040
+ </script>
3041
+ <script id="second" type="application/ld+json">
3042
+ {
3043
+ "@context": {"ex": "http://example.com/"},
3044
+ "@graph": [
3045
+ {"ex:foo": {"@value": "foo"}},
3046
+ {"ex:bar": {"@value": "bar"}}
3047
+ ]
3048
+ }
3049
+ </script>
3050
+ </head>
3051
+ </html>),
3052
+ context: %({"ex": "http://example.com/"}),
3053
+ output: %({
3054
+ "@context": {"ex": "http://example.com/"},
3055
+ "@graph": [
3056
+ {"ex:foo": "foo"},
3057
+ {"ex:bar": "bar"}
3058
+ ]
3059
+ }),
3060
+ base: "http://example.org/doc#second"
3061
+ },
3062
+ "Compacts all script elements with extractAllScripts option": {
3063
+ input: %(
3064
+ <html>
3065
+ <head>
3066
+ <script type="application/ld+json">
3067
+ {
3068
+ "@context": {
3069
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
3070
+ },
3071
+ "foo": [{"@value": "bar"}]
3072
+ }
3073
+ </script>
3074
+ <script type="application/ld+json">
3075
+ {
3076
+ "@context": {"ex": "http://example.com/"},
3077
+ "@graph": [
3078
+ {"ex:foo": {"@value": "foo"}},
3079
+ {"ex:bar": {"@value": "bar"}}
3080
+ ]
3081
+ }
3082
+ </script>
3083
+ </head>
3084
+ </html>),
3085
+ context: %({
3086
+ "ex": "http://example.com/",
3087
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
3088
+ }),
3089
+ output: %({
3090
+ "@context": {
3091
+ "ex": "http://example.com/",
3092
+ "foo": {"@id": "http://example.com/foo", "@container": "@list"}
3093
+ },
3094
+ "@graph": [
3095
+ {"foo": ["bar"]},
3096
+ {
3097
+ "@graph": [
3098
+ {"ex:foo": "foo"},
3099
+ {"ex:bar": "bar"}
3100
+ ]
3101
+ }
3102
+ ]
3103
+ }),
3104
+ extractAllScripts: true
3105
+ },
3106
+ }.each do |title, params|
3107
+ it(title) do
3108
+ params[:input] = StringIO.new(params[:input])
3109
+ params[:input].send(:define_singleton_method, :content_type) {"text/html"}
3110
+ run_compact params.merge(validate: true)
3111
+ end
3112
+ end
3113
+ end
3114
+
3115
+
3116
+ context "problem cases" do
3117
+ {
3118
+ "issue json-ld-framing#64": {
3119
+ input: %({
3120
+ "@context": {
3121
+ "@version": 1.1,
3122
+ "@vocab": "http://example.org/vocab#"
3123
+ },
3124
+ "@id": "http://example.org/1",
3125
+ "@type": "HumanMadeObject",
3126
+ "produced_by": {
3127
+ "@type": "Production",
3128
+ "_label": "Top Production",
3129
+ "part": {
3130
+ "@type": "Production",
3131
+ "_label": "Test Part"
3132
+ }
3133
+ }
3134
+ }),
3135
+ context: %({
3136
+ "@version": 1.1,
3137
+ "@vocab": "http://example.org/vocab#",
3138
+ "Production": {
3139
+ "@context": {
3140
+ "part": {
3141
+ "@type": "@id",
3142
+ "@container": "@set"
3143
+ }
3144
+ }
3145
+ }
3146
+ }),
3147
+ output: %({
3148
+ "@context": {
3149
+ "@version": 1.1,
3150
+ "@vocab": "http://example.org/vocab#",
3151
+ "Production": {
3152
+ "@context": {
3153
+ "part": {
3154
+ "@type": "@id",
3155
+ "@container": "@set"
3156
+ }
3157
+ }
3158
+ }
3159
+ },
3160
+ "@id": "http://example.org/1",
3161
+ "@type": "HumanMadeObject",
3162
+ "produced_by": {
3163
+ "@type": "Production",
3164
+ "part": [{
3165
+ "@type": "Production",
3166
+ "_label": "Test Part"
3167
+ }],
3168
+ "_label": "Top Production"
3169
+ }
3170
+ }),
3171
+ processingMode: "json-ld-1.1"
3172
+ }
3173
+ }.each do |title, params|
3174
+ it title do
3175
+ run_compact(params)
3176
+ end
3177
+ end
3178
+ end
3179
+
2230
3180
  def run_compact(params)
2231
3181
  input, output, context = params[:input], params[:output], params[:context]
3182
+ params[:base] ||= nil
3183
+ context ||= output # Since it will have the context
2232
3184
  input = ::JSON.parse(input) if input.is_a?(String)
2233
3185
  output = ::JSON.parse(output) if output.is_a?(String)
2234
3186
  context = ::JSON.parse(context) if context.is_a?(String)
3187
+ context = context['@context'] if context.has_key?('@context')
2235
3188
  pending params.fetch(:pending, "test implementation") unless input
2236
3189
  if params[:exception]
2237
- expect {JSON::LD::API.compact(input, context, params.merge(logger: logger))}.to raise_error(params[:exception])
3190
+ expect {JSON::LD::API.compact(input, context, logger: logger, **params)}.to raise_error(params[:exception])
2238
3191
  else
2239
- jld = JSON::LD::API.compact(input, context, params.merge(logger: logger))
3192
+ jld = nil
3193
+ if params[:write]
3194
+ expect{jld = JSON::LD::API.compact(input, context, logger: logger, **params)}.to write(params[:write]).to(:error)
3195
+ else
3196
+ expect{jld = JSON::LD::API.compact(input, context, logger: logger, **params)}.not_to write.to(:error)
3197
+ end
3198
+
2240
3199
  expect(jld).to produce_jsonld(output, logger)
2241
3200
 
2242
3201
  # Compare expanded jld/output too to make sure list values remain ordered