From 4d3e32506f1fa014d5af94477537ffb7b760be36 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 28 Dec 2020 16:21:10 -0800 Subject: [PATCH 01/17] Don't use native types for invalid literals. --- lib/json/ld/context.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/json/ld/context.rb b/lib/json/ld/context.rb index 67984c61..2d5677e0 100644 --- a/lib/json/ld/context.rb +++ b/lib/json/ld/context.rb @@ -1523,11 +1523,11 @@ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base res['@language'] = lang end res['@direction'] = dir - elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype) + elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype) && value.valid? res['@type'] = uri(coerce(property)) if coerce(property) res['@value'] = value.object else - value.canonicalize! if value.datatype == RDF::XSD.double + value.canonicalize! if value.valid? && value.datatype == RDF::XSD.double if coerce(property) res['@type'] = uri(coerce(property)).to_s elsif value.has_datatype? From 74a7014b9b387bff8008965b69040322922aac28 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 4 Jan 2021 16:48:49 -0800 Subject: [PATCH 02/17] Update project descriptions. --- README.md | 4 +- etc/doap.ttl | 6 +- example-files/shacl-severity-002-frame.jsonld | 44 ++++ example-files/shacl-severity-002.jsonld | 191 ++++++++++++++++++ json-ld.gemspec | 2 +- 5 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 example-files/shacl-severity-002-frame.jsonld create mode 100644 example-files/shacl-severity-002.jsonld mode change 100644 => 100755 json-ld.gemspec diff --git a/README.md b/README.md index cb522c7c..111b26b5 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Gem Version](https://badge.fury.io/rb/json-ld.png)](https://rubygems.org/gems/json-ld) [![Build Status](https://secure.travis-ci.org/ruby-rdf/json-ld.png?branch=develop)](https://github.com/ruby-rdf/json-ld/actions?query=workflow%3ACI) -[![Coverage Status](https://coveralls.io/repos/ruby-rdf/json-ld/badge.svg)](https://coveralls.io/github/ruby-rdf/json-ld) +[![Coverage Status](https://coveralls.io/repos/ruby-rdf/json-ld/badge.svg?branch=develop)](https://coveralls.io/github/ruby-rdf/json-ld?branch=develop) [![Gitter chat](https://badges.gitter.im/ruby-rdf.png)](https://gitter.im/gitterHQ/gitter) ## Features @@ -17,7 +17,7 @@ JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and * If available, uses [Nokogiri][] and/or [Nokogumbo][] for parsing HTML, falls back to REXML otherwise. * Provisional support for [JSON-LD*][JSON-LD*]. -[Implementation Report](file.earl.html) +[Implementation Report](https://ruby-rdf.github.io/json-ld/etc/earl.html) Install with `gem install json-ld` diff --git a/etc/doap.ttl b/etc/doap.ttl index 5f9b3c1b..e56bcb50 100644 --- a/etc/doap.ttl +++ b/etc/doap.ttl @@ -8,11 +8,13 @@ @prefix xsd: . <> a doap:Project; + doap:name "JSON::LD"^^xsd:string; + doap:shortdesc "JSON-LD support for Ruby."@en; + doap:description "JSON::LD parses and serializes JSON-LD into RDF and implements expansion, compaction and framing API interfaces for the Ruby RDF.rb library suite."@en; dc:creator ; doap:blog ; doap:bug-database ; doap:created "2011-05-07"^^xsd:date; - doap:description "RDF.rb extension for parsing/serializing JSON-LD data."@en; doap:developer ; doap:documenter ; doap:homepage ; @@ -21,9 +23,7 @@ ; doap:license ; doap:maintainer ; - doap:name "JSON::LD"^^xsd:string; doap:programming-language "Ruby"; - doap:shortdesc "JSON-LD support for RDF.rb."@en; foaf:maker . a foaf:Person; diff --git a/example-files/shacl-severity-002-frame.jsonld b/example-files/shacl-severity-002-frame.jsonld new file mode 100644 index 00000000..99c5bdd6 --- /dev/null +++ b/example-files/shacl-severity-002-frame.jsonld @@ -0,0 +1,44 @@ +{ + "@context": { + "id": "@id", + "type": {"@id": "@type", "@container": "@set"}, + "@vocab": "http://www.w3.org/ns/shacl#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "shacl": "http://www.w3.org/ns/shacl#", + "sh": "http://www.w3.org/ns/shacl#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "and": {"@type": "@id", "@container": "@list"}, + "annotationProperty": {"@type": "@id"}, + "class": {"@type": "@id"}, + "comment": "http://www.w3.org/2000/01/rdf-schema#comment", + "condition": {"@type": "@id"}, + "datatype": {"@type": "@vocab"}, + "declare": {"@type": "@id"}, + "disjoint": {"@type": "@id"}, + "disjoint": {"@type": "@id"}, + "entailment": {"@type": "@id"}, + "equals": {"@type": "@id"}, + "ignoredProperties": {"@type": "@id", "@container": "@list"}, + "in": {"@type": "@none", "@container": "@list"}, + "inversePath": {"@type": "@id"}, + "label": "http://www.w3.org/2000/01/rdf-schema#label", + "languageIn": {"@container": "@list"}, + "lessThan": {"@type": "@id"}, + "lessThanOrEquals": {"@type": "@id"}, + "nodeKind": {"@type": "@vocab"}, + "or": {"@type": "@id", "@container": "@list"}, + "path": {"@type": "@none"}, + "property": {"@type": "@id"}, + "severity": {"@type": "@vocab"}, + "targetClass": {"@type": "@id"}, + "targetNode": {"@type": "@none"}, + "xone": {"@type": "@id", "@container": "@list"} + }, + "@requireAll": false, + "@type": ["NodeShape", "PropertyShape"], + "property": {}, + "targetClass": {}, + "targetNode": {}, + "targetObjectsOf": {}, + "targetSubjectsOf": {} +} \ No newline at end of file diff --git a/example-files/shacl-severity-002.jsonld b/example-files/shacl-severity-002.jsonld new file mode 100644 index 00000000..3a010d01 --- /dev/null +++ b/example-files/shacl-severity-002.jsonld @@ -0,0 +1,191 @@ +[ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#InvalidResource1", + "http://datashapes.org/sh/tests/core/misc/severity-002.test#property": [ + { + "@value": true + } + ] + }, + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#TestShape1", + "http://www.w3.org/ns/shacl#nodeKind": [ + { + "@id": "http://www.w3.org/ns/shacl#BlankNode" + } + ], + "http://www.w3.org/ns/shacl#property": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#TestShape2" + } + ], + "http://www.w3.org/ns/shacl#severity": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#MySeverity" + } + ], + "http://www.w3.org/ns/shacl#targetNode": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#InvalidResource1" + } + ] + }, + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#TestShape2", + "http://www.w3.org/ns/shacl#path": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#property" + } + ], + "http://www.w3.org/ns/shacl#datatype": [ + { + "@id": "http://www.w3.org/2001/XMLSchema#integer" + } + ], + "http://www.w3.org/ns/shacl#severity": [ + { + "@id": "http://www.w3.org/ns/shacl#Info" + } + ] + }, + { + "@id": "urn:x-shacl-test:/core/misc/severity-002.ttl", + "@type": [ + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#Manifest" + ], + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#entries": [ + { + "@list": [ + { + "@id": "urn:x-shacl-test:/core/misc/severity-002" + } + ] + } + ] + }, + { + "@id": "urn:x-shacl-test:/core/misc/severity-002", + "@type": [ + "http://www.w3.org/ns/shacl-test#Validate" + ], + "http://www.w3.org/2000/01/rdf-schema#label": [ + { + "@value": "Test of sh:severity 002" + } + ], + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#action": [ + { + "@id": "_:g451300" + } + ], + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#result": [ + { + "@id": "_:g451320" + } + ], + "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#status": [ + { + "@id": "http://www.w3.org/ns/shacl-test#approved" + } + ] + }, + { + "@id": "_:g451300", + "http://www.w3.org/ns/shacl-test#dataGraph": [ + { + "@id": "urn:x-shacl-test:/core/misc/severity-002.ttl" + } + ], + "http://www.w3.org/ns/shacl-test#shapesGraph": [ + { + "@id": "urn:x-shacl-test:/core/misc/severity-002.ttl" + } + ] + }, + { + "@id": "_:g451320", + "@type": [ + "http://www.w3.org/ns/shacl#ValidationReport" + ], + "http://www.w3.org/ns/shacl#conforms": [ + { + "@value": false + } + ], + "http://www.w3.org/ns/shacl#result": [ + { + "@id": "_:g451340" + }, + { + "@id": "_:g451360" + } + ] + }, + { + "@id": "_:g451340", + "@type": [ + "http://www.w3.org/ns/shacl#ValidationResult" + ], + "http://www.w3.org/ns/shacl#focusNode": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#InvalidResource1" + } + ], + "http://www.w3.org/ns/shacl#resultPath": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#property" + } + ], + "http://www.w3.org/ns/shacl#resultSeverity": [ + { + "@id": "http://www.w3.org/ns/shacl#Info" + } + ], + "http://www.w3.org/ns/shacl#sourceConstraintComponent": [ + { + "@id": "http://www.w3.org/ns/shacl#DatatypeConstraintComponent" + } + ], + "http://www.w3.org/ns/shacl#sourceShape": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#TestShape2" + } + ], + "http://www.w3.org/ns/shacl#value": [ + { + "@value": true + } + ] + }, + { + "@id": "_:g451360", + "@type": [ + "http://www.w3.org/ns/shacl#ValidationResult" + ], + "http://www.w3.org/ns/shacl#focusNode": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#InvalidResource1" + } + ], + "http://www.w3.org/ns/shacl#resultSeverity": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#MySeverity" + } + ], + "http://www.w3.org/ns/shacl#sourceConstraintComponent": [ + { + "@id": "http://www.w3.org/ns/shacl#NodeKindConstraintComponent" + } + ], + "http://www.w3.org/ns/shacl#sourceShape": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#TestShape1" + } + ], + "http://www.w3.org/ns/shacl#value": [ + { + "@id": "http://datashapes.org/sh/tests/core/misc/severity-002.test#InvalidResource1" + } + ] + } +] \ No newline at end of file diff --git a/json-ld.gemspec b/json-ld.gemspec old mode 100644 new mode 100755 index 9489a440..db704efa --- a/json-ld.gemspec +++ b/json-ld.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |gem| gem.homepage = "https://github.com/ruby-rdf/json-ld" gem.license = 'Unlicense' gem.summary = "JSON-LD reader/writer for Ruby." - gem.description = "JSON::LD parses and serializes JSON-LD into RDF and implements expansion, compaction and framing API interfaces." + gem.description = "JSON::LD parses and serializes JSON-LD into RDF and implements expansion, compaction and framing API interfaces for the Ruby RDF.rb library suite." gem.authors = ['Gregg Kellogg'] gem.email = 'public-linked-json@w3.org' From d0e29356dd47412755d2f3484e046f53c3f40b06 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 12 Jan 2021 12:40:22 -0800 Subject: [PATCH 03/17] Minor fix to match spec update. --- lib/json/ld/flatten.rb | 2 +- spec/api_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/json/ld/flatten.rb b/lib/json/ld/flatten.rb index 451b73f2..f0f486d1 100644 --- a/lib/json/ld/flatten.rb +++ b/lib/json/ld/flatten.rb @@ -36,7 +36,7 @@ def create_node_map(element, graph_map, raise "Expected hash or array to create_node_map, got #{element.inspect}" else graph = (graph_map[active_graph] ||= {}) - subject_node = graph[active_subject] + subject_node = !active_subject.is_a?(Hash) && graph[active_subject] # Transform BNode types if element.has_key?('@type') diff --git a/spec/api_spec.rb b/spec/api_spec.rb index f692d937..c9ae9997 100644 --- a/spec/api_spec.rb +++ b/spec/api_spec.rb @@ -44,7 +44,7 @@ end context "Test Files" do - %w(oj json_gem ok_json yajl).map(&:to_sym).each do |adapter| + %i(oj json_gem ok_json yajl).each do |adapter| context "with MultiJson adapter #{adapter.inspect}" do Dir.glob(File.expand_path(File.join(File.dirname(__FILE__), 'test-files/*-input.*'))) do |filename| test = File.basename(filename).sub(/-input\..*$/, '') From 0077ddd7d7141859654fc9314078710259e9a30b Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 18 Jan 2021 13:45:58 -0800 Subject: [PATCH 04/17] Run CI on Ruby 3.0. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3768924..9fda18f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,8 @@ jobs: - 2.5 - 2.6 - 2.7 - # - ruby-head # net-http-persistent + - 3.0 + - ruby-head - jruby steps: - name: Clone repository From 15adacacdc01968bd304f7d618d70cfef6ca73be Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sun, 17 Jan 2021 15:42:33 -0800 Subject: [PATCH 05/17] Run JSON-LD star test suite. --- README.md | 15 ++- lib/json/ld.rb | 4 +- lib/json/ld/expand.rb | 25 ++++- spec/.gitignore | 1 + spec/expand_spec.rb | 223 +++++++++++++++++++++++++++++++++++++++++- spec/rdfstar_spec.rb | 24 +++++ spec/suite_helper.rb | 2 + 7 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 spec/rdfstar_spec.rb diff --git a/README.md b/README.md index 111b26b5..035a5d40 100755 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ The [MultiJson](https://rubygems.org/gems/multi_json) gem is used for parsing JS ### JSON-LD* (RDFStar) -The {JSON::LD::API.toRdf} and {JSON::LD::API.fromRdf} API methods, along with the {JSON::LD::Reader} and {JSON::LD::Writer}, include provisional support for [JSON-LD*][JSON-LD*]. +The {JSON::LD::API.expand}, {JSON::LD::API.compact}, {JSON::LD::API.toRdf}, and {JSON::LD::API.fromRdf} API methods, along with the {JSON::LD::Reader} and {JSON::LD::Writer}, include provisional support for [JSON-LD*][JSON-LD*]. Internally, an `RDF::Statement` is treated as another resource, along with `RDF::URI` and `RDF::Node`, which allows an `RDF::Statement` to have a `#subject` or `#object` which is also an `RDF::Statement`. @@ -55,6 +55,19 @@ In JSON-LD, with the `rdfstar` option set, the value of `@id`, in addition to an "ex:certainty": 0.9 } +Additionally, the `@annotation` property (or alias) may be used on a node object or value object to annotate the statement for which the associated node is the object of a triple. + + { + "@context": {"foaf": "http://xmlns.com/foaf/0.1/"}, + "@id": "bob", + "foaf:age" 23, + "@annotation": { + "ex:certainty": 0.9 + } + } + +In the first case, the embedded node is not asserted, and only appears as the subject of a triple. In the second case, the triple is asserted and used as the subject in another statement which annotates it. + **Note: This feature is subject to change or elimination as the standards process progresses.** #### Serializing a Graph containing embedded statements diff --git a/lib/json/ld.rb b/lib/json/ld.rb index 5f3b2635..ef7ab5ad 100644 --- a/lib/json/ld.rb +++ b/lib/json/ld.rb @@ -47,6 +47,7 @@ module LD DEFAULT_CONTEXT = "http://schema.org" KEYWORDS = Set.new(%w( + @annotation @base @container @context @@ -116,6 +117,7 @@ def code class CollidingKeywords < JsonLdError; @code = "colliding keywords"; end class ConflictingIndexes < JsonLdError; @code = "conflicting indexes"; end class CyclicIRIMapping < JsonLdError; @code = "cyclic IRI mapping"; end + class InvalidAnnotation < JsonLdError; @code = "invalid annotation"; end class InvalidBaseIRI < JsonLdError; @code = "invalid base IRI"; end class InvalidContainerMapping < JsonLdError; @code = "invalid container mapping"; end class InvalidContextEntry < JsonLdError; @code = "invalid context entry"; end @@ -137,7 +139,7 @@ class InvalidLocalContext < JsonLdError; @code = "invalid local context"; end class InvalidNestValue < JsonLdError; @code = "invalid @nest value"; end class InvalidPrefixValue < JsonLdError; @code = "invalid @prefix value"; end class InvalidPropagateValue < JsonLdError; @code = "invalid @propagate value"; end - class InvalidEmbeddedNode < JsonLdError; @code = "invalid reified node"; end + class InvalidEmbeddedNode < JsonLdError; @code = "invalid embedded node"; end class InvalidRemoteContext < JsonLdError; @code = "invalid remote context"; end class InvalidReverseProperty < JsonLdError; @code = "invalid reverse property"; end class InvalidReversePropertyMap < JsonLdError; @code = "invalid reverse property map"; end diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb index a8a9a97f..53391023 100644 --- a/lib/json/ld/expand.rb +++ b/lib/json/ld/expand.rb @@ -11,7 +11,7 @@ module Expand # The following constant is used to reduce object allocations CONTAINER_INDEX_ID_TYPE = Set['@index', '@id', '@type'].freeze KEY_ID = %w(@id).freeze - KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w(@value @language @type @index @direction).freeze + KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w(@value @language @type @index @direction @annotation).freeze KEYS_SET_LIST_INDEX = %w(@set @list @index).freeze KEYS_INCLUDED_TYPE = %w(@included @type).freeze @@ -172,6 +172,18 @@ def expand(input, active_property, context, # If result contains the key @set, then set result to the key's associated value. return output_object['@set'] if output_object.key?('@set') + elsif output_object['@annotation'] + # Otherwise, if result contains the key @annotation, + # the array value must all be node objects without an @id property, otherwise, an invalid annotation error has been detected and processing is aborted. + raise JsonLdError::InvalidAnnotation, + "@annotation must reference node objects without @id." unless + output_object['@annotation'].all? {|o| node?(o) && !o.key?('@id')} + + # Additionally, the property must not be used if there is no active property, or the expanded active property is @graph. + raise JsonLdError::InvalidAnnotation, + "@annotation must not be used on a top-level object." if + %w(@graph @included).include?(expanded_active_property || '@graph') + end # If result contains only the key @language, set result to null. @@ -258,6 +270,11 @@ def expand_object(input, active_property, context, output_object, expanded_value = case expanded_property when '@id' + # If expanded active property is `@annotation`, an invalid annotation error has been found and processing is aborted. + raise JsonLdError::InvalidAnnotation, + "an annotation must not contain a property expanding to @id" if + expanded_active_property == '@annotation' && @options[:rdfstar] + # If expanded property is @id and value is not a string, an invalid @id value error has been detected and processing is aborted e_id = case value when String @@ -522,6 +539,12 @@ def expand_object(input, active_property, context, output_object, nests << key # Continue with the next key from element next + when '@annotation' + # Skip unless rdfstar option is set + next unless @options[:rdfstar] + as_array(expand(value, '@annotation', context, + framing: framing, + log_depth: log_depth.to_i + 1)) else # Skip unknown keyword next diff --git a/spec/.gitignore b/spec/.gitignore index d19e5110..55898963 100644 --- a/spec/.gitignore +++ b/spec/.gitignore @@ -2,4 +2,5 @@ /uri-cache/ /json-ld-api /json-ld-framing +/json-ld-star /json-ld-streaming diff --git a/spec/expand_spec.rb b/spec/expand_spec.rb index e4e04118..69b60206 100644 --- a/spec/expand_spec.rb +++ b/spec/expand_spec.rb @@ -3388,6 +3388,36 @@ }), exception: JSON::LD::JsonLdError::InvalidIdValue }, + "node object with @annotation property is ignored without rdfstar option": { + input: %({ + "@id": "ex:bob", + "ex:knows": { + "@id": "ex:fred", + "@annotation": { + "ex:certainty": 0.8 + } + } + }), + output: %([{ + "@id": "ex:bob", + "ex:knows": [{"@id": "ex:fred"}] + }]) + }, + "value object with @annotation property is ignored without rdfstar option": { + input: %({ + "@id": "ex:bob", + "ex:age": { + "@value": 23, + "@annotation": { + "ex:certainty": 0.8 + } + } + }), + output: %([{ + "@id": "ex:bob", + "ex:age": [{"@value": 23}] + }]) + }, }.each do |title, params| it(title) {run_expand params} end @@ -3569,7 +3599,7 @@ }] }]) }, - "illegal node with embedded object having properties": { + "node with embedded object having properties": { input: %({ "@id": "ex:subj", "ex:value": { @@ -3619,6 +3649,197 @@ }] }]) }, + "node with @annotation property on value object": { + input: %({ + "@id": "ex:bob", + "ex:age": { + "@value": 23, + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:bob", + "ex:age": [{ + "@value": 23, + "@annotation": [{"ex:certainty": [{"@value": 0.8}]}] + }] + }]) + }, + "node with @annotation property on node object": { + input: %({ + "@id": "ex:bob", + "ex:name": "Bob", + "ex:knows": { + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:bob", + "ex:name": [{"@value": "Bob"}], + "ex:knows": [{ + "@id": "ex:fred", + "ex:name": [{"@value": "Fred"}], + "@annotation": [{"ex:certainty": [{"@value": 0.8}]}] + }] + }]) + }, + "node with @annotation property multiple values": { + input: %({ + "@id": "ex:bob", + "ex:name": "Bob", + "ex:knows": { + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": [{ + "ex:certainty": 0.8 + }, { + "ex:source": {"@id": "http://example.org/"} + }] + } + }), + output: %([{ + "@id": "ex:bob", + "ex:name": [{"@value": "Bob"}], + "ex:knows": [{ + "@id": "ex:fred", + "ex:name": [{"@value": "Fred"}], + "@annotation": [{ + "ex:certainty": [{"@value": 0.8}] + }, { + "ex:source": [{"@id": "http://example.org/"}] + }] + }] + }]) + }, + "node with @annotation property that is on the top-level is invalid": { + input: %({ + "@id": "ex:bob", + "ex:name": "Bob", + "@annotation": {"ex:prop": "value2"} + }), + exception: JSON::LD::JsonLdError::InvalidAnnotation + }, + "node with @annotation property on a top-level graph node is invalid": { + input: %({ + "@id": "ex:bob", + "ex:name": "Bob", + "@graph": { + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": {"ex:prop": "value2"} + } + }), + exception: JSON::LD::JsonLdError::InvalidAnnotation + }, + "node with @annotation property having @id is invalid": { + input: %({ + "@id": "ex:bob", + "ex:knows": { + "@id": "ex:fred", + "@annotation": { + "@id": "ex:invalid-ann-id", + "ex:prop": "value2" + } + } + }), + exception: JSON::LD::JsonLdError::InvalidAnnotation + }, + "node with @annotation property with value object value is invalid": { + input: %({ + "@id": "ex:bob", + "ex:knows": { + "@id": "fred", + "@annotation": "value2" + } + }), + exception: JSON::LD::JsonLdError::InvalidAnnotation + }, + "node with @annotation on a list": { + input: %({ + "@id": "ex:bob", + "ex:knows": { + "@list": [{"@id": "ex:fred"}], + "@annotation": "value2" + } + }), + exception: JSON::LD::JsonLdError::InvalidSetOrListObject + }, + "node with @annotation on a list value": { + input: %({ + "@id": "ex:bob", + "ex:knows": { + "@list": [ + { + "@id": "ex:fred", + "@annotation": "value2" + } + ] + } + }), + exception: JSON::LD::JsonLdError::InvalidAnnotation + }, + "node with @annotation property on a top-level @included node is invalid": { + input: %({ + "@id": "ex:bob", + "ex:name": "Bob", + "@included": [{ + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": {"ex:prop": "value2"} + }] + }), + exception: JSON::LD::JsonLdError::InvalidAnnotation + }, + "node with @annotation property on embedded subject": { + input: %({ + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "_:value"} + }, + "ex:prop": { + "@value": "value2", + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "_:value"}] + }, + "ex:prop": [{ + "@value": "value2", + "@annotation": [{ + "ex:certainty": [{"@value": 0.8}] + }] + }] + }]) + }, + "node with @annotation property on embedded object": { + input: %({ + "@id": "ex:subj", + "ex:value": { + "@id": { + "@id": "ex:rei", + "ex:prop": "value" + }, + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:subj", + "ex:value": [{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value"}] + }, + "@annotation": [{ + "ex:certainty": [{"@value": 0.8}] + }] + }] + }]) + }, }.each do |title, params| it(title) {run_expand params.merge(rdfstar: true)} end diff --git a/spec/rdfstar_spec.rb b/spec/rdfstar_spec.rb new file mode 100644 index 00000000..535e41d7 --- /dev/null +++ b/spec/rdfstar_spec.rb @@ -0,0 +1,24 @@ +# coding: utf-8 +require_relative 'spec_helper' + +describe JSON::LD do + describe "test suite" do + require_relative 'suite_helper' + %w{ + expand + compact + fromRdf + toRdf + }.each do |partial| + m = Fixtures::SuiteTest::Manifest.open("#{Fixtures::SuiteTest::STAR_SUITE}#{partial}-manifest.jsonld") + describe m.name do + m.entries.each do |t| + specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do + t.options[:ordered] = false + expect {t.run self}.not_to write.to(:error) + end + end + end + end + end +end unless ENV['CI'] \ No newline at end of file diff --git a/spec/suite_helper.rb b/spec/suite_helper.rb index 154adf16..956a0821 100644 --- a/spec/suite_helper.rb +++ b/spec/suite_helper.rb @@ -7,6 +7,7 @@ module File "https://w3c.github.io/json-ld-api/tests/" => ::File.expand_path("../json-ld-api/tests", __FILE__) + '/', "https://w3c.github.io/json-ld-framing/tests/" => ::File.expand_path("../json-ld-framing/tests", __FILE__) + '/', "https://w3c.github.io/json-ld-streaming/tests/" => ::File.expand_path("../json-ld-streaming/tests", __FILE__) + '/', + "https://json-ld.github.io/json-ld-star/tests/" => ::File.expand_path("../json-ld-star/tests", __FILE__) + '/', "file:" => "" } @@ -76,6 +77,7 @@ module SuiteTest SUITE = RDF::URI("https://w3c.github.io/json-ld-api/tests/") FRAME_SUITE = RDF::URI("https://w3c.github.io/json-ld-framing/tests/") STREAM_SUITE = RDF::URI("https://w3c.github.io/json-ld-streaming/tests/") + STAR_SUITE = RDF::URI("https://json-ld.github.io/json-ld-star/tests/") class Manifest < JSON::LD::Resource attr_accessor :manifest_url From 2c7d988823c098a65f447282a09f9c41d18fae69 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Sat, 23 Jan 2021 16:15:55 -0800 Subject: [PATCH 06/17] Minor correction on annotation expansion. --- .coveralls.yml | 1 + Gemfile | 2 +- lib/json/ld/expand.rb | 2 +- spec/expand_spec.rb | 135 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 .coveralls.yml diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..87d13473 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: EUv9wY2KnN7lYmiGWUFhEKH73Ndwtok1A diff --git a/Gemfile b/Gemfile index 55cd8430..b597cb6f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" -gem "nokogiri", '~> 1.8' +gem "nokogiri", '~> 1.10' gem "nokogumbo", platforms: :mri gemspec diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb index 53391023..ac948aee 100644 --- a/lib/json/ld/expand.rb +++ b/lib/json/ld/expand.rb @@ -297,7 +297,7 @@ def expand_object(input, active_property, context, output_object, [{}] elsif @options[:rdfstar] # Result must have just a single statement - rei_node = expand(value, active_property, context, log_depth: log_depth.to_i + 1) + rei_node = expand(value, nil, context, log_depth: log_depth.to_i + 1) statements = to_enum(:item_to_rdf, rei_node) raise JsonLdError::InvalidEmbeddedNode, "Embedded node with #{statements.size} statements" unless diff --git a/spec/expand_spec.rb b/spec/expand_spec.rb index 69b60206..303762fa 100644 --- a/spec/expand_spec.rb +++ b/spec/expand_spec.rb @@ -3840,6 +3840,141 @@ }] }]) }, + "embedded node with reverse relationship": { + input: %({ + "@context": { + "rel": {"@reverse": "ex:rel"} + }, + "@id": { + "@id": "ex:rei", + "rel": {"@id": "ex:value"} + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "@reverse": { + "ex:rel": [{"@id": "ex:value"}] + } + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "embedded node with expanded reverse relationship": { + input: %({ + "@id": { + "@id": "ex:rei", + "@reverse": { + "ex:rel": {"@id": "ex:value"} + } + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "@reverse": { + "ex:rel": [{"@id": "ex:value"}] + } + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "embedded node used as subject in reverse relationship": { + input: %({ + "@context": { + "rel": {"@reverse": "ex:rel"} + }, + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "ex:value"} + }, + "rel": {"@id": "ex:value2"} + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "ex:value"}] + }, + "@reverse": { + "ex:rel": [{"@id": "ex:value2"}] + } + }]) + }, + "embedded node used as object in reverse relationship": { + input: %({ + "@context": { + "rel": {"@reverse": "ex:rel"} + }, + "@id": "ex:subj", + "rel": { + "@id": "ex:rei", + "ex:prop": {"@id": "ex:value"} + } + }), + output: %([{ + "@id": "ex:subj", + "@reverse": { + "ex:rel": [{ + "@id": "ex:rei", + "ex:prop": [{"@id": "ex:value"}] + }] + } + }]) + }, + "node with @annotation property on node object with reverse relationship": { + input: %({ + "@context": { + "knownBy": {"@reverse": "ex:knows"} + }, + "@id": "ex:bob", + "ex:name": "Bob", + "knownBy": { + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:bob", + "ex:name": [{"@value": "Bob"}], + "@reverse": { + "ex:knows": [{ + "@id": "ex:fred", + "ex:name": [{"@value": "Fred"}], + "@annotation": [{"ex:certainty": [{"@value": 0.8}]}] + }] + } + }]) + }, + "reverse relationship inside annotation": { + input: %({ + "@context": { + "claims": {"@reverse": "ex:claims", "@type": "@id"} + }, + "@id": "ex:bob", + "ex:knows": { + "@id": "ex:jane", + "@annotation": { + "ex:certainty": 0.8, + "claims": "ex:sue" + } + } + }), + output: %([{ + "@id": "ex:bob", + "ex:knows": [{ + "@id": "ex:jane", + "@annotation": [{ + "ex:certainty": [{"@value": 0.8}], + "@reverse": { + "ex:claims": [{"@id": "ex:sue"}] + } + }] + }] + }]) + }, }.each do |title, params| it(title) {run_expand params.merge(rdfstar: true)} end From 98ea8080e687a8db57bdaab40564ca8f12745f6f Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 25 Jan 2021 15:40:20 -0800 Subject: [PATCH 07/17] Disallow @reverse within an embedded node (flattening can't handle it). --- lib/json/ld/expand.rb | 4 ++++ spec/expand_spec.rb | 34 ++++++++++++---------------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb index ac948aee..bfeec1aa 100644 --- a/lib/json/ld/expand.rb +++ b/lib/json/ld/expand.rb @@ -298,6 +298,10 @@ def expand_object(input, active_property, context, output_object, elsif @options[:rdfstar] # Result must have just a single statement rei_node = expand(value, nil, context, log_depth: log_depth.to_i + 1) + + # Node must not contain @reverse + raise JsonLdError::InvalidEmbeddedNode, + "Embedded node with @reverse" if rei_node && rei_node.key?('@reverse') statements = to_enum(:item_to_rdf, rei_node) raise JsonLdError::InvalidEmbeddedNode, "Embedded node with #{statements.size} statements" unless diff --git a/spec/expand_spec.rb b/spec/expand_spec.rb index 303762fa..8082b958 100644 --- a/spec/expand_spec.rb +++ b/spec/expand_spec.rb @@ -3851,15 +3851,7 @@ }, "ex:prop": "value2" }), - output: %([{ - "@id": { - "@id": "ex:rei", - "@reverse": { - "ex:rel": [{"@id": "ex:value"}] - } - }, - "ex:prop": [{"@value": "value2"}] - }]) + exception: JSON::LD::JsonLdError::InvalidEmbeddedNode }, "embedded node with expanded reverse relationship": { input: %({ @@ -3871,15 +3863,7 @@ }, "ex:prop": "value2" }), - output: %([{ - "@id": { - "@id": "ex:rei", - "@reverse": { - "ex:rel": [{"@id": "ex:value"}] - } - }, - "ex:prop": [{"@value": "value2"}] - }]) + exception: JSON::LD::JsonLdError::InvalidEmbeddedNode }, "embedded node used as subject in reverse relationship": { input: %({ @@ -3909,16 +3893,22 @@ }, "@id": "ex:subj", "rel": { - "@id": "ex:rei", - "ex:prop": {"@id": "ex:value"} + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "ex:value"} + }, + "ex:prop": {"@id": "ex:value2"} } }), output: %([{ "@id": "ex:subj", "@reverse": { "ex:rel": [{ - "@id": "ex:rei", - "ex:prop": [{"@id": "ex:value"}] + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "ex:value"}] + }, + "ex:prop": [{"@id": "ex:value2"}] }] } }]) From fbc9f7b494ca39118f4f935c86cd0222bfe6f456 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 25 Jan 2021 16:21:09 -0800 Subject: [PATCH 08/17] Update flatten algorithm to handle embedded nodes and annotation objects. Note: there remains a problem with consistent Bnode renaming inside embedded objects. --- lib/json/ld/api.rb | 10 +- lib/json/ld/flatten.rb | 98 +++++++- spec/flatten_spec.rb | 514 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 610 insertions(+), 12 deletions(-) diff --git a/lib/json/ld/api.rb b/lib/json/ld/api.rb index 2a7373d2..8ad0f294 100644 --- a/lib/json/ld/api.rb +++ b/lib/json/ld/api.rb @@ -480,16 +480,16 @@ def self.toRdf(input, expanded: false, **options, &block) extractAllScripts: true, }.merge(options) - # Expand input to simplify processing - expanded_input = expanded ? input : API.expand(input, ordered: false, **options) + # Flatten input to simplify processing + flattened_input = API.flatten(input, nil, expanded: expanded, ordered: false, **options) - API.new(expanded_input, nil, **options) do + API.new(flattened_input, nil, **options) do # 1) Perform the Expansion Algorithm on the JSON-LD input. # This removes any existing context to allow the given context to be cleanly applied. - log_debug(".toRdf") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"} + log_debug(".toRdf") {"flattened input: #{flattened_input.to_json(JSON_STATE) rescue 'malformed json'}"} # Recurse through input - expanded_input.each do |node| + flattened_input.each do |node| item_to_rdf(node) do |statement| next if statement.predicate.node? && !options[:produceGeneralizedRdf] diff --git a/lib/json/ld/flatten.rb b/lib/json/ld/flatten.rb index f0f486d1..04fb092b 100644 --- a/lib/json/ld/flatten.rb +++ b/lib/json/ld/flatten.rb @@ -1,5 +1,7 @@ # -*- encoding: utf-8 -*- # frozen_string_literal: true +require 'json/canonicalization' + module JSON::LD module Flatten include Utils @@ -7,6 +9,10 @@ module Flatten ## # This algorithm creates a JSON object node map holding an indexed representation of the graphs and nodes represented in the passed expanded document. All nodes that are not uniquely identified by an IRI get assigned a (new) blank node identifier. The resulting node map will have a member for every graph in the document whose value is another object with a member for every node represented in the document. The default graph is stored under the @default member, all other graphs are stored under their graph name. # + # For RDF*/JSON-LD*: + # * Values of `@id` can be an object (embedded node); when these are used as keys in a Node Map, they are serialized as canonical JSON, and de-serialized when flattening. + # * The presence of `@annotation` implies an embedded node and the annotation object is removed from the node/value object in which it appears. + # # @param [Array, Hash] element # Expanded JSON-LD input # @param [Hash] graph_map A map of graph name to subjects @@ -16,12 +22,15 @@ module Flatten # Node identifier # @param [String] active_property (nil) # Property within current node + # @param [Boolean] reverse (false) + # Processing a reverse relationship # @param [Array] list (nil) # Used when property value is a list def create_node_map(element, graph_map, active_graph: '@default', active_subject: nil, active_property: nil, + reverse: false, list: nil) if element.is_a?(Array) # If element is an array, process each entry in element recursively by passing item for element, node map, active graph, active subject, active property, and list. @@ -30,13 +39,14 @@ def create_node_map(element, graph_map, active_graph: active_graph, active_subject: active_subject, active_property: active_property, + reverse: false, list: list) end elsif !element.is_a?(Hash) raise "Expected hash or array to create_node_map, got #{element.inspect}" else graph = (graph_map[active_graph] ||= {}) - subject_node = !active_subject.is_a?(Hash) && graph[active_subject] + subject_node = !reverse && graph[active_subject.is_a?(Hash) ? active_subject.to_json_c14n : active_subject] # Transform BNode types if element.has_key?('@type') @@ -45,6 +55,29 @@ def create_node_map(element, graph_map, if value?(element) element['@type'] = element['@type'].first if element ['@type'] + + # For rdfstar, if value contains an `@annotation` member ... + # note: active_subject will not be nil, and may be an object itself. + if element.key?('@annotation') + # rdfstar being true is implicit, as it is checked in expansion + as = node_reference?(active_subject) ? + active_subject['@id'] : + active_subject + star_subject = { + "@id" => as, + active_property => [element] + } + + # Note that annotation is an array, make the reified subject the id of each member of that array. + annotation = element.delete('@annotation').map do |a| + a.merge('@id' => star_subject) + end + + # Invoke recursively using annotation. + create_node_map(annotation, graph_map, + active_graph: active_graph) + end + if list.nil? add_value(subject_node, active_property, element, property_is_array: true, allow_duplicate: false) else @@ -64,13 +97,21 @@ def create_node_map(element, graph_map, end else # Element is a node object - id = element.delete('@id') - id = namer.get_name(id) if blank_node?(id) + ser_id = id = element.delete('@id') + if id.is_a?(Hash) + # recursively rename blank nodes within `id`. + id = rename_embedded(id) + # Index graph using serialized id + ser_id = id.to_json_c14n + elsif blank_node?(id) + ser_id = id = namer.get_name(id) + end - node = graph[id] ||= {'@id' => id} + node = graph[ser_id] ||= {'@id' => id} - if active_subject.is_a?(Hash) - # If subject is a hash, then we're processing a reverse-property relationship. + if reverse + # Note: active_subject is a Hash + # We're processing a reverse-property relationship. add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false) elsif active_property reference = {'@id' => id} @@ -81,6 +122,29 @@ def create_node_map(element, graph_map, end end + # For rdfstar, if node contains an `@annotation` member ... + # note: active_subject will not be nil, and may be an object itself. + # XXX: what if we're reversing an annotation? + if element.key?('@annotation') + # rdfstar being true is implicit, as it is checked in expansion + as = node_reference?(active_subject) ? + active_subject['@id'] : + active_subject + star_subject = reverse ? + {"@id" => node['@id'], active_property => [{'@id' => as}]} : + {"@id" => as, active_property => [{'@id' => node['@id']}]} + + # Note that annotation is an array, make the reified subject the id of each member of that array. + annotation = element.delete('@annotation').map do |a| + a.merge('@id' => star_subject) + end + + # Invoke recursively using annotation. + create_node_map(annotation, graph_map, + active_graph: active_graph, + active_subject: star_subject) + end + if element.has_key?('@type') add_value(node, '@type', element.delete('@type'), property_is_array: true, allow_duplicate: false) end @@ -99,7 +163,8 @@ def create_node_map(element, graph_map, create_node_map(value, graph_map, active_graph: active_graph, active_subject: referenced_node, - active_property: property) + active_property: property, + reverse: true) end end end @@ -128,7 +193,26 @@ def create_node_map(element, graph_map, end end + ## + # Rename blank nodes recursively within an embedded object + # + # @param [Object] node + # @return [Hash] + def rename_embedded(node) + case node + when String + blank_node?(node) ? namer.get_name(node) : node + when Array + node.map {|n| rename_embedded(n)} + when Hash + node.inject({}) {|memo, (k, v)| memo.merge(k => rename_embedded(v))} + else + node + end + end + private + ## # Merge nodes from all graphs in the graph_map into a new node map # diff --git a/spec/flatten_spec.rb b/spec/flatten_spec.rb index 89e81d3f..2c1f3863 100644 --- a/spec/flatten_spec.rb +++ b/spec/flatten_spec.rb @@ -665,6 +665,520 @@ end end + context "JSON-LD*" do + { + "node object with @annotation property is ignored without rdfstar option": { + input: %({ + "@id": "ex:bob", + "ex:knows": { + "@id": "ex:fred", + "@annotation": { + "ex:certainty": 0.8 + } + } + }), + output: %([{ + "@id": "ex:bob", + "ex:knows": [{"@id": "ex:fred"}] + }]) + }, + "value object with @annotation property is ignored without rdfstar option": { + input: %({ + "@id": "ex:bob", + "ex:age": { + "@value": 23, + "@annotation": { + "ex:certainty": 0.8 + } + } + }), + output: %([{ + "@id": "ex:bob", + "ex:age": [{"@value": 23}] + }]) + }, + }.each do |title, params| + it(title) {run_flatten params} + end + + { + "node with embedded subject having no @id": { + input: %({ + "@id": { + "ex:prop": "value" + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "ex:prop": [{"@value": "value"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with embedded subject having IRI @id": { + input: %({ + "@id": { + "@id": "ex:rei", + "ex:prop": "value" + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with embedded subject having BNode @id": { + input: %({ + "@id": { + "@id": "_:rei", + "ex:prop": "value" + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": "_:b0", + "ex:prop": [{"@value": "value"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with embedded subject having a type": { + input: %({ + "@id": { + "@id": "ex:rei", + "@type": "ex:Type" + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "@type": ["ex:Type"] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with embedded subject having an IRI value": { + input: %({ + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "ex:value"} + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "ex:value"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with embedded subject having an BNode value": { + input: %({ + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "_:value"} + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "_:b0"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with recursive embedded subject": { + input: %({ + "@id": { + "@id": { + "@id": "ex:rei", + "ex:prop": "value3" + }, + "ex:prop": "value" + }, + "ex:prop": "value2" + }), + output: %([{ + "@id": { + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value3"}] + }, + "ex:prop": [{"@value": "value"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with embedded object": { + input: %({ + "@id": "ex:subj", + "ex:value": { + "@id": { + "@id": "ex:rei", + "ex:prop": "value" + } + } + }), + output: %([{ + "@id": "ex:subj", + "ex:value": [{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value"}] + } + }] + }]) + }, + "node with embedded object having properties": { + input: %({ + "@id": "ex:subj", + "ex:value": { + "@id": { + "@id": "ex:rei", + "ex:prop": "value" + }, + "ex:prop": "value2" + } + }), + output: %([{ + "@id": "ex:subj", + "ex:value": [{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value"}] + } + }] + }, { + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with recursive embedded object": { + input: %({ + "@id": "ex:subj", + "ex:value": { + "@id": { + "@id": { + "@id": "ex:rei", + "ex:prop": "value3" + }, + "ex:prop": "value" + }, + "ex:prop": "value2" + } + }), + output: %([{ + "@id": "ex:subj", + "ex:value": [{ + "@id": { + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value3"}] + }, + "ex:prop":[{"@value": "value"}] + } + }] + }, { + "@id": { + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value3"}] + }, + "ex:prop":[{"@value": "value"}] + }, + "ex:prop": [{"@value": "value2"}] + }]) + }, + "node with @annotation property on value object": { + input: %({ + "@id": "ex:bob", + "ex:age": { + "@value": 23, + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:bob", + "ex:age": [{"@value": 23}] + }, { + "@id": { + "@id": "ex:bob", + "ex:age": [{"@value": 23}] + }, + "ex:certainty": [{"@value": 0.8}] + }]) + }, + "node with @annotation property on node object": { + input: %({ + "@id": "ex:bob", + "ex:name": "Bob", + "ex:knows": { + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:bob", + "ex:name": [{"@value": "Bob"}], + "ex:knows": [{"@id": "ex:fred"}] + }, { + "@id": "ex:fred", + "ex:name": [{"@value": "Fred"}] + }, { + "@id": { + "@id": "ex:bob", + "ex:knows": [{"@id": "ex:fred"}] + }, + "ex:certainty": [{"@value": 0.8}] + }]) + }, + "node with @annotation property multiple values": { + input: %({ + "@id": "ex:bob", + "ex:name": "Bob", + "ex:knows": { + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": [{ + "ex:certainty": 0.8 + }, { + "ex:source": {"@id": "http://example.org/"} + }] + } + }), + output: %([{ + "@id": "ex:bob", + "ex:name": [{"@value": "Bob"}], + "ex:knows": [{"@id": "ex:fred"}] + }, { + "@id": "ex:fred", + "ex:name": [{"@value": "Fred"}] + }, { + "@id": { + "@id": "ex:bob", + "ex:knows": [{"@id": "ex:fred"}] + }, + "ex:certainty": [{"@value": 0.8}], + "ex:source": [{"@id": "http://example.org/"}] + }]) + }, + "node with @annotation property on embedded subject": { + input: %({ + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "_:value"} + }, + "ex:prop": { + "@value": "value2", + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "_:b0"}] + }, + "ex:prop": [{"@value": "value2"}] + }, { + "@id": { + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "_:b0"}] + }, + "ex:prop": [{"@value": "value2"}] + }, + "ex:certainty": [{"@value": 0.8}] + }]) + }, + "node with @annotation property on embedded object": { + input: %({ + "@id": "ex:subj", + "ex:value": { + "@id": { + "@id": "ex:rei", + "ex:prop": "value" + }, + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:subj", + "ex:value": [{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value"}] + } + }] + }, { + "@id": { + "@id": "ex:subj", + "ex:value": [{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@value": "value"}] + } + }] + }, + "ex:certainty": [{"@value": 0.8}] + }]) + }, + "embedded node used as subject in reverse relationship": { + input: %({ + "@context": { + "rel": {"@reverse": "ex:rel"} + }, + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "ex:value"} + }, + "rel": {"@id": "ex:value2"} + }), + output: %([{ + "@id": "ex:value2", + "ex:rel": [{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "ex:value"}] + } + }] + }]) + }, + "embedded node used as object in reverse relationship": { + input: %({ + "@context": { + "rel": {"@reverse": "ex:rel"} + }, + "@id": "ex:subj", + "rel": { + "@id": { + "@id": "ex:rei", + "ex:prop": {"@id": "ex:value"} + }, + "ex:prop": {"@id": "ex:value2"} + } + }), + output: %([{ + "@id": { + "@id": "ex:rei", + "ex:prop": [{"@id": "ex:value"}] + }, + "ex:rel": [{"@id": "ex:subj"}], + "ex:prop": [{"@id": "ex:value2"}] + } + ]) + }, + "node with @annotation property on node object with reverse relationship": { + input: %({ + "@context": { + "knownBy": {"@reverse": "ex:knows"} + }, + "@id": "ex:bob", + "ex:name": "Bob", + "knownBy": { + "@id": "ex:fred", + "ex:name": "Fred", + "@annotation": {"ex:certainty": 0.8} + } + }), + output: %([{ + "@id": "ex:bob", + "ex:name": [{"@value": "Bob"}] + }, { + "@id": "ex:fred", + "ex:name": [{"@value": "Fred"}], + "ex:knows": [{"@id": "ex:bob"}] + }, { + "@id": { + "@id": "ex:fred", + "ex:knows": [{"@id": "ex:bob"}] + }, + "ex:certainty": [{"@value": 0.8}] + }]) + }, + "reverse relationship inside annotation": { + input: %({ + "@context": { + "claims": {"@reverse": "ex:claims", "@type": "@id"} + }, + "@id": "ex:bob", + "ex:knows": { + "@id": "ex:jane", + "@annotation": { + "ex:certainty": 0.8, + "claims": "ex:sue" + } + } + }), + output: %([{ + "@id": "ex:bob", + "ex:knows": [{"@id": "ex:jane"}] + }, { + "@id": { + "@id": "ex:bob", + "ex:knows": [{"@id": "ex:jane"}] + }, + "ex:certainty": [{"@value": 0.8}] + }, { + "@id": "ex:sue", + "ex:claims": [{ + "@id": { + "@id": "ex:bob", + "ex:knows": [{"@id": "ex:jane"}] + } + }] + }]) + }, + "embedded node with annotation on value object": { + input: %({ + "@context": { + "@base": "http://example.org/", + "@vocab": "http://example.org/", + "claims": {"@type": "@id"} + }, + "@id": { + "@id": "bob", + "knows": {"@id": "alice"} + }, + "certainty": { + "@value": 0.8, + "@annotation": {"claims": "ted"} + } + }), + output: %([{ + "@id": { + "@id": "http://example.org/bob", + "http://example.org/knows": [{"@id": "http://example.org/alice"}] + }, + "http://example.org/certainty": [{"@value": 0.8}] + }, { + "@id": { + "@id": { + "@id": "http://example.org/bob", + "http://example.org/knows": [{"@id": "http://example.org/alice"}] + }, + "http://example.org/certainty": [{"@value": 0.8}] + }, + "http://example.org/claims": [{"@id": "http://example.org/ted"}] + }]) + } + }.each do |title, params| + it(title) {run_flatten params.merge(rdfstar: true)} + end + end + def run_flatten(params) input, output, context = params[:input], params[:output], params[:context] input = ::JSON.parse(input) if input.is_a?(String) From 82882c75a3f213d9e03c89aa746111beddacc598 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 26 Jan 2021 16:01:02 -0800 Subject: [PATCH 09/17] Make renaming bnodes default to false for Reader and to_rdf_spec. --- lib/json/ld/api.rb | 19 ++++++++++++------- lib/json/ld/reader.rb | 1 + spec/suite_helper.rb | 4 ++-- spec/to_rdf_spec.rb | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/json/ld/api.rb b/lib/json/ld/api.rb index 8ad0f294..3aa316c2 100644 --- a/lib/json/ld/api.rb +++ b/lib/json/ld/api.rb @@ -102,13 +102,15 @@ class API # @yield [api] # @yieldparam [API] # @raise [JsonLdError] - def initialize(input, context, rename_bnodes: true, unique_bnodes: false, **options, &block) + def initialize(input, context, **options, &block) @options = { compactArrays: true, ordered: false, extractAllScripts: false, + rename_bnodes: true, + unique_bnodes: false, }.merge(options) - @namer = unique_bnodes ? BlankNodeUniqer.new : (rename_bnodes ? BlankNodeNamer.new("b") : BlankNodeMapper.new) + @namer = @options[:unique_bnodes] ? BlankNodeUniqer.new : (@options[:rename_bnodes] ? BlankNodeNamer.new("b") : BlankNodeMapper.new) @options[:base] = RDF::URI(@options[:base]) if @options[:base] && !@options[:base].is_a?(RDF::URI) # For context via Link header @@ -202,9 +204,9 @@ def self.expand(input, framing: false, **options, &block) # The JSON-LD object to copy and perform the compaction upon. # @param [String, #read, Hash, Array, JSON::LD::Context] context # The base context to use when compacting the input. + # @param [Boolean] expanded (false) Input is already expanded # @param [Hash{Symbol => Object}] options # @option options (see #initialize) - # @option options [Boolean] :expanded Input is already expanded # @yield jsonld # @yieldparam [Hash] jsonld # The compacted JSON-LD document @@ -248,9 +250,9 @@ def self.compact(input, context, expanded: false, **options) # The JSON-LD object or array of JSON-LD objects to flatten or an IRI referencing the JSON-LD document to flatten. # @param [String, #read, Hash, Array, JSON::LD::EvaluationContext] context # An optional external context to use additionally to the context embedded in input when expanding the input. + # @param [Boolean] expanded (false) Input is already expanded # @param [Hash{Symbol => Object}] options # @option options (see #initialize) - # @option options [Boolean] :expanded Input is already expanded # @yield jsonld # @yieldparam [Hash] jsonld # The flattened JSON-LD document @@ -275,6 +277,9 @@ def self.flatten(input, context, expanded: false, **options) API.new(expanded_input, context, no_default_base: true, **options) do log_debug(".flatten") {"expanded input: #{value.to_json(JSON_STATE) rescue 'malformed json'}"} + # Rename blank nodes recusively. Note that this does not create new blank node identifiers where none exist, which is performed in the node map generation algorithm. + #@value = rename_bnodes(@value) if @options[:rename_bnodes] + # Initialize node map to a JSON object consisting of a single member whose key is @default and whose value is an empty JSON object. graph_maps = {'@default' => {}} create_node_map(value, graph_maps) @@ -316,6 +321,7 @@ def self.flatten(input, context, expanded: false, **options) # The JSON-LD object to copy and perform the framing on. # @param [String, #read, Hash, Array] frame # The frame to use when re-arranging the data. + # @param [Boolean] expanded (false) Input is already expanded # @option options (see #initialize) # @option options ['@always', '@link', '@once', '@never'] :embed ('@once') # a flag specifying that objects should be directly embedded in the output, instead of being referred to by their IRI. @@ -325,7 +331,6 @@ def self.flatten(input, context, expanded: false, **options) # A flag specifying that all properties present in the input frame must either have a default value or be present in the JSON-LD input for the frame to match. # @option options [Boolean] :omitDefault (false) # a flag specifying that properties that are missing from the JSON-LD input should be omitted from the output. - # @option options [Boolean] :expanded Input is already expanded # @option options [Boolean] :pruneBlankNodeIdentifiers (true) removes blank node identifiers that are only used once. # @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`. # @yield jsonld @@ -458,10 +463,10 @@ def self.frame(input, frame, expanded: false, **options) # # @param [String, #read, Hash, Array] input # The JSON-LD object to process when outputting statements. + # @param [Boolean] expanded (false) Input is already expanded # @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] :expanded Input is already expanded # @raise [JsonLdError] # @yield statement # @yieldparam [RDF::Statement] statement @@ -470,7 +475,7 @@ def self.toRdf(input, expanded: false, **options, &block) unless block_given? results = [] results.extend(RDF::Enumerable) - self.toRdf(input, **options) do |stmt| + self.toRdf(input, expanded: expanded, **options) do |stmt| results << stmt end return results diff --git a/lib/json/ld/reader.rb b/lib/json/ld/reader.rb index b67404da..f3d11524 100644 --- a/lib/json/ld/reader.rb +++ b/lib/json/ld/reader.rb @@ -68,6 +68,7 @@ def self.options # @raise [RDF::ReaderError] if the JSON document cannot be loaded def initialize(input = $stdin, **options, &block) options[:base_uri] ||= options[:base] + options[:rename_bnodes] = false unless options.key?(:rename_bnodes) super do @options[:base] ||= base_uri.to_s if base_uri # Trim non-JSON stuff in script. diff --git a/spec/suite_helper.rb b/spec/suite_helper.rb index 956a0821..64ec7645 100644 --- a/spec/suite_helper.rb +++ b/spec/suite_helper.rb @@ -237,7 +237,7 @@ def run(rspec_example = nil) repo << statement end else - JSON::LD::API.toRdf(input_loc, logger: logger, **options) do |statement| + JSON::LD::API.toRdf(input_loc, rename_bnodes: false, logger: logger, **options) do |statement| repo << statement end end @@ -327,7 +327,7 @@ def run(rspec_example = nil) if t.manifest_url.to_s.include?('stream') JSON::LD::Reader.open(t.input_loc, stream: true, logger: logger, **options).each_statement {} else - JSON::LD::API.toRdf(t.input_loc, logger: logger, **options) {} + JSON::LD::API.toRdf(t.input_loc, rename_bnodes: false, logger: logger, **options) {} end else success("Unknown test type: #{testType}") diff --git a/spec/to_rdf_spec.rb b/spec/to_rdf_spec.rb index bfabfda8..26017d6e 100644 --- a/spec/to_rdf_spec.rb +++ b/spec/to_rdf_spec.rb @@ -1528,7 +1528,7 @@ def parse(input, **options) graph = options[:graph] || RDF::Graph.new options = {logger: logger, validate: true, canonicalize: false}.merge(options) - JSON::LD::API.toRdf(StringIO.new(input), **options) {|st| graph << st} + JSON::LD::API.toRdf(StringIO.new(input), rename_bnodes: false, **options) {|st| graph << st} graph end @@ -1541,9 +1541,9 @@ def run_to_rdf(params) expect {JSON::LD::API.toRdf(input, **params)}.to raise_error(params[:exception]) else if params[:write] - expect{JSON::LD::API.toRdf(input, base: params[:base], logger: logger, **params) {|st| graph << st}}.to write(params[:write]).to(:error) + expect{JSON::LD::API.toRdf(input, base: params[:base], logger: logger, rename_bnodes: false, **params) {|st| graph << st}}.to write(params[:write]).to(:error) else - expect{JSON::LD::API.toRdf(input, base: params[:base], logger: logger, **params) {|st| graph << st}}.not_to write.to(:error) + expect{JSON::LD::API.toRdf(input, base: params[:base], logger: logger, rename_bnodes: false, **params) {|st| graph << st}}.not_to write.to(:error) end expect(graph).to be_equivalent_graph(output, logger: logger, inputDocument: input) end From 9e69e2ccc0ecb202cde5f3ee6b8294d9290f5c2e Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 26 Jan 2021 16:15:28 -0800 Subject: [PATCH 10/17] Perform bnode renaming before creating the node map to handle embedded nodes. Adds test option to do result comparison by remapping bnode labels based on dataset bijection. --- lib/json/ld/api.rb | 5 ++++- lib/json/ld/flatten.rb | 12 +++++------- spec/flatten_spec.rb | 5 ++++- spec/spec_helper.rb | 33 +++++++++++++++++++++++++++++++++ spec/suite_flatten_spec.rb | 4 ++++ spec/suite_frame_spec.rb | 10 ++++++++++ spec/suite_helper.rb | 4 ++++ 7 files changed, 64 insertions(+), 9 deletions(-) diff --git a/lib/json/ld/api.rb b/lib/json/ld/api.rb index 3aa316c2..754577c1 100644 --- a/lib/json/ld/api.rb +++ b/lib/json/ld/api.rb @@ -278,7 +278,7 @@ def self.flatten(input, context, expanded: false, **options) log_debug(".flatten") {"expanded input: #{value.to_json(JSON_STATE) rescue 'malformed json'}"} # Rename blank nodes recusively. Note that this does not create new blank node identifiers where none exist, which is performed in the node map generation algorithm. - #@value = rename_bnodes(@value) if @options[:rename_bnodes] + @value = rename_bnodes(@value) if @options[:rename_bnodes] # Initialize node map to a JSON object consisting of a single member whose key is @default and whose value is an empty JSON object. graph_maps = {'@default' => {}} @@ -400,6 +400,9 @@ def self.frame(input, frame, expanded: false, **options) options[:omitGraph] = context.processingMode('json-ld-1.1') end + # Rename blank nodes recusively. Note that this does not create new blank node identifiers where none exist, which is performed in the node map generation algorithm. + @value = rename_bnodes(@value) + # Get framing nodes from expanded input, replacing Blank Node identifiers as necessary create_node_map(value, framing_state[:graphMap], active_graph: '@default') diff --git a/lib/json/ld/flatten.rb b/lib/json/ld/flatten.rb index 04fb092b..ca03ea08 100644 --- a/lib/json/ld/flatten.rb +++ b/lib/json/ld/flatten.rb @@ -99,12 +99,10 @@ def create_node_map(element, graph_map, # Element is a node object ser_id = id = element.delete('@id') if id.is_a?(Hash) - # recursively rename blank nodes within `id`. - id = rename_embedded(id) # Index graph using serialized id ser_id = id.to_json_c14n - elsif blank_node?(id) - ser_id = id = namer.get_name(id) + elsif id.nil? + ser_id = id = namer.get_name end node = graph[ser_id] ||= {'@id' => id} @@ -198,14 +196,14 @@ def create_node_map(element, graph_map, # # @param [Object] node # @return [Hash] - def rename_embedded(node) + def rename_bnodes(node) case node when String blank_node?(node) ? namer.get_name(node) : node when Array - node.map {|n| rename_embedded(n)} + node.map {|n| rename_bnodes(n)} when Hash - node.inject({}) {|memo, (k, v)| memo.merge(k => rename_embedded(v))} + node.inject({}) {|memo, (k, v)| memo.merge(k => rename_bnodes(v))} else node end diff --git a/spec/flatten_spec.rb b/spec/flatten_spec.rb index 2c1f3863..a7d87c32 100644 --- a/spec/flatten_spec.rb +++ b/spec/flatten_spec.rb @@ -206,7 +206,8 @@ "http://example.org/bar": [ { "@id": "_:b0" } ] } ] - } + }, + remap_nodes: true }, "@list with embedded object": { input: %([{ @@ -1195,6 +1196,8 @@ def run_flatten(params) else expect{jld = JSON::LD::API.flatten(input, context, logger: logger, **params)}.not_to write.to(:error) end + + jld = remap_bnodes(jld, output) if params[:remap_nodes] expect(jld).to produce_jsonld(output, logger) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0ac13ea4..82c74eff 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -67,6 +67,39 @@ def detect_format(stream) end end +# Creates a bijection between the two objects and replaces nodes in actual from expected. +def remap_bnodes(actual, expected) + # Transform each to RDF and perform a blank node bijection. + # Replace the blank nodes in action with the mapping from bijection. + ds_actual = RDF::Repository.new << JSON::LD::API.toRdf(actual, rdfstar: true, rename_bnodes: false) + ds_expected = RDF::Repository.new << JSON::LD::API.toRdf(expected, rdfstar: true, rename_bnodes: false) + if bijection = ds_actual.bijection_to(ds_expected) + bijection = bijection.inject({}) {|memo, (k, v)| memo.merge(k.to_s => v.to_s)} + + # Recursively replace blank nodes in actual with the bijection + #require 'byebug'; byebug + replace_nodes(actual, bijection) + else + actual + end +end + +def replace_nodes(object, bijection) + case object + when Array + object.map {|o| replace_nodes(o, bijection)} + when Hash + object.inject({}) do |memo, (k, v)| + memo.merge(bijection.fetch(k, k) => replace_nodes(v, bijection)) + end + when String + bijection.fetch(object, object) + else + object + end +end + + LIBRARY_INPUT = JSON.parse(%([ { "@id": "http://example.org/library", diff --git a/spec/suite_flatten_spec.rb b/spec/suite_flatten_spec.rb index 7f429ee6..f2ed27f6 100644 --- a/spec/suite_flatten_spec.rb +++ b/spec/suite_flatten_spec.rb @@ -7,6 +7,8 @@ m = Fixtures::SuiteTest::Manifest.open("#{Fixtures::SuiteTest::SUITE}flatten-manifest.jsonld") describe m.name do m.entries.each do |t| + t.options[:remap_bnodes] = %w(#t0045).include?(t.property('@id')) + specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do t.options[:ordered] = false if %w(#t0005).include?(t.property('@id')) @@ -16,6 +18,8 @@ end end + # Skip ordered tests when remapping bnodes + next if t.options[:remap_bnodes] specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do t.options[:ordered] = true if %w(#t0005).include?(t.property('@id')) diff --git a/spec/suite_frame_spec.rb b/spec/suite_frame_spec.rb index 7b2cb90d..8e2318a9 100644 --- a/spec/suite_frame_spec.rb +++ b/spec/suite_frame_spec.rb @@ -7,13 +7,23 @@ m = Fixtures::SuiteTest::Manifest.open("#{Fixtures::SuiteTest::FRAME_SUITE}frame-manifest.jsonld") describe m.name do m.entries.each do |t| + t.options[:remap_bnodes] = %w(#t0021 #tp021).include?(t.property('@id')) + specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do t.options[:ordered] = false + if %w(#t0021 #tp021).include?(t.property('@id')) + pending("changes due to blank node reordering") + end expect {t.run self}.not_to write.to(:error) end + # Skip ordered tests when remapping bnodes + next if t.options[:remap_bnodes] specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do t.options[:ordered] = true + if %w(#t0021 #tp021).include?(t.property('@id')) + pending("changes due to blank node reordering") + end expect {t.run self}.not_to write.to(:error) end end diff --git a/spec/suite_helper.rb b/spec/suite_helper.rb index 64ec7645..509492c1 100644 --- a/spec/suite_helper.rb +++ b/spec/suite_helper.rb @@ -264,6 +264,10 @@ def run(rspec_example = nil) } else expected = JSON.load(expect) + + # If called for, remap bnodes + result = remap_bnodes(result, expected) if options[:remap_bnodes] + if options[:ordered] # Compare without transformation rspec_example.instance_eval { From 7ddcbbdb8475652f2d9deec1f8977e41c5b9c250 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 27 Jan 2021 13:49:45 -0800 Subject: [PATCH 11/17] Run flattening specs. --- spec/flatten_spec.rb | 3 +-- spec/rdfstar_spec.rb | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/flatten_spec.rb b/spec/flatten_spec.rb index a7d87c32..b5763c3e 100644 --- a/spec/flatten_spec.rb +++ b/spec/flatten_spec.rb @@ -1078,8 +1078,7 @@ }, "ex:rel": [{"@id": "ex:subj"}], "ex:prop": [{"@id": "ex:value2"}] - } - ]) + }]) }, "node with @annotation property on node object with reverse relationship": { input: %({ diff --git a/spec/rdfstar_spec.rb b/spec/rdfstar_spec.rb index 535e41d7..cb9ea7af 100644 --- a/spec/rdfstar_spec.rb +++ b/spec/rdfstar_spec.rb @@ -7,13 +7,14 @@ %w{ expand compact + flatten fromRdf toRdf }.each do |partial| m = Fixtures::SuiteTest::Manifest.open("#{Fixtures::SuiteTest::STAR_SUITE}#{partial}-manifest.jsonld") describe m.name do m.entries.each do |t| - specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do + specify "#{t.property('@id')}: #{t.name}#{' (negative test)' unless t.positiveTest?}" do t.options[:ordered] = false expect {t.run self}.not_to write.to(:error) end From 612a8b9530107bb9ded6cfb1f41682fc5c6d58e7 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 27 Jan 2021 14:02:50 -0800 Subject: [PATCH 12/17] Fix expansion bug where a property is `@reverse` and the same subject has an explicit `@reverse`. --- lib/json/ld/expand.rb | 4 ++-- spec/expand_spec.rb | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb index bfeec1aa..be149677 100644 --- a/lib/json/ld/expand.rb +++ b/lib/json/ld/expand.rb @@ -13,7 +13,7 @@ module Expand KEY_ID = %w(@id).freeze KEYS_VALUE_LANGUAGE_TYPE_INDEX_DIRECTION = %w(@value @language @type @index @direction @annotation).freeze KEYS_SET_LIST_INDEX = %w(@set @list @index).freeze - KEYS_INCLUDED_TYPE = %w(@included @type).freeze + KEYS_INCLUDED_TYPE_REVERSE = %w(@included @type @reverse).freeze ## # Expand an Array or Object given an active context and performing local context expansion. @@ -266,7 +266,7 @@ def expand_object(input, active_property, context, output_object, # If result has already an expanded property member (other than @type), an colliding keywords error has been detected and processing is aborted. raise JsonLdError::CollidingKeywords, - "#{expanded_property} already exists in result" if output_object.has_key?(expanded_property) && !KEYS_INCLUDED_TYPE.include?(expanded_property) + "#{expanded_property} already exists in result" if output_object.has_key?(expanded_property) && !KEYS_INCLUDED_TYPE_REVERSE.include?(expanded_property) expanded_value = case expanded_property when '@id' diff --git a/spec/expand_spec.rb b/spec/expand_spec.rb index 8082b958..2a6d52df 100644 --- a/spec/expand_spec.rb +++ b/spec/expand_spec.rb @@ -3371,6 +3371,43 @@ }), exception: JSON::LD::JsonLdError::InvalidReversePropertyMap, }, + "Explicit and implicit @reverse in same object": { + input: %({ + "@context": { + "fooOf": {"@reverse": "ex:foo", "@type": "@id"} + }, + "@id": "ex:s", + "fooOf": "ex:o1", + "@reverse": { + "ex:bar": {"@id": "ex:o2"} + } + }), + output: %([{ + "@id": "ex:s", + "@reverse": { + "ex:bar": [{"@id": "ex:o2"}], + "ex:foo": [{"@id": "ex:o1"}] + } + }]) + }, + "Two properties both with @reverse": { + input: %({ + "@context": { + "fooOf": {"@reverse": "ex:foo", "@type": "@id"}, + "barOf": {"@reverse": "ex:bar", "@type": "@id"} + }, + "@id": "ex:s", + "fooOf": "ex:o1", + "barOf": "ex:o2" + }), + output: %([{ + "@id": "ex:s", + "@reverse": { + "ex:bar": [{"@id": "ex:o2"}], + "ex:foo": [{"@id": "ex:o1"}] + } + }]) + }, }.each do |title, params| it(title) {run_expand params} end From 7f66e4eac2d6ca6e4c3193a650424e7c95daca14 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 27 Jan 2021 17:16:39 -0800 Subject: [PATCH 13/17] Update some ruby conventions. --- bin/jsonld | 6 +-- etc/template.haml | 2 +- lib/json/ld/api.rb | 4 +- lib/json/ld/compact.rb | 4 +- lib/json/ld/context.rb | 84 ++++++++++++++++----------------- lib/json/ld/expand.rb | 8 ++-- lib/json/ld/extensions.rb | 8 ++-- lib/json/ld/flatten.rb | 4 +- lib/json/ld/format.rb | 2 +- lib/json/ld/frame.rb | 16 +++---- lib/json/ld/from_rdf.rb | 4 +- lib/json/ld/streaming_reader.rb | 10 ++-- lib/json/ld/streaming_writer.rb | 8 ++-- lib/json/ld/to_rdf.rb | 4 +- lib/json/ld/utils.rb | 26 +++++----- lib/json/ld/writer.rb | 4 +- profiler/test-loaders.rb | 2 +- script/parse | 2 +- spec/compact_spec.rb | 2 +- 19 files changed, 100 insertions(+), 100 deletions(-) diff --git a/bin/jsonld b/bin/jsonld index f82f723b..e71864f7 100755 --- a/bin/jsonld +++ b/bin/jsonld @@ -24,7 +24,7 @@ def run(input, options, parser_options) start = Time.new if options[:expand] - parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.has_key?(:context) + parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context) input = JSON::LD::API.fromRdf(reader) if reader output = JSON::LD::API.expand(input, parser_options) secs = Time.new - start @@ -49,7 +49,7 @@ def run(input, options, parser_options) options[:output].puts output.to_json(JSON::LD::JSON_STATE) STDERR.puts "Framed in #{secs} seconds." unless options[:quiet] else - parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.has_key?(:context) + parser_options = parser_options.merge(expandContext: parser_options.delete(:context)) if parser_options.key?(:context) parser_options[:standard_prefixes] = true reader ||= JSON::LD::Reader.new(input, parser_options) num = 0 @@ -181,7 +181,7 @@ opts.each do |opt, arg| end # Hack -if !(options.keys & [:expand, :compact, :flatten, :frame]).empty? && +if !(options.keys & %i{expand compact flatten frame}).empty? && (parser_options[:stream] || options[:output_format] != :jsonld) STDERR.puts "Incompatible options" exit(1) diff --git a/etc/template.haml b/etc/template.haml index 9518912e..acddba0e 100644 --- a/etc/template.haml +++ b/etc/template.haml @@ -102,7 +102,7 @@ %dd{rel: "doap:developer"} - subject['developer'].each do |dev| %div{resource: dev['@id'], typeof: Array(dev['@type']).join(" ")} - - if dev.has_key?('@id') + - if dev.key?('@id') %a{href: dev['@id']} %span{property: "foaf:name"}< ~ CGI.escapeHTML dev['foaf:name'].to_s diff --git a/lib/json/ld/api.rb b/lib/json/ld/api.rb index 754577c1..39a3ba78 100644 --- a/lib/json/ld/api.rb +++ b/lib/json/ld/api.rb @@ -396,7 +396,7 @@ def self.frame(input, frame, expanded: false, **options) end # Set omitGraph option, if not present, based on processingMode - unless options.has_key?(:omitGraph) + unless options.key?(:omitGraph) options[:omitGraph] = context.processingMode('json-ld-1.1') end @@ -423,7 +423,7 @@ def self.frame(input, frame, expanded: false, **options) frame(framing_state, framing_state[:subjects].keys.opt_sort(ordered: @options[:ordered]), (expanded_frame.first || {}), parent: result, **options) # Default to based on processinMode - if !options.has_key?(:pruneBlankNodeIdentifiers) + if !options.key?(:pruneBlankNodeIdentifiers) options[:pruneBlankNodeIdentifiers] = context.processingMode('json-ld-1.1') end diff --git a/lib/json/ld/compact.rb b/lib/json/ld/compact.rb index 58f88d6e..a5d16211 100644 --- a/lib/json/ld/compact.rb +++ b/lib/json/ld/compact.rb @@ -231,7 +231,7 @@ def compact(element, unless container.include?('@list') al = context.compact_iri('@list', vocab: true) compacted_item = {al => compacted_item} - if expanded_item.has_key?('@index') + if expanded_item.key?('@index') key = context.compact_iri('@index', vocab: true) compacted_item[key] = expanded_item['@index'] end @@ -276,7 +276,7 @@ def compact(element, al = context.compact_iri('@id', vocab: true) compacted_item[al] = context.compact_iri(expanded_item['@id'], vocab: false) end - if expanded_item.has_key?('@index') + if expanded_item.key?('@index') key = context.compact_iri('@index', vocab: true) compacted_item[key] = expanded_item['@index'] end diff --git a/lib/json/ld/context.rb b/lib/json/ld/context.rb index 2d5677e0..bc6c9a02 100644 --- a/lib/json/ld/context.rb +++ b/lib/json/ld/context.rb @@ -314,7 +314,7 @@ def parse(local_context, #context_opts.delete(:headers) JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc| # 3.2.5) Dereference context. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member. - raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context') + raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context') # Parse stand-alone ctx = Context.new(unfrozen: true, **options).dup @@ -352,7 +352,7 @@ def parse(local_context, '@propagate' => :propagate=, '@vocab' => :vocab=, }.each do |key, setter| - next unless context.has_key?(key) + next unless context.key?(key) if key == '@import' # Retrieve remote context and merge the remaining context object into the result. raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0") @@ -367,11 +367,11 @@ def parse(local_context, # FIXME: should cache this, but ContextCache is for parsed contexts JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc| # Dereference import_loc. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member. - raise JsonLdError::InvalidRemoteContext, "#{import_loc}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context') + raise JsonLdError::InvalidRemoteContext, "#{import_loc}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context') import_context = remote_doc.document['@context'] import_context.delete('@base') raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash) - raise JsonLdError::InvalidContextEntry, "#{import_context.to_json} must not include @import entry" if import_context.has_key?('@import') + raise JsonLdError::InvalidContextEntry, "#{import_context.to_json} must not include @import entry" if import_context.key?('@import') context.delete(key) context = import_context.merge(context) end @@ -542,7 +542,7 @@ def create_term_definition(local_context, term, defined, # Potentially note that the term is protected definition.protected = value.fetch('@protected', protected) - if value.has_key?('@type') + if value.key?('@type') type = value['@type'] # SPEC FIXME: @type may be nil type = case type @@ -566,7 +566,7 @@ def create_term_definition(local_context, term, defined, definition.type_mapping = type end - if value.has_key?('@reverse') + if value.key?('@reverse') raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if value.key?('@id') || value.key?('@nest') raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless @@ -592,7 +592,7 @@ def create_term_definition(local_context, term, defined, warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:") # If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted. - if value.has_key?('@container') + if value.key?('@container') container = value['@container'] raise JsonLdError::InvalidReverseProperty, "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless @@ -600,9 +600,9 @@ def create_term_definition(local_context, term, defined, definition.container_mapping = check_container(container, local_context, defined, term) end definition.reverse_property = true - elsif value.has_key?('@id') && value['@id'].nil? + elsif value.key?('@id') && value['@id'].nil? # Allowed to reserve a null term, which may be protected - elsif value.has_key?('@id') && value['@id'] != term + elsif value.key?('@id') && value['@id'] != term raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless value['@id'].is_a?(String) @@ -637,7 +637,7 @@ def create_term_definition(local_context, term, defined, elsif term[1..-1].include?(':') # If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined. prefix, suffix = term.split(':', 2) - create_term_definition(local_context, prefix, defined, protected: protected) if local_context.has_key?(prefix) + create_term_definition(local_context, prefix, defined, protected: protected) if local_context.key?(prefix) definition.id = if td = term_definitions[prefix] # If term's prefix has a term definition in active context, set the IRI mapping for definition to the result of concatenating the value associated with the prefix's IRI mapping and the term's suffix. @@ -664,7 +664,7 @@ def create_term_definition(local_context, term, defined, @iri_to_term[definition.id] = term if simple_term && definition.id - if value.has_key?('@container') + if value.key?('@container') #log_debug("") {"container_mapping: #{value['@container'].inspect}"} definition.container_mapping = check_container(value['@container'], local_context, defined, term) @@ -679,14 +679,14 @@ def create_term_definition(local_context, term, defined, end end - if value.has_key?('@index') + if value.key?('@index') # property-based indexing raise JsonLdError::InvalidTermDefinition, "@index without @index in @container: #{value['@index']} on term #{term.inspect}" unless definition.container_mapping.include?('@index') raise JsonLdError::InvalidTermDefinition, "@index must expand to an IRI: #{value['@index']} on term #{term.inspect}" unless value['@index'].is_a?(String) && !value['@index'].start_with?('@') definition.index = value['@index'].to_s end - if value.has_key?('@context') + if value.key?('@context') begin new_ctx = self.parse(value['@context'], base: base, @@ -704,7 +704,7 @@ def create_term_definition(local_context, term, defined, end end - if value.has_key?('@language') + if value.key?('@language') language = value['@language'] language = case value['@language'] when String @@ -722,14 +722,14 @@ def create_term_definition(local_context, term, defined, definition.language_mapping = language || false end - if value.has_key?('@direction') + if value.key?('@direction') direction = value['@direction'] raise JsonLdError::InvalidBaseDirection, "direction must be null, 'ltr', or 'rtl', was #{language.inspect}} on term #{term.inspect}" unless direction.nil? || %w(ltr rtl).include?(direction) #log_debug("") {"direction_mapping: #{direction.inspect}"} definition.direction_mapping = direction || false end - if value.has_key?('@nest') + if value.key?('@nest') nest = value['@nest'] raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String) raise JsonLdError::InvalidNestValue, "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}" if nest.match?(/^@[a-zA-Z]+$/) && nest != '@nest' @@ -737,7 +737,7 @@ def create_term_definition(local_context, term, defined, definition.nest = nest end - if value.has_key?('@prefix') + if value.key?('@prefix') raise JsonLdError::InvalidTermDefinition, "@prefix used on compact or relative IRI term #{term.inspect}" if term.match?(%r{:|/}) case pfx = value['@prefix'] when TrueClass, FalseClass @@ -1018,7 +1018,7 @@ def set_mapping(term, value) term_sym = term.empty? ? "" : term.to_sym iri_to_term.delete(term_definitions[term].id.to_s) if term_definitions[term].id.is_a?(String) - @options[:prefixes][term_sym] = value if @options.has_key?(:prefixes) + @options[:prefixes][term_sym] = value if @options.key?(:prefixes) iri_to_term[value.to_s] = term term_definitions[term] end @@ -1134,7 +1134,7 @@ def reverse?(term) # @return [Term] related term definition def reverse_term(term) # Direct lookup of term - term = term_definitions[term.to_s] if term_definitions.has_key?(term.to_s) && !term.is_a?(TermDefinition) + term = term_definitions[term.to_s] if term_definitions.key?(term.to_s) && !term.is_a?(TermDefinition) # Lookup term, assuming term is an IRI unless term.is_a?(TermDefinition) @@ -1182,7 +1182,7 @@ def expand_iri(value, defined = defined || {} # if we initialized in the keyword arg we would allocate {} at each invokation, even in the 2 (common) early returns above. # If local context is not null, it contains a key that equals value, and the value associated with the key that equals value in defined is not true, then invoke the Create Term Definition subalgorithm, passing active context, local context, value as term, and defined. This will ensure that a term definition is created for value in active context during Context Processing. - if local_context && local_context.has_key?(value) && !defined[value] + if local_context && local_context.key?(value) && !defined[value] create_term_definition(local_context, value, defined) end @@ -1212,7 +1212,7 @@ def expand_iri(value, end # If local context is not null, it contains a key that equals prefix, and the value associated with the key that equals prefix in defined is not true, invoke the Create Term Definition algorithm, passing active context, local context, prefix as term, and defined. This will ensure that a term definition is created for prefix in active context during Context Processing. - if local_context && local_context.has_key?(prefix) && !defined[prefix] + if local_context && local_context.key?(prefix) && !defined[prefix] create_term_definition(local_context, prefix, defined) end @@ -1287,7 +1287,7 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) return if iri.nil? iri = iri.to_s - if vocab && inverse_context.has_key?(iri) + if vocab && inverse_context.key?(iri) default_language = if self.default_direction "#{self.default_language}_#{self.default_direction}".downcase else @@ -1298,7 +1298,7 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) containers.concat(CONTAINERS_INDEX_SET) if index?(value) && !graph?(value) # If the value is a JSON Object with the key @preserve, use the value of @preserve. - value = value['@preserve'].first if value.is_a?(Hash) && value.has_key?('@preserve') + value = value['@preserve'].first if value.is_a?(Hash) && value.key?('@preserve') if reverse tl, tl_value = "@type", "@reverse" @@ -1312,11 +1312,11 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) list.each do |item| item_language, item_type = "@none", "@none" if value?(item) - if item.has_key?('@direction') + if item.key?('@direction') item_language = "#{item['@language']}_#{item['@direction']}".downcase - elsif item.has_key?('@language') + elsif item.key?('@language') item_language = item['@language'].downcase - elsif item.has_key?('@type') + elsif item.key?('@type') item_type = item['@type'] else item_language = "@null" @@ -1344,14 +1344,14 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) elsif graph?(value) # Prefer @index and @id containers, then @graph, then @index containers.concat(CONTAINERS_GRAPH_INDEX_INDEX) if index?(value) - containers.concat(CONTAINERS_GRAPH) if value.has_key?('@id') + containers.concat(CONTAINERS_GRAPH) if value.key?('@id') # Prefer an @graph container next containers.concat(CONTAINERS_GRAPH_SET) # Lastly, in 1.1, any graph can be indexed on @index or @id, so add if we haven't already containers.concat(CONTAINERS_GRAPH_INDEX) unless index?(value) - containers.concat(CONTAINERS_GRAPH) unless value.has_key?('@id') + containers.concat(CONTAINERS_GRAPH) unless value.key?('@id') containers.concat(CONTAINERS_INDEX_SET) unless index?(value) containers << '@set' @@ -1359,13 +1359,13 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) else if value?(value) # In 1.1, an language map can be used to index values using @none - if value.has_key?('@language') && !index?(value) + if value.key?('@language') && !index?(value) tl_value = value['@language'].downcase tl_value += "_#{value['@direction']}" if value['@direction'] containers.concat(CONTAINERS_LANGUAGE) - elsif value.has_key?('@direction') && !index?(value) + elsif value.key?('@direction') && !index?(value) tl_value = "_#{value['@direction']}" - elsif value.has_key?('@type') + elsif value.key?('@type') tl_value = value['@type'] tl = '@type' end @@ -1387,7 +1387,7 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) tl_value ||= '@null' preferred_values = [] preferred_values << '@reverse' if tl_value == '@reverse' - if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.has_key?('@id') + if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.key?('@id') t_iri = compact_iri(value['@id'], vocab: true, base: base) if (r_td = term_definitions[t_iri]) && r_td.id == value['@id'] preferred_values.concat(CONTAINERS_VOCAB_ID) @@ -1413,7 +1413,7 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) # At this point, there is no simple term that iri can be compacted to. If vocab is true and active context has a vocabulary mapping: if vocab && self.vocab && iri.start_with?(self.vocab) && iri.length > self.vocab.length suffix = iri[self.vocab.length..-1] - return suffix unless term_definitions.has_key?(suffix) + return suffix unless term_definitions.key?(suffix) end # The iri could not be compacted using the active context's vocabulary mapping. Try to create a compact IRI, starting by initializing compact IRI to null. This variable will be used to tore the created compact IRI, if any. @@ -1427,7 +1427,7 @@ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil) suffix = iri[td.id.length..-1] ciri = "#{term}:#{suffix}" - candidates << ciri unless value && term_definitions.has_key?(ciri) + candidates << ciri unless value && term_definitions.key?(ciri) end return candidates.sort.first if !candidates.empty? @@ -1530,9 +1530,9 @@ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base value.canonicalize! if value.valid? && value.datatype == RDF::XSD.double if coerce(property) res['@type'] = uri(coerce(property)).to_s - elsif value.has_datatype? + elsif value.datatype? res['@type'] = uri(value.datatype).to_s - elsif value.has_language? || language(property) + elsif value.language? || language(property) res['@language'] = (value.language || language(property)).to_s end res['@value'] = value.to_s @@ -1580,15 +1580,15 @@ def compact_value(property, value, base: nil) direction = direction(property) result = case - when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty? + when coerce(property) == '@id' && value.key?('@id') && (value.keys - %w(@id @index)).empty? # Compact an @id coercion #log_debug("") {" (@id & coerce)"} compact_iri(value['@id'], base: base) - when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty? + when coerce(property) == '@vocab' && value.key?('@id') && (value.keys - %w(@id @index)).empty? # Compact an @id coercion #log_debug("") {" (@id & coerce & vocab)"} compact_iri(value['@id'], vocab: true) - when value.has_key?('@id') + when value.key?('@id') #log_debug("") {" (@id)"} # return value as is value @@ -1609,7 +1609,7 @@ def compact_value(property, value, base: nil) value end - if result.is_a?(Hash) && result.has_key?('@type') && value['@type'] != '@json' + if result.is_a?(Hash) && result.key?('@type') && value['@type'] != '@json' # Compact values of @type c_type = if result['@type'].is_a?(Array) result['@type'].map {|t| compact_iri(t, vocab: true)} @@ -1857,11 +1857,11 @@ def select_term(iri, containers, type_language, preferred_values) container_map = inverse_context[iri] #log_debug(" ") {"container_map: #{container_map.inspect}"} containers.each do |container| - next unless container_map.has_key?(container) + next unless container_map.key?(container) tl_map = container_map[container] value_map = tl_map[type_language] preferred_values.each do |item| - next unless value_map.has_key?(item) + next unless value_map.key?(item) #log_debug("=>") {value_map[item].inspect} return value_map[item] end diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb index be149677..cb89f3ee 100644 --- a/lib/json/ld/expand.rb +++ b/lib/json/ld/expand.rb @@ -81,7 +81,7 @@ def expand(input, active_property, context, log_debug("expand", depth: log_depth.to_i) {"after property_scoped_context: #{context.inspect}"} unless property_scoped_context.nil? # If element contains the key @context, set active context to the result of the Context Processing algorithm, passing active context and the value of the @context key as local context. - if input.has_key?('@context') + if input.key?('@context') context = context.parse(input.delete('@context'), base: @options[:base]) log_debug("expand", depth: log_depth.to_i) {"context: #{context.inspect}"} end @@ -142,7 +142,7 @@ def expand(input, active_property, context, if output_object['@type'] == '@json' && context.processingMode('json-ld-1.1') # Any value of @value is okay if @type: @json - elsif !ary.all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.has_key?('@language') + elsif !ary.all? {|v| v.is_a?(String) || v.is_a?(Hash) && v.empty?} && output_object.key?('@language') # Otherwise, if the value of result's @value member is not a string and result contains the key @language, an invalid language-tagged value error has been detected (only strings can be language-tagged) and processing is aborted. raise JsonLdError::InvalidLanguageTaggedValue, "when @language is used, @value must be a string: #{output_object.inspect}" @@ -266,7 +266,7 @@ def expand_object(input, active_property, context, output_object, # If result has already an expanded property member (other than @type), an colliding keywords error has been detected and processing is aborted. raise JsonLdError::CollidingKeywords, - "#{expanded_property} already exists in result" if output_object.has_key?(expanded_property) && !KEYS_INCLUDED_TYPE_REVERSE.include?(expanded_property) + "#{expanded_property} already exists in result" if output_object.key?(expanded_property) && !KEYS_INCLUDED_TYPE_REVERSE.include?(expanded_property) expanded_value = case expanded_property when '@id' @@ -504,7 +504,7 @@ def expand_object(input, active_property, context, output_object, log_depth: log_depth.to_i + 1) # If expanded value contains an @reverse member, i.e., properties that are reversed twice, execute for each of its property and item the following steps: - if value.has_key?('@reverse') + if value.key?('@reverse') #log_debug("@reverse", depth: log_depth.to_i) {"double reverse: #{value.inspect}"} value['@reverse'].each do |property, item| # If result does not have a property member, create one and set its value to an empty array. diff --git a/lib/json/ld/extensions.rb b/lib/json/ld/extensions.rb index 5b2f12c5..e24f46cc 100644 --- a/lib/json/ld/extensions.rb +++ b/lib/json/ld/extensions.rb @@ -11,10 +11,10 @@ def +(value) class Statement # Validate extended RDF def valid_extended? - has_subject? && subject.resource? && subject.valid_extended? && - has_predicate? && predicate.resource? && predicate.valid_extended? && - has_object? && object.term? && object.valid_extended? && - (has_graph? ? (graph_name.resource? && graph_name.valid_extended?) : true) + subject? && subject.resource? && subject.valid_extended? && + predicate? && predicate.resource? && predicate.valid_extended? && + object? && object.term? && object.valid_extended? && + (graph? ? (graph_name.resource? && graph_name.valid_extended?) : true) end end diff --git a/lib/json/ld/flatten.rb b/lib/json/ld/flatten.rb index ca03ea08..f4c9dbaf 100644 --- a/lib/json/ld/flatten.rb +++ b/lib/json/ld/flatten.rb @@ -49,7 +49,7 @@ def create_node_map(element, graph_map, subject_node = !reverse && graph[active_subject.is_a?(Hash) ? active_subject.to_json_c14n : active_subject] # Transform BNode types - if element.has_key?('@type') + if element.key?('@type') element['@type'] = Array(element['@type']).map {|t| blank_node?(t) ? namer.get_name(t) : t} end @@ -143,7 +143,7 @@ def create_node_map(element, graph_map, active_subject: star_subject) end - if element.has_key?('@type') + if element.key?('@type') add_value(node, '@type', element.delete('@type'), property_is_array: true, allow_duplicate: false) end diff --git a/lib/json/ld/format.rb b/lib/json/ld/format.rb index b482300e..d971459a 100644 --- a/lib/json/ld/format.rb +++ b/lib/json/ld/format.rb @@ -57,7 +57,7 @@ def self.cli_commands lambda: ->(files, **options) do out = options[:output] || $stdout out.set_encoding(Encoding::UTF_8) if RUBY_PLATFORM == "java" - options = options.merge(expandContext: options.delete(:context)) if options.has_key?(:context) + options = options.merge(expandContext: options.delete(:context)) if options.key?(:context) options[:base] ||= options[:base_uri] if options[:format] == :jsonld if files.empty? diff --git a/lib/json/ld/frame.rb b/lib/json/ld/frame.rb index 85d47a2a..e9b2b943 100644 --- a/lib/json/ld/frame.rb +++ b/lib/json/ld/frame.rb @@ -52,7 +52,7 @@ def frame(state, subjects, frame, parent: nil, property: nil, ordered: false, ** state[:uniqueEmbeds][state[:graph]] ||= {} end - if flags[:embed] == '@link' && link.has_key?(id) + if flags[:embed] == '@link' && link.key?(id) # add existing linked subject add_frame_output(parent, property, link[id]) next @@ -66,7 +66,7 @@ def frame(state, subjects, frame, parent: nil, property: nil, ordered: false, ** warn "[DEPRECATION] #{flags[:embed]} is not a valid value of @embed in 1.1 mode.\n" end - if !state[:embedded] && state[:uniqueEmbeds][state[:graph]].has_key?(id) + if !state[:embedded] && state[:uniqueEmbeds][state[:graph]].key?(id) # Skip adding this node object to the top-level, as it was included in another node object next elsif state[:embedded] && @@ -76,7 +76,7 @@ def frame(state, subjects, frame, parent: nil, property: nil, ordered: false, ** next elsif state[:embedded] && %w(@first @once).include?(flags[:embed]) && - state[:uniqueEmbeds][state[:graph]].has_key?(id) + state[:uniqueEmbeds][state[:graph]].key?(id) # if only the first match should be embedded # Embed unless already embedded @@ -97,7 +97,7 @@ def frame(state, subjects, frame, parent: nil, property: nil, ordered: false, ** state[:subjectStack] << {subject: subject, graph: state[:graph]} # Subject is also the name of a graph - if state[:graphMap].has_key?(id) + if state[:graphMap].key?(id) # check frame's "@graph" to see what to do next # 1. if it doesn't exist and state.graph === "@merged", don't recurse # 2. if it doesn't exist and state.graph !== "@merged", recurse @@ -105,7 +105,7 @@ def frame(state, subjects, frame, parent: nil, property: nil, ordered: false, ** # 4. if "@default" then don't recurse # 5. recurse recurse, subframe = false, nil - if !frame.has_key?('@graph') + if !frame.key?('@graph') recurse, subframe = (state[:graph] != '@merged'), {} else subframe = frame['@graph'].first @@ -134,7 +134,7 @@ def frame(state, subjects, frame, parent: nil, property: nil, ordered: false, ** end # explicit is on and property isn't in frame, skip processing - next if flags[:explicit] && !frame.has_key?(prop) + next if flags[:explicit] && !frame.key?(prop) # add objects objects.each do |o| @@ -267,7 +267,7 @@ def cleanup_preserve(input) # If, after replacement, an array contains only the value null remove the value, leaving an empty array. input.map {|o| cleanup_preserve(o)} when Hash - if input.has_key?('@preserve') + if input.key?('@preserve') # Replace with the content of `@preserve` cleanup_preserve(input['@preserve'].first) else @@ -388,7 +388,7 @@ def filter_subject(subject, frame, state, flags) is_empty = v.empty? if v = v.first validate_frame(v) - has_default = v.has_key?('@default') + has_default = v.key?('@default') end # No longer a wildcard pattern if frame has any non-keyword properties diff --git a/lib/json/ld/from_rdf.rb b/lib/json/ld/from_rdf.rb index f7c09550..27788e03 100644 --- a/lib/json/ld/from_rdf.rb +++ b/lib/json/ld/from_rdf.rb @@ -75,7 +75,7 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false) property: statement.predicate.to_s, value: value }) - elsif referenced_once.has_key?(statement.object.to_s) + elsif referenced_once.key?(statement.object.to_s) referenced_once[statement.object.to_s] = false elsif statement.object.node? referenced_once[statement.object.to_s] = { @@ -146,7 +146,7 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false) result = [] default_graph.keys.opt_sort(ordered: @options[:ordered]).each do |subject| node = default_graph[subject] - if graph_map.has_key?(subject) + if graph_map.key?(subject) node['@graph'] = [] graph_map[subject].keys.opt_sort(ordered: @options[:ordered]).each do |s| n = graph_map[subject][s] diff --git a/lib/json/ld/streaming_reader.rb b/lib/json/ld/streaming_reader.rb index 23f19360..98429a56 100644 --- a/lib/json/ld/streaming_reader.rb +++ b/lib/json/ld/streaming_reader.rb @@ -136,7 +136,7 @@ def parse_object(input, active_property, context, when '@type' # Set the type-scoped context to the context on input, for use later raise JsonLdError::InvalidStreamingKeyOrder, - "found #{key} in state #{state}" unless [:await_context, :await_type].include?(state) + "found #{key} in state #{state}" unless %i(await_context await_type).include?(state) type_scoped_context = context as_array(value).sort.each do |term| @@ -159,7 +159,7 @@ def parse_object(input, active_property, context, raise JsonLdError::InvalidSetOrListObject, "found #{key} in state #{state}" if is_list_or_set raise JsonLdError::CollidingKeywords, - "found #{key} in state #{state}" unless [:await_context, :await_type, :await_id].include?(state) + "found #{key} in state #{state}" unless %i(await_context await_type await_id).include?(state) # Set our actual id, and use for replacing any provisional statements using our existing node_id, which is provisional raise JsonLdError::InvalidIdValue, @@ -209,7 +209,7 @@ def parse_object(input, active_property, context, # Expanded values must be node objects have_statements = false parse_object(value, active_property, context) do |st| - have_statements ||= st.has_subject? + have_statements ||= st.subject? block.call(st) end raise JsonLdError::InvalidIncludedValue, "values of @included must expand to node objects" unless have_statements @@ -232,7 +232,7 @@ def parse_object(input, active_property, context, when '@list' raise JsonLdError::InvalidSetOrListObject, "found #{key} in state #{state}" if - ![:await_context, :await_type, :await_id].include?(state) + !%i(await_context await_type await_id).include?(state) is_list_or_set = true if subject node_id = parse_list(value, active_property, context, &block) @@ -277,7 +277,7 @@ def parse_object(input, active_property, context, when '@set' raise JsonLdError::InvalidSetOrListObject, "found #{key} in state #{state}" if - ![:await_context, :await_type, :await_id].include?(state) + !%i(await_context await_type await_id).include?(state) is_list_or_set = true value = as_array(value).compact parse_object(value, active_property, context, subject: subject, predicate: predicate, &block) diff --git a/lib/json/ld/streaming_writer.rb b/lib/json/ld/streaming_writer.rb index 5b12e4a5..481babe7 100644 --- a/lib/json/ld/streaming_writer.rb +++ b/lib/json/ld/streaming_writer.rb @@ -64,8 +64,8 @@ def stream_statement(statement) {"@value" => MultiJson.load(statement.object.to_s), "@type" => "@json"} else lit = {"@value" => statement.object.to_s} - lit["@type"] = statement.object.datatype.to_s if statement.object.has_datatype? - lit["@language"] = statement.object.language.to_s if statement.object.has_language? + lit["@type"] = statement.object.datatype.to_s if statement.object.datatype? + lit["@language"] = statement.object.language.to_s if statement.object.language? lit end end @@ -91,7 +91,7 @@ def stream_epilogue def start_graph(resource) #log_debug("start_graph") {"state: #{@state.inspect}, resource: #{resource}"} if resource - @output.puts(",") if [:wrote_node, :wrote_graph].include?(@state) + @output.puts(",") if %i(wrote_node wrote_graph).include?(@state) @output.puts %({"@id": "#{resource}", "@graph": [) @state = :in_graph end @@ -109,7 +109,7 @@ def end_graph def end_node #log_debug("end_node") {"state: #{@state.inspect}, node: #{@current_node_def.to_json}"} - @output.puts(",") if [:wrote_node, :wrote_graph].include?(@state) + @output.puts(",") if %i(wrote_node wrote_graph).include?(@state) if @current_node_def node_def = if context compacted = JSON::LD::API.compact(@current_node_def, context, rename_bnodes: false, **@options) diff --git a/lib/json/ld/to_rdf.rb b/lib/json/ld/to_rdf.rb index d3840d6c..fb4967f0 100644 --- a/lib/json/ld/to_rdf.rb +++ b/lib/json/ld/to_rdf.rb @@ -48,7 +48,7 @@ def item_to_rdf(item, graph_name: nil, &block) # Only valid for rdf:JSON value = value.to_json_c14n else - if item.has_key?('@direction') && @options[:rdfDirection] + if item.key?('@direction') && @options[:rdfDirection] # Either serialize using a datatype, or a compound-literal case @options[:rdfDirection] when 'i18n-datatype' @@ -63,7 +63,7 @@ def item_to_rdf(item, graph_name: nil, &block) end # Otherwise, if datatype is null, set it to xsd:string or xsd:langString, depending on if item has a @language key. - datatype ||= item.has_key?('@language') ? RDF.langString : RDF::XSD.string + datatype ||= item.key?('@language') ? RDF.langString : RDF::XSD.string if datatype == RDF::URI(RDF.to_uri + "JSON") value = value.to_json_c14n end diff --git a/lib/json/ld/utils.rb b/lib/json/ld/utils.rb index b5b58b4f..8b48effd 100644 --- a/lib/json/ld/utils.rb +++ b/lib/json/ld/utils.rb @@ -11,8 +11,8 @@ module Utils # @return [Boolean] def node?(value) value.is_a?(Hash) && - !(value.has_key?('@value') || value.has_key?('@list') || value.has_key?('@set')) && - (value.length > 1 || !value.has_key?('@id')) + !(value.key?('@value') || value.key?('@list') || value.key?('@set')) && + (value.length > 1 || !value.key?('@id')) end ## @@ -29,7 +29,7 @@ def node_reference?(value) # @return [Boolean] def node_or_ref?(value) value.is_a?(Hash) && - !(value.has_key?('@value') || value.has_key?('@list') || value.has_key?('@set')) + !(value.key?('@value') || value.key?('@list') || value.key?('@set')) end ## @@ -66,7 +66,7 @@ def graph?(value) # @param [Object] value # @return [Boolean] def simple_graph?(value) - graph?(value) && !value.has_key?('@id') + graph?(value) && !value.key?('@id') end ## @@ -75,7 +75,7 @@ def simple_graph?(value) # @param [Object] value # @return [Boolean] def list?(value) - value.is_a?(Hash) && value.has_key?('@list') + value.is_a?(Hash) && value.key?('@list') end ## @@ -84,7 +84,7 @@ def list?(value) # @param [Object] value # @return [Boolean] def index?(value) - value.is_a?(Hash) && value.has_key?('@index') + value.is_a?(Hash) && value.key?('@index') end ## @@ -93,7 +93,7 @@ def index?(value) # @param [Object] value # @return [Boolean] def value?(value) - value.is_a?(Hash) && value.has_key?('@value') + value.is_a?(Hash) && value.key?('@value') end ## @@ -170,7 +170,7 @@ def add_value(subject, property, value, property_is_array: false, value_is_array end elsif subject[property] # check if subject already has value if duplicates not allowed - _has_value = !allow_duplicate && has_value(subject, property, value) + _has_value = !allow_duplicate && has_value?(subject, property, value) # make property an array if value not present or always an array if !subject[property].is_a?(Array) && (!_has_value || property_is_array) @@ -188,7 +188,7 @@ def add_value(subject, property, value, property_is_array: false, value_is_array # @param property the property to look for. # # @return [Boolean] true if the subject has the given property, false if not. - def has_property(subject, property) + def property?(subject, property) return false unless value = subject[property] !value.is_a?(Array) || !value.empty? end @@ -200,8 +200,8 @@ def has_property(subject, property) # @param [Object] value the value to check. # # @return [Boolean] true if the value exists, false if not. - def has_value(subject, property, value) - if has_property(subject, property) + def has_value?(subject, property, value) + if property?(subject, property) val = subject[property] is_list = list?(val) if val.is_a?(Array) || is_list @@ -265,7 +265,7 @@ class BlankNodeUniqer < BlankNodeMapper # @return [String] def get_sym(old = "") old = old.to_s.sub(/_:/, '') - if old && self.has_key?(old) + if old && self.key?(old) self[old] elsif !old.empty? self[old] = RDF::Node.new.to_unique_base[2..-1] @@ -289,7 +289,7 @@ def initialize(prefix) # @return [String] def get_sym(old = "") old = old.to_s.sub(/_:/, '') - if !old.empty? && self.has_key?(old) + if !old.empty? && self.key?(old) self[old] elsif !old.empty? @num += 1 diff --git a/lib/json/ld/writer.rb b/lib/json/ld/writer.rb index 0e4edbed..f8794277 100644 --- a/lib/json/ld/writer.rb +++ b/lib/json/ld/writer.rb @@ -237,8 +237,8 @@ def default_context=(url); @default_context = url; end # @yield [writer] # @yieldparam [RDF::Writer] writer def initialize(output = $stdout, **options, &block) - options[:base_uri] ||= options[:base] if options.has_key?(:base) - options[:base] ||= options[:base_uri] if options.has_key?(:base_uri) + options[:base_uri] ||= options[:base] if options.key?(:base) + options[:base] ||= options[:base_uri] if options.key?(:base_uri) super do @repo = RDF::Repository.new diff --git a/profiler/test-loaders.rb b/profiler/test-loaders.rb index f7eb2d31..bf407578 100755 --- a/profiler/test-loaders.rb +++ b/profiler/test-loaders.rb @@ -73,7 +73,7 @@ def usage contentType: "application/ld+json") options[:documentLoader] = Proc.new do |url, **options, &block| - raise "Context not pre-cached: #{url}" unless doc_cache.has_key?(url.to_s) + raise "Context not pre-cached: #{url}" unless doc_cache.key?(url.to_s) block.call doc_cache[url.to_s] end diff --git a/script/parse b/script/parse index 9c7bff78..6d0ad295 100755 --- a/script/parse +++ b/script/parse @@ -43,7 +43,7 @@ def run(input, options) options[:output].puts output.to_json(JSON::LD::JSON_STATE) puts "Flattened in #{secs} seconds." elsif options[:expand] - options = options.merge(expandContext: options.delete(:context)) if options.has_key?(:context) + options = options.merge(expandContext: options.delete(:context)) if options.key?(:context) output = JSON::LD::API.expand(input, **options) secs = Time.new - start options[:output].puts output.to_json(JSON::LD::JSON_STATE) diff --git a/spec/compact_spec.rb b/spec/compact_spec.rb index 4037dfe7..103cf405 100644 --- a/spec/compact_spec.rb +++ b/spec/compact_spec.rb @@ -3389,7 +3389,7 @@ def run_compact(params) input = ::JSON.parse(input) if input.is_a?(String) output = ::JSON.parse(output) if output.is_a?(String) context = ::JSON.parse(context) if context.is_a?(String) - context = context['@context'] if context.has_key?('@context') + context = context['@context'] if context.key?('@context') pending params.fetch(:pending, "test implementation") unless input if params[:exception] expect {JSON::LD::API.compact(input, context, logger: logger, **params)}.to raise_error(params[:exception]) From 9b753b548784020306e4d88072f3620ca320e0cb Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Wed, 27 Jan 2021 17:54:54 -0800 Subject: [PATCH 14/17] Only rename a blank node if it is the value of `@id`. --- lib/json/ld/flatten.rb | 7 ++++--- spec/suite_frame_spec.rb | 5 +---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/json/ld/flatten.rb b/lib/json/ld/flatten.rb index f4c9dbaf..836c4e9f 100644 --- a/lib/json/ld/flatten.rb +++ b/lib/json/ld/flatten.rb @@ -198,12 +198,13 @@ def create_node_map(element, graph_map, # @return [Hash] def rename_bnodes(node) case node - when String - blank_node?(node) ? namer.get_name(node) : node when Array node.map {|n| rename_bnodes(n)} when Hash - node.inject({}) {|memo, (k, v)| memo.merge(k => rename_bnodes(v))} + node.inject({}) do |memo, (k, v)| + v = namer.get_name(v) if k == '@id' && v.is_a?(String) && blank_node?(v) + memo.merge(k => rename_bnodes(v)) + end else node end diff --git a/spec/suite_frame_spec.rb b/spec/suite_frame_spec.rb index 8e2318a9..79c314ce 100644 --- a/spec/suite_frame_spec.rb +++ b/spec/suite_frame_spec.rb @@ -11,9 +11,6 @@ specify "#{t.property('@id')}: #{t.name} unordered#{' (negative test)' unless t.positiveTest?}" do t.options[:ordered] = false - if %w(#t0021 #tp021).include?(t.property('@id')) - pending("changes due to blank node reordering") - end expect {t.run self}.not_to write.to(:error) end @@ -21,7 +18,7 @@ next if t.options[:remap_bnodes] specify "#{t.property('@id')}: #{t.name} ordered#{' (negative test)' unless t.positiveTest?}" do t.options[:ordered] = true - if %w(#t0021 #tp021).include?(t.property('@id')) + if %w(#tp021).include?(t.property('@id')) pending("changes due to blank node reordering") end expect {t.run self}.not_to write.to(:error) From 940ca011f43d314951a142adca99e39a3f02074d Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 2 Feb 2021 13:54:26 -0800 Subject: [PATCH 15/17] Don't compact an `@id` or `@value` to a string if it contains an annotation. --- lib/json/ld/compact.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/json/ld/compact.rb b/lib/json/ld/compact.rb index a5d16211..c13a7231 100644 --- a/lib/json/ld/compact.rb +++ b/lib/json/ld/compact.rb @@ -64,7 +64,7 @@ def compact(element, log_debug("prop-scoped", depth: log_depth.to_i) {"context: #{self.context.inspect}"} end - if element.key?('@id') || element.key?('@value') + if (element.key?('@id') || element.key?('@value')) && !element.key?('@annotation') result = context.compact_value(property, element, base: @options[:base]) if !result.is_a?(Hash) || context.coerce(property) == '@json' log_debug("", depth: log_depth.to_i) {"=> scalar result: #{result.inspect}"} From 83ed915504a93867357e8b029fde6f8c0e4ef20c Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 2 Feb 2021 13:55:45 -0800 Subject: [PATCH 16/17] Make sure list members don't have annotations. --- lib/json/ld/expand.rb | 13 ++++++++++++- script/tc | 11 +++++++---- spec/expand_spec.rb | 4 ++-- spec/suite_helper.rb | 10 +++++----- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb index cb89f3ee..76b56f6a 100644 --- a/lib/json/ld/expand.rb +++ b/lib/json/ld/expand.rb @@ -49,7 +49,13 @@ def expand(input, active_property, context, log_depth: log_depth.to_i + 1) # If the active property is @list or its container mapping is set to @list and v is an array, change it to a list object - v = {"@list" => v} if is_list && v.is_a?(Array) + if is_list && v.is_a?(Array) + # Make sure that no member of v contains an annotation object + raise JsonLdError::InvalidAnnotation, + "A list element must not contain @annotation." if + v.any? {|n| n.is_a?(Hash) && n.key?('@annotation')} + v = {"@list" => v} + end case v when nil then nil @@ -486,6 +492,11 @@ def expand_object(input, active_property, context, output_object, # Spec FIXME: need to be sure that result is an array value = as_array(value) + # Make sure that no member of value contains an annotation object + raise JsonLdError::InvalidAnnotation, + "A list element must not contain @annotation." if + value.any? {|n| n.is_a?(Hash) && n.key?('@annotation')} + value when '@set' # If expanded property is @set, set expanded value to the result of using this algorithm recursively, passing active context, active property, and value for element. diff --git a/script/tc b/script/tc index a247951d..baf8f807 100755 --- a/script/tc +++ b/script/tc @@ -98,7 +98,7 @@ def run_tc(man, tc, options) expected = JSON.load(tc.expect) if tc.evaluationTest? && tc.positiveTest? compare_results(tc, output, expected) when 'jld:FromRDFTest' - repo = RDF::Repository.load(tc.input_loc, format: :nquads) + repo = RDF::Repository.load(tc.input_loc, format: :nquads, rdfstar: tc.options[:rdfstar]) output = if options[:stream] JSON.parse(JSON::LD::Writer.buffer(stream: true, validate: true, **tc.options) {|w| w << repo}) else @@ -129,11 +129,11 @@ def run_tc(man, tc, options) # toRdf/e075 is hard to test, but verified manually output == tc.expect ? 'passed' : (tc.input_loc.include?('e075') ? 'passed' : 'failed') else - expected = RDF::Repository.new << RDF::NQuads::Reader.new(tc.expect, validate: false, logger: []) + expected = RDF::Repository.new << RDF::NQuads::Reader.new(tc.expect, rdfstar: tc.options[:rdfstar], validate: false, logger: []) output.isomorphic?(expected) ? 'passed' : 'failed' end rescue RDF::ReaderError, JSON::LD::JsonLdError - quads = JSON::LD::API.toRdf(tc.input_loc, tc.options.merge(validate: false)).map do |statement| + quads = JSON::LD::API.toRdf(tc.input_loc, rdfstar: tc.options[:rdfstar], **tc.options.merge(validate: false)).map do |statement| # Not really RDF, try different test method tc.to_quad(statement) end @@ -254,7 +254,10 @@ else %w(expand compact flatten fromRdf html remote-doc toRdf).map do |man| "#{Fixtures::SuiteTest::SUITE}#{man}-manifest.jsonld" end + - ["#{Fixtures::SuiteTest::FRAME_SUITE}frame-manifest.jsonld"] + ["#{Fixtures::SuiteTest::FRAME_SUITE}frame-manifest.jsonld"] + + %w{expand compact flatten fromRdf toRdf}.map do |man| + "#{Fixtures::SuiteTest::STAR_SUITE}#{man}-manifest.jsonld" + end end earl_preamble(options) if options[:earl] diff --git a/spec/expand_spec.rb b/spec/expand_spec.rb index 2a6d52df..4f09ae4c 100644 --- a/spec/expand_spec.rb +++ b/spec/expand_spec.rb @@ -3798,7 +3798,7 @@ "@id": "ex:bob", "ex:knows": { "@list": [{"@id": "ex:fred"}], - "@annotation": "value2" + "@annotation": {"ex:prop": "value2"} } }), exception: JSON::LD::JsonLdError::InvalidSetOrListObject @@ -3810,7 +3810,7 @@ "@list": [ { "@id": "ex:fred", - "@annotation": "value2" + "@annotation": {"ex:prop": "value2"} } ] } diff --git a/spec/suite_helper.rb b/spec/suite_helper.rb index 509492c1..8d1e846b 100644 --- a/spec/suite_helper.rb +++ b/spec/suite_helper.rb @@ -203,8 +203,8 @@ def run(rspec_example = nil) logger.info "frame: #{frame}" if frame_loc options = self.options - unless options[:specVersion] == "json-ld-1.1" - skip "not a 1.1 test" + if options[:specVersion] == "json-ld-1.0" + skip "1.0 test" return end @@ -225,7 +225,7 @@ def run(rspec_example = nil) JSON::LD::API.frame(input_loc, frame_loc, logger: logger, **options) when "jld:FromRDFTest" # Use an array, to preserve input order - repo = RDF::NQuads::Reader.open(input_loc) do |reader| + repo = RDF::NQuads::Reader.open(input_loc, rdfstar: options[:rdfstar]) do |reader| reader.each_statement.to_a end.to_a.uniq.extend(RDF::Enumerable) logger.info "repo: #{repo.dump(self.id == '#t0012' ? :nquads : :trig)}" @@ -258,7 +258,7 @@ def run(rspec_example = nil) end if evaluationTest? if testType == "jld:ToRDFTest" - expected = RDF::Repository.new << RDF::NQuads::Reader.new(expect, logger: []) + expected = RDF::Repository.new << RDF::NQuads::Reader.new(expect, rdfstar: options[:rdfstar], logger: []) rspec_example.instance_eval { expect(result).to be_equivalent_graph(expected, logger) } @@ -314,7 +314,7 @@ def run(rspec_example = nil) when "jld:FrameTest" JSON::LD::API.frame(t.input_loc, t.frame_loc, logger: logger, **options) when "jld:FromRDFTest" - repo = RDF::Repository.load(t.input_loc) + repo = RDF::Repository.load(t.input_loc, rdfstar: options[:rdfstar]) logger.info "repo: #{repo.dump(t.id == '#t0012' ? :nquads : :trig)}" JSON::LD::API.fromRdf(repo, logger: logger, **options) when "jld:HttpTest" From 24ef57050df88053e439c5a17b8f5ba7c4ca4844 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Tue, 2 Feb 2021 13:59:03 -0800 Subject: [PATCH 17/17] Version 3.1.8. --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 23887f6e..c848fb9c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.7 +3.1.8