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
@@ -111,13 +111,13 @@ describe JSON::LD::StreamingWriter do
111
111
  t.logger.info "source: #{t.input}"
112
112
  specify "#{t.property('@id')}: #{t.name}" do
113
113
  repo = RDF::Repository.load(t.input_loc, format: :nquads)
114
- jsonld = JSON::LD::Writer.buffer(stream: true, context: ctx, logger: t.logger) do |writer|
114
+ jsonld = JSON::LD::Writer.buffer(stream: true, context: ctx, logger: t.logger, **t.options) do |writer|
115
115
  writer << repo
116
116
  end
117
117
  t.logger.info "Generated: #{jsonld}"
118
118
 
119
119
  # And then, re-generate jsonld as RDF
120
- expect(parse(jsonld, format: :jsonld)).to be_equivalent_graph(repo, t)
120
+ expect(parse(jsonld, format: :jsonld, **t.options)).to be_equivalent_graph(repo, t)
121
121
  end
122
122
  end
123
123
  end
@@ -127,12 +127,12 @@ describe JSON::LD::StreamingWriter do
127
127
 
128
128
  def parse(input, format: :trig, **options)
129
129
  reader = RDF::Reader.for(format)
130
- RDF::Repository.new << reader.new(input, options)
130
+ RDF::Repository.new << reader.new(input, **options)
131
131
  end
132
132
 
133
133
  # Serialize ntstr to a string and compare against regexps
134
134
  def serialize(ntstr, **options)
135
- g = ntstr.is_a?(String) ? parse(ntstr, options) : ntstr
135
+ g = ntstr.is_a?(String) ? parse(ntstr, **options) : ntstr
136
136
  logger = RDF::Spec.logger
137
137
  logger.info(g.dump(:ttl))
138
138
  result = JSON::LD::Writer.buffer(logger: logger, stream: true, **options) do |writer|
@@ -9,12 +9,12 @@ describe JSON::LD do
9
9
  m.entries.each do |t|
10
10
  specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
11
11
  t.options[:ordered] = false
12
- t.run self
12
+ expect{t.run self}.not_to write.to(:error)
13
13
  end
14
14
 
15
15
  specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do
16
16
  t.options[:ordered] = true
17
- t.run self
17
+ expect {t.run self}.not_to write.to(:error)
18
18
  end
19
19
  end
20
20
  end
@@ -9,12 +9,24 @@ describe JSON::LD do
9
9
  m.entries.each do |t|
10
10
  specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
11
11
  t.options[:ordered] = false
