diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99fd5701..0ffb2622 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['3.0', 3.1, 3.2, ruby-head, jruby] + ruby: ['3.0', 3.1, 3.2, 3.3, ruby-head, jruby] steps: - name: Clone repository uses: actions/checkout@v3 @@ -39,7 +39,7 @@ jobs: run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES - name: Coveralls GitHub Action uses: coverallsapp/github-action@v2 - if: "matrix.ruby == '3.0'" + if: "matrix.ruby == '3.3'" with: github-token: ${{ secrets.GITHUB_TOKEN }} wintests: @@ -48,12 +48,12 @@ jobs: runs-on: windows-latest env: CI: true - ALLOW_FAILURES: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'jruby' || matrix.ruby == '3.1' }} + ALLOW_FAILURES: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'jruby' }} strategy: fail-fast: false matrix: ruby: - - 3.1 + - 3.2 steps: - name: Clone repository uses: actions/checkout@v3 diff --git a/CHANGES.md b/CHANGES.md deleted file mode 100644 index 9991bab9..00000000 --- a/CHANGES.md +++ /dev/null @@ -1,92 +0,0 @@ -Release 2.0.0 -============= - -* A new class `RDF::Changeset` has been added. This is meant to replace any - previous use of `RDF::Transaction`, which in fact used to in RDF.rb 1.x - represent more of a buffered changeset than a genuine transaction scope. - - - Instead of `RDF::Transaction.execute`, use `RDF::Changeset.apply`. - - Instead of `RDF::Transaction#execute`, use `RDF::Changeset#apply`. - -* The `RDF::Transaction` class has been substantially revamped, including - some minor backwards-incompatible changes. These changes will mostly - affect repository implementors, not so much general RDF.rb users. - - The changes reflect the expanded purpose of the class: instead of being a - mere buffered changeset (for which, see `RDF::Changeset`), transactions - are now intended to provide a proper ACID scope for repository queries and - mutations. - - We always now also carefully distinguish between read-only and read/write - transactions, in order to enable repository implementations to take out the - appropriate locks for concurrency control. Note as well that transactions - are now read-only by default; mutability must be explicitly requested on - construction in order to obtain a read/write transaction. - - In case repository implementations should be unable to provide actual ACID - guarantees for transactions, that must be clearly indicated in their - documentation. Similarly, implementations should throw an exception when - appropriate in case they don't provide write transaction support. - - - `RDF::Transaction#initialize` now takes the target repository as its - first argument. Transactions are now always tied to a specific - repository instance, instead of being free-floating objects as they used - to be (for that, see `RDF::Changeset`). - - - `RDF::Transaction` now mixes in `RDF::Queryable` and `RDF::Enumerable`, - enabling quad-pattern matches and BGP queries to execute in a proper - transaction scope. - - - The `RDF::Transaction#context` accessor, and its aliases, have been - removed. Transactions aren't necessarily scoped to a single graph only. - - - There is a new `RDF::Transaction#repository` accessor for retrieving the - target repository object that the transaction operates upon. - - - There is a new `RDF::Transaction#buffered?` predicate for testing - whether the changeset that constitutes a transaction is available for - introspection. Particular repository implementations may support both - options and permit the user the choice on transaction construction. - - - The `RDF::Transaction#inserts` and `#deletes` methods are deprecated. - Instead, there is a new `RDF::Transaction#changes` accessor to retrieve - an `RDF::Changeset` instance, which contains corresponding methods. - For unbuffered transactions, `#changes` returns `nil`. - -* Enumerables vs. Enumerators - - - `RDF::Queryable#query` and `RDF::Query#execute` did not return an enumerable, which may be an enumerator. Most internal uses return an Array now, which aides performance for small result sets, but potentially causes problems for large result sets. Implementations may still return an Enumerator, and Enumerators may be passed as arguments. - - `RDF::Enumerable#statements`, `#quads`, `#triples`, `#subjects`, `#predicates`, `#objects`, and `#contexts` now return an array rather than an Enumerator. - -* The following vocabularies are deprecated and have been moved to the rdf-vocab gem. - - - `RDF::CC` - Creative Commons (CC) - - `RDF::CERT` - W3 Authentication Certificate (CERT) - - `RDF::DC` - Dublin Core (DC) - - `RDF::DC11` - Dublin Core 1.1 (DC11) _deprecated_ - - `RDF::DOAP` - Description of a Project (DOAP) - - `RDF::EXIF` - Exchangeable Image File Format (EXIF) - - `RDF::FOAF` - Friend of a Friend (FOAF) - - `RDF::GEO` - WGS84 Geo Positioning (GEO) - - `RDF::GR` - GoodRelations (GR) - - `RDF::HT` - Hypertext Transfer Protocol (HT) - - `RDF::ICAL` - RDF Calendar Workspace (ICAL) - - `RDF::MA` - Media Resources (MA) - - `RDF::MO` - Music Ontology (MO) - - `RDF::OG` - Open Graph protocol (OG) - - `RDF::PROV` - Provenance on the web (PROV) - - `RDF::RSA` - W3 RSA Keys (RSA) - - `RDF::RSS` - RDF Site Summary (RSS) - - `RDF::SCHEMA` - Schema.org (SCHEMA) - - `RDF::SIOC` - Semantically-Interlinked Online Communities (SIOC) - - `RDF::SKOS` - Simple Knowledge Organization System (SKOS) - - `RDF::SKOSXL` - SKOS eXtension for Labels (SKOSXL) - - `RDF::V` - RDF data vocabulary (V) - - `RDF::VCARD` - Ontology for vCards (VCARD) - - `RDF::VMD` - Data-Vocabulary.org (VMD) - - `RDF::VOID` - Vocabulary of Interlinked Datasets (VOID) - - `RDF::VS` - SemWeb Vocab Status ontology (VS) - - `RDF::WDRS` - Protocol for Web Description Resources (WDRS) - - `RDF::WOT` - Web of Trust (WOT) - - `RDF::XHTML` - Extensible HyperText Markup Language (XHTML) - - `RDF::XHV` - XHTML Vocabulary (XHV) diff --git a/README.md b/README.md index 45c66584..ea24b6d2 100644 --- a/README.md +++ b/README.md @@ -265,9 +265,15 @@ 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-star CG + +[RDF.rb][] includes provisional support for [RDF-star][] with an N-Triples/N-Quads syntax for quoted triples in the _subject_ or _object_ position. + +Support for RDF-star quoted triples is now deprecated, use RDF 1.2 triple terms instead. + ## RDF 1.2 -[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][] with an N-Triples/N-Quads syntax for triple terms in the _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`. @@ -394,6 +400,9 @@ from BNode identity (i.e., they each entail the other) * {RDF::RDFV} - RDF Vocabulary (RDFV) * {RDF::XSD} - XML Schema (XSD) +## Change Log + +See [Release Notes on GitHub](https://github.com/ruby-rdf/rdf/releases) ## Dependencies @@ -498,6 +507,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-star]: https://www.w3.org/2021/12/rdf-star.html [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/ diff --git a/VERSION b/VERSION index bea438e9..47725433 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.1 +3.3.2 diff --git a/etc/n-triples.ebnf b/etc/n-triples.ebnf index 4471ec11..fae8f48d 100644 --- a/etc/n-triples.ebnf +++ b/etc/n-triples.ebnf @@ -2,8 +2,9 @@ ntriplesDoc ::= triple? (EOL triple)* EOL? triple ::= subject predicate object '.' subject ::= IRIREF | BLANK_NODE_LABEL | quotedTriple predicate ::= IRIREF -object ::= IRIREF | BLANK_NODE_LABEL | literal | quotedTriple +object ::= IRIREF | BLANK_NODE_LABEL | literal | tripleTerm | quotedTriple literal ::= STRING_LITERAL_QUOTE ('^^' IRIREF | LANG_DIR )? +tripleTerm ::= '<<' subject predicate object '>>' quotedTriple ::= '<<' subject predicate object '>>' @terminals diff --git a/lib/rdf/mixin/enumerable.rb b/lib/rdf/mixin/enumerable.rb index 7400ffea..03a6fc11 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. - # * `:quoted_triples` supports RDF 1.2 quoted triples. + # * `:rdf_full` supports RDF 1.2 Full profile, including support for embedded Triple Terms. + # * `:quoted_triples` supports RDF-star quoted triples. # * `:base_direction` supports RDF 1.2 directional language-tagged strings. # # @param [Symbol, #to_sym] feature diff --git a/lib/rdf/mixin/queryable.rb b/lib/rdf/mixin/queryable.rb index 82bee7b1..576f35d0 100644 --- a/lib/rdf/mixin/queryable.rb +++ b/lib/rdf/mixin/queryable.rb @@ -146,6 +146,8 @@ def query_execute(query, **options, &block) # # Patterns may also have embedded patterns as either a subject or object, recursively. # + # Patterns with a variable `graph_name` do not match the default graph. + # # When matching, match an embedded pattern against embedded statements, recursively. (see {RDF::Query::Pattern#eql?}) # # @param [RDF::Query::Pattern] pattern diff --git a/lib/rdf/mixin/writable.rb b/lib/rdf/mixin/writable.rb index 987958e2..52331daa 100644 --- a/lib/rdf/mixin/writable.rb +++ b/lib/rdf/mixin/writable.rb @@ -127,10 +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?(:quoted_triples) + # FIXME: quoted triples are now deprecated + if statement.embedded? && respond_to?(:supports?) && !(supports?(:quoted_triples) || supports?(:rdf_full)) raise ArgumentError, "Writable does not support quoted triples" end - if statement.object && statement.object.literal? && statement.object.direction? && !supports?(:base_direction) + if statement.object && statement.object.literal? && statement.object.direction? && respond_to?(:supports?) && !supports?(:base_direction) raise ArgumentError, "Writable does not support directional languaged-tagged strings" end insert_statement(statement) diff --git a/lib/rdf/model/dataset.rb b/lib/rdf/model/dataset.rb index f40f3c36..e4045c03 100644 --- a/lib/rdf/model/dataset.rb +++ b/lib/rdf/model/dataset.rb @@ -104,7 +104,8 @@ def isolation_level # @private # @see RDF::Enumerable#supports? def supports?(feature) - return true if %i(graph_name quoted_triples).include?(feature) + # FIXME: quoted triples are now deprecated + return true if %i(graph_name quoted_triples rdf_full).include?(feature) super end diff --git a/lib/rdf/model/graph.rb b/lib/rdf/model/graph.rb index e45f24ad..c049adf4 100644 --- a/lib/rdf/model/graph.rb +++ b/lib/rdf/model/graph.rb @@ -305,8 +305,9 @@ def query_pattern(pattern, **options, &block) # @private # @see RDF::Mutable#insert def insert_statement(statement) - if statement.embedded? && !@data.supports?(:quoted_triples) - raise ArgumentError, "Graph does not support quoted triples" + # FIXME: quoted triples are now deprecated + if statement.embedded? && !(@data.supports?(:quoted_triples) || @data.supports?(:rdf_full)) + raise ArgumentError, "Graph does not support the RDF Full profile" end if statement.object && statement.object.literal? && statement.object.direction? && !@data.supports?(:base_direction) raise ArgumentError, "Graph does not support directional languaged-tagged strings" diff --git a/lib/rdf/model/literal.rb b/lib/rdf/model/literal.rb index 28e06f04..992fbd17 100644 --- a/lib/rdf/model/literal.rb +++ b/lib/rdf/model/literal.rb @@ -194,7 +194,7 @@ def initialize(value, language: nil, datatype: nil, direction: nil, lexical: nil @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 + @direction = direction.to_s.to_sym if direction @datatype = RDF::URI(datatype).freeze if datatype @datatype ||= self.class.const_get(:DATATYPE) if self.class.const_defined?(:DATATYPE) @datatype ||= if instance_variable_defined?(:@language) && @language && @@ -451,6 +451,16 @@ def valid? false end + ## + # Returns `true` if this is a language-tagged literal in the English + # language. + # + # @return [Boolean] `true` or `false` + # @since 3.3.2 + def english? + /\Aen(?:-[A-Za-z]{2})?\z/ === language.to_s + end + ## # Validates the value using {RDF::Value#valid?}, raising an error if the value is # invalid. diff --git a/lib/rdf/model/statement.rb b/lib/rdf/model/statement.rb index 91a0bbd6..dffdd3a4 100644 --- a/lib/rdf/model/statement.rb +++ b/lib/rdf/model/statement.rb @@ -71,7 +71,8 @@ def self.from(statement, graph_name: nil, **options) # @option options [RDF::Term] :graph_name (nil) # Note, in RDF 1.1, a graph name MUST be an {Resource}. # @option options [Boolean] :inferred used as a marker to record that this statement was inferred based on semantic relationships (T-Box). - # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement. + # @option options [Boolean] :tripleTerm used as a marker to record that this statement appears as the object of another RDF::Statement. + # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement (deprecated). # @return [RDF::Statement] # # @overload initialize(subject, predicate, object, **options) @@ -84,7 +85,8 @@ def self.from(statement, graph_name: nil, **options) # @option options [RDF::Term] :graph_name (nil) # Note, in RDF 1.1, a graph name MUST be an {Resource}. # @option options [Boolean] :inferred used as a marker to record that this statement was inferred based on semantic relationships (T-Box). - # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement. + # @option options [Boolean] :tripleTerm used as a marker to record that this statement appears as the object of another RDF::Statement. + # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement (deprecated). # @return [RDF::Statement] def initialize(subject = nil, predicate = nil, object = nil, options = {}) if subject.is_a?(Hash) @@ -211,6 +213,13 @@ def asserted? ## # @return [Boolean] + def tripleTerm? + !!@options[:tripleTerm] + end + + ## + # @return [Boolean] + # @deprecated Quoted triples are now deprecated def quoted? !!@options[:quoted] end diff --git a/lib/rdf/model/uri.rb b/lib/rdf/model/uri.rb index e6deb7aa..878ffd1d 100644 --- a/lib/rdf/model/uri.rb +++ b/lib/rdf/model/uri.rb @@ -70,10 +70,10 @@ class URI IUSERINFO = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS})|:)*").freeze IAUTHORITY = Regexp.compile("(?:#{IUSERINFO}@)?#{IHOST}(?::#{PORT})?").freeze - IRELATIVE_PART = Regexp.compile("(?:(?://#{IAUTHORITY}(?:#{IPATH_ABEMPTY}))|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_NOSCHEME})|(?:#{IPATH_EMPTY}))").freeze - IRELATIVE_REF = Regexp.compile("^#{IRELATIVE_PART}(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze + IRELATIVE_PART = Regexp.compile("//#{IAUTHORITY}(?:#{IPATH_ABEMPTY})|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_NOSCHEME})|(?:#{IPATH_EMPTY})").freeze + IRELATIVE_REF = Regexp.compile("^(?:#{IRELATIVE_PART})(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze - IHIER_PART = Regexp.compile("(?:(?://#{IAUTHORITY}#{IPATH_ABEMPTY})|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_ROOTLESS})|(?:#{IPATH_EMPTY}))").freeze + IHIER_PART = Regexp.compile("//#{IAUTHORITY}(?:#{IPATH_ABEMPTY})|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_ROOTLESS})|(?:#{IPATH_EMPTY})").freeze IRI = Regexp.compile("^#{SCHEME}:(?:#{IHIER_PART})(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze # Split an IRI into it's component parts @@ -1240,17 +1240,18 @@ def query_values(return_type=Hash) query.to_s.split('&'). inject(return_type == Hash ? {} : []) do |memo,kv| k,v = kv.to_s.split('=', 2) - next if k.to_s.empty? - k = CGI.unescape(k) - v = CGI.unescape(v) if v - if return_type == Hash - case memo[k] - when nil then memo[k] = v - when Array then memo[k] << v - else memo[k] = [memo[k], v] + unless k.to_s.empty? + k = CGI.unescape(k) + v = CGI.unescape(v) if v + if return_type == Hash + case memo[k] + when nil then memo[k] = v + when Array then memo[k] << v + else memo[k] = [memo[k], v] + end + else + memo << [k, v].compact end - else - memo << [k, v].compact end memo end diff --git a/lib/rdf/model/value.rb b/lib/rdf/model/value.rb index 528c2ffb..a09b1ba9 100644 --- a/lib/rdf/model/value.rb +++ b/lib/rdf/model/value.rb @@ -197,19 +197,20 @@ def validate! alias_method :validate, :validate! ## - # Returns `true` if this Value starts with the given `string`. + # Returns `true` if this Value starts with any of the given strings. # # @example # RDF::URI('http://example.org/').start_with?('http') #=> true # RDF::Node('_:foo').start_with?('_:bar') #=> false # RDF::Litera('Apple').start_with?('Orange') #=> false + # RDF::Litera('Apple').start_with?('Orange', 'Apple') #=> true # - # @param [String, #to_s] string + # @param [Array<#to_s>] *args Any number of strings to check against. # @return [Boolean] `true` or `false` # @see String#start_with? # @since 0.3.0 - def start_with?(string) - to_s.start_with?(string.to_s) + def start_with?(*args) + to_s.start_with?(*args.map(&:to_s)) end alias_method :starts_with?, :start_with? diff --git a/lib/rdf/nquads.rb b/lib/rdf/nquads.rb index 272b26c0..ef4b4aad 100644 --- a/lib/rdf/nquads.rb +++ b/lib/rdf/nquads.rb @@ -71,9 +71,10 @@ def read_triple begin unless blank? || read_comment + # FIXME: quoted triples are now deprecated subject = read_uriref || read_node || read_quotedTriple || fail_subject predicate = read_uriref(intern: true) || fail_predicate - object = read_uriref || read_node || read_literal || read_quotedTriple || fail_object + object = read_uriref || read_node || read_literal || read_tripleTerm || read_quotedTriple || fail_object graph_name = read_uriref || read_node if validate? && !read_eos log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) diff --git a/lib/rdf/ntriples.rb b/lib/rdf/ntriples.rb index f5e526ae..b8247f67 100644 --- a/lib/rdf/ntriples.rb +++ b/lib/rdf/ntriples.rb @@ -15,7 +15,11 @@ module RDF # # "rdf" . # - # ## Quoted Triples + # ## Triple terms + # + # Supports statements as resources using `<<(s p o)>>`. + + # ## Quoted Triples (Deprecated) # # Supports statements as resources using `<>`. # diff --git a/lib/rdf/ntriples/reader.rb b/lib/rdf/ntriples/reader.rb index 832f8942..7894d4d7 100644 --- a/lib/rdf/ntriples/reader.rb +++ b/lib/rdf/ntriples/reader.rb @@ -51,14 +51,14 @@ class Reader < RDF::Reader # @see http://www.w3.org/TR/turtle/ ## # Unicode regular expressions. - U_CHARS1 = Regexp.compile(<<-EOS.gsub(/\s+/, '')) - [\\u00C0-\\u00D6]|[\\u00D8-\\u00F6]|[\\u00F8-\\u02FF]| - [\\u0370-\\u037D]|[\\u037F-\\u1FFF]|[\\u200C-\\u200D]| - [\\u2070-\\u218F]|[\\u2C00-\\u2FEF]|[\\u3001-\\uD7FF]| - [\\uF900-\\uFDCF]|[\\uFDF0-\\uFFFD]|[\\u{10000}-\\u{EFFFF}] - EOS - U_CHARS2 = Regexp.compile("\\u00B7|[\\u0300-\\u036F]|[\\u203F-\\u2040]").freeze - IRI_RANGE = Regexp.compile("[[^<>\"{}\|\^`\\\\]&&[^\\x00-\\x20]]").freeze + U_CHARS1 = Regexp.compile(<<-EOS.gsub(/\s+/, '')) + [\\u00C0-\\u00D6]|[\\u00D8-\\u00F6]|[\\u00F8-\\u02FF]| + [\\u0370-\\u037D]|[\\u037F-\\u1FFF]|[\\u200C-\\u200D]| + [\\u2070-\\u218F]|[\\u2C00-\\u2FEF]|[\\u3001-\\uD7FF]| + [\\uF900-\\uFDCF]|[\\uFDF0-\\uFFFD]|[\\u{10000}-\\u{EFFFF}] + EOS + U_CHARS2 = Regexp.compile("\\u00B7|[\\u0300-\\u036F]|[\\u203F-\\u2040]").freeze + IRI_RANGE = Regexp.compile("[[^<>\"{}\|\^`\\\\]&&[^\\x00-\\x20]]").freeze PN_CHARS_BASE = /[A-Z]|[a-z]|#{U_CHARS1}/.freeze PN_CHARS_U = /_|#{PN_CHARS_BASE}/.freeze @@ -70,8 +70,11 @@ class Reader < RDF::Reader LANG_DIR = /@([a-zA-Z]+(?:-[a-zA-Z0-9]+)*(?:--[a-zA-Z]+)?)/.freeze STRING_LITERAL_QUOTE = /"((?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*)"/.freeze - ST_START = /^<>/.freeze + TT_START = /^<<\(/.freeze + TT_END = /^\s*\)>>/.freeze + + QT_START = /^<>/.freeze # @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar COMMENT = /^#\s*(.*)$/.freeze @@ -208,7 +211,7 @@ def read_value begin read_statement rescue RDF::ReaderError - value = read_uriref || read_node || read_literal || read_quotedTriple + value = read_uriref || read_node || read_literal || read_tripleTerm || read_quotedTriple log_recover value end @@ -226,7 +229,7 @@ def read_triple unless blank? || read_comment subject = read_uriref || read_node || read_quotedTriple || fail_subject predicate = read_uriref(intern: true) || fail_predicate - object = read_uriref || read_node || read_literal || read_quotedTriple || fail_object + object = read_uriref || read_node || read_literal || read_tripleTerm || read_quotedTriple || fail_object if validate? && !read_eos log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) @@ -242,12 +245,29 @@ def read_triple ## # @return [RDF::Statement] + def read_tripleTerm + if @options[:rdfstar] && match(TT_START) + subject = read_uriref || read_node || fail_subject + predicate = read_uriref(intern: true) || fail_predicate + object = read_uriref || read_node || read_literal || read_tripleTerm || fail_object + if !match(TT_END) + log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) + end + RDF::Statement.new(subject, predicate, object, tripleTerm: true) + end + end + + ## + # @return [RDF::Statement] + # @deprecated Quoted triples are now deprecated def read_quotedTriple - if @options[:rdfstar] && match(ST_START) + if @options[:rdfstar] && !match(TT_START) && match(QT_START) + warn "[DEPRECATION] RDF-star quoted triples are deprecated and will be removed in a future version.\n" + + "Called from #{Gem.location_of_caller.join(':')}" subject = read_uriref || read_node || read_quotedTriple || fail_subject predicate = read_uriref(intern: true) || fail_predicate object = read_uriref || read_node || read_literal || read_quotedTriple || fail_object - if !match(ST_END) + if !match(QT_END) log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) end RDF::Statement.new(subject, predicate, object, quoted: true) diff --git a/lib/rdf/ntriples/writer.rb b/lib/rdf/ntriples/writer.rb index 3a1513a7..a69fc94a 100644 --- a/lib/rdf/ntriples/writer.rb +++ b/lib/rdf/ntriples/writer.rb @@ -223,15 +223,28 @@ def format_statement(statement, **options) format_triple(*statement.to_triple, **options) end + ## + # Returns the N-Triples representation of an RDF 1.2 triple term. + # + # @param [RDF::Statement] statement + # @param [Hash{Symbol => Object}] options ({}) + # @return [String] + def format_tripleTerm(statement, **options) + "<<(%s %s %s)>>" % statement.to_a.map { |value| format_term(value, **options) } + end + ## # Returns the N-Triples representation of an RDF-star quoted triple. # # @param [RDF::Statement] statement # @param [Hash{Symbol => Object}] options ({}) # @return [String] + # @deprecated Quoted triples are now deprecated def format_quotedTriple(statement, **options) + # FIXME: quoted triples are now deprecated "<<%s %s %s>>" % statement.to_a.map { |value| format_term(value, **options) } end + ## # Returns the N-Triples representation of a triple. # diff --git a/lib/rdf/query.rb b/lib/rdf/query.rb index a91c0f40..c5d68a75 100644 --- a/lib/rdf/query.rb +++ b/lib/rdf/query.rb @@ -151,10 +151,9 @@ def self.Solutions(*args) # @option options [RDF::Query::Solutions] :solutions (Solutions.new) # @option options [RDF::Resource, RDF::Query::Variable, false] :graph_name (nil) # Default graph name for matching against queryable. - # Named queries either match against a specifically named + # Queries with a graph name match against a specifically named # graphs if the name is an {RDF::Resource} or bound {RDF::Query::Variable}. - # Names that are against unbound variables match either default - # or named graphs. + # Queries using an unbound variable as a graph name only match against named graphs, and will not match the default graph. # The name of `false` will only match against the default graph. # @option options [RDF::Resource, RDF::Query::Variable, false] :name (nil) # Alias for `:graph_name`. @@ -168,10 +167,9 @@ def self.Solutions(*args) # @param [RDF::Query::Solutions] solutions (Solutions.new) # @param [RDF::Resource, RDF::Query::Variable, false] graph_name (false) # Default graph name for matching against queryable. - # Named queries either match against a specifically named + # Queries with a graph name match against a specifically named # graphs if the name is an {RDF::Resource} or bound {RDF::Query::Variable}. - # Names that are against unbound variables match either default - # or named graphs. + # Queries using an unbound variable as a graph name only match against named graphs, and will not match the default graph. # The name of `false` will only match against the default graph. # @param [RDF::Resource, RDF::Query::Variable, false] name (false) # Alias for `:graph_name`. @@ -285,10 +283,9 @@ def optimize!(**options) # @param [RDF::Query::Solutions] solutions (Solutions.new) # @param [RDF::Resource, RDF::Query::Variable, false] graph_name (nil) # Default graph name for matching against queryable. - # Named queries either match against a specifically named + # Queries with a graph name match against a specifically named # graphs if the name is an {RDF::Resource} or bound {RDF::Query::Variable}. - # Names that are against unbound variables match either default - # or named graphs. + # Queries using an unbound variable as a graph name only match against named graphs, and will not match the default graph. # The name of `false` will only match against the default graph. # @param [RDF::Resource, RDF::Query::Variable, false] name (nil) # Alias for `:graph_name`. diff --git a/lib/rdf/query/pattern.rb b/lib/rdf/query/pattern.rb index 5a9ef841..5e141086 100644 --- a/lib/rdf/query/pattern.rb +++ b/lib/rdf/query/pattern.rb @@ -23,7 +23,7 @@ def self.from(pattern, graph_name: nil, **options) # @option options [Variable, URI, Symbol, nil] :predicate (nil) # @option options [Variable, Term, Symbol, nil] :object (nil) # @option options [Variable, Resource, Symbol, nil, false] :graph_name (nil) - # A graph_name of nil matches any graph, a graph_name of false, matches only the default graph. + # A graph_name of nil matches any graph, a graph_name of false, matches only the default graph. (See {RDF::Query#initialize}) # @option options [Boolean] :optional (false) # # @overload initialize(subject, predicate, object, options = {}) @@ -32,7 +32,7 @@ def self.from(pattern, graph_name: nil, **options) # @param [Variable, Termm, Symbol, nil] object # @param [Hash{Symbol => Object}] options # @option options [Variable, Resource, Symbol, nil, false] :graph_name (nil) - # A graph_name of nil matches any graph, a graph_name of false, matches only the default graph. + # A graph_name of nil matches any graph, a graph_name of false, matches only the default graph. (See {RDF::Query#initialize}) # @option options [Boolean] :optional (false) # # @note {Statement} treats symbols as interned {Node} instances, in a {Pattern}, they are treated as {Variable}. diff --git a/lib/rdf/query/variable.rb b/lib/rdf/query/variable.rb index 7d28deeb..2c29c693 100644 --- a/lib/rdf/query/variable.rb +++ b/lib/rdf/query/variable.rb @@ -233,6 +233,8 @@ def hash # Returns `true` if this variable is equivalent to a given `other` # variable. Or, to another Term if bound, or to any other Term # + # @note when comparing against the default graph in an {RDF::Dataset}, `other` will be `false` and not be equal to an unbound variable. + # # @param [Object] other # @return [Boolean] `true` or `false` # @since 0.3.0 diff --git a/lib/rdf/repository.rb b/lib/rdf/repository.rb index 69f053bd..c7cfb0ca 100644 --- a/lib/rdf/repository.rb +++ b/lib/rdf/repository.rb @@ -182,6 +182,8 @@ def supports?(feature) when :validity then @options.fetch(:with_validity, true) when :literal_equality then true when :atomic_write then false + when :rdf_full then false + # FIXME: quoted triples are now deprecated when :quoted_triples then false when :base_direction then false when :snapshots then false @@ -270,6 +272,7 @@ def supports?(feature) when :validity then @options.fetch(:with_validity, true) when :literal_equality then true when :atomic_write then true + when :rdf_full then true when :quoted_triples then true when :base_direction then true when :snapshots then true diff --git a/lib/rdf/vocab/rdfv.rb b/lib/rdf/vocab/rdfv.rb index 03560665..76f72d61 100644 --- a/lib/rdf/vocab/rdfv.rb +++ b/lib/rdf/vocab/rdfv.rb @@ -72,6 +72,10 @@ module RDF # # @return [RDF::Vocabulary::Term] # # @attr_reader :value # + # # Reification predicate + # # @return [RDF::Vocabulary::Term] + # # @attr_reader :reifies + # # # The datatype of RDF literals storing fragments of HTML content. # # @return [RDF::Vocabulary::Term] # # @attr_reader :HTML @@ -253,6 +257,12 @@ def name; "RDF"; end range: "http://www.w3.org/2000/01/rdf-schema#Resource".freeze, isDefinedBy: %(http://www.w3.org/1999/02/22-rdf-syntax-ns#).freeze, type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#Property".freeze + property :reifies, + comment: %(Property relating to a Triple Term.).freeze, + domain: "http://www.w3.org/2000/01/rdf-schema#Resource".freeze, + label: "reifies".freeze, + isDefinedBy: %(http://www.w3.org/1999/02/22-rdf-syntax-ns#).freeze, + type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#Property".freeze # Datatype definitions term :HTML, diff --git a/lib/rdf/writer.rb b/lib/rdf/writer.rb index c8d1ee46..a9afbbdc 100644 --- a/lib/rdf/writer.rb +++ b/lib/rdf/writer.rb @@ -518,7 +518,8 @@ def format_term(term, **options) when RDF::Literal then format_literal(term, **options) when RDF::URI then format_uri(term, **options) when RDF::Node then format_node(term, **options) - when RDF::Statement then format_quotedTriple(term, **options) + # FIXME: quoted triples are now deprecated + when RDF::Statement then term.tripleTerm? ? format_tripleTerm(term, **options) : format_quotedTriple(term, **options) else nil end end @@ -566,7 +567,7 @@ def format_list(value, **options) end ## - # Formats a referenced triple. + # Formats a referenced triple term. # # @example # <<

