Skip to content

Commit

Permalink
Finish 3.3.2
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Jul 29, 2024
2 parents 85e64aa + bdc66c2 commit 6527307
Show file tree
Hide file tree
Showing 12 changed files with 368 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ['3.0', 3.1, 3.2, ruby-head, jruby]
ruby: ['3.0', 3.1, 3.2, 3.3, ruby-head, jruby]
steps:
- name: Clone repository
uses: actions/checkout@v3
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,10 @@ To get a local working copy of the development repository, do:

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

## Change Log

See [Release Notes on GitHub](https://github.com/ruby-rdf/json-ld/releases)

## Mailing List
* <https://lists.w3.org/Archives/Public/public-rdf-ruby/>

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.1
3.3.2
62 changes: 62 additions & 0 deletions example-files/vc1373-2.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"@context": [
{
"@protected": true,
"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#",
"id": "@id",
"type": "@type",
"VerifiablePresentation": {
"@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
"@context": {
"@protected": true,
"id": "@id",
"type": "@type",
"holder": {
"@id": "https://www.w3.org/2018/credentials#holder",
"@type": "@id"
},
"proof": {
"@id": "https://w3id.org/security#proof",
"@type": "@id",
"@container": "@graph"
},
"verifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#verifiableCredential",
"@type": "@id",
"@container": "@graph",
"@context": null
},
"termsOfUse": {
"@id": "https://www.w3.org/2018/credentials#termsOfUse",
"@type": "@id"
}
}
},
"issuer": {
"@id": "https://www.w3.org/2018/credentials#issuer",
"@type": "@id",
"@context": {
"@protected": true,

"id": "@id",
"type": "@type",

"description": {
"@id": "https://schema.org/description",
"@context": {
"value": "@value", "lang": "@language", "dir": "@direction"
}
},
"name": {
"@id": "https://schema.org/name",
"@context": {
"value": "@value", "lang": "@language", "dir": "@direction"
}
}
}
}
}
],
"type": "VerifiablePresentation",
"verifiableCredential": ["http://university.example/credentials/1872"]
}
126 changes: 126 additions & 0 deletions example-files/vc1373.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{
"@context": [
{
"@protected": true,
"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#",
"id": "@id",
"type": "@type",
"VerifiablePresentation": {
"@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
"@context": {
"@protected": true,
"id": "@id",
"type": "@type",
"holder": {
"@id": "https://www.w3.org/2018/credentials#holder",
"@type": "@id"
},
"proof": {
"@id": "https://w3id.org/security#proof",
"@type": "@id",
"@container": "@graph"
},
"verifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#verifiableCredential",
"@type": "@id",
"@container": "@graph",
"@context": null
},
"termsOfUse": {
"@id": "https://www.w3.org/2018/credentials#termsOfUse",
"@type": "@id"
}
}
},
"issuer": {
"@id": "https://www.w3.org/2018/credentials#issuer",
"@type": "@id",
"@context": {
"@protected": true,

"id": "@id",
"type": "@type",

"description": {
"@id": "https://schema.org/description",
"@context": {
"value": "@value", "lang": "@language", "dir": "@direction"
}
},
"name": {
"@id": "https://schema.org/name",
"@context": {
"value": "@value", "lang": "@language", "dir": "@direction"
}
}
}
}
}
],
"type": "VerifiablePresentation",
"verifiableCredential": [
{
"@context": [
{
"@protected": true,
"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#",
"id": "@id",
"type": "@type",
"VerifiablePresentation": {
"@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
"@context": {
"@protected": true,
"id": "@id",
"type": "@type",
"holder": {
"@id": "https://www.w3.org/2018/credentials#holder",
"@type": "@id"
},
"proof": {
"@id": "https://w3id.org/security#proof",
"@type": "@id",
"@container": "@graph"
},
"verifiableCredential": {
"@id": "https://www.w3.org/2018/credentials#verifiableCredential",
"@type": "@id",
"@container": "@graph",
"@context": null
},
"termsOfUse": {
"@id": "https://www.w3.org/2018/credentials#termsOfUse",
"@type": "@id"
}
}
},
"issuer": {
"@id": "https://www.w3.org/2018/credentials#issuer",
"@type": "@id",
"@context": {
"@protected": true,

"id": "@id",
"type": "@type",

"description": {
"@id": "https://schema.org/description",
"@context": {
"value": "@value", "lang": "@language", "dir": "@direction"
}
},
"name": {
"@id": "https://schema.org/name",
"@context": {
"value": "@value", "lang": "@language", "dir": "@direction"
}
}
}
}
}
],
"id": "http://university.example/credentials/1872",
"type": "VerifiableCredential",
"issuer": "https://university.example/issuers/565049"
}
]
}
2 changes: 2 additions & 0 deletions json-ld.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency 'multi_json', '~> 1.15'
gem.add_runtime_dependency "rack", '>= 2.2', '< 4'
gem.add_runtime_dependency 'rdf', '~> 3.3'
gem.add_runtime_dependency 'rexml', '~> 3.2'
gem.add_development_dependency 'getoptlong', '~> 0.2'
gem.add_development_dependency 'jsonlint', '~> 0.4' unless is_java
gem.add_development_dependency 'oj', '~> 3.15' unless is_java
gem.add_development_dependency 'rack-test', '~> 2.1'
Expand Down
92 changes: 60 additions & 32 deletions lib/json/ld/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,7 @@ def self.compact(input, context, expanded: false, serializer: nil, **options)
def self.flatten(input, context, expanded: false, serializer: nil, **options)
flattened = []
options = {
compactToRelative: true,
extractAllScripts: true
compactToRelative: true
}.merge(options)

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

