diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fda18f8..b69b3821 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,12 +22,11 @@ jobs: runs-on: ubuntu-latest env: CI: true - ALLOW_FAILURES: false ${{ endsWith(matrix.ruby, 'head') }} + ALLOW_FAILURES: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'jruby' }} strategy: fail-fast: false matrix: ruby: - - 2.4 - 2.5 - 2.6 - 2.7 @@ -44,5 +43,9 @@ jobs: - name: Install dependencies run: bundle install --jobs 4 --retry 3 - name: Run tests - run: bundle exec rspec spec || $ALLOW_FAILURES - + run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v1.1.2 + if: "matrix.ruby == '3.0'" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 20af913f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: ruby -script: "bundle exec rspec spec" -env: - global: - - CI=true -rvm: - - 2.4 - - 2.5 - - 2.6 - - 2.7 - - jruby -#cache: bundler -sudo: false -matrix: - allow_failures: - - rvm: jruby diff --git a/Gemfile b/Gemfile index b597cb6f..728aca41 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source "https://rubygems.org" gem "nokogiri", '~> 1.10' -gem "nokogumbo", platforms: :mri gemspec gem 'rdf', git: "https://github.com/ruby-rdf/rdf", branch: "develop" @@ -38,8 +37,8 @@ group :development do end group :development, :test do - gem 'simplecov', platforms: :mri - gem 'coveralls', '~> 0.8', platforms: :mri + gem 'simplecov', '~> 0.21', platforms: :mri + gem 'simplecov-lcov', '~> 0.8', platforms: :mri gem 'psych', platforms: [:mri, :rbx] gem 'benchmark-ips' gem 'rake' diff --git a/README.md b/README.md index ece1fbc8..ec03bc85 100755 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ JSON::LD parses and serializes [JSON-LD][] into [RDF][] and implements expansion JSON::LD can now be used to create a _context_ from an RDFS/OWL definition, and optionally include a JSON-LD representation of the ontology itself. This is currently accessed through the `script/gen_context` script. * If the [jsonlint][] gem is installed, it will be used when validating an input document. -* If available, uses [Nokogiri][] and/or [Nokogumbo][] for parsing HTML, falls back to REXML otherwise. +* If available, uses [Nokogiri][] for parsing HTML, falls back to REXML otherwise. * Provisional support for [JSON-LD-star][JSON-LD-star]. [Implementation Report](https://ruby-rdf.github.io/json-ld/etc/earl.html) diff --git a/VERSION b/VERSION index 7148b0a9..c7a24988 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.9 +3.1.10 diff --git a/lib/json/ld/compact.rb b/lib/json/ld/compact.rb index deee309a..e5fb177d 100644 --- a/lib/json/ld/compact.rb +++ b/lib/json/ld/compact.rb @@ -297,6 +297,7 @@ def compact(element, if index_key == '@index' map_key = expanded_item['@index'] else + index_key = context.expand_iri(index_key, vocab: true) container_key = context.compact_iri(index_key, vocab: true) map_key, *others = Array(compacted_item[container_key]) if map_key.is_a?(String) diff --git a/lib/json/ld/context.rb b/lib/json/ld/context.rb index 67ec566a..013e4fa5 100644 --- a/lib/json/ld/context.rb +++ b/lib/json/ld/context.rb @@ -253,6 +253,7 @@ def parse(local_context, local_context = as_array(local_context) + log_depth do local_context.each do |context| case context when nil,false @@ -266,22 +267,22 @@ def parse(local_context, "Attempt to clear a context with protected terms" end when Context - #log_debug("parse") {"context: #{context.inspect}"} + log_debug("parse") {"context: #{context.inspect}"} result = result.merge(context) when IO, StringIO - #log_debug("parse") {"io: #{context}"} + log_debug("parse") {"io: #{context}"} # Load context document, if it is an open file begin ctx = JSON.load(context) raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil? result = result.parse(ctx["@context"] ? ctx["@context"] : {}) rescue JSON::ParserError => e - #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"} + log_info("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"} raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate] self end when String, RDF::URI - #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"} + log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"} # 3.2.1) Set context to the result of resolving value against the base IRI which is established as specified in section 5.1 Establishing a Base URI of [RFC3986]. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987]. context = RDF::URI(result.context_base || base).join(context) @@ -296,11 +297,11 @@ def parse(local_context, cached_context = if PRELOADED[context_canon.to_s] # If we have a cached context, merge it into the current context (result) and use as the new context - #log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"} + log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"} # If this is a Proc, then replace the entry with the result of running the Proc if PRELOADED[context_canon.to_s].respond_to?(:call) - #log_debug("parse") {"=> (call)"} + log_debug("parse") {"=> (call)"} PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call end PRELOADED[context_canon.to_s] @@ -320,16 +321,17 @@ def parse(local_context, ctx = Context.new(unfrozen: true, **options).dup ctx.context_base = context.to_s ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup) + ctx.context_base = context.to_s # In case it was altered ctx.instance_variable_set(:@base, nil) ctx end rescue JsonLdError::LoadingDocumentFailed => e - #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"} + log_info("parse") {"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"} raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace rescue JsonLdError raise rescue StandardError => e - #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"} + log_info("parse") {"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"} raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace end end @@ -406,6 +408,7 @@ def parse(local_context, raise JsonLdError::InvalidLocalContext, "must be a URL, JSON object or array of same: #{context.inspect}" end end + end result end @@ -475,7 +478,7 @@ def create_term_definition(local_context, term, defined, remote_contexts: [], validate_scoped: true) # Expand a string value, unless it matches a keyword - #log_debug("create_term_definition") {"term = #{term.inspect}"} + log_debug("create_term_definition") {"term = #{term.inspect}"} # If defined contains the key term, then the associated value must be true, indicating that the term definition has already been created, so return. Otherwise, a cyclical term definition has been detected, which is an error. case defined[term] @@ -646,7 +649,7 @@ def create_term_definition(local_context, term, defined, # Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term term end - #log_debug("") {"=> #{definition.id}"} + log_debug("") {"=> #{definition.id}"} elsif term.include?('/') # If term is a relative IRI definition.id = expand_iri(term, vocab: true) @@ -659,7 +662,7 @@ def create_term_definition(local_context, term, defined, # Otherwise, active context must have a vocabulary mapping, otherwise an invalid value has been detected, which is an error. Set the IRI mapping for definition to the result of concatenating the value associated with the vocabulary mapping and term. raise JsonLdError::InvalidIRIMapping, "relative term definition without vocab: #{term} on term #{term.inspect}" unless vocab definition.id = vocab + term - #log_debug("") {"=> #{definition.id}"} + log_debug("") {"=> #{definition.id}"} end @iri_to_term[definition.id] = term if simple_term && definition.id @@ -699,6 +702,7 @@ def create_term_definition(local_context, term, defined, when nil then [nil] else value['@context'] end + log_debug("") {"context: #{definition.context.inspect}"} rescue JsonLdError => e raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}" end @@ -1884,7 +1888,7 @@ def remove_base(base, iri) @base_and_parents ||= begin u = base iri_set = u.to_s.end_with?('/') ? [u.to_s] : [] - iri_set << u.to_s while (u = u.parent) + iri_set << u.to_s while (u != './' && u = u.parent) iri_set end b = base.to_s diff --git a/lib/json/ld/expand.rb b/lib/json/ld/expand.rb index 76b56f6a..39563494 100644 --- a/lib/json/ld/expand.rb +++ b/lib/json/ld/expand.rb @@ -107,7 +107,7 @@ def expand(input, active_property, context, Array(input[tk]).sort.each do |term| term_context = type_scoped_context.term_definitions[term].context if type_scoped_context.term_definitions[term] unless term_context.nil? - log_debug("expand", depth: log_depth.to_i) {"term_context: #{term_context.inspect}"} + log_debug("expand", depth: log_depth.to_i) {"term_context[#{term}]: #{term_context.inspect}"} context = context.parse(term_context, base: @options[:base], propagate: false) end end @@ -258,10 +258,10 @@ def expand_object(input, active_property, context, output_object, expanded_property.to_s.start_with?("_:") && context.processingMode('json-ld-1.1') - #log_debug("expand property", depth: log_depth.to_i) {"ap: #{active_property.inspect}, expanded: #{expanded_property.inspect}, value: #{value.inspect}"} + log_debug("expand property", depth: log_depth.to_i) {"ap: #{active_property.inspect}, expanded: #{expanded_property.inspect}, value: #{value.inspect}"} if expanded_property.nil? - #log_debug(" => ", depth: log_depth.to_i) {"skip nil property"} + log_debug(" => ", depth: log_depth.to_i) {"skip nil property"} next end @@ -341,7 +341,7 @@ def expand_object(input, active_property, context, output_object, Array(output_object['@included']) + included_result when '@type' # If expanded property is @type and value is neither a string nor an array of strings, an invalid type value error has been detected and processing is aborted. Otherwise, set expanded value to the result of using the IRI Expansion algorithm, passing active context, true for vocab, and true for document relative to expand the value or each of its items. - #log_debug("@type", depth: log_depth.to_i) {"value: #{value.inspect}"} + log_debug("@type", depth: log_depth.to_i) {"value: #{value.inspect}"} e_type = case value when Array value.map do |v| @@ -516,7 +516,7 @@ def expand_object(input, active_property, context, output_object, # 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.key?('@reverse') - #log_debug("@reverse", depth: log_depth.to_i) {"double reverse: #{value.inspect}"} + 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. # Append item to the value of the property member of result. @@ -566,7 +566,7 @@ def expand_object(input, active_property, context, output_object, end # Unless expanded value is null, set the expanded property member of result to expanded value. - #log_debug("expand #{expanded_property}", depth: log_depth.to_i) { expanded_value.inspect} + log_debug("expand #{expanded_property}", depth: log_depth.to_i) { expanded_value.inspect} output_object[expanded_property] = expanded_value unless expanded_value.nil? && expanded_property == '@value' && input_type != '@json' next end @@ -688,21 +688,21 @@ def expand_object(input, active_property, context, output_object, # If expanded value is null, ignore key by continuing to the next key from element. if expanded_value.nil? - #log_debug(" => skip nil value", depth: log_depth.to_i) + log_debug(" => skip nil value", depth: log_depth.to_i) next end - #log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"} + log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"} # If the container mapping associated to key in active context is @list and expanded value is not already a list object, convert expanded value to a list object by first setting it to an array containing only expanded value if it is not already an array, and then by setting it to a JSON object containing the key-value pair @list-expanded value. if container.first == '@list' && container.length == 1 && !list?(expanded_value) - #log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"} + log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"} expanded_value = {'@list' => as_array(expanded_value)} end - #log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"} + log_debug(depth: log_depth.to_i) {" => #{expanded_value.inspect}"} # convert expanded value to @graph if container specifies it if container.first == '@graph' && container.length == 1 - #log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"} + log_debug(" => ", depth: log_depth.to_i) { "convert #{expanded_value.inspect} to list"} expanded_value = as_array(expanded_value).map do |v| {'@graph' => as_array(v)} end diff --git a/lib/json/ld/html/nokogiri.rb b/lib/json/ld/html/nokogiri.rb index d5ca7f84..d709ae4a 100644 --- a/lib/json/ld/html/nokogiri.rb +++ b/lib/json/ld/html/nokogiri.rb @@ -136,11 +136,10 @@ def initialize_html_nokogiri(input, options = {}) input else begin - require 'nokogumbo' unless defined?(::Nokogumbo) input = input.read if input.respond_to?(:read) - ::Nokogiri::HTML5(input.dup.force_encoding('utf-8'), max_parse_errors: 1000) - rescue LoadError - ::Nokogiri::HTML.parse(input, 'utf-8') + ::Nokogiri::HTML5(input.force_encoding('utf-8'), max_parse_errors: 1000) + rescue LoadError, NoMethodError + ::Nokogiri::HTML.parse(input, base_uri.to_s, 'utf-8') end end diff --git a/spec/compact_spec.rb b/spec/compact_spec.rb index cedbc071..717ce72c 100644 --- a/spec/compact_spec.rb +++ b/spec/compact_spec.rb @@ -954,6 +954,76 @@ }), processingMode: 'json-ld-1.1' }, + "issue-514": { + input: %({ + "http://example.org/ns/prop": [{ + "@id": "http://example.org/ns/bar", + "http://example.org/ns/name": "bar" + }, { + "@id": "http://example.org/ns/foo", + "http://example.org/ns/name": "foo" + }] + }), + context: %({ + "@context": { + "ex": "http://example.org/ns/", + "prop": { + "@id": "ex:prop", + "@container": "@index", + "@index": "ex:name" + } + } + }), + output: %({ + "@context": { + "ex": "http://example.org/ns/", + "prop": { + "@id": "ex:prop", + "@container": "@index", + "@index": "ex:name" + } + }, + "prop": { + "foo": { "@id": "ex:foo"}, + "bar": { "@id": "ex:bar"} + } + }) + }, + "issue-514b": { + input: %({ + "http://example.org/ns/prop": [{ + "@id": "http://example.org/ns/bar", + "http://example.org/ns/name": "bar" + }, { + "@id": "http://example.org/ns/foo", + "http://example.org/ns/name": "foo" + }] + }), + context: %({ + "@context": { + "ex": "http://example.org/ns/", + "prop": { + "@id": "ex:prop", + "@container": "@index", + "@index": "http://example.org/ns/name" + } + } + }), + output: %({ + "@context": { + "ex": "http://example.org/ns/", + "prop": { + "@id": "ex:prop", + "@container": "@index", + "@index": "http://example.org/ns/name" + } + }, + "prop": { + "foo": { "@id": "ex:foo"}, + "bar": { "@id": "ex:bar"} + } + }) + }, }.each_pair do |title, params| it(title) {run_compact(params)} end diff --git a/spec/context_spec.rb b/spec/context_spec.rb index ae83c984..1cb9dc53 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -181,7 +181,14 @@ def containers before {JSON::LD::Context.instance_variable_set(:@cache, nil)} it "retrieves and parses a remote context document" do expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc) - subject.parse(ctx) + ec = subject.parse(ctx) + expect(ec.send(:mappings)).to produce({ + "xsd" => "http://www.w3.org/2001/XMLSchema#", + "name" => "http://xmlns.com/foaf/0.1/name", + "homepage" => "http://xmlns.com/foaf/0.1/homepage", + "avatar" => "http://xmlns.com/foaf/0.1/avatar", + "integer" => "http://www.w3.org/2001/XMLSchema#integer" + }, logger) end end diff --git a/spec/frame_spec.rb b/spec/frame_spec.rb index d85d2b4f..fdecfb9d 100644 --- a/spec/frame_spec.rb +++ b/spec/frame_spec.rb @@ -992,6 +992,95 @@ end end + context "omitGraph option" do + { + "Defaults to false in 1.0": { + input: %([{ + "http://example.org/prop": [{"@value": "value"}], + "http://example.org/foo": [{"@value": "bar"}] + }]), + frame: %({ + "@context": { + "@vocab": "http://example.org/" + } + }), + output: %({ + "@context": { + "@vocab": "http://example.org/" + }, + "@graph": [{ + "foo": "bar", + "prop": "value" + }] + }), + processingMode: "json-ld-1.0" + }, + "Set with option in 1.0": { + input: %([{ + "http://example.org/prop": [{"@value": "value"}], + "http://example.org/foo": [{"@value": "bar"}] + }]), + frame: %({ + "@context": { + "@vocab": "http://example.org/" + } + }), + output: %({ + "@context": { + "@vocab": "http://example.org/" + }, + "foo": "bar", + "prop": "value" + }), + processingMode: "json-ld-1.0", + omitGraph: true + }, + "Defaults to true in 1.1": { + input: %([{ + "http://example.org/prop": [{"@value": "value"}], + "http://example.org/foo": [{"@value": "bar"}] + }]), + frame: %({ + "@context": { + "@vocab": "http://example.org/" + } + }), + output: %({ + "@context": { + "@vocab": "http://example.org/" + }, + "foo": "bar", + "prop": "value" + }), + processingMode: "json-ld-1.1" + }, + "Set with option in 1.1": { + input: %([{ + "http://example.org/prop": [{"@value": "value"}], + "http://example.org/foo": [{"@value": "bar"}] + }]), + frame: %({ + "@context": { + "@vocab": "http://example.org/" + } + }), + output: %({ + "@context": { + "@vocab": "http://example.org/" + }, + "@graph": [{ + "foo": "bar", + "prop": "value" + }] + }), + processingMode: "json-ld-1.1", + omitGraph: false + }, + }.each do |title, params| + it(title) {do_frame(params.merge(pruneBlankNodeIdentifiers: true))} + end + end + context "@included" do { "Basic Included array": { @@ -2359,15 +2448,16 @@ def do_frame(params) begin - input, frame, output, processingMode = params[:input], params[:frame], params[:output], params.fetch(:processingMode, 'json-ld-1.0') + input, frame, output = params[:input], params[:frame], params[:output] + params = {processingMode: 'json-ld-1.0'}.merge(params) input = ::JSON.parse(input) if input.is_a?(String) frame = ::JSON.parse(frame) if frame.is_a?(String) output = ::JSON.parse(output) if output.is_a?(String) jld = nil if params[:write] - expect{jld = JSON::LD::API.frame(input, frame, logger: logger, processingMode: processingMode)}.to write(params[:write]).to(:error) + expect{jld = JSON::LD::API.frame(input, frame, logger: logger, **params)}.to write(params[:write]).to(:error) else - expect{jld = JSON::LD::API.frame(input, frame, logger: logger, processingMode: processingMode)}.not_to write.to(:error) + expect{jld = JSON::LD::API.frame(input, frame, logger: logger, **params)}.not_to write.to(:error) end expect(jld).to produce_jsonld(output, logger) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2500bc74..541b2ff5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -15,10 +15,16 @@ require 'yaml' begin require 'simplecov' - require 'coveralls' unless ENV['NOCOVERALLS'] + require 'simplecov-lcov' + SimpleCov::Formatter::LcovFormatter.config do |config| + #Coveralls is coverage by default/lcov. Send info results + config.report_with_single_file = true + config.single_report_path = 'coverage/lcov.info' + end + SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, - (Coveralls::SimpleCov::Formatter unless ENV['NOCOVERALLS']) + SimpleCov::Formatter::LcovFormatter ]) SimpleCov.start do add_filter "/spec/"