>>

. @@ -576,8 +577,24 @@ def format_list(value, **options) # @return [String] # @raise [NotImplementedError] unless implemented in subclass # @abstract + def format_tripleTerm(value, **options) + raise NotImplementedError.new("#{self.class}#format_tripleTerm") # override in subclasses + end + + ## + # Formats a referenced quoted triple. + # + # @example + # <<

>>

. + # + # @param [RDF::Statement] value + # @param [Hash{Symbol => Object}] options = ({}) + # @return [String] + # @raise [NotImplementedError] unless implemented in subclass + # @abstract + # @deprecated Quoted Triples are now deprecated in favor of Triple Terms def format_quotedTriple(value, **options) - raise NotImplementedError.new("#{self.class}#format_statement") # override in subclasses + raise NotImplementedError.new("#{self.class}#format_quotedTriple") # override in subclasses end protected diff --git a/rdf.gemspec b/rdf.gemspec index 401e3a81..aaad04e4 100755 --- a/rdf.gemspec +++ b/rdf.gemspec @@ -31,14 +31,16 @@ Gem::Specification.new do |gem| gem.requirements = [] gem.add_runtime_dependency 'link_header', '~> 0.0', '>= 0.0.8' gem.add_runtime_dependency 'bcp47_spec', '~> 0.2' + gem.add_runtime_dependency 'bigdecimal', '~> 3.1', '>= 3.1.5' + gem.add_development_dependency 'base64', '~> 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', '~> 3.13' gem.add_development_dependency 'rspec-its', '~> 1.3' - gem.add_development_dependency 'webmock', '~> 3.19' + gem.add_development_dependency 'webmock', '~> 3.23' 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/model_literal_spec.rb b/spec/model_literal_spec.rb index de00da2b..1cc024bf 100644 --- a/spec/model_literal_spec.rb +++ b/spec/model_literal_spec.rb @@ -133,6 +133,22 @@ def self.literals(*selector) end end + describe "#english?" do + literals(:all).each do |args| + options = args.last.is_a?(Hash) ? args.pop : {} + lit = RDF::Literal.new(*args, **options) + if lit.language? && lit.language.to_s.downcase.start_with?('en') + it "returns true for #{lit.inspect}" do + expect(lit).to be_english + end + else + it "returns false for #{lit.inspect}" do + expect(lit).not_to be_english + end + end + end + end + describe "#datatype" do literals(:all_simple).each do |args| it "returns xsd:string for #{args.inspect}" do @@ -179,6 +195,7 @@ def self.literals(*selector) it "#start_with?" do expect(RDF::Literal('foo')).to be_start_with('foo') expect(RDF::Literal('bar')).not_to be_start_with('foo') + expect(RDF::Literal('foo')).to be_start_with('foo', 'nope') end describe "#==" do diff --git a/spec/model_statement_spec.rb b/spec/model_statement_spec.rb index a159edb8..46558700 100644 --- a/spec/model_statement_spec.rb +++ b/spec/model_statement_spec.rb @@ -75,7 +75,8 @@ it {is_expected.to have_object} its(:object) {is_expected.not_to be_nil} it {is_expected.to be_asserted} - it {is_expected.not_to be_quoted} + it {is_expected.not_to be_tripleTerm} + it {is_expected.not_to be_quoted} # FIXME: quoted triples are deprecated it {is_expected.to be_statement} it {is_expected.not_to be_inferred} its(:terms) {is_expected.to include(s, p, o)} @@ -210,6 +211,12 @@ it {is_expected.to be_inferred} end + context "when marked as tripleTerm" do + subject {RDF::Statement.new(RDF::Node.new, p, o, tripleTerm: true)} + it {is_expected.to be_tripleTerm} + end + + # FIXME: quoted triples are deprecated context "when marked as quoted" do subject {RDF::Statement.new(RDF::Node.new, p, o, quoted: true)} it {is_expected.to be_quoted} diff --git a/spec/model_uri_spec.rb b/spec/model_uri_spec.rb index 8d00d7ba..4813d3b7 100644 --- a/spec/model_uri_spec.rb +++ b/spec/model_uri_spec.rb @@ -749,6 +749,7 @@ "?one.two.three=four" => {"one.two.three" => "four"}, "?one[two][three]=four&one[two][five]=six" => {"one[two][three]" => "four", "one[two][five]" => "six"}, "?one=two&one=three&one=four" => {'one' => ['two', 'three', 'four']}, + "?&a" => {'a' => nil}, }.each do |uri, result| it uri do if result diff --git a/spec/nquads_spec.rb b/spec/nquads_spec.rb index 9dd84178..3e5b1dfa 100644 --- a/spec/nquads_spec.rb +++ b/spec/nquads_spec.rb @@ -180,7 +180,8 @@ end end - context "RDF-star" do + # FIXME: quoted triples are deprecated + context "quoted triples" do statements = { "subject-iii": '<< >> .', "subject-iib": '<< _:o1>> .', @@ -205,7 +206,13 @@ statements.each do |name, st| context name do - let(:graph) {RDF::Graph.new << RDF::NQuads::Reader.new(st, rdfstar: true)} + let(:graph) do + g = RDF::Graph.new + expect do + g << RDF::NQuads::Reader.new(st, rdfstar: true) + end.to write('[DEPRECATION]').to(:error) + g + end it "creates two statements" do expect(graph.count).to eql(1) diff --git a/spec/ntriples_spec.rb b/spec/ntriples_spec.rb index f2828aff..95ae52fa 100644 --- a/spec/ntriples_spec.rb +++ b/spec/ntriples_spec.rb @@ -322,6 +322,7 @@ { "language" => ' "Hello"@en .', "direction" => ' "Hello"@en--ltr .', + "direction2" => ' "Hello"@en--rtl .', }.each_pair do |name, triple| specify "test #{name}" do stmt = reader.new(triple, rdfstar: true).first @@ -431,6 +432,78 @@ end end + context "triple terms" do + ill_statements = { + "subject-iii": '<<( )>> .', + "subject-iib": '<<( _:o1)>> .', + "subject-iil": '<<( "o1")>> .', + "subject-bii": '<<(_:s1 )>> .', + "subject-bib": '<<(_:s1 _:o1)>> .', + "subject-bil": '<<(_:s1 "o")>> .', + "subject-ws": '<<( )>> .', + "recursive-subject": '<<(<<( )>> )>> .', + } + + statements = { + "object-iii": ' <<( )>> .', + "object-iib": ' <<( _:o1)>> .', + "object-iil": ' <<( "o1")>> .', + "object-bii": ' <<(_:s1 )>> .', + "object-bib": ' <<(_:s1 _:o1)>> .', + "object-bil": ' <<(_:s1 "o1")>> .', + "object-ws": ' <<( )>> .', + + "recursive-object": ' <<( <<( )>>)>> .', + } + + context "without rdfstar option" do + it "Raises an error" do + expect do + expect {parse(statements.values.first)}.to raise_error(RDF::ReaderError) + end.to write(:something).to(:error) + end + end + + context "with rdfstar option" do + ill_statements.each do |name, st| + context name do + it "Raises an error" do + expect do + expect {parse(st)}.to raise_error(RDF::ReaderError) + end.to write(:something).to(:error) + end + end + end + + statements.each do |name, st| + context name do + let(:graph) {parse(st, rdfstar: true)} + + it "creates two unquoted statements" do + expect(graph.count).to eql(1) + graph.statements.each do |stmt| + expect(stmt).not_to be_quoted + end + end + + it "has a statement whose object is a statement" do + referencing = graph.statements.first + expect(referencing).to be_a_statement + expect(referencing.object).to be_a_statement + end + + it "statements which are object of another statement are triple terms" do + referencing = graph.statements.first + expect(referencing).to be_a_statement + expect(referencing.object).to be_a_statement + expect(referencing.object).to be_tripleTerm + end + end + end + end + end + + # FIXME: quoted triples are deprecated context "quoted triples" do statements = { "subject-iii": '<< >> .', @@ -458,7 +531,7 @@ context "with rdfstar option" do statements.each do |name, st| context name do - let(:graph) {parse(st, rdfstar: true)} + let(:graph) {parse(st, rdfstar: true, deprecated: true)} it "creates two unquoted statements" do expect(graph.count).to eql(1) @@ -518,6 +591,10 @@ %q( "string"@--ltr .), %r(Expected end of statement) ], + "xx bad dir 3" => [ + %q( "string"@en--LTR .), + %r(Invalid Literal) + ], "nt-syntax-bad-string-05" => [ %q( """abc""" .), %r(Expected end of statement \(found: .* \."\)) @@ -1200,10 +1277,19 @@ def parse(input, **options) options = { validate: false, canonicalize: false, + deprecated: false, }.merge(options) graph = options[:graph] || RDF::Graph.new - RDF::NTriples::Reader.new(input, **options).each do |statement| - graph << statement + if options[:deprecated] + expect do + RDF::NTriples::Reader.new(input, **options).each do |statement| + graph << statement + end + end.to write('[DEPRECATION]').to(:error) + else + RDF::NTriples::Reader.new(input, **options).each do |statement| + graph << statement + end end graph end diff --git a/spec/query_pattern_spec.rb b/spec/query_pattern_spec.rb index 37a68783..bbd1d466 100644 --- a/spec/query_pattern_spec.rb +++ b/spec/query_pattern_spec.rb @@ -282,7 +282,7 @@ end end - context "quoted triples" do + context "triple terms" 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/query_spec.rb b/spec/query_spec.rb index 1aa98941..09f8be51 100644 --- a/spec/query_spec.rb +++ b/spec/query_spec.rb @@ -965,6 +965,43 @@ end end + context "Issues" do + it "issue #442" do + repository = RDF::Repository.new do |r| + # Adding a statement in the default graph + r << RDF::Statement.new( + RDF::URI('http://www.example.com#alice'), + RDF::URI('http://www.example.com#knows'), + RDF::URI('http://www.example.com#bob') + ) + + # Adding a statement in a named graph + r << RDF::Statement.new( + RDF::URI('http://www.example.com#alice'), + RDF::URI('http://www.example.com#knows'), + RDF::URI('http://www.example.com#charlie'), + graph_name: RDF::URI('http://www.example.com#named_graph') + ) + end + + query = RDF::Query.new( + { + RDF::URI('http://www.example.com#alice') => { + RDF::URI('http://www.example.com#knows') => :friend + } + }, + graph_name: RDF::Query::Variable.new(:graph) + ) + + solutions = query.execute(repository) + expect(solutions.count).to eql(1) + expect(solutions.first).to eql(RDF::Query::Solution.new( + friend: RDF::URI('http://www.example.com#charlie'), + graph: RDF::URI('http://www.example.com#named_graph') + )) + end + end + context "Examples" do let!(:graph) {RDF::Graph.new.insert(RDF::Spec.triples.extend(RDF::Enumerable))} subject { diff --git a/spec/vocabulary_spec.rb b/spec/vocabulary_spec.rb index 3c096c6b..548e4a8e 100644 --- a/spec/vocabulary_spec.rb +++ b/spec/vocabulary_spec.rb @@ -479,7 +479,7 @@ }.each do |v, r| context v.to_uri do subject {v} - its(:imported_from) {is_expected.to eq r} + its(:imported_from) {is_expected.to include(*r)} end end