json-ld 3.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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