##
# Hash of recognized script types and the loaders that decode them
# into a hash or array of hashes.
#
# @return Hash{type, Proc}
SCRIPT_LOADERS = {
'application/ld+json' => ->(content, url:, **options) do
validate_input(content, url: url) if options[:validate]
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
MultiJson.load(content, **mj_opts)
end
}

##
# Adds a loader for some specific content type
#
# @param [String] type
# @param [Proc] loader
def self.add_script_loader(type, loader)
SCRIPT_LOADERS[type] = loader
end

##
# Load one or more script tags from an HTML source.
# Unescapes and uncomments input, returns the internal representation
Expand Down Expand Up @@ -812,47 +835,52 @@ def self.load_html(input, url:,
element = input.at_xpath("//script[@id='#{id}']")
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found with id=#{id}" unless element

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

content = element.inner_html
validate_input(content, url: url) if options[:validate]
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
MultiJson.load(content, **mj_opts)
loader = SCRIPT_LOADERS[script_type]
loader.call(element.inner_html, url: url, **options)
elsif extractAllScripts
res = []
elements = if profile
es = input.xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]")
# If no profile script, just take a single script without profile
es = [input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")].compact if es.empty?
es
else
input.xpath("//script[starts-with(@type, 'application/ld+json')]")
end
elements.each do |element|
content = element.inner_html
validate_input(content, url: url) if options[:validate]
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
r = MultiJson.load(content, **mj_opts)
if r.is_a?(Hash)
res << r
elsif r.is_a?(Array)
res.concat(r)

SCRIPT_LOADERS.each do |type, loader|
elements = if profile
es = input.xpath("//script[starts-with(@type, '#{type};profile=#{profile}')]")
# If no profile script, just take a single script without profile
es = [input.at_xpath("//script[starts-with(@type, '#{type}')]")].compact if es.empty?
es
else
input.xpath("//script[starts-with(@type, '#{type}')]")
end
elements.each do |element|
content = element.inner_html
r = loader.call(content, url: url, extractAllScripts: true, **options)
if r.is_a?(Hash)
res << r
elsif r.is_a?(Array)
res.concat(r)
end
end
end
res
else
# Find the first script with type application/ld+json.
element = input.at_xpath("//script[starts-with(@type, 'application/ld+json;profile=#{profile}')]") if profile
element ||= input.at_xpath("//script[starts-with(@type, 'application/ld+json')]")
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found" unless element
# Find the first script with a known type
script_type, element = nil, nil
SCRIPT_LOADERS.keys.each do |type|
next if script_type # already found the type
element = input.at_xpath("//script[starts-with(@type, '#{type};profile=#{profile}')]") if profile
element ||= input.at_xpath("//script[starts-with(@type, '#{type}')]")
script_type = type if element
end
unless script_type
raise JSON::LD::JsonLdError::LoadingDocumentFailed, "No script tag found" unless element
end

content = element.inner_html
validate_input(content, url: url) if options[:validate]
mj_opts = options.keep_if { |k, v| k != :adapter || MUTLI_JSON_ADAPTERS.include?(v) }
MultiJson.load(content, **mj_opts)
SCRIPT_LOADERS[script_type].call(content, url: url, **options)
end
rescue MultiJson::ParseError => e
raise JSON::LD::JsonLdError::InvalidScriptElement, e.message
Expand Down
10 changes: 5 additions & 5 deletions lib/json/ld/compact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,10 @@ def compact(element,
else
index_key = context.expand_iri(index_key, vocab: true)
container_key = context.compact_iri(index_key, vocab: true)
map_key, *others = Array(compacted_item[container_key])
map_key, *others = Array(compacted_item.is_a?(Hash) && compacted_item[container_key])
if map_key.is_a?(String)
case others.length
when 0 then compacted_item.delete(container_key)
when 0 then compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
when 1 then compacted_item[container_key] = others.first
else compacted_item[container_key] = others
end
Expand All @@ -316,15 +316,15 @@ def compact(element,
map_key = expanded_item['@language']
value?(expanded_item) ? expanded_item['@value'] : compacted_item
elsif container.include?('@type')
map_key, *types = Array(compacted_item[container_key])
map_key, *types = Array(compacted_item.is_a?(Hash) && compacted_item[container_key])
case types.length
when 0 then compacted_item.delete(container_key)
when 0 then compacted_item.delete(container_key) if compacted_item.is_a?(Hash)
when 1 then compacted_item[container_key] = types.first
else compacted_item[container_key] = types
end

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

0 comments on commit 6527307

Please sign in to comment.