12
- t.run self
12
+ if %w(#t0068).include?(t.property('@id'))
13
+ expect{t.run self}.to write("[DEPRECATION]").to(:error)
14
+ elsif %w(#t0005 #tpr34 #tpr35 #tpr36 #tpr37 #t0119 #t0120).include?(t.property('@id'))
15
+ expect{t.run self}.to write("beginning with '@' are reserved for future use").to(:error)
16
+ else
17
+ expect {t.run self}.not_to write.to(:error)
18
+ end
13
19
  end
14
20
 
15
21
  specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do
16
22
  t.options[:ordered] = true
17
- t.run self
23
+ if %w(#t0068).include?(t.property('@id'))
24
+ expect{t.run self}.to write("[DEPRECATION]").to(:error)
25
+ elsif %w(#t0005 #tpr34 #tpr35 #tpr36 #tpr37 #t0119 #t0120).include?(t.property('@id'))
26
+ expect{t.run self}.to write("beginning with '@' are reserved for future use").to(:error)
27
+ else
28
+ expect {t.run self}.not_to write.to(:error)
29
+ end
18
30
  end
19
31
  end
20
32
  end
@@ -9,12 +9,20 @@ describe JSON::LD do
9
9
  m.entries.each do |t|
10
10
  specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
11
11
  t.options[:ordered] = false
12
- t.run self
12
+ if %w(#t0005).include?(t.property('@id'))
13
+ expect{t.run self}.to write("Terms beginning with '@' are reserved for future use").to(:error)
14
+ else
15
+ expect {t.run self}.not_to write.to(:error)
16
+ end
13
17
  end
14
18
 
15
19
  specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do
16
20
  t.options[:ordered] = true
17
- t.run self
21
+ if %w(#t0005).include?(t.property('@id'))
22
+ expect{t.run self}.to write("Terms beginning with '@' are reserved for future use").to(:error)
23
+ else
24
+ expect {t.run self}.not_to write.to(:error)
25
+ end
18
26
  end
19
27
  end
20
28
  end
@@ -9,12 +9,13 @@ describe JSON::LD do
9
9
  m.entries.each do |t|
10
10
  specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
11
11
  t.options[:ordered] = false
12
- t.run self
12
+ expect {t.run self}.not_to write.to(:error)
13
13
  end
14
14
 
15
15
  specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do
16
+ pending "Ordered version of in03" if %w(#tin03).include?(t.property('@id'))
16
17
  t.options[:ordered] = true
17
- t.run self
18
+ expect {t.run self}.not_to write.to(:error)
18
19
  end
19
20
  end
20
21
  end
@@ -9,12 +9,12 @@ describe JSON::LD do
9
9
  m.entries.each do |t|
10
10
  specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
11
11
  t.options[:ordered] = false
12
- t.run self
12
+ expect {t.run self}.not_to write.to(:error)
13
13
  end
14
14
 
15
15
  specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do
16
16
  t.options[:ordered] = true
17
- t.run self
17
+ expect {t.run self}.not_to write.to(:error)
18
18
  end
19
19
  end
20
20
  end
@@ -26,7 +26,10 @@ module RDF::Util
26
26
  LOCAL_PATHS.each do |r, l|
27
27
  next unless Dir.exist?(l) && filename_or_url.start_with?(r)
28
28
  #puts "attempt to open #{filename_or_url} locally"
29
- localpath = filename_or_url.to_s.sub(r, l)
29
+ url_no_frag_or_query = RDF::URI(filename_or_url).dup
30
+ url_no_frag_or_query.query = nil
31
+ url_no_frag_or_query.fragment = nil
32
+ localpath = url_no_frag_or_query.to_s.sub(r, l)
30
33
  response = begin
31
34
  ::File.open(localpath)
32
35
  rescue Errno::ENOENT => e
@@ -40,9 +43,11 @@ module RDF::Util
40
43
  headers: options.fetch(:headers, {})
41
44
  }
42
45
  #puts "use #{filename_or_url} locally"
43
- document_options[:headers][:content_type] = case filename_or_url.to_s
46
+ document_options[:headers][:content_type] = case localpath
44
47
  when /\.ttl$/ then 'text/turtle'
48
+ when /\.nq$/ then 'application/n-quads'
45
49
  when /\.nt$/ then 'application/n-triples'
50
+ when /\.html$/ then 'text/html'
46
51
  when /\.jsonld$/ then 'application/ld+json'
47
52
  when /\.json$/ then 'application/json'
48
53
  else 'unknown'
@@ -52,7 +57,7 @@ module RDF::Util
52
57
  # For overriding content type from test data
53
58
  document_options[:headers][:content_type] = options[:contentType] if options[:contentType]
54
59
 
55
- remote_document = RDF::Util::File::RemoteDocument.new(response.read, document_options)
60
+ remote_document = RDF::Util::File::RemoteDocument.new(response.read, **document_options)
56
61
  if block_given?
57
62
  return yield remote_document
58
63
  else
@@ -60,7 +65,7 @@ module RDF::Util
60
65
  end
61
66
  end
62
67
 
63
- original_open_file(filename_or_url, options, &block)
68
+ original_open_file(filename_or_url, **options, &block)
64
69
  end
65
70
  end
66
71
  end
@@ -112,15 +117,18 @@ module Fixtures
112
117
  super
113
118
  end
114
119
 
115
- # Base is expanded input file
120
+ # Base is expanded input if not specified
116
121
  def base
117
122
  options.fetch('base', manifest_url.join(property('input')).to_s)
118
123
  end
119
124
 
120
125
  def options
121
126
  @options ||= begin
122
- opts = {documentLoader: Fixtures::SuiteTest.method(:documentLoader)}
123
- opts = {}
127
+ opts = {
128
+ documentLoader: Fixtures::SuiteTest.method(:documentLoader),
129
+ validate: true,
130
+ lowercaseLanguage: true,
131
+ }
124
132
  {'specVersion' => "json-ld-1.1"}.merge(property('option') || {}).each do |k, v|
125
133
  opts[k.to_sym] = v
126
134
  end
@@ -136,7 +144,7 @@ module Fixtures
136
144
  file = self.send("#{m}_loc".to_sym)
137
145
 
138
146
  dl_opts = {safe: true}
139
- RDF::Util::File.open_file(file, dl_opts) do |remote_doc|
147
+ RDF::Util::File.open_file(file, **dl_opts) do |remote_doc|
140
148
  res = remote_doc.read
141
149
  end
142
150
  res
@@ -181,14 +189,15 @@ module Fixtures
181
189
  logger.info "options: #{options.inspect}" unless options.empty?
182
190
  logger.info "frame: #{frame}" if frame_loc
183
191
 
184
- options = self.options.merge(documentLoader: Fixtures::SuiteTest.method(:documentLoader))
185
- options = {validate: true}.merge(options)
186
-
192
+ options = self.options
187
193
  unless options[:specVersion] == "json-ld-1.1"
188
194
  skip "not a 1.1 test"
189
195
  return
190
196
  end
191
197
 
198
+ # Because we're doing exact comparisons when ordered.
199
+ options[:lowercaseLanguage] = true if options[:ordered]
200
+
192
201
  if positiveTest?
193
202
  logger.info "expected: #{expect rescue nil}" if expect_loc
194
203
  begin
@@ -205,16 +214,32 @@ module Fixtures
205
214
  # Use an array, to preserve input order
206
215
  repo = RDF::NQuads::Reader.open(input_loc) do |reader|
207
216
  reader.each_statement.to_a
208
- end.uniq.extend(RDF::Enumerable)
209
- logger.info "repo: #{repo.dump(id == '#t0012' ? :nquads : :trig)}"
217
+ end.to_a.uniq.extend(RDF::Enumerable)
218
+ logger.info "repo: #{repo.dump(self.id == '#t0012' ? :nquads : :trig)}"
210
219
  JSON::LD::API.fromRdf(repo, logger: logger, **options)
211
220
  when "jld:ToRDFTest"
212
221
  repo = RDF::Repository.new
213
222
  JSON::LD::API.toRdf(input_loc, logger: logger, **options) do |statement|
223
+ # To properly compare values of rdf:language and i18n datatypes, normalize to lower case
224
+ if statement.predicate == RDF.to_uri + 'language'
225
+ statement.object = RDF::Literal(statement.object.to_s.downcase) if statement.object.literal?
226
+ elsif statement.object.literal? && statement.object.datatype.to_s.start_with?('https://www.w3.org/ns/i18n#')
227
+ statement.object.datatype = RDF::URI(statement.object.datatype.to_s.downcase)
228
+ end
214
229
  repo << statement
215
230
  end
216
- logger.info "nq: #{repo.to_nquads}"
231
+ logger.info "nq: #{repo.map(&:to_nquads)}"
217
232
  repo
233
+ when "jld:HttpTest"
234
+ res = input_json
235
+ rspec_example.instance_eval do
236
+ # use the parsed input file as @result for Rack Test application
237
+ @results = res
238
+ get "/", {}, "HTTP_ACCEPT" => options.fetch(:httpAccept, ""), "HTTP_LINK" => options.fetch(:httpLink, nil)
239
+ expect(last_response.status).to eq 200
240
+ expect(last_response.content_type).to eq options.fetch(:contentType, "")
241
+ JSON.parse(last_response.body)
242
+ end
218
243
  else
219
244
  fail("Unknown test type: #{testType}")
220
245
  end
@@ -273,14 +298,24 @@ module Fixtures
273
298
  JSON::LD::API.frame(t.input_loc, t.frame_loc, logger: logger, **options)
274
299
  when "jld:FromRDFTest"
275
300
  repo = RDF::Repository.load(t.input_loc)
276
- logger.info "repo: #{repo.dump(id == '#t0012' ? :nquads : :trig)}"
301
+ logger.info "repo: #{repo.dump(t.id == '#t0012' ? :nquads : :trig)}"
277
302
  JSON::LD::API.fromRdf(repo, logger: logger, **options)
303
+ when "jld:HttpTest"
304
+ rspec_example.instance_eval do
305
+ # use the parsed input file as @result for Rack Test application
306
+ @results = t.input_json
307
+ get "/", {}, "HTTP_ACCEPT" => options.fetch(:httpAccept, "")
308
+ expect(last_response.status).to eq t.property('expect')
309
+ expect(last_response.content_type).to eq options.fetch(:contentType, "")
310
+ raise "406" if t.property('expect') == 406
311
+ raise "Expected status #{t.property('expectErrorCode')}, not #{last_response.status}"
312
+ end
278
313
  when "jld:ToRDFTest"
279
314
  JSON::LD::API.toRdf(t.input_loc, logger: logger, **options) {}
280
315
  else
281
316
  success("Unknown test type: #{testType}")
282
317
  end
283
- end.to raise_error(/#{t.property('expect')}/)
318
+ end.to raise_error(/#{t.property('expectErrorCode')}/)
284
319
  else
285
320
  fail("No support for NegativeSyntaxTest")
286
321
  end
@@ -335,16 +370,16 @@ module Fixtures
335
370
  # @param [Hash<Symbol => Object>] options
336
371
  # @option options [Boolean] :validate
337
372
  # Allow only appropriate content types
338
- # @return [RemoteDocument] retrieved remote document and context information unless block given
373
+ # @return [RDF::Util::File::RemoteDocument] retrieved remote document and context information unless block given
339
374
  # @yield remote_document
340
- # @yieldparam [RemoteDocument] remote_document
375
+ # @yieldparam [RDF::Util::File::RemoteDocument] remote_document
341
376
  # @raise [JsonLdError]
342
377
  def documentLoader(url, **options, &block)
343
- options[:headers] ||= JSON::LD::API::OPEN_OPTS[:headers]
378
+ options[:headers] ||= JSON::LD::API::OPEN_OPTS[:headers].dup
344
379
  options[:headers][:link] = Array(options[:httpLink]).join(',') if options[:httpLink]
345
380
 
346
381
  url = url.to_s[5..-1] if url.to_s.start_with?("file:")
347
- JSON::LD::API.documentLoader(url, options, &block)
382
+ JSON::LD::API.documentLoader(url, **options, &block)
348
383
  rescue JSON::LD::JsonLdError::LoadingDocumentFailed, JSON::LD::JsonLdError::MultipleContextLinkHeaders
349
384
  raise unless options[:safe]
350
385
  "don't raise error"
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ require_relative 'spec_helper'
3
+
4
+ describe JSON::LD do
5
+ describe "test suite" do
6
+ require_relative 'suite_helper'
7
+ m = Fixtures::SuiteTest::Manifest.open("#{Fixtures::SuiteTest::SUITE}html-manifest.jsonld")
8
+ describe m.name do
9
+ m.entries.each do |t|
10
+ specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
11
+ t.options[:ordered] = false
12
+ expect {t.run self}.not_to write.to(:error)
13
+ end
14
+
15
+ specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do
16
+ t.options[:ordered] = true
17
+ expect {t.run self}.not_to write.to(:error)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end unless ENV['CI']
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ require_relative 'spec_helper'
3
+ require 'rack/linkeddata'
4
+ require 'rack/test'
5
+
6
+ begin
7
+ describe JSON::LD do
8
+ describe "test suite" do
9
+ require_relative 'suite_helper'
10
+ m = Fixtures::SuiteTest::Manifest.open("#{Fixtures::SuiteTest::SUITE}http-manifest.jsonld")
11
+ describe m.name do
12
+ include ::Rack::Test::Methods
13
+ before(:all) {JSON::LD::Writer.default_context = "#{Fixtures::SuiteTest::SUITE}http/default-context.jsonld"}
14
+ after(:all) {JSON::LD::Writer.default_context = nil}
15
+ let(:app) do
16
+ JSON::LD::ContentNegotiation.new(
17
+ Rack::LinkedData::ContentNegotiation.new(
18
+ double("Target Rack Application", :call => [200, {}, @results]),
19
+ {}
20
+ )
21
+ )
22
+ end
23
+
24
+ m.entries.each do |t|
25
+ specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
26
+ t.options[:ordered] = false
27
+ expect {t.run self}.not_to write.to(:error)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end unless ENV['CI']
33
+ rescue IOError
34
+ # Skip this until such a test suite is re-added
35
+ end
@@ -9,12 +9,12 @@ describe JSON::LD do
9
9
  m.entries.each do |t|
10
10
  specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do
11
11
  t.options[:ordered] = false
12
- t.run self
12
+ expect {t.run self}.not_to write.to(:error)
13
13
  end
14
14
 
15
15
  specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do
16
16
  t.options[:ordered] = true
17
- t.run self
17
+ expect {t.run self}.not_to write.to(:error)
18
18
  end
19
19
  end
20
20
  end
@@ -8,9 +8,20 @@ describe JSON::LD do
8
8
  describe m.name do
9
9
  m.entries.each do |t|
10
10
  specify "#{t.property('@id')}: #{t.name}#{' (negative test)' unless t.positiveTest?}" do
11
- skip "Native value fidelity" if %w(toRdf/0035-in.jsonld).include?(t.property('input'))
12
- pending "Generalized RDF" if %w(toRdf/0118-in.jsonld).include?(t.property('input'))
13
- t.run self
11
+ pending "Generalized RDF" if t.options[:produceGeneralizedRdf]
12
+ if %w(#t0118).include?(t.property('@id'))
13
+ expect {t.run self}.to write(/Statement .* is invalid/).to(:error)
14
+ elsif %w(#te075).include?(t.property('@id'))
15
+ expect {t.run self}.to write(/is invalid/).to(:error)
16
+ elsif %w(#te005 #tpr34 #tpr35 #tpr36 #tpr37 #te119 #te120).include?(t.property('@id'))
17
+ expect {t.run self}.to write("beginning with '@' are reserved for future use").to(:error)
18
+ elsif %w(#te068).include?(t.property('@id'))
19
+ expect {t.run self}.to write("[DEPRECATION]").to(:error)
20
+ elsif %w(#twf05).include?(t.property('@id'))
21
+ expect {t.run self}.to write("@language must be valid BCP47").to(:error)
22
+ else
23
+ expect {t.run self}.not_to write.to(:error)
24
+ end
14
25
  end
15
26
  end
16
27
  end
@@ -9,7 +9,11 @@ class Hash
9
9
  return false unless other.is_a?(Hash) && other.length == length
10
10
  all? do |key, value|
11
11
  # List values are still ordered
12
- value.equivalent_jsonld?(other[key], ordered: key == '@list')
12
+ if key == '@language' && value.is_a?(String)
13
+ value.downcase.equivalent_jsonld?(other[key].to_s.downcase, ordered: key == '@list')
14
+ else
15
+ value.equivalent_jsonld?(other[key], ordered: key == '@list')
16
+ end
13
17
  end
14
18
  end
15
19
 
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "@context": {
3
- "": "http://manu.sporny.org/",
3
+ "foo": "http://manu.sporny.org/",
4
4
  "foaf": "http://xmlns.com/foaf/0.1/"
5
5
  },
6
- "@id": ":#me",
6
+ "@id": "foo:#me",
7
7
  "@type": "foaf:Person",
8
8
  "foaf:name": "Manu Sporny",
9
- "foaf:homepage": { "@id": ":" }
9
+ "foaf:homepage": { "@id": "foo:" }
10
10
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "@context": {
3
- "": "http://manu.sporny.org/",
3
+ "foo": "http://manu.sporny.org/",
4
4
  "foaf": "http://xmlns.com/foaf/0.1/"
5
5
  },
6
- "@id": ":#me",
6
+ "@id": "foo:#me",
7
7
  "@type": "foaf:Person",
8
8
  "foaf:name": "Manu Sporny",
9
9
  "foaf:knows": {
@@ -8,22 +8,18 @@
8
8
  "Library": "http://example.org/vocab#Library",
9
9
  "title": "http://purl.org/dc/terms/title"
10
10
  },
11
- "@graph": [
12
- {
13
- "@id": "http://example.com/library",
14
- "@type": "Library",
15
- "contains": {
16
- "@id": "http://example.org/library/the-republic",
17
- "@type": "Book",
18
- "contains": {
19
- "@id": "http://example.org/library/the-republic#introduction",
20
- "@type": "Chapter",
21
- "description": "An introductory chapter on The Republic.",
22
- "title": "The Introduction"
23
- },
24
- "creator": "Plato",
25
- "title": "The Republic"
26
- }
27
- }
28
- ]
11
+ "@id": "http://example.com/library",
12
+ "@type": "Library",
13
+ "contains": {
14
+ "@id": "http://example.org/library/the-republic",
15
+ "@type": "Book",
16
+ "contains": {
17
+ "@id": "http://example.org/library/the-republic#introduction",
18
+ "@type": "Chapter",
19
+ "description": "An introductory chapter on The Republic.",
20
+ "title": "The Introduction"
21
+ },
22
+ "creator": "Plato",
23
+ "title": "The Republic"
24
+ }
29
25
  }