Skip to content

Commit 6527307

Browse files
committed
Finish 3.3.2
2 parents 85e64aa + bdc66c2 commit 6527307

File tree

12 files changed

+368
-43
lines changed

12 files changed

+368
-43
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
fail-fast: false
2121
matrix:
22-
ruby: ['3.0', 3.1, 3.2, ruby-head, jruby]
22+
ruby: ['3.0', 3.1, 3.2, 3.3, ruby-head, jruby]
2323
steps:
2424
- name: Clone repository
2525
uses: actions/checkout@v3

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,10 @@ To get a local working copy of the development repository, do:
603603

604604
% git clone git://github.com/ruby-rdf/json-ld.git
605605

606+
## Change Log
607+
608+
See [Release Notes on GitHub](https://github.com/ruby-rdf/json-ld/releases)
609+
606610
## Mailing List
607611
* <https://lists.w3.org/Archives/Public/public-rdf-ruby/>
608612

VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.3.1
1+
3.3.2

example-files/vc1373-2.jsonld

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"@context": [
3+
{
4+
"@protected": true,
5+
"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#",
6+
"id": "@id",
7+
"type": "@type",
8+
"VerifiablePresentation": {
9+
"@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
10+
"@context": {
11+
"@protected": true,
12+
"id": "@id",
13+
"type": "@type",
14+
"holder": {
15+
"@id": "https://www.w3.org/2018/credentials#holder",
16+
"@type": "@id"
17+
},
18+
"proof": {
19+
"@id": "https://w3id.org/security#proof",
20+
"@type": "@id",
21+
"@container": "@graph"
22+
},
23+
"verifiableCredential": {
24+
"@id": "https://www.w3.org/2018/credentials#verifiableCredential",
25+
"@type": "@id",
26+
"@container": "@graph",
27+
"@context": null
28+
},
29+
"termsOfUse": {
30+
"@id": "https://www.w3.org/2018/credentials#termsOfUse",
31+
"@type": "@id"
32+
}
33+
}
34+
},
35+
"issuer": {
36+
"@id": "https://www.w3.org/2018/credentials#issuer",
37+
"@type": "@id",
38+
"@context": {
39+
"@protected": true,
40+
41+
"id": "@id",
42+
"type": "@type",
43+
44+
"description": {
45+
"@id": "https://schema.org/description",
46+
"@context": {
47+
"value": "@value", "lang": "@language", "dir": "@direction"
48+
}
49+
},
50+
"name": {
51+
"@id": "https://schema.org/name",
52+
"@context": {
53+
"value": "@value", "lang": "@language", "dir": "@direction"
54+
}
55+
}
56+
}
57+
}
58+
}
59+
],
60+
"type": "VerifiablePresentation",
61+
"verifiableCredential": ["http://university.example/credentials/1872"]
62+
}

example-files/vc1373.jsonld

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"@context": [
3+
{
4+
"@protected": true,
5+
"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#",
6+
"id": "@id",
7+
"type": "@type",
8+
"VerifiablePresentation": {
9+
"@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
10+
"@context": {
11+
"@protected": true,
12+
"id": "@id",
13+
"type": "@type",
14+
"holder": {
15+
"@id": "https://www.w3.org/2018/credentials#holder",
16+
"@type": "@id"
17+
},
18+
"proof": {
19+
"@id": "https://w3id.org/security#proof",
20+
"@type": "@id",
21+
"@container": "@graph"
22+
},
23+
"verifiableCredential": {
24+
"@id": "https://www.w3.org/2018/credentials#verifiableCredential",
25+
"@type": "@id",
26+
"@container": "@graph",
27+
"@context": null
28+
},
29+
"termsOfUse": {
30+
"@id": "https://www.w3.org/2018/credentials#termsOfUse",
31+
"@type": "@id"
32+
}
33+
}
34+
},
35+
"issuer": {
36+
"@id": "https://www.w3.org/2018/credentials#issuer",
37+
"@type": "@id",
38+
"@context": {
39+
"@protected": true,
40+
41+
"id": "@id",
42+
"type": "@type",
43+
44+
"description": {
45+
"@id": "https://schema.org/description",
46+
"@context": {
47+
"value": "@value", "lang": "@language", "dir": "@direction"
48+
}
49+
},
50+
"name": {
51+
"@id": "https://schema.org/name",
52+
"@context": {
53+
"value": "@value", "lang": "@language", "dir": "@direction"
54+
}
55+
}
56+
}
57+
}
58+
}
59+
],
60+
"type": "VerifiablePresentation",
61+
"verifiableCredential": [
62+
{
63+
"@context": [
64+
{
65+
"@protected": true,
66+
"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#",
67+
"id": "@id",
68+
"type": "@type",
69+
"VerifiablePresentation": {
70+
"@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
71+
"@context": {
72+
"@protected": true,
73+
"id": "@id",
74+
"type": "@type",
75+
"holder": {
76+
"@id": "https://www.w3.org/2018/credentials#holder",
77+
"@type": "@id"
78+
},
79+
"proof": {
80+
"@id": "https://w3id.org/security#proof",
81+
"@type": "@id",
82+
"@container": "@graph"
83+
},
84+
"verifiableCredential": {
85+
"@id": "https://www.w3.org/2018/credentials#verifiableCredential",
86+
"@type": "@id",
87+
"@container": "@graph",
88+
"@context": null
89+
},
90+
"termsOfUse": {
91+
"@id": "https://www.w3.org/2018/credentials#termsOfUse",
92+
"@type": "@id"
93+
}
94+
}
95+
},
96+
"issuer": {
97+
"@id": "https://www.w3.org/2018/credentials#issuer",
98+
"@type": "@id",
99+
"@context": {
100+
"@protected": true,
101+
102+
"id": "@id",
103+
"type": "@type",
104+
105+
"description": {
106+
"@id": "https://schema.org/description",
107+
"@context": {
108+
"value": "@value", "lang": "@language", "dir": "@direction"
109+
}
110+
},
111+
"name": {
112+
"@id": "https://schema.org/name",
113+
"@context": {
114+
"value": "@value", "lang": "@language", "dir": "@direction"
115+
}
116+
}
117+
}
118+
}
119+
}
120+
],
121+
"id": "http://university.example/credentials/1872",
122+
"type": "VerifiableCredential",
123+
"issuer": "https://university.example/issuers/565049"
124+
}
125+
]
126+
}

