diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 583871d6..99fd5701 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [2.6, 2.7, '3.0', 3.1, 3.2, ruby-head, jruby] + ruby: ['3.0', 3.1, 3.2, ruby-head, jruby] steps: - name: Clone repository uses: actions/checkout@v3 diff --git a/Gemfile b/Gemfile index 47c82945..1e5c080e 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,6 @@ group :test do gem "rake" gem "equivalent-xml" gem 'fasterer' - gem 'simplecov', '~> 0.21', platforms: :mri + gem 'simplecov', '~> 0.22', platforms: :mri gem 'simplecov-lcov', '~> 0.8', platforms: :mri end diff --git a/README.md b/README.md index 9313d87a..45c66584 100644 --- a/README.md +++ b/README.md @@ -14,26 +14,28 @@ This is a pure-Ruby library for working with [Resource Description Framework 1. [Features](#features) 2. [Differences between RDF 1.0 and RDF 1.1](#differences-between-rdf-1-0-and-rdf-1-1) -3. [Tutorials](#tutorials) -4. [Command Line](#command-line) -5. [Examples](#examples) -6. [Reader/Writer convenience methods](#reader/writer-convenience-methods) -7. [RDF* (RDFStar)](#rdf*-(rdfstar)) -8. [Documentation](#documentation) -9. [Dependencies](#dependencies) -10. [Installation](#installation) -11. [Download](#download) -12. [Resources](#resources) -13. [Mailing List](#mailing-list) -14. [Authors](#authors) -15. [Contributors](#contributors) -16. [Contributing](#contributing) -17. [License](#license) +3. [Differences between RDF 1.1 and RDF 1.2](#differences-between-rdf-1-1-and-rdf-1-2) +4. [Tutorials](#tutorials) +5. [Command Line](#command-line) +6. [Examples](#examples) +7. [Reader/Writer convenience methods](#reader/writer-convenience-methods) +8. [RDF 1.2](#rdf\_12) +9. [Documentation](#documentation) +10. [Dependencies](#dependencies) +11. [Installation](#installation) +12. [Download](#download) +13. [Resources](#resources) +14. [Mailing List](#mailing-list) +15. [Authors](#authors) +16. [Contributors](#contributors) +17. [Contributing](#contributing) +18. [License](#license) ## Features * 100% pure Ruby with minimal dependencies and no bloat. * Fully compatible with [RDF 1.1][] specifications. +* Provisional support for [RDF 1.2][] specifications. * 100% free and unencumbered [public domain](https://unlicense.org/) software. * Provides a clean, well-designed RDF object model and related APIs. * Supports parsing and serializing [N-Triples][] and [N-Quads][] out of the box, with more @@ -45,11 +47,10 @@ This is a pure-Ruby library for working with [Resource Description Framework not modify any of Ruby's core classes or standard library. * Based entirely on Ruby's autoloading, meaning that you can generally make use of any one part of the library without needing to load up the rest. -* Compatible with Ruby Ruby >= 2.4, Rubinius and JRuby 9.0+. - * Note, changes in mapping hashes to keyword arguments for Ruby 2.7+ may require that arguments be passed more explicitly, especially when the first argument is a Hash and there are optional keyword arguments. In this case, Hash argument may need to be explicitly included within `{}` and the optional keyword arguments may need to be specified using `**{}` if there are no keyword arguments. +* Compatible with Ruby Ruby >= 3.0, Rubinius and JRuby 9.0+. + * Note, changes in mapping hashes to keyword arguments for Ruby 3+ may require that arguments be passed more explicitly, especially when the first argument is a Hash and there are optional keyword arguments. In this case, Hash argument may need to be explicitly included within `{}` and the optional keyword arguments may need to be specified using `**{}` if there are no keyword arguments. * Performs auto-detection of input to select appropriate Reader class if one cannot be determined from file characteristics. -* Provisional support for [RDF*][]. ### HTTP requests @@ -102,6 +103,10 @@ the 1.1 release of RDF.rb: Notably, {RDF::Queryable#query} and {RDF::Query#execute} are now completely symmetric; this allows an implementation of {RDF::Queryable} to optimize queries using implementation-specific logic, allowing for substantial performance improvements when executing BGP queries. +## Differences between RDF 1.1 and RDF 1.2 +* {RDF::Literal} has an optional `direction` property for directional language-tagged strings. +* Removes support for legacy `text/plain` (as an alias for `application/n-triples`) and `text/x-nquads` (as an alias for `application/n-quads`) + ## Tutorials * [Getting data from the Semantic Web using Ruby and RDF.rb](https://semanticweb.org/wiki/Getting_data_from_the_Semantic_Web_%28Ruby%29) @@ -260,15 +265,16 @@ A separate [SPARQL][SPARQL doc] gem builds on basic BGP support to provide full foaf[:name] #=> RDF::URI("http://xmlns.com/foaf/0.1/name") foaf['mbox'] #=> RDF::URI("http://xmlns.com/foaf/0.1/mbox") -## RDF* (RDFStar) +## RDF 1.2 -[RDF.rb][] includes provisional support for [RDF*][] with an N-Triples/N-Quads syntax extension that uses inline statements in the _subject_ or _object_ position. +[RDF.rb][] includes provisional support for [RDF 1.2][] with an N-Triples/N-Quads syntax for quoted triples in the _subject_ or _object_ position. +[RDF.rb][] includes provisional support for [RDF 1.2][] directional language-tagged strings, which are literals of type `rdf:dirLangString` having both a `language` and `direction`. 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`. **Note: This feature is subject to change or elimination as the standards process progresses.** -### Serializing a Graph containing embedded statements +### Serializing a Graph containing quoted triples require 'rdf/ntriples' statement = RDF::Statement(RDF::URI('bob'), RDF::Vocab::FOAF.age, RDF::Literal(23)) @@ -276,7 +282,7 @@ Internally, an `RDF::Statement` is treated as another resource, along with `RDF: graph.dump(:ntriples, validate: false) # => '<< "23"^^>> "0.9"^^ .' -### Reading a Graph containing embedded statements +### Reading a Graph containing quoted triples By default, the N-Triples reader will reject a document containing a subject resource. @@ -286,13 +292,6 @@ By default, the N-Triples reader will reject a document containing a subject res end # => RDF::ReaderError -Readers support a boolean valued `rdfstar` option. - - graph = RDF::Graph.new do |graph| - RDF::NTriples::Reader.new(nt, rdfstar: true) {|reader| graph << reader} - end - graph.count #=> 1 - ## Documentation @@ -398,8 +397,9 @@ from BNode identity (i.e., they each entail the other) ## Dependencies -* [Ruby](https://ruby-lang.org/) (>= 2.6) +* [Ruby](https://ruby-lang.org/) (>= 3.0) * [LinkHeader][] (>= 0.0.8) +* [bcp47_spec][] ( ~> 0.2) * Soft dependency on [RestClient][] (>= 2.1) ## Installation @@ -407,7 +407,7 @@ from BNode identity (i.e., they each entail the other) The recommended installation method is via [RubyGems](https://rubygems.org/). To install the latest official release of RDF.rb, do: - % [sudo] gem install rdf # Ruby 2.6+ + % [sudo] gem install rdf # Ruby 3+ ## Download @@ -481,8 +481,10 @@ This is free and unencumbered public domain software. For more information, see or the accompanying {file:UNLICENSE} file. [RDF]: https://www.w3.org/RDF/ -[N-Triples]: https://www.w3.org/TR/n-triples/ -[N-Quads]: https://www.w3.org/TR/n-quads/ +[LinkHeader]: https://github.com/asplake/link_header +[bcp47_spec]: https://github.com/dadah89/bcp47_spec +[N-Triples]: https://www.w3.org/TR/rdf-n-triples/ +[N-Quads]: https://www.w3.org/TR/rdf-n-quads/ [YARD]: https://yardoc.org/ [YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md [PDD]: https://unlicense.org/#unlicensing-contributions @@ -496,6 +498,7 @@ see or the accompanying {file:UNLICENSE} file. [SPARQL doc]: https://ruby-rdf.github.io/sparql [RDF 1.0]: https://www.w3.org/TR/2004/REC-rdf-concepts-20040210/ [RDF 1.1]: https://www.w3.org/TR/rdf11-concepts/ +[RDF 1.2]: https://www.w3.org/TR/rdf12-concepts/ [SPARQL 1.1]: https://www.w3.org/TR/sparql11-query/ [RDF.rb]: https://ruby-rdf.github.io/ [RDF::DO]: https://ruby-rdf.github.io/rdf-do @@ -510,7 +513,6 @@ see or the accompanying {file:UNLICENSE} file. [RDF::TriX]: https://ruby-rdf.github.io/rdf-trix [RDF::Turtle]: https://ruby-rdf.github.io/rdf-turtle [RDF::Raptor]: https://ruby-rdf.github.io/rdf-raptor -[RDF*]: https://w3c.github.io/rdf-star/rdf-star-cg-spec.html [LinkedData]: https://ruby-rdf.github.io/linkeddata [JSON::LD]: https://ruby-rdf.github.io/json-ld [RestClient]: https://rubygems.org/gems/rest-client diff --git a/VERSION b/VERSION index 17ce9180..15a27998 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.11 +3.3.0 diff --git a/etc/n-triples-star.ebnf b/etc/n-triples-star.ebnf deleted file mode 100644 index c788ba99..00000000 --- a/etc/n-triples-star.ebnf +++ /dev/null @@ -1,7 +0,0 @@ -[1] ntriplesDoc ::= triple? (EOL triple)* EOL? -[2] triple ::= subject predicate object '.' -[3] subject ::= IRIREF | BLANK_NODE_LABEL | quotedTriple -[4] predicate ::= IRIREF -[5] object ::= IRIREF | BLANK_NODE_LABEL | literal | quotedTriple -[6] literal ::= STRING_LITERAL_QUOTE ('^^' IRIREF | LANGTAG)? -[7] quotedTriple ::= '<<' subject predicate object '>>' diff --git a/etc/n-triples.ebnf b/etc/n-triples.ebnf index 1b413c43..4471ec11 100644 --- a/etc/n-triples.ebnf +++ b/etc/n-triples.ebnf @@ -1,6 +1,40 @@ -[1] ntriplesDoc ::= triple? (EOL triple)* EOL? -[2] triple ::= subject predicate object '.' -[3] subject ::= IRIREF | BLANK_NODE_LABEL -[4] predicate ::= IRIREF -[5] object ::= IRIREF | BLANK_NODE_LABEL | literal -[6] literal ::= STRING_LITERAL_QUOTE ('^^' IRIREF | LANGTAG)? +ntriplesDoc ::= triple? (EOL triple)* EOL? +triple ::= subject predicate object '.' +subject ::= IRIREF | BLANK_NODE_LABEL | quotedTriple +predicate ::= IRIREF +object ::= IRIREF | BLANK_NODE_LABEL | literal | quotedTriple +literal ::= STRING_LITERAL_QUOTE ('^^' IRIREF | LANG_DIR )? +quotedTriple ::= '<<' subject predicate object '>>' + +@terminals + +IRIREF ::= '<' ([^#x00-#x20<>"{}|^`\] | UCHAR)* '>' +BLANK_NODE_LABEL ::= '_:' ( PN_CHARS_U | [0-9] ) ((PN_CHARS|'.')* PN_CHARS)? +LANG_DIR ::= "@" [a-zA-Z]+ ( "-" [a-zA-Z0-9]+ )* ('--' [a-zA-Z]+)?` +STRING_LITERAL_QUOTE ::= '"' ( [^#x22#x5C#xA#xD] | ECHAR | UCHAR )* '"' +UCHAR ::= ( "\u" HEX HEX HEX HEX ) + | ( "\U" HEX HEX HEX HEX HEX HEX HEX HEX ) +ECHAR ::= ("\" [tbnrf"']) +PN_CHARS_BASE ::= ([A-Z] + | [a-z] + | [#x00C0-#x00D6] + | [#x00D8-#x00F6] + | [#x00F8-#x02FF] + | [#x0370-#x037D] + | [#x037F-#x1FFF] + | [#x200C-#x200D] + | [#x2070-#x218F] + | [#x2C00-#x2FEF] + | [#x3001-#xD7FF] + | [#xF900-#xFDCF] + | [#xFDF0-#xFFFD] + | [#x10000-#xEFFFF]) +PN_CHARS_U ::= PN_CHARS_BASE | '_' +PN_CHARS ::= (PN_CHARS_U + | "-" + | [0-9] + | #x00B7 + | [#x0300-#x036F] + | [#x203F-#x2040]) +HEX ::= ([0-9] | [A-F] | [a-f]) +EOL ::= [#xD#xA]+ diff --git a/lib/rdf/cli.rb b/lib/rdf/cli.rb index 0606c9e9..6e1574fc 100644 --- a/lib/rdf/cli.rb +++ b/lib/rdf/cli.rb @@ -60,7 +60,7 @@ module RDF # RDF::CLI::Option.new( # symbol: :canonicalize, # on: ["--canonicalize"], - # description: "Canonicalize input/output.") {true}, + # description: "Canonicalize URI/literal forms.") {true}, # RDF::CLI::Option.new( # symbol: :uri, # on: ["--uri STRING"], diff --git a/lib/rdf/mixin/enumerable.rb b/lib/rdf/mixin/enumerable.rb index a598af27..7400ffea 100644 --- a/lib/rdf/mixin/enumerable.rb +++ b/lib/rdf/mixin/enumerable.rb @@ -83,7 +83,8 @@ def to_a # * `:literal_equality' preserves [term-equality](https://www.w3.org/TR/rdf11-concepts/#dfn-literal-term-equality) for literals. Literals are equal only if their lexical values and datatypes are equal, character by character. Literals may be "inlined" to value-space for efficiency only if `:literal_equality` is `false`. # * `:validity` allows a concrete Enumerable implementation to indicate that it does or does not support valididty checking. By default implementations are assumed to support validity checking. # * `:skolemize` supports [Skolemization](https://www.w3.org/wiki/BnodeSkolemization) of an `Enumerable`. Implementations supporting this feature must implement a `#skolemize` method, taking a base URI used for minting URIs for BNodes as stable identifiers and a `#deskolemize` method, also taking a base URI used for turning URIs having that prefix back into the same BNodes which were originally skolemized. - # * `:rdfstar` supports RDF* where statements may be subjects or objects of other statements. + # * `:quoted_triples` supports RDF 1.2 quoted triples. + # * `:base_direction` supports RDF 1.2 directional language-tagged strings. # # @param [Symbol, #to_sym] feature # @return [Boolean] @@ -720,13 +721,33 @@ def enum_graph end alias_method :enum_graphs, :enum_graph + ## + # Enumerates each statement using its canonical representation. + # + # @note This is updated by `RDF::Normalize` to also canonicalize blank nodes. + # + # @return [RDF::Enumerable] + def canonicalize + this = self + Enumerable::Enumerator.new do |yielder| + this.send(:each_statement) {|y| yielder << y.canonicalize} + end + end + + ## + # Mutating canonicalization not supported + # + # @raise NotImplementedError + def canonicalize! + raise NotImplementedError, "Canonicalizing enumerables not supported" + end + ## # Returns all RDF statements in `self` as an array. # # Mixes in `RDF::Enumerable` into the returned object. # # @return [Array] - # @since 0.2.0 def to_a super.extend(RDF::Enumerable) end diff --git a/lib/rdf/mixin/queryable.rb b/lib/rdf/mixin/queryable.rb index 42438823..82bee7b1 100644 --- a/lib/rdf/mixin/queryable.rb +++ b/lib/rdf/mixin/queryable.rb @@ -140,7 +140,7 @@ def query_execute(query, **options, &block) # method in order to provide for storage-specific optimized triple # pattern matching. # - # ## RDFStar (RDF*) + # ## RDF-star # # Statements may have embedded statements as either a subject or object, recursively. # diff --git a/lib/rdf/mixin/writable.rb b/lib/rdf/mixin/writable.rb index fcf1b992..987958e2 100644 --- a/lib/rdf/mixin/writable.rb +++ b/lib/rdf/mixin/writable.rb @@ -127,8 +127,11 @@ def insert_graph(graph) def insert_statements(statements) each = statements.respond_to?(:each_statement) ? :each_statement : :each statements.__send__(each) do |statement| - if statement.embedded? && respond_to?(:supports?) && !supports?(:rdfstar) - raise ArgumentError, "Wriable does not support embedded statements" + if statement.embedded? && respond_to?(:supports?) && !supports?(:quoted_triples) + raise ArgumentError, "Writable does not support quoted triples" + end + if statement.object && statement.object.literal? && statement.object.direction? && !supports?(:base_direction) + raise ArgumentError, "Writable does not support directional languaged-tagged strings" end insert_statement(statement) end diff --git a/lib/rdf/model/dataset.rb b/lib/rdf/model/dataset.rb index 27ec4d2e..f40f3c36 100644 --- a/lib/rdf/model/dataset.rb +++ b/lib/rdf/model/dataset.rb @@ -104,7 +104,7 @@ def isolation_level # @private # @see RDF::Enumerable#supports? def supports?(feature) - return true if %i(graph_name rdfstar).include?(feature) + return true if %i(graph_name quoted_triples).include?(feature) super end diff --git a/lib/rdf/model/graph.rb b/lib/rdf/model/graph.rb index cdb8b39e..e45f24ad 100644 --- a/lib/rdf/model/graph.rb +++ b/lib/rdf/model/graph.rb @@ -305,8 +305,11 @@ def query_pattern(pattern, **options, &block) # @private # @see RDF::Mutable#insert def insert_statement(statement) - if statement.embedded? && !@data.supports?(:rdfstar) - raise ArgumentError, "Graph does not support embedded statements" + if statement.embedded? && !@data.supports?(:quoted_triples) + raise ArgumentError, "Graph does not support quoted triples" + end + if statement.object && statement.object.literal? && statement.object.direction? && !@data.supports?(:base_direction) + raise ArgumentError, "Graph does not support directional languaged-tagged strings" end statement = statement.dup statement.graph_name = graph_name diff --git a/lib/rdf/model/literal.rb b/lib/rdf/model/literal.rb index 9760afd6..28e06f04 100644 --- a/lib/rdf/model/literal.rb +++ b/lib/rdf/model/literal.rb @@ -1,4 +1,7 @@ # -*- encoding: utf-8 -*- + +require 'bcp47_spec' + module RDF ## # An RDF literal. @@ -9,7 +12,9 @@ module RDF # # Specific typed literals may have behavior different from the default implementation. See the following defined sub-classes for specific documentation. Additional sub-classes may be defined, and will interoperate by defining `DATATYPE` and `GRAMMAR` constants, in addition other required overrides of RDF::Literal behavior. # - # In RDF 1.1, all literals are typed, including plain literals and language tagged literals. Internally, plain literals are given the `xsd:string` datatype and language tagged literals are given the `rdf:langString` datatype. Creating a plain literal, without a datatype or language, will automatically provide the `xsd:string` datatype; similar for language tagged literals. Note that most serialization formats will remove this datatype. Code which depends on a literal having the `xsd:string` datatype being different from a plain literal (formally, without a datatype) may break. However note that the `#has\_datatype?` will continue to return `false` for plain or language-tagged literals. + # In RDF 1.1, all literals are typed, including plain literals and language-tagged strings. Internally, plain literals are given the `xsd:string` datatype and language-tagged strings are given the `rdf:langString` datatype. Creating a plain literal, without a datatype or language, will automatically provide the `xsd:string` datatype; similar for language-tagged strings. Note that most serialization formats will remove this datatype. Code which depends on a literal having the `xsd:string` datatype being different from a plain literal (formally, without a datatype) may break. However note that the `#has\_datatype?` will continue to return `false` for plain or language-tagged strings. + # + # RDF 1.2 adds **directional language-tagged strings** which are effectively a subclass of **language-tagged strings** contining an additional **direction** component with value either **ltr** or **rtl** for Left-to-Right or Right-to-Left. This determines the general direction of a string when presented in n a user agent, where it might be in conflict with the inherent direction of the leading Unicode code points. Directional language-tagged strings are given the `rdf:langString` datatype. # # * {RDF::Literal::Boolean} # * {RDF::Literal::Date} @@ -23,16 +28,23 @@ module RDF # value = RDF::Literal.new("Hello, world!") # value.plain? #=> true` # - # @example Creating a language-tagged literal (1) + # @example Creating a language-tagged string (1) # value = RDF::Literal.new("Hello!", language: :en) # value.language? #=> true # value.language #=> :en # - # @example Creating a language-tagged literal (2) + # @example Creating a language-tagged string (2) # RDF::Literal.new("Wazup?", language: :"en-US") # RDF::Literal.new("Hej!", language: :sv) # RDF::Literal.new("¡Hola!", language: :es) # + # @example Creating a directional language-tagged string + # value = RDF::Literal.new("Hello!", language: :en, direction: :ltr) + # value.language? #=> true + # value.language #=> :en + # value.direction? #=> true + # value.direction #=> :ltr + # # @example Creating an explicitly datatyped literal # value = RDF::Literal.new("2009-12-31", datatype: RDF::XSD.date) # value.datatype? #=> true @@ -105,8 +117,14 @@ def self.datatyped_class(uri) ## # @private - def self.new(value, language: nil, datatype: nil, lexical: nil, validate: false, canonicalize: false, **options) - raise ArgumentError, "datatype with language must be rdf:langString" if language && (datatype || RDF.langString).to_s != RDF.langString.to_s + def self.new(value, language: nil, datatype: nil, direction: nil, lexical: nil, validate: false, canonicalize: false, **options) + if language && direction + raise ArgumentError, "datatype with language and direction must be rdf:dirLangString" if (datatype || RDF.dirLangString).to_s != RDF.dirLangString.to_s + elsif language + raise ArgumentError, "datatype with language must be rdf:langString" if (datatype || RDF.langString).to_s != RDF.langString.to_s + else + raise ArgumentError, "datatype not compatible with language or direction" if language || direction + end klass = case when !self.equal?(RDF::Literal) @@ -128,7 +146,7 @@ def self.new(value, language: nil, datatype: nil, lexical: nil, validate: false, end end literal = klass.allocate - literal.send(:initialize, value, language: language, datatype: datatype, **options) + literal.send(:initialize, value, language: language, datatype: datatype, direction: direction, **options) literal.validate! if validate literal.canonicalize! if canonicalize literal @@ -137,18 +155,24 @@ def self.new(value, language: nil, datatype: nil, lexical: nil, validate: false, TRUE = RDF::Literal.new(true) FALSE = RDF::Literal.new(false) ZERO = RDF::Literal.new(0) + XSD_STRING = RDF::URI("http://www.w3.org/2001/XMLSchema#string") - # @return [Symbol] The language tag (optional). + # @return [Symbol] The language-tag (optional). Implies `datatype` is `rdf:langString`. attr_accessor :language + # @return [Symbol] The base direction (optional). Implies `datatype` is `rdf:dirLangString`. + attr_accessor :direction + # @return [URI] The XML Schema datatype URI (optional). attr_accessor :datatype ## - # Literals without a datatype are given either xsd:string or rdf:langString - # depending on if there is language + # Literals without a datatype are given either `xsd:string`, `rdf:langString`, or `rdf:dirLangString`, + # depending on if there is `language` and/or `direction`. # # @param [Object] value + # @param [Symbol] direction (nil) + # Initial text direction. # @param [Symbol] language (nil) # Language is downcased to ensure proper matching # @param [String] lexical (nil) @@ -163,16 +187,24 @@ def self.new(value, language: nil, datatype: nil, lexical: nil, validate: false, # @see http://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal # @see http://www.w3.org/TR/rdf11-concepts/#section-Datatypes # @see #to_s - def initialize(value, language: nil, datatype: nil, lexical: nil, validate: false, canonicalize: false, **options) + def initialize(value, language: nil, datatype: nil, direction: nil, lexical: nil, validate: false, canonicalize: false, **options) @object = value.freeze @string = lexical if lexical @string = value if !defined?(@string) && value.is_a?(String) @string = @string.encode(Encoding::UTF_8).freeze if instance_variable_defined?(:@string) @object = @string if instance_variable_defined?(:@string) && @object.is_a?(String) @language = language.to_s.downcase.to_sym if language + @direction = direction.to_s.downcase.to_sym if direction @datatype = RDF::URI(datatype).freeze if datatype @datatype ||= self.class.const_get(:DATATYPE) if self.class.const_defined?(:DATATYPE) - @datatype ||= instance_variable_defined?(:@language) && @language ? RDF.langString : RDF::URI("http://www.w3.org/2001/XMLSchema#string") + @datatype ||= if instance_variable_defined?(:@language) && @language && + instance_variable_defined?(:@direction) && @direction + RDF.dirLangString + elsif instance_variable_defined?(:@language) && @language + RDF.langString + else + XSD_STRING + end end ## @@ -202,8 +234,8 @@ def literal? # # Compatibility of two arguments is defined as: # * The arguments are simple literals or literals typed as xsd:string - # * The arguments are plain literals with identical language tags - # * The first argument is a plain literal with language tag and the second argument is a simple literal or literal typed as xsd:string + # * The arguments are plain literals with identical language-tags and directions + # * The first argument is a plain literal with language-tag and the second argument is a simple literal or literal typed as xsd:string # # @example # compatible?("abc" "b") #=> true @@ -224,11 +256,11 @@ def compatible?(other) return false unless other.literal? && plain? && other.plain? # * The arguments are simple literals or literals typed as xsd:string - # * The arguments are plain literals with identical language tags - # * The first argument is a plain literal with language tag and the second argument is a simple literal or literal typed as xsd:string - language? ? - (language == other.language || other.datatype == RDF::URI("http://www.w3.org/2001/XMLSchema#string")) : - other.datatype == RDF::URI("http://www.w3.org/2001/XMLSchema#string") + # * The arguments are plain literals with identical language-tags + # * The first argument is a plain literal with language-tag and the second argument is a simple literal or literal typed as xsd:string + language? || direction? ? + (language == other.language && direction == other.direction || other.datatype == XSD_STRING) : + other.datatype == XSD_STRING end ## @@ -236,7 +268,7 @@ def compatible?(other) # # @return [Integer] def hash - @hash ||= [to_s, datatype, language].hash + @hash ||= [to_s, datatype, language, direction].compact.hash end @@ -270,6 +302,7 @@ def eql?(other) self.value_hash == other.value_hash && self.value.eql?(other.value) && self.language.to_s.eql?(other.language.to_s) && + self.direction.to_s.eql?(other.direction.to_s) && self.datatype.eql?(other.datatype)) end @@ -290,7 +323,10 @@ def ==(other) case when self.eql?(other) true - when self.language? && self.language.to_s == other.language.to_s + when self.direction? && self.direction == other.direction + # Literals with directions can compare if languages and directions are identical + self.value_hash == other.value_hash && self.value == other.value + when self.language? && self.language == other.language # Literals with languages can compare if languages are identical self.value_hash == other.value_hash && self.value == other.value when self.simple? && other.simple? @@ -342,14 +378,18 @@ def <=>(other) ## # Returns `true` if this is a plain literal. A plain literal - # may have a language, but may not have a datatype. For + # may have a language and direction, but may not have a datatype. For # all practical purposes, this includes xsd:string literals # too. # # @return [Boolean] `true` or `false` # @see http://www.w3.org/TR/rdf-concepts/#dfn-plain-literal def plain? - [RDF.langString, RDF::URI("http://www.w3.org/2001/XMLSchema#string")].include?(datatype) + [ + RDF.langString, + RDF.dirLangString, + XSD_STRING + ].include?(datatype) end ## @@ -359,19 +399,28 @@ def plain? # @return [Boolean] `true` or `false` # @see http://www.w3.org/TR/sparql11-query/#simple_literal def simple? - datatype == RDF::URI("http://www.w3.org/2001/XMLSchema#string") + datatype == XSD_STRING end ## - # Returns `true` if this is a language-tagged literal. + # Returns `true` if this is a language-tagged string. # # @return [Boolean] `true` or `false` - # @see http://www.w3.org/TR/rdf-concepts/#dfn-plain-literal + # @see https://www.w3.org/TR/rdf-concepts/#dfn-language-tagged-string def language? - datatype == RDF.langString + [RDF.langString, RDF.dirLangString].include?(datatype) end alias_method :has_language?, :language? + ## + # Returns `true` if this is a directional language-tagged string. + # + # @return [Boolean] `true` or `false` + # @see https://www.w3.org/TR/rdf-concepts/#dfn-dir-lang-string + def direction? + datatype == RDF.dirLangString + end + ## # Returns `true` if this is a datatyped literal. # @@ -380,7 +429,7 @@ def language? # @return [Boolean] `true` or `false` # @see http://www.w3.org/TR/rdf-concepts/#dfn-typed-literal def datatype? - !plain? && !language? + !plain? && !language? && !direction? end alias_method :has_datatype?, :datatype? alias_method :typed?, :datatype? @@ -393,10 +442,13 @@ def datatype? # @return [Boolean] `true` or `false` # @since 0.2.1 def valid? - return false if language? && language.to_s !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/ + BCP47.parse(language.to_s) if language? + return false if direction? && !%i{ltr rtl}.include?(direction) return false if datatype? && datatype.invalid? grammar = self.class.const_get(:GRAMMAR) rescue nil grammar.nil? || value.match?(grammar) + rescue BCP47::InvalidLanguageTag + false end ## @@ -536,12 +588,12 @@ def inspect ## # @overload #to_str - # This method is implemented when the datatype is `xsd:string` or `rdf:langString` + # This method is implemented when the datatype is `xsd:string`, `rdf:langString`, or `rdf:dirLangString` # @return [String] def method_missing(name, *args) case name when :to_str - return to_s if @datatype == RDF.langString || @datatype == RDF::URI("http://www.w3.org/2001/XMLSchema#string") + return to_s if [RDF.langString, RDF.dirLangString, XSD_STRING].include?(@datatype) end super end @@ -549,7 +601,7 @@ def method_missing(name, *args) def respond_to_missing?(name, include_private = false) case name when :to_str - return true if @datatype == RDF.langString || @datatype == RDF::URI("http://www.w3.org/2001/XMLSchema#string") + return true if [RDF.langString, RDF.dirLangString, XSD_STRING].include?(@datatype) end super end diff --git a/lib/rdf/model/literal/decimal.rb b/lib/rdf/model/literal/decimal.rb index a32ebcd8..653c201b 100644 --- a/lib/rdf/model/literal/decimal.rb +++ b/lib/rdf/model/literal/decimal.rb @@ -26,7 +26,7 @@ def initialize(value, datatype: nil, lexical: nil, **options) when value.is_a?(::Numeric) then BigDecimal(value) else value = value.to_s - value += "0" if value.end_with?(".") # Normalization required in Ruby 2.4 + value += "0" if value.end_with?(".") BigDecimal(value) rescue BigDecimal(0) end end diff --git a/lib/rdf/model/statement.rb b/lib/rdf/model/statement.rb index a1902bae..91a0bbd6 100644 --- a/lib/rdf/model/statement.rb +++ b/lib/rdf/model/statement.rb @@ -182,6 +182,7 @@ def variable?(*args) ## # Returns `true` if any element of the statement is, itself, a statement. # + # Note: Nomenclature is evolving, alternatives could include `#complex?` and `#nested?` # @return [Boolean] def embedded? subject && subject.statement? || object && object.statement? @@ -410,7 +411,7 @@ def terms end ## - # Canonicalizes each unfrozen term in the statement + # Canonicalizes each unfrozen term in the statement. # # @return [RDF::Statement] `self` # @since 1.0.8 @@ -436,6 +437,18 @@ def canonicalize nil end + # New statement with duplicated components (other than blank nodes) + # + # @return [RDF::Statement] + def dup + options = Hash[@options] + options[:subject] = subject.is_a?(RDF::Node) ? subject : subject.dup + options[:predicate] = predicate.dup + options[:object] = object.is_a?(RDF::Node) ? object : object.dup + options[:graph_name] = graph_name.is_a?(RDF::Node) ? graph_name : graph_name.dup if graph_name + RDF::Statement.new(options) + end + ## # Returns the terms of this statement as a `Hash`. # diff --git a/lib/rdf/nquads.rb b/lib/rdf/nquads.rb index dc142716..272b26c0 100644 --- a/lib/rdf/nquads.rb +++ b/lib/rdf/nquads.rb @@ -22,7 +22,6 @@ module NQuads class Format < RDF::Format content_type 'application/n-quads', extension: :nq, - alias: 'text/x-nquads;q=0.2', uri: RDF::URI("http://www.w3.org/ns/formats/N-Quads") content_encoding 'utf-8' diff --git a/lib/rdf/ntriples.rb b/lib/rdf/ntriples.rb index ba6198fa..f5e526ae 100644 --- a/lib/rdf/ntriples.rb +++ b/lib/rdf/ntriples.rb @@ -8,14 +8,14 @@ module RDF # [Turtle](http://www.w3.org/TeamSubmission/turtle/) and # [Notation3](http://www.w3.org/TeamSubmission/n3/) (N3). # - # The MIME content type for N-Triples files is `text/plain` and the + # The MIME content type for N-Triples files is `application/n-triples` and the # recommended file extension is `.nt`. # # An example of an RDF statement in N-Triples format: # # "rdf" . # - # ## RDFStar (RDF*) + # ## Quoted Triples # # Supports statements as resources using `<>`. # diff --git a/lib/rdf/ntriples/format.rb b/lib/rdf/ntriples/format.rb index afafab8a..b9917a1d 100644 --- a/lib/rdf/ntriples/format.rb +++ b/lib/rdf/ntriples/format.rb @@ -18,7 +18,6 @@ module RDF::NTriples class Format < RDF::Format content_type 'application/n-triples', extension: :nt, - alias: 'text/plain;q=0.2', uri: RDF::URI("http://www.w3.org/ns/formats/N-Triples") content_encoding 'utf-8' diff --git a/lib/rdf/ntriples/reader.rb b/lib/rdf/ntriples/reader.rb index 03e0eb29..832f8942 100644 --- a/lib/rdf/ntriples/reader.rb +++ b/lib/rdf/ntriples/reader.rb @@ -28,7 +28,7 @@ module RDF::NTriples # end # end # - # ** RDFStar (RDF*) + # ** RDF=star # # Supports statements as resources using `<>`. # @@ -60,24 +60,16 @@ class Reader < RDF::Reader U_CHARS2 = Regexp.compile("\\u00B7|[\\u0300-\\u036F]|[\\u203F-\\u2040]").freeze IRI_RANGE = Regexp.compile("[[^<>\"{}\|\^`\\\\]&&[^\\x00-\\x20]]").freeze - # 163s PN_CHARS_BASE = /[A-Z]|[a-z]|#{U_CHARS1}/.freeze - # 164s PN_CHARS_U = /_|#{PN_CHARS_BASE}/.freeze - # 166s PN_CHARS = /-|[0-9]|#{PN_CHARS_U}|#{U_CHARS2}/.freeze - # 159s ECHAR = /\\[tbnrf"'\\]/.freeze - # 18 + IRIREF = /<((?:#{IRI_RANGE}|#{UCHAR})*)>/.freeze - # 141s BLANK_NODE_LABEL = /_:((?:[0-9]|#{PN_CHARS_U})(?:(?:#{PN_CHARS}|\.)*#{PN_CHARS})?)/.freeze - # 144s - LANGTAG = /@([a-zA-Z]+(?:-[a-zA-Z0-9]+)*)/.freeze - # 22 + LANG_DIR = /@([a-zA-Z]+(?:-[a-zA-Z0-9]+)*(?:--[a-zA-Z]+)?)/.freeze STRING_LITERAL_QUOTE = /"((?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*)"/.freeze - # RDF* ST_START = /^<>/.freeze @@ -86,7 +78,7 @@ class Reader < RDF::Reader NODEID = /^#{BLANK_NODE_LABEL}/.freeze URIREF = /^#{IRIREF}/.freeze LITERAL_PLAIN = /^#{STRING_LITERAL_QUOTE}/.freeze - LITERAL_WITH_LANGUAGE = /^#{STRING_LITERAL_QUOTE}#{LANGTAG}/.freeze + LITERAL_WITH_LANGUAGE = /^#{STRING_LITERAL_QUOTE}#{LANG_DIR}/.freeze LITERAL_WITH_DATATYPE = /^#{STRING_LITERAL_QUOTE}\^\^#{IRIREF}/.freeze DATATYPE_URI = /^\^\^#{IRIREF}/.freeze LITERAL = Regexp.union(LITERAL_WITH_LANGUAGE, LITERAL_WITH_DATATYPE, LITERAL_PLAIN).freeze @@ -95,6 +87,9 @@ class Reader < RDF::Reader OBJECT = Regexp.union(URIREF, NODEID, LITERAL).freeze END_OF_STATEMENT = /^\s*\.\s*(?:#.*)?$/.freeze + # LANGTAG is deprecated + LANGTAG = LANG_DIR + ## # Reconstructs an RDF value from its serialized N-Triples # representation. @@ -299,8 +294,10 @@ def read_literal if literal_str = match(LITERAL_PLAIN) literal_str = self.class.unescape(literal_str) literal = case - when language = match(LANGTAG) - RDF::Literal.new(literal_str, language: language) + when lang_dir = match(LANG_DIR) + language, direction = lang_dir.split('--') + raise ArgumentError if direction && !@options[:rdfstar] + RDF::Literal.new(literal_str, language: language, direction: direction) when datatype = match(/^(\^\^)/) # FIXME RDF::Literal.new(literal_str, datatype: read_uriref || fail_object) else @@ -310,6 +307,10 @@ def read_literal literal.canonicalize! if canonicalize? literal end + rescue ArgumentError + v = literal_str + v += "@#{lang_dir}" if lang_dir + log_error("Invalid Literal (found: \"#{v}\")", lineno: lineno, token: "#v", exception: RDF::ReaderError) end ## diff --git a/lib/rdf/ntriples/writer.rb b/lib/rdf/ntriples/writer.rb index c155bd74..3a1513a7 100644 --- a/lib/rdf/ntriples/writer.rb +++ b/lib/rdf/ntriples/writer.rb @@ -224,7 +224,7 @@ def format_statement(statement, **options) end ## - # Returns the N-Triples representation of an RDF* reified statement. + # Returns the N-Triples representation of an RDF-star quoted triple. # # @param [RDF::Statement] statement # @param [Hash{Symbol => Object}] options ({}) @@ -312,6 +312,7 @@ def format_literal(literal, **options) # Note, escaping here is more robust than in Term text = quoted(escaped(literal.value)) text << "@#{literal.language}" if literal.language? + text << "--#{literal.direction}" if literal.direction? text << "^^<#{uri_for(literal.datatype)}>" if literal.datatype? text else diff --git a/lib/rdf/query/solution.rb b/lib/rdf/query/solution.rb index d1d071b4..f8e83bf6 100644 --- a/lib/rdf/query/solution.rb +++ b/lib/rdf/query/solution.rb @@ -209,7 +209,7 @@ def []=(name, value) # Merges the bindings from the given `other` query solution into this # one, overwriting any existing ones having the same name. # - # ## RDFStar (RDF*) + # ## RDF-star # # If merging a binding for a statement to a pattern, # merge their embedded solutions. diff --git a/lib/rdf/reader.rb b/lib/rdf/reader.rb index 0fd0911e..0f42867b 100644 --- a/lib/rdf/reader.rb +++ b/lib/rdf/reader.rb @@ -133,7 +133,7 @@ def self.options on: ["--canonicalize"], control: :checkbox, default: false, - description: "Canonicalize input/output.") {true}, + description: "Canonicalize URI/literal forms") {true}, RDF::CLI::Option.new( symbol: :encoding, datatype: Encoding, @@ -163,7 +163,7 @@ def self.options datatype: TrueClass, control: :checkbox, on: ["--rdfstar"], - description: "Parse RDF*."), + description: "Parse RDF-star for preliminary RDF 1.2 support."), RDF::CLI::Option.new( symbol: :validate, datatype: TrueClass, @@ -271,13 +271,13 @@ def to_sym # the base URI to use when resolving relative URIs (not supported by # all readers) # @param [Boolean] canonicalize (false) - # whether to canonicalize parsed literals + # whether to canonicalize parsed URIs and Literals. # @param [Encoding] encoding (Encoding::UTF_8) # the encoding of the input stream # @param [Boolean] intern (true) # whether to intern all parsed URIs # @param [Boolean] rdfstar (false) - # support parsing RDF* statement resources. + # Preliminary support for RDF 1.2. # @param [Hash] prefixes (Hash.new) # the prefix mappings to use (not supported by all readers) # @param [Hash{Symbol => Object}] options @@ -608,7 +608,9 @@ def validate? end ## - # Returns `true` if parsed values should be canonicalized. + # Returns `true` if parsed values should be in canonical form. + # + # @note This is for term canonicalization, for graph/dataset canonicalization use `RDF::Normalize`. # # @return [Boolean] `true` or `false` # @since 0.3.0 diff --git a/lib/rdf/repository.rb b/lib/rdf/repository.rb index fc81b205..69f053bd 100644 --- a/lib/rdf/repository.rb +++ b/lib/rdf/repository.rb @@ -182,7 +182,8 @@ def supports?(feature) when :validity then @options.fetch(:with_validity, true) when :literal_equality then true when :atomic_write then false - when :rdfstar then false + when :quoted_triples then false + when :base_direction then false when :snapshots then false else false end @@ -269,7 +270,8 @@ def supports?(feature) when :validity then @options.fetch(:with_validity, true) when :literal_equality then true when :atomic_write then true - when :rdfstar then true + when :quoted_triples then true + when :base_direction then true when :snapshots then true else false end diff --git a/lib/rdf/util/cache.rb b/lib/rdf/util/cache.rb index 7426f8aa..355d18c8 100644 --- a/lib/rdf/util/cache.rb +++ b/lib/rdf/util/cache.rb @@ -110,7 +110,7 @@ def finalizer_proc ## # This implementation uses the `WeakRef` class from Ruby's standard - # library, and provides adequate performance on JRuby and on Ruby 2.x. + # library, and provides adequate performance on JRuby and on Ruby 3.x. # # @see http://ruby-doc.org/stdlib-2.2.0/libdoc/weakref/rdoc/WeakRef.html class WeakRefCache < Cache diff --git a/lib/rdf/vocab/rdfv.rb b/lib/rdf/vocab/rdfv.rb index 31f82e0b..03560665 100644 --- a/lib/rdf/vocab/rdfv.rb +++ b/lib/rdf/vocab/rdfv.rb @@ -92,6 +92,10 @@ module RDF # # @return [RDF::Vocabulary::Term] # # @attr_reader :langString # + # # The datatype of directional language-tagged string values. + # # @return [RDF::Vocabulary::Term] + # # @attr_reader :dirLangString + # # # RDF/XML node element. # # @return [RDF::Vocabulary::Term] # # @attr_reader :Description @@ -283,6 +287,13 @@ def name; "RDF"; end "http://www.w3.org/2000/01/rdf-schema#seeAlso": %(http://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal).freeze, subClassOf: "http://www.w3.org/2000/01/rdf-schema#Literal".freeze, type: "http://www.w3.org/2000/01/rdf-schema#Datatype".freeze + term :dirLangString, + comment: %(The datatype of directional language-tagged string values).freeze, + label: "dirLangString".freeze, + isDefinedBy: %(http://www.w3.org/1999/02/22-rdf-syntax-ns#).freeze, + "http://www.w3.org/2000/01/rdf-schema#seeAlso": %(http://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal).freeze, + subClassOf: "http://www.w3.org/2000/01/rdf-schema#Literal".freeze, + type: "http://www.w3.org/2000/01/rdf-schema#Datatype".freeze # Extra definitions term :Description, diff --git a/lib/rdf/writer.rb b/lib/rdf/writer.rb index 8a551ace..c8d1ee46 100644 --- a/lib/rdf/writer.rb +++ b/lib/rdf/writer.rb @@ -392,7 +392,9 @@ def validate? end ## - # Returns `true` if terms should be canonicalized. + # Returns `true` if terms should be in canonical form. + # + # @note This is for term canonicalization, for graph/dataset canonicalization use `RDF::Normalize`. # # @return [Boolean] `true` or `false` # @since 1.0.8 diff --git a/rdf.gemspec b/rdf.gemspec index a6f3daf9..401e3a81 100755 --- a/rdf.gemspec +++ b/rdf.gemspec @@ -27,17 +27,18 @@ Gem::Specification.new do |gem| gem.executables = %w(rdf) gem.require_paths = %w(lib) - gem.required_ruby_version = '>= 2.6' + gem.required_ruby_version = '>= 3.0' gem.requirements = [] gem.add_runtime_dependency 'link_header', '~> 0.0', '>= 0.0.8' - gem.add_development_dependency 'rdf-spec', '~> 3.2' - gem.add_development_dependency 'rdf-turtle', '~> 3.2' - gem.add_development_dependency 'rdf-vocab', '~> 3.2' - gem.add_development_dependency 'rdf-xsd', '~> 3.2', '>= 3.2.1' + gem.add_runtime_dependency 'bcp47_spec', '~> 0.2' + gem.add_development_dependency 'rdf-spec', '~> 3.3' + gem.add_development_dependency 'rdf-turtle', '~> 3.3' + gem.add_development_dependency 'rdf-vocab', '~> 3.3' + gem.add_development_dependency 'rdf-xsd', '~> 3.3' gem.add_development_dependency 'rest-client', '~> 2.1' gem.add_development_dependency 'rspec', '~> 3.12' gem.add_development_dependency 'rspec-its', '~> 1.3' - gem.add_development_dependency 'webmock', '~> 3.18' + gem.add_development_dependency 'webmock', '~> 3.19' gem.add_development_dependency 'yard', '~> 0.9' gem.add_development_dependency 'faraday', '~> 1.10' gem.add_development_dependency 'faraday_middleware', '~> 1.2' diff --git a/spec/format_spec.rb b/spec/format_spec.rb index cb1bcfc0..5a363266 100644 --- a/spec/format_spec.rb +++ b/spec/format_spec.rb @@ -114,8 +114,8 @@ def self.to_sym; :foo_bar; end describe ".reader_types" do it "returns content-types of available readers" do expect(RDF::Format.reader_types).to include(*%w( - application/n-triples text/plain - application/n-quads text/x-nquads + application/n-triples + application/n-quads application/test application/x-test )) end @@ -124,8 +124,8 @@ def self.to_sym; :foo_bar; end describe ".accept_types" do it "returns accept-types of available readers with quality" do expect(RDF::Format.accept_types).to include(*%w( - application/n-triples text/plain;q=0.2 - application/n-quads text/x-nquads;q=0.2 + application/n-triples + application/n-quads application/test application/x-test;q=0.1 )) end @@ -134,8 +134,8 @@ def self.to_sym; :foo_bar; end describe ".uris" do it "returns accept-types of available readers with quality" do expect(RDF::Format.accept_types).to include(*%w( - application/n-triples text/plain;q=0.2 - application/n-quads text/x-nquads;q=0.2 + application/n-triples + application/n-quads application/test application/x-test;q=0.1 )) end @@ -155,8 +155,8 @@ def self.to_sym; :foo_bar; end describe ".writer_types" do it "returns content-types of available writers" do %w( - application/n-triples text/plain - application/n-quads text/x-nquads + application/n-triples + application/n-quads ).each do |ct| expect(RDF::Format.writer_types).to include(ct) end diff --git a/spec/model_literal_spec.rb b/spec/model_literal_spec.rb index 2aa3ce01..de00da2b 100644 --- a/spec/model_literal_spec.rb +++ b/spec/model_literal_spec.rb @@ -1,4 +1,5 @@ # coding: utf-8 +# frozen_string_literal: true require_relative 'spec_helper' require 'rdf/spec/literal' require 'rdf/xsd' @@ -8,15 +9,19 @@ def self.literal(selector) case selector - when :empty then [''.freeze] - when :plain then ['Hello'.freeze] - when :empty_lang then [''.freeze, {language: :en}] - when :plain_lang then ['Hello'.freeze, {language: :en}] + when :empty then [''] + when :plain then ['Hello'] + when :empty_lang then ['', {language: :en}] + when :plain_lang then ['Hello', {language: :en}] # langString language: must not contain spaces - when :wrong_lang then ['WrongLang'.freeze, {language: "en f"}] + when :wrong_lang then ['WrongLang', {language: "en f"}] # langString language: must be non-empty valid language - when :unset_lang then ['NoLanguage'.freeze, {datatype: RDF::langString}] - when :string then ['String'.freeze, {datatype: RDF::XSD.string}] + when :unset_lang then ['NoLanguage', {datatype: RDF.langString}] + when :lang_dir then ['Hello', {language: :en, direction: :ltr}] + when :wrong_dir then ['Hello', {language: :en, direction: "center-out"}] + when :dir_no_lang then ['Hello', {direction: :ltr}] + when :unset_dir then ['NoDir', {language: :en, datatype: RDF.dirLangString}] + when :string then ['String', {datatype: RDF::XSD.string}] when :false then [false] when :true then [true] when :int then [123] @@ -35,9 +40,9 @@ def self.literals(*selector) selector.inject([]) do |ary, sel| ary += case sel when :all_simple then %i(empty plain string).map {|s| literal(s)} - when :all_plain_lang then %i(empty_lang plain_lang).map {|s| literal(s)} + when :all_plain_lang then %i(empty_lang plain_lang lang_dir).map {|s| literal(s)} when :all_native then %i(false true int long decimal double time date datetime).map {|s| literal(s)} - when :all_invalid_lang then %i(wrong_lang unset_lang).map {|s| literal(s)} + when :all_invalid_lang then %i(wrong_lang unset_lang wrong_dir).map {|s| literal(s)} when :all_plain then literals(:all_simple, :all_plain_lang) else literals(:all_plain, :all_native) end @@ -2327,6 +2332,7 @@ def self.literals(*selector) { "language with xsd:string" => {value: "foo", language: "en", datatype: RDF::XSD.string}, "language with xsd:date" => {value: "foo", language: "en", datatype: RDF::XSD.date}, + "direction without language" => {value: "foo", direction: "ltr"} }.each do |name, opts| it "raises error for #{name}" do expect {RDF::Literal.new(opts.delete(:value), **opts)}.to raise_error(ArgumentError) diff --git a/spec/model_statement_spec.rb b/spec/model_statement_spec.rb index 2a069fac..a159edb8 100644 --- a/spec/model_statement_spec.rb +++ b/spec/model_statement_spec.rb @@ -79,6 +79,18 @@ it {is_expected.to be_statement} it {is_expected.not_to be_inferred} its(:terms) {is_expected.to include(s, p, o)} + + describe "#dup" do + its(:dup) {is_expected.to eql(subject)} + its(:dup) {is_expected.not_to equal(subject)} + + [:subject, :predicate, :object].each do |c| + it "#{c} duplicated as appropriate" do + expect(subject.dup.send(c)).to eql(subject.send(c)) + expect(subject.dup.send(c)).not_to equal(subject.send(c)) unless subject.send(c).is_a?(RDF::Node) + end + end + end end context "when created with a blank node subject" do @@ -103,6 +115,18 @@ its(:graph_name) {is_expected.not_to be_nil} it {is_expected.to eq stmt} it {is_expected.not_to eql stmt} + + describe "#dup" do + its(:dup) {is_expected.to eql(subject)} + its(:dup) {is_expected.not_to equal(subject)} + + [:subject, :predicate, :object, :graph_name].each do |c| + it "#{c} duplicated as appropriate" do + expect(subject.dup.send(c)).to eql(subject.send(c)) + expect(subject.dup.send(c)).not_to equal(subject.send(c)) unless subject.send(c).is_a?(RDF::Node) + end + end + end end context "when created with a default graph" do @@ -296,7 +320,7 @@ end end - context "RDF*" do + context "RDF-star" do it "is not embedded for plain statements" do expect(RDF::Statement(:s, :p, :o)).not_to be_embedded end diff --git a/spec/model_value_spec.rb b/spec/model_value_spec.rb index af400945..b23775d9 100644 --- a/spec/model_value_spec.rb +++ b/spec/model_value_spec.rb @@ -135,9 +135,10 @@ end it "#canonicalize!" do - [statement, uri, node, literal, graph, statement, variable, list].each do |v| + [statement, uri, node, literal, statement, variable, list].each do |v| expect(v.canonicalize!).to equal v end + expect {graph.canonicalize!}.to raise_error(NotImplementedError) end it "#to_rdf" do diff --git a/spec/nquads_spec.rb b/spec/nquads_spec.rb index e88b579c..9dd84178 100644 --- a/spec/nquads_spec.rb +++ b/spec/nquads_spec.rb @@ -20,7 +20,6 @@ {file_name: 'etc/doap.nq'}, {file_extension: 'nq'}, {content_type: 'application/n-quads'}, - {content_type: 'text/x-nquads'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Format.for(arg)).to eq subject @@ -105,7 +104,6 @@ {file_name: 'etc/doap.nq'}, {file_extension: 'nq'}, {content_type: 'application/n-quads'}, - {content_type: 'text/x-nquads'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Reader.for(arg)).to eq RDF::NQuads::Reader @@ -182,7 +180,7 @@ end end - context "RDF*" do + context "RDF-star" do statements = { "subject-iii": '<< >> .', "subject-iib": '<< _:o1>> .', @@ -242,7 +240,6 @@ {file_name: 'etc/doap.nq'}, {file_extension: 'nq'}, {content_type: 'application/n-quads'}, - {content_type: 'text/x-nquads'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Writer.for(arg)).to eq RDF::NQuads::Writer diff --git a/spec/ntriples_spec.rb b/spec/ntriples_spec.rb index 1622cdf1..f2828aff 100644 --- a/spec/ntriples_spec.rb +++ b/spec/ntriples_spec.rb @@ -27,7 +27,6 @@ {file_name: 'etc/doap.nt'}, {file_extension: 'nt'}, {content_type: 'application/n-triples'}, - {content_type: 'text/plain'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Format.for(arg)).to eq subject @@ -103,15 +102,15 @@ {file_name: 'etc/doap.nt'}, {file_extension: 'nt'}, {content_type: 'application/n-triples'}, - {content_type: 'text/plain'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Reader.for(arg)).to eq described_class end end - context "content_type text/plain with non-N-Triples content" do + context "content_type text/plain" do { + :ntriples => " . ", :nquads => " . ", :nq_literal => ' "literal" .', :nq_multi_line => %(\n \n "literal"\n \n .), @@ -310,6 +309,32 @@ end end + context "base direction" do + context "without rdfstar option" do + it "Raises an error" do + expect do + expect {parse(' "Hello"@en--ltr .')}.to raise_error(RDF::ReaderError) + end.to write(:something).to(:error) + end + end + + context 'parse language/direction' do + { + "language" => ' "Hello"@en .', + "direction" => ' "Hello"@en--ltr .', + }.each_pair do |name, triple| + specify "test #{name}" do + stmt = reader.new(triple, rdfstar: true).first + if name.include?('dir') + expect(stmt.object.datatype).to eql RDF.dirLangString + else + expect(stmt.object.datatype).to eql RDF.langString + end + end + end + end + end + context 'should parse a value that was written without passing through the writer encoding' do [ %( "Procreation Metaphors in S\xC3\xA9an \xC3\x93 R\xC3\xADord\xC3\xA1in's Poetry" .), @@ -353,8 +378,10 @@ "XML Literals as Datatyped Literals (8)" => ' "a\n\nc"^^ .', "XML Literals as Datatyped Literals (9)" => ' "chat"^^ .', - "Plain literals with languages (1)" => ' "chat"@fr .', - "Plain literals with languages (2)" => ' "chat"@en .', + "Literals with languages (1)" => ' "chat"@fr .', + "Literals with languages (2)" => ' "chat"@en .', + # FIXME: once rdfstar option is no longer used + #"Literals with language and direction" => ' "chat"@en--ltr .', "Typed Literals" => ' "abc"^^ .', "Plain lieral with embedded quote" => %q( "From \\"Voyage dans l’intérieur de l’Amérique du Nord, executé pendant les années 1832, 1833 et 1834, par le prince Maximilien de Wied-Neuwied\\" (Paris & Coblenz, 1839-1843)" .), }.each_pair do |name, nt| @@ -404,7 +431,7 @@ end end - context "RDF*" do + context "quoted triples" do statements = { "subject-iii": '<< >> .', "subject-iib": '<< _:o1>> .', @@ -479,6 +506,18 @@ %q( "string"@1 .), %r(Expected end of statement \(found: "@1 \."\)) ], + "xx bad lang 2" => [ + %q( "string"@cantbethislong .), + %r(Invalid Literal) + ], + "xx bad dir 1" => [ + %q( "string"@en--UTD .), + %r(Invalid Literal) + ], + "xx bad dir 2" => [ + %q( "string"@--ltr .), + %r(Expected end of statement) + ], "nt-syntax-bad-string-05" => [ %q( """abc""" .), %r(Expected end of statement \(found: .* \."\)) @@ -538,7 +577,6 @@ {file_name: 'etc/doap.nt'}, {file_extension: 'nt'}, {content_type: 'application/n-triples'}, - {content_type: 'text/plain'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Writer.for(arg)).to eq RDF::NTriples::Writer @@ -608,6 +646,10 @@ expect(writer.new.format_literal(RDF::Literal.new('Hello, world!', language: :en))).to eq '"Hello, world!"@en' end + it "should correctly format directional language-tagged literals" do + expect(writer.new.format_literal(RDF::Literal.new('Hello, world!', language: :en, direction: :ltr))).to eq '"Hello, world!"@en--ltr' + end + it "should correctly format datatyped literals" do expect(writer.new.format_literal(RDF::Literal.new(3.1415))).to eq '"3.1415"^^' end @@ -715,7 +757,7 @@ end end - context "RDF*" do + context "quoted triples" do { "subject-iii": { input: RDF::Statement( diff --git a/spec/query_pattern_spec.rb b/spec/query_pattern_spec.rb index 86564663..37a68783 100644 --- a/spec/query_pattern_spec.rb +++ b/spec/query_pattern_spec.rb @@ -282,7 +282,7 @@ end end - context "RDF*" do + context "quoted triples" do let(:s) {RDF::Query::Variable.new(:s)} let(:p) {RDF::Query::Variable.new(:p)} let(:o) {RDF::Query::Variable.new(:o)} diff --git a/spec/reader_spec.rb b/spec/reader_spec.rb index a3555ac4..08b646df 100644 --- a/spec/reader_spec.rb +++ b/spec/reader_spec.rb @@ -23,7 +23,6 @@ {file_name: 'etc/doap.nt'}, {file_extension: 'nt'}, {content_type: 'application/n-triples'}, - {content_type: 'text/plain'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Reader.for(arg)).to eq RDF::NTriples::Reader diff --git a/spec/writer_spec.rb b/spec/writer_spec.rb index 7e762eb1..b7a6947f 100644 --- a/spec/writer_spec.rb +++ b/spec/writer_spec.rb @@ -10,7 +10,6 @@ {file_name: 'etc/doap.nt'}, {file_extension: 'nt'}, {content_type: 'application/n-triples'}, - {content_type: 'text/plain'}, ].each do |arg| it "discovers with #{arg.inspect}" do expect(RDF::Writer.for(arg)).to eq RDF::NTriples::Writer