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.
- checksums.yaml +4 -4
- data/AUTHORS +1 -1
- data/README.md +90 -53
- data/UNLICENSE +1 -1
- data/VERSION +1 -1
- data/bin/jsonld +4 -4
- data/lib/json/ld.rb +27 -10
- data/lib/json/ld/api.rb +325 -96
- data/lib/json/ld/compact.rb +75 -27
- data/lib/json/ld/conneg.rb +188 -0
- data/lib/json/ld/context.rb +677 -292
- data/lib/json/ld/expand.rb +240 -75
- data/lib/json/ld/flatten.rb +5 -3
- data/lib/json/ld/format.rb +19 -19
- data/lib/json/ld/frame.rb +135 -85
- data/lib/json/ld/from_rdf.rb +44 -17
- data/lib/json/ld/html/nokogiri.rb +151 -0
- data/lib/json/ld/html/rexml.rb +186 -0
- data/lib/json/ld/reader.rb +25 -5
- data/lib/json/ld/resource.rb +2 -2
- data/lib/json/ld/streaming_writer.rb +3 -1
- data/lib/json/ld/to_rdf.rb +47 -17
- data/lib/json/ld/utils.rb +4 -2
- data/lib/json/ld/writer.rb +75 -14
- data/spec/api_spec.rb +13 -34
- data/spec/compact_spec.rb +968 -9
- data/spec/conneg_spec.rb +373 -0
- data/spec/context_spec.rb +447 -53
- data/spec/expand_spec.rb +1872 -416
- data/spec/flatten_spec.rb +434 -47
- data/spec/frame_spec.rb +979 -344
- data/spec/from_rdf_spec.rb +305 -5
- data/spec/spec_helper.rb +177 -0
- data/spec/streaming_writer_spec.rb +4 -4
- data/spec/suite_compact_spec.rb +2 -2
- data/spec/suite_expand_spec.rb +14 -2
- data/spec/suite_flatten_spec.rb +10 -2
- data/spec/suite_frame_spec.rb +3 -2
- data/spec/suite_from_rdf_spec.rb +2 -2
- data/spec/suite_helper.rb +55 -20
- data/spec/suite_html_spec.rb +22 -0
- data/spec/suite_http_spec.rb +35 -0
- data/spec/suite_remote_doc_spec.rb +2 -2
- data/spec/suite_to_rdf_spec.rb +14 -3
- data/spec/support/extensions.rb +5 -1
- data/spec/test-files/test-4-input.json +3 -3
- data/spec/test-files/test-5-input.json +2 -2
- data/spec/test-files/test-8-framed.json +14 -18
- data/spec/to_rdf_spec.rb +606 -16
- data/spec/writer_spec.rb +5 -5
- metadata +144 -88
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9fcb46a6c6bf33bfed92adcc8a751598cec5d6511e0450d38c9920bfdb696ce
|
4
|
+
data.tar.gz: 6fec905be8d5e0335149da757fef899d6e3fa948e852f74e9c88d0bff45801ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a7d07de17bc462f2f38fc24ae7c2a04a2c1ffd24d0a431626771259d59ffd6fe4b33b4ea8501add71d710e3c4501897454aada60ff6054fa9a12ef69cf488f3
|
7
|
+
data.tar.gz: 48da4933e6660ccd7cffe9cdcb08b576d883f1c744a889867b5e8619f9c3908fad00af02a739bc9af5c1abd7495d3cb9517d914ee12f7ac4f48acf94b5b9b637
|
data/AUTHORS
CHANGED
@@ -1 +1 @@
|
|
1
|
-
* Gregg Kellogg <gregg@
|
1
|
+
* Gregg Kellogg <gregg@greggkellogg.net>
|
data/README.md
CHANGED
@@ -2,17 +2,18 @@
|
|
2
2
|
|
3
3
|
[JSON-LD][] reader/writer for [RDF.rb][RDF.rb] and fully conforming [JSON-LD API][] processor. Additionally this gem implements [JSON-LD Framing][].
|
4
4
|
|
5
|
-
[![Gem Version](https://badge.fury.io/rb/json-ld.png)](
|
6
|
-
[![Build Status](https://secure.travis-ci.org/ruby-rdf/json-ld.png?branch=master)](
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/json-ld.png)](https://badge.fury.io/rb/json-ld)
|
6
|
+
[![Build Status](https://secure.travis-ci.org/ruby-rdf/json-ld.png?branch=master)](https://travis-ci.org/ruby-rdf/json-ld)
|
7
7
|
[![Coverage Status](https://coveralls.io/repos/ruby-rdf/json-ld/badge.svg)](https://coveralls.io/r/ruby-rdf/json-ld)
|
8
8
|
|
9
9
|
## Features
|
10
10
|
|
11
|
-
JSON::LD parses and serializes [JSON-LD][] into [RDF][] and implements expansion, compaction and framing API interfaces.
|
11
|
+
JSON::LD parses and serializes [JSON-LD][] into [RDF][] and implements expansion, compaction and framing API interfaces. It also extracts JSON-LD from HTML.
|
12
12
|
|
13
13
|
JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and optionally include a JSON-LD representation of the ontology itself. This is currently accessed through the `script/gen_context` script.
|
14
14
|
|
15
|
-
If the [jsonlint][] gem is installed, it will be used when validating an input document.
|
15
|
+
* If the [jsonlint][] gem is installed, it will be used when validating an input document.
|
16
|
+
* If available, uses [Nokogiri][] and/or [Nokogumbo][] for parsing HTML, falls back to REXML otherwise.
|
16
17
|
|
17
18
|
[Implementation Report](file.earl.html)
|
18
19
|
|
@@ -48,16 +49,16 @@ require 'json/ld'
|
|
48
49
|
|
49
50
|
[{
|
50
51
|
"http://xmlns.com/foaf/0.1/name": [{"@value"=>"Manu Sporny"}],
|
51
|
-
"http://xmlns.com/foaf/0.1/homepage": [{"@value"=>"
|
52
|
-
"http://xmlns.com/foaf/0.1/avatar": [{"@value": "
|
52
|
+
"http://xmlns.com/foaf/0.1/homepage": [{"@value"=>"https://manu.sporny.org/"}],
|
53
|
+
"http://xmlns.com/foaf/0.1/avatar": [{"@value": "https://twitter.com/account/profile_image/manusporny"}]
|
53
54
|
}]
|
54
55
|
```
|
55
56
|
### Compact a Document
|
56
57
|
```ruby
|
57
58
|
input = JSON.parse %([{
|
58
59
|
"http://xmlns.com/foaf/0.1/name": ["Manu Sporny"],
|
59
|
-
"http://xmlns.com/foaf/0.1/homepage": [{"@id": "
|
60
|
-
"http://xmlns.com/foaf/0.1/avatar": [{"@id": "
|
60
|
+
"http://xmlns.com/foaf/0.1/homepage": [{"@id": "https://manu.sporny.org/"}],
|
61
|
+
"http://xmlns.com/foaf/0.1/avatar": [{"@id": "https://twitter.com/account/profile_image/manusporny"}]
|
61
62
|
}])
|
62
63
|
|
63
64
|
context = JSON.parse(%({
|
@@ -75,8 +76,8 @@ require 'json/ld'
|
|
75
76
|
"homepage": {"@id": "http://xmlns.com/foaf/0.1/homepage", "@type": "@id"},
|
76
77
|
"avatar": {"@id": "http://xmlns.com/foaf/0.1/avatar", "@type": "@id"}
|
77
78
|
},
|
78
|
-
"avatar": "
|
79
|
-
"homepage": "
|
79
|
+
"avatar": "https://twitter.com/account/profile_image/manusporny",
|
80
|
+
"homepage": "https://manu.sporny.org/",
|
80
81
|
"name": "Manu Sporny"
|
81
82
|
}
|
82
83
|
```
|
@@ -167,7 +168,7 @@ require 'json/ld'
|
|
167
168
|
```ruby
|
168
169
|
input = JSON.parse %({
|
169
170
|
"@context": {
|
170
|
-
"": "
|
171
|
+
"": "https://manu.sporny.org/",
|
171
172
|
"foaf": "http://xmlns.com/foaf/0.1/"
|
172
173
|
},
|
173
174
|
"@id": "http://example.org/people#joebob",
|
@@ -192,7 +193,7 @@ require 'json/ld'
|
|
192
193
|
input = RDF::Graph.new << RDF::Turtle::Reader.new(%(
|
193
194
|
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
|
194
195
|
|
195
|
-
<
|
196
|
+
<https://manu.sporny.org/#me> a foaf:Person;
|
196
197
|
foaf:knows [ a foaf:Person;
|
197
198
|
foaf:name "Gregg Kellogg"];
|
198
199
|
foaf:name "Manu Sporny" .
|
@@ -200,7 +201,7 @@ require 'json/ld'
|
|
200
201
|
|
201
202
|
context = JSON.parse %({
|
202
203
|
"@context": {
|
203
|
-
"": "
|
204
|
+
"": "https://manu.sporny.org/",
|
204
205
|
"foaf": "http://xmlns.com/foaf/0.1/"
|
205
206
|
}
|
206
207
|
})
|
@@ -217,7 +218,7 @@ require 'json/ld'
|
|
217
218
|
"http://xmlns.com/foaf/0.1/name": [{"@value": "Gregg Kellogg"}]
|
218
219
|
},
|
219
220
|
{
|
220
|
-
"@id": "
|
221
|
+
"@id": "https://manu.sporny.org/#me",
|
221
222
|
"@type": ["http://xmlns.com/foaf/0.1/Person"],
|
222
223
|
"http://xmlns.com/foaf/0.1/knows": [{"@id": "_:g70265766605380"}],
|
223
224
|
"http://xmlns.com/foaf/0.1/name": [{"@value": "Manu Sporny"}]
|
@@ -225,22 +226,22 @@ require 'json/ld'
|
|
225
226
|
]
|
226
227
|
```
|
227
228
|
## Use a custom Document Loader
|
228
|
-
In some cases, the built-in document loader {JSON::LD::API.documentLoader} is inadequate; for example, when using `http://schema.org` as a remote context, it will be re-loaded every time.
|
229
|
+
In some cases, the built-in document loader {JSON::LD::API.documentLoader} is inadequate; for example, when using `http://schema.org` as a remote context, it will be re-loaded every time (however, see [json-ld-preloaded](https://rubygems.org/gems/json-ld-preloaded)).
|
229
230
|
|
230
231
|
All entries into the {JSON::LD::API} accept a `:documentLoader` option, which can be used to provide an alternative method to use when loading remote documents. For example:
|
231
232
|
```ruby
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
233
|
+
def load_document_local(url, options={}, &block)
|
234
|
+
if RDF::URI(url, canonicalize: true) == RDF::URI('http://schema.org/')
|
235
|
+
remote_document = JSON::LD::API::RemoteDocument.new(url, File.read("etc/schema.org.jsonld"))
|
236
|
+
return block_given? ? yield(remote_document) : remote_document
|
237
|
+
else
|
238
|
+
JSON::LD::API.documentLoader(url, options, &block)
|
239
|
+
end
|
240
|
+
end
|
240
241
|
```
|
241
242
|
Then, when performing something like expansion:
|
242
243
|
```ruby
|
243
|
-
|
244
|
+
JSON::LD::API.expand(input, documentLoader: load_document_local)
|
244
245
|
```
|
245
246
|
|
246
247
|
## Preloading contexts
|
@@ -433,19 +434,52 @@ Many JSON APIs separate properties from their entities using an intermediate obj
|
|
433
434
|
```
|
434
435
|
In this way, nesting survives round-tripping through expansion, and framed output can include nested properties.
|
435
436
|
|
436
|
-
|
437
|
-
|
437
|
+
## Sinatra/Rack support
|
438
|
+
JSON-LD 1.1 describes support for the _profile_ parameter to a media type in an HTTP ACCEPT header. This allows an HTTP request to specify the format (expanded/compacted/flattened/framed) along with a reference to a context or frame to use to format the returned document.
|
439
|
+
|
440
|
+
An HTTP header may be constructed as follows:
|
441
|
+
|
442
|
+
GET /ordinary-json-document.json HTTP/1.1
|
443
|
+
Host: example.com
|
444
|
+
Accept: application/ld+json;profile="http://www.w3.org/ns/json-ld#compacted http://conneg.example.com/context", application/ld+json
|
445
|
+
|
446
|
+
This tells a server that the top priority is to return JSON-LD compacted using a context at `http://conneg.example.com/context`, and if not available, to just return any form of JSON-LD.
|
447
|
+
|
448
|
+
The {JSON::LD::ContentNegotiation} class provides a [Rack][Rack] `call` method, and [Sinatra][Sinatra] `registered` class method to allow content-negotiation using such profile parameters. For example:
|
449
|
+
|
450
|
+
#!/usr/bin/env rackup
|
451
|
+
require 'sinatra/base'
|
452
|
+
require 'json/ld'
|
453
|
+
|
454
|
+
module My
|
455
|
+
class Application < Sinatra::Base
|
456
|
+
register JSON::LD::ContentNegotiation
|
457
|
+
|
458
|
+
get '/hello' do
|
459
|
+
[{
|
460
|
+
"http://example.org/input": [{
|
461
|
+
"@id": "http://example.com/g1",
|
462
|
+
"@graph": [{
|
463
|
+
"http://example.org/value": [{"@value": "x"}]
|
464
|
+
}]
|
465
|
+
}]
|
466
|
+
}])
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
run My::Application
|
472
|
+
|
473
|
+
The {JSON::LD::ContentNegotiation#call} method looks for a result which includes an object, with an acceptable `Accept` header and formats the result as JSON-LD, considering the profile parameters. This can be tested using something like the following:
|
474
|
+
|
475
|
+
$ rackup config.ru
|
476
|
+
|
477
|
+
$ curl -iH 'Accept: application/ld+json;profile="http://www.w3.org/ns/json-ld#compacted http://conneg.example.com/context"' http://localhost:9292/hello
|
438
478
|
|
439
|
-
|
440
|
-
* [Graph framing](https://github.com/json-ld/json-ld.org/issues/118) – previously, only the merged graph can be framed, this update allows arbitrary graphs to be framed.
|
441
|
-
* Use `@graph` in frame, matches the default graph, not the merged graph.
|
442
|
-
* Use `@graph` in property value, causes the apropriatly named graph to be used for filling in values.
|
443
|
-
* [Reverse properties](https://github.com/json-ld/json-ld.org/issues/311) – `@reverse` (or a property defined with `@reverse`) can cause matching values to be included, allowing a matched object to include reverse references to any objects referencing it.
|
444
|
-
* [@omitDefault behavior](https://github.com/json-ld/json-ld.org/issues/389) – In addition to `true` and `false`, `@omitDefault` can take `@last`, `@always`, `@never`, and `@link`.
|
445
|
-
* [multiple `@id` matching](https://github.com/json-ld/json-ld.org/issues/424) – A frame can match based on one or more specific object `@id` values.
|
479
|
+
See [Rack::LinkedData][] to do the same thing with an RDF Graph or Dataset as the source, rather than Ruby objects.
|
446
480
|
|
447
481
|
## Documentation
|
448
|
-
Full documentation available on [RubyDoc](
|
482
|
+
Full documentation available on [RubyDoc](https://rubydoc.info/gems/json-ld/file/README.md)
|
449
483
|
|
450
484
|
## Differences from [JSON-LD API][]
|
451
485
|
The specified JSON-LD API is based on a WebIDL definition implementing [Promises][] intended for use within a browser.
|
@@ -468,12 +502,12 @@ Note, the API method signatures differed in versions before 1.0, in that they al
|
|
468
502
|
* {JSON::LD::Writer}
|
469
503
|
|
470
504
|
## Dependencies
|
471
|
-
* [Ruby](
|
472
|
-
* [RDF.rb](
|
473
|
-
* [JSON](https://rubygems.org/gems/json) (>= 2.
|
505
|
+
* [Ruby](https://ruby-lang.org/) (>= 2.4)
|
506
|
+
* [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.1)
|
507
|
+
* [JSON](https://rubygems.org/gems/json) (>= 2.2)
|
474
508
|
|
475
509
|
## Installation
|
476
|
-
The recommended installation method is via [RubyGems](
|
510
|
+
The recommended installation method is via [RubyGems](https://rubygems.org/).
|
477
511
|
To install the latest official release of the `JSON-LD` gem, do:
|
478
512
|
```bash
|
479
513
|
% [sudo] gem install json-ld
|
@@ -484,10 +518,10 @@ To get a local working copy of the development repository, do:
|
|
484
518
|
% git clone git://github.com/ruby-rdf/json-ld.git
|
485
519
|
```
|
486
520
|
## Mailing List
|
487
|
-
* <
|
521
|
+
* <https://lists.w3.org/Archives/Public/public-rdf-ruby/>
|
488
522
|
|
489
523
|
## Author
|
490
|
-
* [Gregg Kellogg](
|
524
|
+
* [Gregg Kellogg](https://github.com/gkellogg) - <https://greggkellogg.net/>
|
491
525
|
|
492
526
|
## Contributing
|
493
527
|
* Do your best to adhere to the existing coding conventions and idioms.
|
@@ -506,17 +540,20 @@ License
|
|
506
540
|
-------
|
507
541
|
|
508
542
|
This is free and unencumbered public domain software. For more information,
|
509
|
-
see <
|
510
|
-
|
511
|
-
[Ruby]:
|
512
|
-
[RDF]:
|
513
|
-
[YARD]:
|
514
|
-
[YARD-GS]:
|
515
|
-
[PDD]:
|
516
|
-
[RDF.rb]:
|
517
|
-
[
|
518
|
-
[
|
519
|
-
[JSON-LD
|
520
|
-
[JSON-LD
|
521
|
-
[
|
543
|
+
see <https://unlicense.org/> or the accompanying {file:UNLICENSE} file.
|
544
|
+
|
545
|
+
[Ruby]: https://ruby-lang.org/
|
546
|
+
[RDF]: https://www.w3.org/RDF/
|
547
|
+
[YARD]: https://yardoc.org/
|
548
|
+
[YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md
|
549
|
+
[PDD]: https://lists.w3.org/Archives/Public/public-rdf-ruby/2010May/0013.html
|
550
|
+
[RDF.rb]: https://rubygems.org/gems/rdf
|
551
|
+
[Rack::LinkedData]: https://rubygems.org/gems/rack-linkeddata
|
552
|
+
[Backports]: https://rubygems.org/gems/backports
|
553
|
+
[JSON-LD]: https://www.w3.org/TR/json-ld11/ "JSON-LD 1.1"
|
554
|
+
[JSON-LD API]: https://www.w3.org/TR/json-ld11-api/ "JSON-LD 1.1 Processing Algorithms and API"
|
555
|
+
[JSON-LD Framing]: https://www.w3.org/TR/json-ld11-framing/ "JSON-LD Framing 1.1"
|
556
|
+
[Promises]: https://dom.spec.whatwg.org/#promises
|
522
557
|
[jsonlint]: https://rubygems.org/gems/jsonlint
|
558
|
+
[Sinatra]: https://www.sinatrarb.com/
|
559
|
+
[Rack]: https://rack.github.com/
|
data/UNLICENSE
CHANGED
@@ -21,4 +21,4 @@ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
21
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
22
|
OTHER DEALINGS IN THE SOFTWARE.
|
23
23
|
|
24
|
-
For more information, please refer to <
|
24
|
+
For more information, please refer to <https://unlicense.org/>
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.0
|
1
|
+
3.1.0
|
data/bin/jsonld
CHANGED
@@ -168,14 +168,14 @@ opts.each do |opt, arg|
|
|
168
168
|
when '--help' then usage
|
169
169
|
when '--embed'
|
170
170
|
case arg
|
171
|
-
when '@always', '@never', '@link', '@last'
|
171
|
+
when '@always', '@never', '@link', '@last', '@first'
|
172
172
|
options[:embed] = arg
|
173
173
|
when 'true'
|
174
|
-
options[:embed] =
|
174
|
+
options[:embed] = '@never'
|
175
175
|
when 'false'
|
176
|
-
options[:embed] =
|
176
|
+
options[:embed] = '@first'
|
177
177
|
else
|
178
|
-
STDERR.puts "--embed option takes one of
|
178
|
+
STDERR.puts "--embed option takes one of @always, @never, @link, @first, or @last"
|
179
179
|
exit(1)
|
180
180
|
end
|
181
181
|
end
|
data/lib/json/ld.rb
CHANGED
@@ -29,6 +29,7 @@ module JSON
|
|
29
29
|
require 'json/ld/format'
|
30
30
|
require 'json/ld/utils'
|
31
31
|
autoload :API, 'json/ld/api'
|
32
|
+
autoload :ContentNegotiation, 'json/ld/conneg'
|
32
33
|
autoload :Context, 'json/ld/context'
|
33
34
|
autoload :Normalize, 'json/ld/normalize'
|
34
35
|
autoload :Reader, 'json/ld/reader'
|
@@ -36,27 +37,35 @@ module JSON
|
|
36
37
|
autoload :VERSION, 'json/ld/version'
|
37
38
|
autoload :Writer, 'json/ld/writer'
|
38
39
|
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
# JSON-LD profiles
|
41
|
+
JSON_LD_NS = "http://www.w3.org/ns/json-ld#"
|
42
|
+
PROFILES = %w(expanded compacted flattened framed).map {|p| JSON_LD_NS + p}.freeze
|
43
|
+
|
44
|
+
# Default context when compacting without one being specified
|
45
|
+
DEFAULT_CONTEXT = "http://schema.org"
|
44
46
|
|
45
47
|
KEYWORDS = Set.new(%w(
|
46
48
|
@base
|
47
49
|
@container
|
48
50
|
@context
|
49
51
|
@default
|
52
|
+
@direction
|
50
53
|
@embed
|
51
54
|
@explicit
|
55
|
+
@json
|
52
56
|
@id
|
57
|
+
@included
|
53
58
|
@index
|
59
|
+
@first
|
54
60
|
@graph
|
61
|
+
@import
|
55
62
|
@language
|
56
63
|
@list
|
57
64
|
@nest
|
58
65
|
@none
|
59
66
|
@omitDefault
|
67
|
+
@propagate
|
68
|
+
@protected
|
60
69
|
@requireAll
|
61
70
|
@reverse
|
62
71
|
@set
|
@@ -106,11 +115,16 @@ module JSON
|
|
106
115
|
class CyclicIRIMapping < JsonLdError; @code = "cyclic IRI mapping"; end
|
107
116
|
class InvalidBaseIRI < JsonLdError; @code = "invalid base IRI"; end
|
108
117
|
class InvalidContainerMapping < JsonLdError; @code = "invalid container mapping"; end
|
118
|
+
class InvalidContextMember < JsonLdError; @code = "invalid context member"; end
|
119
|
+
class InvalidContextNullification < JsonLdError; @code = "invalid context nullification"; end
|
109
120
|
class InvalidDefaultLanguage < JsonLdError; @code = "invalid default language"; end
|
110
121
|
class InvalidIdValue < JsonLdError; @code = "invalid @id value"; end
|
111
122
|
class InvalidIndexValue < JsonLdError; @code = "invalid @index value"; end
|
112
123
|
class InvalidVersionValue < JsonLdError; @code = "invalid @version value"; end
|
124
|
+
class InvalidImportValue < JsonLdError; @code = "invalid @import value"; end
|
125
|
+
class InvalidIncludedValue < JsonLdError; @code = "invalid @included value"; end
|
113
126
|
class InvalidIRIMapping < JsonLdError; @code = "invalid IRI mapping"; end
|
127
|
+
class InvalidJsonLiteral < JsonLdError; @code = "invalid JSON literal"; end
|
114
128
|
class InvalidKeywordAlias < JsonLdError; @code = "invalid keyword alias"; end
|
115
129
|
class InvalidLanguageMapping < JsonLdError; @code = "invalid language mapping"; end
|
116
130
|
class InvalidLanguageMapValue < JsonLdError; @code = "invalid language map value"; end
|
@@ -119,31 +133,34 @@ module JSON
|
|
119
133
|
class InvalidLocalContext < JsonLdError; @code = "invalid local context"; end
|
120
134
|
class InvalidNestValue < JsonLdError; @code = "invalid @nest value"; end
|
121
135
|
class InvalidPrefixValue < JsonLdError; @code = "invalid @prefix value"; end
|
136
|
+
class InvalidPropagateValue < JsonLdError; @code = "invalid @propagate value"; end
|
122
137
|
class InvalidRemoteContext < JsonLdError; @code = "invalid remote context"; end
|
123
138
|
class InvalidReverseProperty < JsonLdError; @code = "invalid reverse property"; end
|
124
139
|
class InvalidReversePropertyMap < JsonLdError; @code = "invalid reverse property map"; end
|
125
140
|
class InvalidReversePropertyValue < JsonLdError; @code = "invalid reverse property value"; end
|
126
141
|
class InvalidReverseValue < JsonLdError; @code = "invalid @reverse value"; end
|
127
142
|
class InvalidScopedContext < JsonLdError; @code = "invalid scoped context"; end
|
143
|
+
class InvalidScriptElement < JsonLdError; @code = "invalid script element"; end
|
128
144
|
class InvalidSetOrListObject < JsonLdError; @code = "invalid set or list object"; end
|
129
145
|
class InvalidTermDefinition < JsonLdError; @code = "invalid term definition"; end
|
146
|
+
class InvalidBaseDirection < JsonLdError; @code = "invalid base direction"; end
|
130
147
|
class InvalidTypedValue < JsonLdError; @code = "invalid typed value"; end
|
131
148
|
class InvalidTypeMapping < JsonLdError; @code = "invalid type mapping"; end
|
132
149
|
class InvalidTypeValue < JsonLdError; @code = "invalid type value"; end
|
133
150
|
class InvalidValueObject < JsonLdError; @code = "invalid value object"; end
|
134
151
|
class InvalidValueObjectValue < JsonLdError; @code = "invalid value object value"; end
|
135
152
|
class InvalidVocabMapping < JsonLdError; @code = "invalid vocab mapping"; end
|
153
|
+
class IRIConfusedWithPrefix < JsonLdError; @code = "IRI confused with prefix"; end
|
136
154
|
class KeywordRedefinition < JsonLdError; @code = "keyword redefinition"; end
|
137
155
|
class LoadingDocumentFailed < JsonLdError; @code = "loading document failed"; end
|
138
156
|
class LoadingRemoteContextFailed < JsonLdError; @code = "loading remote context failed"; end
|
139
157
|
class ContextOverflow < JsonLdError; @code = "maximum number of @context URLs exceeded"; end
|
158
|
+
class MissingIncludedReferent < JsonLdError; @code = "missing @included referent"; end
|
140
159
|
class MultipleContextLinkHeaders < JsonLdError; @code = "multiple context link headers"; end
|
160
|
+
class ProtectedTermRedefinition < JsonLdError; @code = "protected term redefinition"; end
|
141
161
|
class ProcessingModeConflict < JsonLdError; @code = "processing mode conflict"; end
|
142
|
-
class
|
143
|
-
class
|
144
|
-
class MultipleEmbeds < InvalidFrame; end
|
145
|
-
class Syntax < InvalidFrame; end
|
146
|
-
end
|
162
|
+
class InvalidFrame < JsonLdError; @code = "invalid frame"; end
|
163
|
+
class InvalidEmbedValue < InvalidFrame; @code = "invalid @embed value"; end
|
147
164
|
end
|
148
165
|
end
|
149
166
|
end
|
data/lib/json/ld/api.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
# frozen_string_literal: true
|
3
3
|
require 'openssl'
|
4
|
+
require 'cgi'
|
4
5
|
require 'json/ld/expand'
|
5
6
|
require 'json/ld/compact'
|
6
7
|
require 'json/ld/flatten'
|
@@ -21,7 +22,7 @@ module JSON::LD
|
|
21
22
|
#
|
22
23
|
# Note that the API method signatures are somewhat different than what is specified, as the use of Futures and explicit callback parameters is not as relevant for Ruby-based interfaces.
|
23
24
|
#
|
24
|
-
# @see
|
25
|
+
# @see https://www.w3.org/TR/json-ld11-api/#the-application-programming-interface
|
25
26
|
# @author [Gregg Kellogg](http://greggkellogg.net/)
|
26
27
|
class API
|
27
28
|
include Expand
|
@@ -34,9 +35,15 @@ module JSON::LD
|
|
34
35
|
|
35
36
|
# Options used for open_file
|
36
37
|
OPEN_OPTS = {
|
37
|
-
headers: {"Accept" => "application/ld+json, application/json"}
|
38
|
+
headers: {"Accept" => "application/ld+json, text/html;q=0.8, application/json;q=0.5"}
|
38
39
|
}
|
39
40
|
|
41
|
+
# The following constants are used to reduce object allocations
|
42
|
+
LINK_REL_CONTEXT = %w(rel http://www.w3.org/ns/json-ld#context).freeze
|
43
|
+
LINK_REL_ALTERNATE = %w(rel alternate).freeze
|
44
|
+
LINK_TYPE_JSONLD = %w(type application/ld+json).freeze
|
45
|
+
JSON_LD_PROCESSING_MODES = %w(json-ld-1.0 json-ld-1.1).freeze
|
46
|
+
|
40
47
|
# Current input
|
41
48
|
# @!attribute [rw] input
|
42
49
|
# @return [String, #read, Hash, Array]
|
@@ -67,12 +74,18 @@ module JSON::LD
|
|
67
74
|
# Creates document relative IRIs when compacting, if `true`, otherwise leaves expanded.
|
68
75
|
# @option options [Proc] :documentLoader
|
69
76
|
# The callback of the loader to be used to retrieve remote documents and contexts. If specified, it must be used to retrieve remote documents and contexts; otherwise, if not specified, the processor's built-in loader must be used. See {documentLoader} for the method signature.
|
77
|
+
# @option options [Boolean] :lowercaseLanguage
|
78
|
+
# By default, language tags are left as is. To normalize to lowercase, set this option to `true`.
|
70
79
|
# @option options [String, #read, Hash, Array, JSON::LD::Context] :expandContext
|
71
80
|
# A context that is used to initialize the active context when expanding a document.
|
81
|
+
# @option options [Boolean] :extractAllScripts
|
82
|
+
# If set, when given an HTML input without a fragment identifier, extracts all `script` elements with type `application/ld+json` into an array during expansion.
|
72
83
|
# @option options [Boolean, String, RDF::URI] :flatten
|
73
84
|
# If set to a value that is not `false`, the JSON-LD processor must modify the output of the Compaction Algorithm or the Expansion Algorithm by coalescing all properties associated with each subject via the Flattening Algorithm. The value of `flatten must` be either an _IRI_ value representing the name of the graph to flatten, or `true`. If the value is `true`, then the first graph encountered in the input document is selected and flattened.
|
74
85
|
# @option options [String] :language
|
75
86
|
# When set, this has the effect of inserting a context definition with `@language` set to the associated value, creating a default language for interpreting string values.
|
87
|
+
# @option options [Symbol] :library
|
88
|
+
# One of :nokogiri or :rexml. If nil/unspecified uses :nokogiri if available, :rexml otherwise.
|
76
89
|
# @option options [String] :processingMode
|
77
90
|
# Processing mode, json-ld-1.0 or json-ld-1.1.
|
78
91
|
# If `processingMode` is not specified, a mode of `json-ld-1.0` or `json-ld-1.1` is set, the context used for `expansion` or `compaction`.
|
@@ -91,7 +104,7 @@ module JSON::LD
|
|
91
104
|
@options = {
|
92
105
|
compactArrays: true,
|
93
106
|
ordered: false,
|
94
|
-
|
107
|
+
extractAllScripts: false,
|
95
108
|
}.merge(options)
|
96
109
|
@namer = unique_bnodes ? BlankNodeUniqer.new : (rename_bnodes ? BlankNodeNamer.new("b") : BlankNodeMapper.new)
|
97
110
|
|
@@ -100,41 +113,24 @@ module JSON::LD
|
|
100
113
|
|
101
114
|
@value = case input
|
102
115
|
when Array, Hash then input.dup
|
103
|
-
when IO, StringIO
|
104
|
-
|
105
|
-
|
106
|
-
# if input impelements #links, attempt to get a contextUrl from that link
|
107
|
-
content_type = input.respond_to?(:content_type) ? input.content_type : "application/json"
|
108
|
-
context_ref = if content_type.start_with?('application/json') && input.respond_to?(:links)
|
109
|
-
link = input.links.find_link(%w(rel http://www.w3.org/ns/json-ld#context))
|
110
|
-
link.href if link
|
111
|
-
end
|
112
|
-
|
113
|
-
validate_input(input) if options[:validate]
|
114
|
-
|
115
|
-
MultiJson.load(input.read, options)
|
116
|
-
when String
|
117
|
-
remote_doc = @options[:documentLoader].call(input, @options)
|
116
|
+
when IO, StringIO, String
|
117
|
+
remote_doc = self.class.loadRemoteDocument(input, **@options)
|
118
118
|
|
119
119
|
context_ref = remote_doc.contextUrl
|
120
|
-
@options =
|
120
|
+
@options[:base] = remote_doc.documentUrl if remote_doc.documentUrl && !@options[:no_default_base]
|
121
121
|
|
122
122
|
case remote_doc.document
|
123
123
|
when String
|
124
|
-
|
125
|
-
MultiJson.load(remote_doc.document, options)
|
124
|
+
MultiJson.load(remote_doc.document, **options)
|
126
125
|
else
|
126
|
+
# Already parsed
|
127
127
|
remote_doc.document
|
128
128
|
end
|
129
129
|
end
|
130
130
|
|
131
131
|
# If not provided, first use context from document, or from a Link header
|
132
|
-
context ||=
|
133
|
-
@context = Context.parse(context || {},
|
134
|
-
|
135
|
-
# If not set explicitly, the context figures out the processing mode
|
136
|
-
@options[:processingMode] ||= @context.processingMode || "json-ld-1.0"
|
137
|
-
@options[:validate] ||= %w(json-ld-1.0 json-ld-1.1).include?(@options[:processingMode])
|
132
|
+
context ||= context_ref || {}
|
133
|
+
@context = Context.parse(context || {}, **@options)
|
138
134
|
|
139
135
|
if block_given?
|
140
136
|
case block.arity
|
@@ -165,10 +161,10 @@ module JSON::LD
|
|
165
161
|
# @yieldreturn [Object] returned object
|
166
162
|
# @return [Object, Array<Hash>]
|
167
163
|
# If a block is given, the result of evaluating the block is returned, otherwise, the expanded JSON-LD document
|
168
|
-
# @see
|
164
|
+
# @see https://www.w3.org/TR/json-ld11-api/#expansion-algorithm
|
169
165
|
def self.expand(input, framing: false, **options, &block)
|
170
166
|
result, doc_base = nil
|
171
|
-
API.new(input, options[:expandContext], options) do
|
167
|
+
API.new(input, options[:expandContext], **options) do
|
172
168
|
result = self.expand(self.value, nil, self.context,
|
173
169
|
ordered: @options[:ordered],
|
174
170
|
framing: framing)
|
@@ -214,14 +210,14 @@ module JSON::LD
|
|
214
210
|
# @return [Object, Hash]
|
215
211
|
# If a block is given, the result of evaluating the block is returned, otherwise, the compacted JSON-LD document
|
216
212
|
# @raise [JsonLdError]
|
217
|
-
# @see
|
213
|
+
# @see https://www.w3.org/TR/json-ld11-api/#compaction-algorithm
|
218
214
|
def self.compact(input, context, expanded: false, **options)
|
219
215
|
result = nil
|
220
216
|
options = {compactToRelative: true}.merge(options)
|
221
217
|
|
222
218
|
# 1) Perform the Expansion Algorithm on the JSON-LD input.
|
223
219
|
# This removes any existing context to allow the given context to be cleanly applied.
|
224
|
-
expanded_input = expanded ? input : API.expand(input,
|
220
|
+
expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
|
225
221
|
options[:base] ||= base_iri if options[:compactToRelative]
|
226
222
|
res
|
227
223
|
end
|
@@ -259,13 +255,16 @@ module JSON::LD
|
|
259
255
|
# @yieldreturn [Object] returned object
|
260
256
|
# @return [Object, Hash]
|
261
257
|
# If a block is given, the result of evaluating the block is returned, otherwise, the flattened JSON-LD document
|
262
|
-
# @see
|
258
|
+
# @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
|
263
259
|
def self.flatten(input, context, expanded: false, **options)
|
264
260
|
flattened = []
|
265
|
-
options = {
|
261
|
+
options = {
|
262
|
+
compactToRelative: true,
|
263
|
+
extractAllScripts: true,
|
264
|
+
}.merge(options)
|
266
265
|
|
267
266
|
# Expand input to simplify processing
|
268
|
-
expanded_input = expanded ? input : API.expand(input, options) do |result, base_iri|
|
267
|
+
expanded_input = expanded ? input : API.expand(input, **options) do |result, base_iri|
|
269
268
|
options[:base] ||= base_iri if options[:compactToRelative]
|
270
269
|
result
|
271
270
|
end
|
@@ -314,7 +313,7 @@ module JSON::LD
|
|
314
313
|
# @param [String, #read, Hash, Array] frame
|
315
314
|
# The frame to use when re-arranging the data.
|
316
315
|
# @option options (see #initialize)
|
317
|
-
# @option options ['@
|
316
|
+
# @option options ['@always', '@first', '@last', '@link', '@once', '@never'] :embed ('@last')
|
318
317
|
# a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI.
|
319
318
|
# @option options [Boolean] :explicit (false)
|
320
319
|
# a flag specifying that for properties to be included in the output, they must be explicitly declared in the framing context.
|
@@ -323,6 +322,7 @@ module JSON::LD
|
|
323
322
|
# @option options [Boolean] :omitDefault (false)
|
324
323
|
# a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output.
|
325
324
|
# @option options [Boolean] :expanded Input is already expanded
|
325
|
+
# @option options [Boolean] :pruneBlankNodeIdentifiers (true) removes blank node identifiers that are only used once.
|
326
326
|
# @option options [Boolean] :omitGraph does not use `@graph` at top level unless necessary to describe multiple objects, defaults to `true` if processingMode is 1.1, otherwise `false`.
|
327
327
|
# @yield jsonld
|
328
328
|
# @yieldparam [Hash] jsonld
|
@@ -331,18 +331,17 @@ module JSON::LD
|
|
331
331
|
# @return [Object, Hash]
|
332
332
|
# If a block is given, the result of evaluating the block is returned, otherwise, the framed JSON-LD document
|
333
333
|
# @raise [InvalidFrame]
|
334
|
-
# @see
|
334
|
+
# @see https://www.w3.org/TR/json-ld11-api/#framing-algorithm
|
335
335
|
def self.frame(input, frame, expanded: false, **options)
|
336
336
|
result = nil
|
337
337
|
options = {
|
338
338
|
base: (input if input.is_a?(String)),
|
339
339
|
compactArrays: true,
|
340
340
|
compactToRelative: true,
|
341
|
-
embed: '@
|
341
|
+
embed: '@once',
|
342
342
|
explicit: false,
|
343
|
-
requireAll:
|
343
|
+
requireAll: false,
|
344
344
|
omitDefault: false,
|
345
|
-
documentLoader: method(:documentLoader)
|
346
345
|
}.merge(options)
|
347
346
|
|
348
347
|
framing_state = {
|
@@ -350,37 +349,46 @@ module JSON::LD
|
|
350
349
|
graphStack: [],
|
351
350
|
subjectStack: [],
|
352
351
|
link: {},
|
352
|
+
embedded: false # False at the top-level
|
353
353
|
}
|
354
354
|
|
355
355
|
# de-reference frame to create the framing object
|
356
356
|
frame = case frame
|
357
357
|
when Hash then frame.dup
|
358
|
-
when IO, StringIO
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
358
|
+
when IO, StringIO, String
|
359
|
+
remote_doc = loadRemoteDocument(frame,
|
360
|
+
profile: 'http://www.w3.org/ns/json-ld#frame',
|
361
|
+
requestProfile: 'http://www.w3.org/ns/json-ld#frame',
|
362
|
+
**options)
|
363
|
+
if remote_doc.document.is_a?(String)
|
364
|
+
MultiJson.load(remote_doc.document)
|
365
|
+
else
|
366
|
+
remote_doc.document
|
364
367
|
end
|
365
368
|
end
|
366
369
|
|
367
370
|
# Expand input to simplify processing
|
368
|
-
expanded_input = expanded ? input : API.expand(input,
|
371
|
+
expanded_input = expanded ? input : API.expand(input, ordered: false, **options) do |res, base_iri|
|
369
372
|
options[:base] ||= base_iri if options[:compactToRelative]
|
370
373
|
res
|
371
374
|
end
|
372
375
|
|
373
376
|
# Expand frame to simplify processing
|
374
|
-
expanded_frame = API.expand(frame,
|
377
|
+
expanded_frame = API.expand(frame, framing: true, ordered: false, **options)
|
375
378
|
|
376
379
|
# Initialize input using frame as context
|
377
380
|
API.new(expanded_input, frame['@context'], no_default_base: true, **options) do
|
378
381
|
log_debug(".frame") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
|
379
382
|
log_debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE) rescue 'malformed json'}"}
|
380
383
|
|
384
|
+
if %w(@first @last).include?(options[:embed]) && context.processingMode('json-ld-1.1')
|
385
|
+
raise JSON::LD::JsonLdError::InvalidEmbedValue, "#{options[:embed]} is not a valid value of @embed in 1.1 mode" if @options[:validate]
|
386
|
+
warn "[DEPRECATION] #{options[:embed]} is not a valid value of @embed in 1.1 mode.\n"
|
387
|
+
end
|
388
|
+
|
381
389
|
# Set omitGraph option, if not present, based on processingMode
|
382
390
|
unless options.has_key?(:omitGraph)
|
383
|
-
options[:omitGraph] =
|
391
|
+
options[:omitGraph] = context.processingMode('json-ld-1.1')
|
384
392
|
end
|
385
393
|
|
386
394
|
# Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
|
@@ -402,16 +410,26 @@ module JSON::LD
|
|
402
410
|
result = []
|
403
411
|
frame(framing_state, framing_state[:subjects].keys.opt_sort(ordered: @options[:ordered]), (expanded_frame.first || {}), parent: result, **options)
|
404
412
|
|
413
|
+
# Default to based on processinMode
|
414
|
+
if !options.has_key?(:pruneBlankNodeIdentifiers)
|
415
|
+
options[:pruneBlankNodeIdentifiers] = context.processingMode('json-ld-1.1')
|
416
|
+
end
|
417
|
+
|
405
418
|
# Count blank node identifiers used in the document, if pruning
|
406
|
-
|
419
|
+
if options[:pruneBlankNodeIdentifiers]
|
407
420
|
bnodes_to_clear = count_blank_node_identifiers(result).collect {|k, v| k if v == 1}.compact
|
408
421
|
result = prune_bnodes(result, bnodes_to_clear)
|
409
422
|
end
|
410
423
|
|
411
|
-
#
|
412
|
-
|
424
|
+
# Replace values with `@preserve` with the content of its entry.
|
425
|
+
result = cleanup_preserve(result)
|
426
|
+
log_debug(".frame") {"expanded result: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
|
427
|
+
|
413
428
|
# Compact result
|
414
429
|
compacted = compact(result, ordered: @options[:ordered])
|
430
|
+
|
431
|
+
# @replace `@null` with nil, compacting arrays
|
432
|
+
compacted = cleanup_null(compacted)
|
415
433
|
compacted = [compacted] unless options[:omitGraph] || compacted.is_a?(Array)
|
416
434
|
|
417
435
|
# Add the given context to the output
|
@@ -422,7 +440,7 @@ module JSON::LD
|
|
422
440
|
context.serialize.merge({kwgraph => compacted})
|
423
441
|
end
|
424
442
|
log_debug(".frame") {"after compact: #{result.to_json(JSON_STATE) rescue 'malformed json'}"}
|
425
|
-
result
|
443
|
+
result
|
426
444
|
end
|
427
445
|
|
428
446
|
block_given? ? yield(result) : result
|
@@ -445,16 +463,20 @@ module JSON::LD
|
|
445
463
|
unless block_given?
|
446
464
|
results = []
|
447
465
|
results.extend(RDF::Enumerable)
|
448
|
-
self.toRdf(input, options) do |stmt|
|
466
|
+
self.toRdf(input, **options) do |stmt|
|
449
467
|
results << stmt
|
450
468
|
end
|
451
469
|
return results
|
452
470
|
end
|
453
471
|
|
472
|
+
options = {
|
473
|
+
extractAllScripts: true,
|
474
|
+
}.merge(options)
|
475
|
+
|
454
476
|
# Expand input to simplify processing
|
455
|
-
expanded_input = expanded ? input : API.expand(input,
|
477
|
+
expanded_input = expanded ? input : API.expand(input, ordered: false, **options)
|
456
478
|
|
457
|
-
API.new(expanded_input, nil, options) do
|
479
|
+
API.new(expanded_input, nil, **options) do
|
458
480
|
# 1) Perform the Expansion Algorithm on the JSON-LD input.
|
459
481
|
# This removes any existing context to allow the given context to be cleanly applied.
|
460
482
|
log_debug(".toRdf") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
|
@@ -496,7 +518,7 @@ module JSON::LD
|
|
496
518
|
def self.fromRdf(input, useRdfType: false, useNativeTypes: false, **options, &block)
|
497
519
|
result = nil
|
498
520
|
|
499
|
-
API.new(nil, nil, options) do
|
521
|
+
API.new(nil, nil, **options) do
|
500
522
|
result = from_statements(input,
|
501
523
|
useRdfType: useRdfType,
|
502
524
|
useNativeTypes: useNativeTypes,
|
@@ -507,62 +529,253 @@ module JSON::LD
|
|
507
529
|
end
|
508
530
|
|
509
531
|
##
|
510
|
-
#
|
532
|
+
# Uses built-in or provided documentLoader to retrieve a parsed document.
|
533
|
+
#
|
511
534
|
# @param [RDF::URI, String] url
|
512
|
-
# @param [
|
513
|
-
#
|
535
|
+
# @param [Boolean] extractAllScripts
|
536
|
+
# If set to `true`, when extracting JSON-LD script elements from HTML, unless a specific fragment identifier is targeted, extracts all encountered JSON-LD script elements using an array form, if necessary.
|
537
|
+
# @param [String] profile
|
538
|
+
# When the resulting `contentType` is `text/html`, this option determines the profile to use for selecting a JSON-LD script elements.
|
539
|
+
# @param [String] requestProfile
|
540
|
+
# One or more IRIs to use in the request as a profile parameter.
|
541
|
+
# @param [Boolean] validate
|
514
542
|
# Allow only appropriate content types
|
543
|
+
# @param [String, RDF::URI] base
|
544
|
+
# Location to use as documentUrl instead of `url`.
|
545
|
+
# @param [Hash<Symbol => Object>] options
|
515
546
|
# @yield remote_document
|
516
|
-
# @yieldparam [RemoteDocument] remote_document
|
547
|
+
# @yieldparam [RemoteDocumentRemoteDocument, RDF::Util::File::RemoteDocument] remote_document
|
517
548
|
# @yieldreturn [Object] returned object
|
518
549
|
# @return [Object, RemoteDocument]
|
519
550
|
# If a block is given, the result of evaluating the block is returned, otherwise, the retrieved remote document and context information unless block given
|
520
551
|
# @raise [JsonLdError]
|
521
|
-
def self.
|
552
|
+
def self.loadRemoteDocument(url,
|
553
|
+
extractAllScripts: false,
|
554
|
+
profile: nil,
|
555
|
+
requestProfile: nil,
|
556
|
+
validate: false,
|
557
|
+
base: nil,
|
558
|
+
**options)
|
559
|
+
documentLoader = options.fetch(:documentLoader, self.method(:documentLoader))
|
522
560
|
options = OPEN_OPTS.merge(options)
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
561
|
+
if requestProfile
|
562
|
+
# Add any request profile
|
563
|
+
options[:headers]['Accept'] = options[:headers]['Accept'].sub('application/ld+json,', "application/ld+json;profile=#{requestProfile}, application/ld+json;q=0.9,")
|
564
|
+
end
|
565
|
+
documentLoader.call(url, **options) do |remote_doc|
|
566
|
+
case remote_doc
|
567
|
+
when RDF::Util::File::RemoteDocument
|
568
|
+
# Convert to RemoteDocument
|
569
|
+
context_url = if remote_doc.content_type != 'application/ld+json' &&
|
570
|
+
(remote_doc.content_type == 'application/json' ||
|
571
|
+
remote_doc.content_type.to_s.match?(%r(application/\w+\+json)))
|
572
|
+
# Get context link(s)
|
573
|
+
# Note, we can't simply use #find_link, as we need to detect multiple
|
574
|
+
links = remote_doc.links.links.select do |link|
|
575
|
+
link.attr_pairs.include?(LINK_REL_CONTEXT)
|
576
|
+
end
|
577
|
+
raise JSON::LD::JsonLdError::MultipleContextLinkHeaders,
|
578
|
+
"expected at most 1 Link header with rel=jsonld:context, got #{links.length}" if links.length > 1
|
579
|
+
Array(links.first).first
|
580
|
+
end
|
581
|
+
|
582
|
+
# If content-type is not application/ld+json, nor any other +json and a link with rel=alternate and type='application/ld+json' is found, use that instead
|
583
|
+
alternate = !remote_doc.content_type.match?(%r(application/(\w*\+)?json)) && remote_doc.links.links.detect do |link|
|
584
|
+
link.attr_pairs.include?(LINK_REL_ALTERNATE) &&
|
585
|
+
link.attr_pairs.include?(LINK_TYPE_JSONLD)
|
586
|
+
end
|
587
|
+
|
588
|
+
remote_doc = if alternate
|
589
|
+
# Load alternate relative to URL
|
590
|
+
loadRemoteDocument(RDF::URI(url).join(alternate.href),
|
591
|
+
extractAllScripts: extractAllScripts,
|
592
|
+
profile: profile,
|
593
|
+
requestProfile: requestProfile,
|
594
|
+
validate: validate,
|
595
|
+
base: base,
|
596
|
+
**options)
|
597
|
+
else
|
598
|
+
RemoteDocument.new(remote_doc.read,
|
599
|
+
documentUrl: remote_doc.base_uri,
|
600
|
+
contentType: remote_doc.content_type,
|
601
|
+
contextUrl: context_url)
|
602
|
+
end
|
603
|
+
when RemoteDocument
|
604
|
+
# Pass through
|
605
|
+
else
|
606
|
+
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "unknown result from documentLoader: #{remote_doc.class}"
|
531
607
|
end
|
532
608
|
|
533
|
-
#
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
609
|
+
# Use specified document location
|
610
|
+
remote_doc.documentUrl = base if base
|
611
|
+
|
612
|
+
# Parse any HTML
|
613
|
+
if remote_doc.document.is_a?(String)
|
614
|
+
remote_doc.document = case remote_doc.contentType
|
615
|
+
when 'text/html'
|
616
|
+
load_html(remote_doc.document,
|
617
|
+
url: remote_doc.documentUrl,
|
618
|
+
extractAllScripts: extractAllScripts,
|
619
|
+
profile: profile,
|
620
|
+
**options) do |base|
|
621
|
+
remote_doc.documentUrl = base
|
622
|
+
end
|
623
|
+
else
|
624
|
+
validate_input(remote_doc.document, url: remote_doc.documentUrl) if validate
|
625
|
+
MultiJson.load(remote_doc.document, **options)
|
539
626
|
end
|
540
|
-
raise JSON::LD::JsonLdError::MultipleContextLinkHeaders,
|
541
|
-
"expected at most 1 Link header with rel=jsonld:context, got #{links.length}" if links.length > 1
|
542
|
-
Array(links.first).first
|
543
627
|
end
|
544
628
|
|
545
|
-
|
546
|
-
|
547
|
-
|
629
|
+
if remote_doc.contentType && validate
|
630
|
+
raise IOError, "url: #{url}, contentType: #{remote_doc.contentType}" unless
|
631
|
+
remote_doc.contentType.match?(/application\/(.+\+)?json|text\/html/)
|
632
|
+
end
|
633
|
+
block_given? ? yield(remote_doc) : remote_doc
|
548
634
|
end
|
549
|
-
rescue IOError => e
|
635
|
+
rescue IOError, MultiJson::ParseError => e
|
550
636
|
raise JSON::LD::JsonLdError::LoadingDocumentFailed, e.message
|
551
637
|
end
|
552
638
|
|
639
|
+
##
|
640
|
+
# Default document loader.
|
641
|
+
# @param [RDF::URI, String] url
|
642
|
+
# @param [Boolean] extractAllScripts
|
643
|
+
# If set to `true`, when extracting JSON-LD script elements from HTML, unless a specific fragment identifier is targeted, extracts all encountered JSON-LD script elements using an array form, if necessary.
|
644
|
+
# @param [String] profile
|
645
|
+
# When the resulting `contentType` is `text/html`, this option determines the profile to use for selecting a JSON-LD script elements.
|
646
|
+
# @param [String] requestProfile
|
647
|
+
# One or more IRIs to use in the request as a profile parameter.
|
648
|
+
# @param [Hash<Symbol => Object>] options
|
649
|
+
# @yield remote_document
|
650
|
+
# @yieldparam [RemoteDocument, RDF::Util::File::RemoteDocument] remote_document
|
651
|
+
# @raise [IOError]
|
652
|
+
def self.documentLoader(url, extractAllScripts: false, profile: nil, requestProfile: nil, **options, &block)
|
653
|
+
case url
|
654
|
+
when IO, StringIO
|
655
|
+
base_uri = options[:base]
|
656
|
+
base_uri ||= url.base_uri if url.respond_to?(:base_uri)
|
657
|
+
content_type = options[:content_type]
|
658
|
+
content_type ||= url.content_type if url.respond_to?(:content_type)
|
659
|
+
context_url = if url.respond_to?(:links) && url.links
|
660
|
+
(content_type == 'appliaction/json' || content_type.match?(%r(application/(^ld)+json)))
|
661
|
+
link = url.links.find_link(LINK_REL_CONTEXT)
|
662
|
+
link.href if link
|
663
|
+
end
|
664
|
+
|
665
|
+
block.call(RemoteDocument.new(url.read,
|
666
|
+
documentUrl: base_uri,
|
667
|
+
contentType: content_type,
|
668
|
+
contextUrl: context_url))
|
669
|
+
else
|
670
|
+
RDF::Util::File.open_file(url, **options, &block)
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
553
674
|
# Add class method aliases for backwards compatibility
|
554
675
|
class << self
|
555
676
|
alias :toRDF :toRdf
|
556
677
|
alias :fromRDF :fromRdf
|
557
678
|
end
|
558
679
|
|
680
|
+
##
|
681
|
+
# Load one or more script tags from an HTML source.
|
682
|
+
# Unescapes and uncomments input, returns the internal representation
|
683
|
+
# Yields document base
|
684
|
+
# @param [String] input
|
685
|
+
# @param [String] url Original URL
|
686
|
+
# @param [:nokogiri, :rexml] library (nil)
|
687
|
+
# @param [Boolean] extractAllScripts (false)
|
688
|
+
# @param [Boolean] profile (nil) Optional priortized profile when loading a single script by type.
|
689
|
+
# @param [Hash{Symbol => Object}] options
|
690
|
+
def self.load_html(input, url:,
|
691
|
+
library: nil,
|
692
|
+
extractAllScripts: false,
|
693
|
+
profile: nil,
|
694
|
+
**options)
|
695
|
+
|
696
|
+
if input.is_a?(String)
|
697
|
+
library ||= begin
|
698
|
+
require 'nokogiri'
|
699
|
+
:nokogiri
|
700
|
+
rescue LoadError
|
701
|
+
:rexml
|
702
|
+
end
|
703
|
+
require "json/ld/html/#{library}"
|
704
|
+
|
705
|
+
# Parse HTML using the appropriate library
|
706
|
+
@implementation = case library
|
707
|
+
when :nokogiri then Nokogiri
|
708
|
+
when :rexml then REXML
|
709
|
+
end
|
710
|
+
self.extend(@implementation)
|
711
|
+
|
712
|
+
input = begin
|
713
|
+
initialize_html(input, **options)
|
714
|
+
rescue
|
715
|
+
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "Malformed HTML document: #{$!.message}"
|
716
|
+
end
|
717
|
+
|
718
|
+
# Potentially update options[:base]
|
719
|
+
if html_base = input.at_xpath("/html/head/base/@href")
|
720
|
+
base = RDF::URI(url) if url
|
721
|
+
html_base = RDF::URI(html_base)
|
722
|
+
html_base = base.join(html_base) if base
|
723
|
+
yield html_base
|
724
|
+
end
|
725
|
+
end
|
726
|
+
|
727
|
+
url = RDF::URI.parse(url)
|
728
|
+
if url.fragment
|
729
|
+
id = CGI.unescape(url.fragment)
|
730
|
+
# Find script with an ID based on that fragment.
|
731
|
+
element = input.at_xpath("//script[@id='#{id}']")
|
732
|
+
raise JSON::LD::JsonLdError::InvalidScriptElement, "No script tag found with id=#{id}" unless element
|
733
|
+
raise JSON::LD::JsonLdError::InvalidScriptElement, "Script tag has type=#{element.attributes['type']}" unless element.attributes['type'].to_s.start_with?('application/ld+json')
|
734
|
+
content = element.inner_html
|
735
|
+
validate_input(content, url: url) if options[:validate]
|
736
|
+
MultiJson.load(content, **options)
|
737
|
+
elsif extractAllScripts
|
738
|
+
res = []
|
739
|
+
elements = if profile
|
740
|
+
es = input.xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]")
|
741
|
+
# If no profile script, just take a single script without profile
|
742
|
+
es = [input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")] if es.empty?
|
743
|
+
es
|
744
|
+
else
|
745
|
+
input.xpath("//script[starts-with(@type, 'application/ld+json')]")
|
746
|
+
end
|
747
|
+
elements.each do |element|
|
748
|
+
content = element.inner_html
|
749
|
+
validate_input(content, url: url) if options[:validate]
|
750
|
+
r = MultiJson.load(content, **options)
|
751
|
+
if r.is_a?(Hash)
|
752
|
+
res << r
|
753
|
+
elsif r.is_a?(Array)
|
754
|
+
res = res.concat(r)
|
755
|
+
end
|
756
|
+
end
|
757
|
+
res
|
758
|
+
else
|
759
|
+
# Find the first script with type application/ld+json.
|
760
|
+
element = input.at_xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]") if profile
|
761
|
+
element ||= input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")
|
762
|
+
content = element ? element.inner_html : "[]"
|
763
|
+
validate_input(content, url: url) if options[:validate]
|
764
|
+
MultiJson.load(content, **options)
|
765
|
+
end
|
766
|
+
rescue JSON::LD::JsonLdError::LoadingDocumentFailed, MultiJson::ParseError => e
|
767
|
+
raise JSON::LD::JsonLdError::InvalidScriptElement, e.message
|
768
|
+
end
|
769
|
+
|
770
|
+
##
|
771
|
+
# Validate JSON using JsonLint, if loaded
|
559
772
|
private
|
560
|
-
def validate_input(input)
|
773
|
+
def self.validate_input(input, url:)
|
561
774
|
return unless defined?(JsonLint)
|
562
775
|
jsonlint = JsonLint::Linter.new
|
563
776
|
input = StringIO.new(input) unless input.respond_to?(:read)
|
564
777
|
unless jsonlint.check_stream(input)
|
565
|
-
raise JsonLdError::LoadingDocumentFailed, jsonlint.errors[''].join("\n")
|
778
|
+
raise JsonLdError::LoadingDocumentFailed, "url: #{url}\n" + jsonlint.errors[''].join("\n")
|
566
779
|
end
|
567
780
|
input.rewind
|
568
781
|
end
|
@@ -570,26 +783,42 @@ module JSON::LD
|
|
570
783
|
##
|
571
784
|
# A {RemoteDocument} is returned from a {documentLoader}.
|
572
785
|
class RemoteDocument
|
573
|
-
#
|
574
|
-
|
786
|
+
# The final URL of the loaded document. This is important to handle HTTP redirects properly.
|
787
|
+
# @return [String]
|
788
|
+
attr_accessor :documentUrl
|
575
789
|
|
576
|
-
#
|
577
|
-
#
|
578
|
-
attr_reader :
|
790
|
+
# The Content-Type of the loaded document, exclusive of any optional parameters.
|
791
|
+
# @return [String]
|
792
|
+
attr_reader :contentType
|
579
793
|
|
580
794
|
# @return [String]
|
581
795
|
# The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
|
582
796
|
attr_accessor :contextUrl
|
583
797
|
|
584
|
-
#
|
585
|
-
# @
|
586
|
-
|
587
|
-
|
798
|
+
# The parsed retrieved document.
|
799
|
+
# @return [Array<Hash>, Hash]
|
800
|
+
attr_accessor :document
|
801
|
+
|
802
|
+
# The value of any profile parameter retrieved as part of the original contentType.
|
803
|
+
# @return [String]
|
804
|
+
attr_accessor :profile
|
805
|
+
|
806
|
+
# @param [RDF::Util::File::RemoteDocument] document
|
807
|
+
# @param [String] documentUrl
|
808
|
+
# The final URL of the loaded document. This is important to handle HTTP redirects properly.
|
809
|
+
# @param [String] contentType
|
810
|
+
# The Content-Type of the loaded document, exclusive of any optional parameters.
|
811
|
+
# @param [String] contextUrl
|
588
812
|
# The URL of a remote context as specified by an HTTP Link header with rel=`http://www.w3.org/ns/json-ld#context`
|
589
|
-
|
590
|
-
|
813
|
+
# @param [String] profile
|
814
|
+
# The value of any profile parameter retrieved as part of the original contentType.
|
815
|
+
# @option options [Hash{Symbol => Object}] options
|
816
|
+
def initialize(document, documentUrl: nil, contentType: nil, contextUrl: nil, profile: nil, **options)
|
591
817
|
@document = document
|
592
|
-
@
|
818
|
+
@documentUrl = documentUrl || options[:base_uri]
|
819
|
+
@contentType = contentType || options[:content_type]
|
820
|
+
@contextUrl = contextUrl
|
821
|
+
@profile = profile
|
593
822
|
end
|
594
823
|
end
|
595
824
|
end
|