json-ld.gemspec

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Gem::Specification.new do |gem|
3737
gem.add_runtime_dependency 'multi_json', '~> 1.15'
3838
gem.add_runtime_dependency "rack", '>= 2.2', '< 4'
3939
gem.add_runtime_dependency 'rdf', '~> 3.3'
40+
gem.add_runtime_dependency 'rexml', '~> 3.2'
41+
gem.add_development_dependency 'getoptlong', '~> 0.2'
4042
gem.add_development_dependency 'jsonlint', '~> 0.4' unless is_java
4143
gem.add_development_dependency 'oj', '~> 3.15' unless is_java
4244
gem.add_development_dependency 'rack-test', '~> 2.1'

lib/json/ld/api.rb

+60-32
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,7 @@ def self.compact(input, context, expanded: false, serializer: nil, **options)
290290
def self.flatten(input, context, expanded: false, serializer: nil, **options)
291291
flattened = []
292292
options = {
293-
compactToRelative: true,
294-
extractAllScripts: true
293+
compactToRelative: true
295294
}.merge(options)
296295

297296
# Expand input to simplify processing
@@ -518,6 +517,8 @@ def self.frame(input, frame, expanded: false, serializer: nil, **options)
518517
# @option options (see #initialize)
519518
# @option options [Boolean] :produceGeneralizedRdf (false)
520519
# If true, output will include statements having blank node predicates, otherwise they are dropped.
520+
# @option options [Boolean] :extractAllScripts (true)
521+
# 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.
521522
# @raise [JsonLdError]
522523
# @yield statement
523524
# @yieldparam [RDF::Statement] statement
@@ -638,7 +639,7 @@ def self.loadRemoteDocument(url,
638639
options[:headers]['Accept'].sub('application/ld+json,',
639640
"application/ld+json;profile=#{requestProfile}, application/ld+json;q=0.9,")
640641
end
641-
documentLoader.call(url, **options) do |remote_doc|
642+
documentLoader.call(url, extractAllScripts: extractAllScripts, **options) do |remote_doc|
642643
case remote_doc
643644
when RDF::Util::File::RemoteDocument
644645
# Convert to RemoteDocument
@@ -758,6 +759,28 @@ class << self
758759
alias fromRDF fromRdf
759760
end
760761

762+
##
763+
# Hash of recognized script types and the loaders that decode them
764+
# into a hash or array of hashes.
765+
#
766+
# @return Hash{type, Proc}
767+
SCRIPT_LOADERS = {
768+
'application/ld+json' => ->(content, url:, **options) do
769+
validate_input(content, url: url) if options[:validate]
770+
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
771+
MultiJson.load(content, **mj_opts)
772+
end
773+
}
774+
775+
##
776+
# Adds a loader for some specific content type
777+
#
778+
# @param [String] type
779+
# @param [Proc] loader
780+
def self.add_script_loader(type, loader)
781+
SCRIPT_LOADERS[type] = loader
782+
end
783+
761784
##
762785
# Load one or more script tags from an HTML source.
763786
# Unescapes and uncomments input, returns the internal representation
@@ -812,47 +835,52 @@ def self.load_html(input, url:,
812835
element = input.at_xpath("//script[@id='#{id}']")
813836
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found with id=#{id}" unless element
814837

815-
unless element.attributes['type'].to_s.start_with?('application/ld+json')
838+
script_type = SCRIPT_LOADERS.keys.detect {|type| element.attributes['type'].to_s.start_with?(type)}
839+
unless script_type
816840
raise JSON::LD::JsonLdError::LoadingDocumentFailed,
817841
"Script tag has type=#{element.attributes['type']}"
818842
end
819843

820-
content = element.inner_html
821-
validate_input(content, url: url) if options[:validate]
822-
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
823-
MultiJson.load(content, **mj_opts)
844+
loader = SCRIPT_LOADERS[script_type]
845+
loader.call(element.inner_html, url: url, **options)
824846
elsif extractAllScripts
825847
res = []
826-
elements = if profile
827-
es = input.xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]")
828-
# If no profile script, just take a single script without profile
829-
es = [input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")].compact if es.empty?
830-
es
831-
else
832-
input.xpath("//script[starts-with(@type, 'application/ld+json')]")
833-
end
834-
elements.each do |element|
835-
content = element.inner_html
836-
validate_input(content, url: url) if options[:validate]
837-
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
838-
r = MultiJson.load(content, **mj_opts)
839-
if r.is_a?(Hash)
840-
res << r
841-
elsif r.is_a?(Array)
842-
res.concat(r)
848+
849+
SCRIPT_LOADERS.each do |type, loader|
850+
elements = if profile
851+
es = input.xpath("//script[starts-with(@type, '#{type};profile=#{profile}')]")
852+
# If no profile script, just take a single script without profile
853+
es = [input.at_xpath("//script[starts-with(@type, '#{type}')]")].compact if es.empty?
854+
es
855+
else
856+
input.xpath("//script[starts-with(@type, '#{type}')]")
857+
end
858+
elements.each do |element|
859+
content = element.inner_html
860+
r = loader.call(content, url: url, extractAllScripts: true, **options)
861+
if r.is_a?(Hash)
862+
res << r
863+
elsif r.is_a?(Array)
864+
res.concat(r)
865+
end
843866
end
844867
end
845868
res
846869
else
847-
# Find the first script with type application/ld+json.
848-
element = input.at_xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]") if profile
849-
element ||= input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")
850-
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found" unless element
870+
# Find the first script with a known type
871+
script_type, element = nil, nil
872+
SCRIPT_LOADERS.keys.each do |type|
873+
next if script_type # already found the type
874+
element = input.at_xpath("//script[starts-with(@type, '#{type};profile=#{profile}')]") if profile
875+
element ||= input.at_xpath("//script[starts-with(@type, '#{type}')]")
876+
script_type = type if element
877+
end
878+
unless script_type
879+
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found" unless element
880+
end
851881

852882
content = element.inner_html
853-
validate_input(content, url: url) if options[:validate]
854-
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
855-
MultiJson.load(content, **mj_opts)
883+
SCRIPT_LOADERS[script_type].call(content, url: url, **options)
856884
end
857885
rescue MultiJson::ParseError => e
858886
raise JSON::LD::JsonLdError::InvalidScriptElement, e.message

lib/json/ld/compact.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,10 @@ def compact(element,
299299
else
300300
index_key = context.expand_iri(index_key, vocab: true)
301301
container_key = context.compact_iri(index_key, vocab: true)
302-
map_key, *others = Array(compacted_item[container_key])
302+
map_key, *others = Array(compacted_item.is_a?(Hash) && compacted_item[container_key])
303303
if map_key.is_a?(String)
304304
case others.length
305-
when 0 then compacted_item.delete(container_key)
305+
when 0 then compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
306306
when 1 then compacted_item[container_key] = others.first
307307
else compacted_item[container_key] = others
308308
end
@@ -316,15 +316,15 @@ def compact(element,
316316
map_key = expanded_item['@language']
317317
value?(expanded_item) ? expanded_item['@value'] : compacted_item
318318
elsif container.include?('@type')
319-
map_key, *types = Array(compacted_item[container_key])
319+
map_key, *types = Array(compacted_item.is_a?(Hash) && compacted_item[container_key])
320320
case types.length
321-
when 0 then compacted_item.delete(container_key)
321+
when 0 then compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
322322
when 1 then compacted_item[container_key] = types.first
323323
else compacted_item[container_key] = types
324324
end
325325

326326
# if compacted_item contains a single entry who's key maps to @id, then recompact the item without @type
327-
if compacted_item.keys.length == 1 && expanded_item.key?('@id')
327+
if compacted_item.is_a?(Hash) && compacted_item.keys.length == 1 && expanded_item.key?('@id')
328328
compacted_item = compact({ '@id' => expanded_item['@id'] },
329329
base: base,
330330
property: item_active_property,

0 commit comments

Comments
 (0)