├── .coveralls.yml ├── .github └── workflows │ ├── ci.yml │ └── generate-docs.yml ├── .gitignore ├── AUTHORS ├── CONTRIBUTING.md ├── Gemfile ├── README.md ├── Rakefile ├── UNLICENSE ├── VERSION ├── dependencyci.yml ├── etc └── doap.ttl ├── examples ├── entail_prof.rb ├── gs1-langString.html ├── gs1.html ├── no_class.ttl └── ogp-example.html ├── lib └── rdf │ ├── reasoner.rb │ └── reasoner │ ├── extensions.rb │ ├── format.rb │ ├── owl.rb │ ├── rdfs.rb │ ├── schema.rb │ └── version.rb ├── rdf-reasoner.gemspec ├── script └── reason └── spec ├── .gitignore ├── format_spec.rb ├── lint_spec.rb ├── matchers.rb ├── owl_spec.rb ├── rdfs_spec.rb ├── readme_spec.rb ├── schema_spec.rb ├── spec_helper.rb ├── suite_helper.rb └── suite_spec.rb /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 5dBrPdCfXwVQ74ngZ0JjaCm1RLqSRmK6R 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs continuous CI across different versions of ruby on all branches and pull requests to develop. 2 | 3 | name: CI 4 | on: 5 | push: 6 | branches: [ '**' ] 7 | pull_request: 8 | branches: [ develop ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | tests: 13 | name: Ruby ${{ matrix.ruby }} 14 | if: "contains(github.event.commits[0].message, '[ci skip]') == false" 15 | runs-on: ubuntu-latest 16 | env: 17 | CI: true 18 | ALLOW_FAILURES: ${{ endsWith(matrix.ruby, 'head') }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | ruby: ['3.0', 3.1, 3.2, 3.3, ruby-head, jruby] 23 | steps: 24 | - name: Clone repository 25 | uses: actions/checkout@v3 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | - name: Install dependencies 31 | run: bundle install --jobs 4 --retry 3 32 | - name: Run tests 33 | run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES 34 | - name: Coveralls GitHub Action 35 | uses: coverallsapp/github-action@v2 36 | if: "matrix.ruby == '3.3'" 37 | with: 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/generate-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build & deploy documentation 2 | on: 3 | push: 4 | branches: 5 | - master 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: Update gh-pages with docs 11 | steps: 12 | - name: Clone repository 13 | uses: actions/checkout@v3 14 | - name: Set up Ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: "3.1" 18 | - name: Install required gem dependencies 19 | run: gem install yard --no-document 20 | - name: Build YARD Ruby Documentation 21 | run: yardoc 22 | - name: Deploy 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./doc/yard 27 | publish_branch: gh-pages 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | /Gemfile.lock 20 | /.byebug_history 21 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Gregg Kellogg 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Community contributions are essential for keeping Ruby RDF great. We want to keep it as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. 4 | 5 | ## Development 6 | 7 | This repository uses [Git Flow](https://github.com/nvie/gitflow) to manage development and release activity. All submissions _must_ be on a feature branch based on the _develop_ branch to ease staging and integration. 8 | 9 | * create or respond to an issue on the [Github Repository](https://github.com/ruby-rdf/rdf-reasoner/issues) 10 | * Fork and clone the repo: 11 | `git clone git@github.com:your-username/rdf-reasoner.git` 12 | * Install bundle: 13 | `bundle install` 14 | * Create tests in RSpec and make sure you achieve at least 90% code coverage for the feature your adding or behavior being modified. 15 | * Push to your fork and [submit a pull request][pr]. 16 | 17 | ## Do's and Dont's 18 | * Do your best to adhere to the existing coding conventions and idioms. 19 | * Don't use hard tabs, and don't leave trailing whitespace on any line. 20 | Before committing, run `git diff --check` to make sure of this. 21 | * Do document every method you add using [YARD][] annotations. Read the 22 | [tutorial][YARD-GS] or just look at the existing code for examples. 23 | * Don't touch the `.gemspec` or `VERSION` files. If you need to change them, 24 | do so on your private branch only. 25 | * Do feel free to add yourself to the `CREDITS` file and the 26 | corresponding list in the the `README`. Alphabetical order applies. 27 | * Don't touch the `AUTHORS` file. If your contributions are significant 28 | enough, be assured we will eventually add you in there. 29 | * Do note that in order for us to merge any non-trivial changes (as a rule 30 | of thumb, additions larger than about 15 lines of code), we need an 31 | explicit [public domain dedication][PDD] on record from you, 32 | which you will be asked to agree to on the first commit to a repo within the organization. 33 | Note that the agreement applies to all repos in the [Ruby RDF](https://github.com/ruby-rdf/) organization. 34 | 35 | [YARD]: https://yardoc.org/ 36 | [YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md 37 | [PDD]: https://unlicense.org/#unlicensing-contributions 38 | [pr]: https://github.com/ruby-rdf/rdf/compare/ 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rdf", github: "ruby-rdf/rdf", branch: "develop" 6 | gem 'rdf-xsd', github: "ruby-rdf/rdf-xsd", branch: "develop" 7 | 8 | group :development, :test do 9 | gem 'ebnf', github: "dryruby/ebnf", branch: "develop" 10 | gem 'json-ld', github: "ruby-rdf/json-ld", branch: "develop" 11 | gem "rdf-aggregate-repo", git: "https://github.com/ruby-rdf/rdf-aggregate-repo", branch: "develop" 12 | gem 'rdf-isomorphic', github: "ruby-rdf/rdf-isomorphic", branch: "develop" 13 | gem "rdf-rdfa", github: "ruby-rdf/rdf-rdfa", branch: "develop" 14 | gem "rdf-spec", github: "ruby-rdf/rdf-spec", branch: "develop" 15 | gem 'rdf-turtle', github: "ruby-rdf/rdf-turtle", branch: "develop" 16 | gem "rdf-vocab", github: "ruby-rdf/rdf-vocab", branch: "develop" 17 | gem 'sxp', github: "dryruby/sxp.rb", branch: "develop" 18 | gem 'rake' 19 | gem 'simplecov', '~> 0.22', platforms: :mri 20 | gem 'simplecov-lcov', '~> 0.8', platforms: :mri 21 | end 22 | 23 | group :debug do 24 | gem "redcarpet", platforms: :ruby 25 | gem "byebug", platforms: :mri 26 | end 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RDF::Reasoner 2 | 3 | A partial RDFS/OWL/schema.org reasoner for [RDF.rb][]. 4 | 5 | [![Gem Version](https://badge.fury.io/rb/rdf-reasoner.svg)](https://badge.fury.io/rb/rdf-reasoner) 6 | [![Build Status](https://github.com/ruby-rdf/rdf-reasoner/workflows/CI/badge.svg?branch=develop)](https://github.com/ruby-rdf/rdf-reasoner/actions?query=workflow%3ACI) 7 | [![Coverage Status](https://coveralls.io/repos/ruby-rdf/rdf-reasoner/badge.svg?branch=develop)](https://coveralls.io/github/ruby-rdf/rdf-reasoner?branch=develop) 8 | [![Gitter chat](https://badges.gitter.im/ruby-rdf/rdf.png)](https://gitter.im/ruby-rdf/rdf) 9 | 10 | ## Description 11 | 12 | Reasons over RDFS/OWL vocabularies and schema.org to generate statements which are entailed based on base RDFS/OWL rules along with vocabulary information. It can also be used to ask specific questions, such as if a given object is consistent with the vocabulary ruleset. This can be used to implement [SPARQL Entailment][] Regimes and [RDF Schema][] entailment. 13 | 14 | ## Features 15 | 16 | * Entail `rdfs:subClassOf` generating an array of terms which are ancestors of the subject. 17 | * Entail `rdfs:subPropertyOf` generating an array of terms which are ancestors of the subject. 18 | * Entail `rdfs:domain` and `rdfs:range` adding `rdf:type` assertions on the subject or object. 19 | * Inverse `rdfs:subClassOf` entailment, to find descendant classes of the subject term. 20 | * Inverse `rdfs:subPropertyOf` entailment, to find descendant properties of the subject term. 21 | * Entail `owl:equivalentClass` generating an array of terms equivalent to the subject. 22 | * Entail `owl:equivalentProperty` generating an array of terms equivalent to the subject. 23 | * `domainCompatible?` determines if a particular resource is compatible with the domain definition of a given predicate, based on the intersection of entailed subclasses with the property domain. 24 | * `rangeCompatible?` determines if a particular resource is compatible with the range definition of a given predicate, based on the intersection of entailed subclasses or literal types with the property domain. 25 | * adds `entail` and `lint` commands to the `rdf` command line interface 26 | 27 | Domain and Range entailment include specific rules for schema.org vocabularies. 28 | 29 | * A plain literal is an acceptable value for any property. 30 | * If `resource` is of type `schema:Role`, `resource` is domain acceptable if any other resource references `resource` using the same property. 31 | * If `resource` is of type `schema:Role`, it is range acceptable if it has the same property with an acceptable value. 32 | * If `resource` is of type `rdf:List` (must be previously entailed), it is range acceptable if all members of the list are otherwise range acceptable on the same property. 33 | 34 | ### Limiting vocabularies used for reasoning 35 | 36 | As loading vocabularies can dominate processing time, the `RDF::Vocabulary.limit_vocabs` method can be used to set a specific set of vocabularies over which to reason. For example: 37 | 38 | RDF::Vocabulary.limit_vocabs(:rdf, :rdf, :schema) 39 | 40 | will limit the vocabularies which are returned from `RDF::Vocabulary.each`, which is used for reasoning and other operations over vocabularies and terms. 41 | 42 | ## Examples 43 | ### Determine super-classes of a class 44 | 45 | require 'rdf/reasoner' 46 | 47 | RDF::Reasoner.apply(:rdfs) 48 | term = RDF::Vocabulary.find_term("http://xmlns.com/foaf/0.1/Person") 49 | term.entail(:subClassOf) 50 | # => [ 51 | foaf:Agent, 52 | http://www.w3.org/2000/10/swap/pim/contact#Person, 53 | geo:SpatialThing, 54 | foaf:Person 55 | ] 56 | 57 | ### Determine sub-classes of a class 58 | 59 | require 'rdf/reasoner' 60 | 61 | RDF::Reasoner.apply(:rdfs) 62 | term = RDF::Vocab::FOAF.Person 63 | term.entail(:subClass) # => [foaf:Person, mo:SoloMusicArtist] 64 | 65 | ### Determine if a resource is compatible with the domains of a property 66 | 67 | require 'rdf/reasoner' 68 | require 'rdf/turtle' 69 | 70 | RDF::Reasoner.apply(:rdfs) 71 | graph = RDF::Graph.load("etc/doap.ttl") 72 | subj = RDF::URI("https://rubygems.org/gems/rdf-reasoner") 73 | RDF::Vocab::DOAP.name.domain_compatible?(subj, graph) # => true 74 | 75 | ### Determine if a resource is compatible with the ranges of a property 76 | 77 | require 'rdf/reasoner' 78 | require 'rdf/turtle' 79 | 80 | RDF::Reasoner.apply(:rdfs) 81 | graph = RDF::Graph.load("etc/doap.ttl") 82 | obj = RDF::Literal(Date.new) 83 | RDF::Vocab::DOAP.created.range_compatible?(obj, graph) # => true 84 | 85 | ### Perform equivalentClass entailment on a graph 86 | 87 | require 'rdf/reasoner' 88 | require 'rdf/turtle' 89 | 90 | RDF::Reasoner.apply(:owl) 91 | graph = RDF::Graph.load("etc/doap.ttl") 92 | graph.entail!(:equivalentClass) 93 | 94 | ### Yield all entailed statements for all entailment methods 95 | 96 | require 'rdf/reasoner' 97 | require 'rdf/turtle' 98 | 99 | RDF::Reasoner.apply(:rdfs, :owl) 100 | graph = RDF::Graph.load("etc/doap.ttl") 101 | graph.enum_statement.entail.count # >= graph.enum_statement.count 102 | 103 | ### Lint an expanded graph 104 | 105 | require 'rdf/reasoner' 106 | require 'rdf/turtle' 107 | 108 | RDF::Reasoner.apply(:rdfs, :owl) 109 | graph = RDF::Graph.load("etc/doap.ttl") 110 | graph.entail! 111 | messages = graph.lint 112 | messages.each do |kind, term_messages| 113 | term_messages.each do |term, messages| 114 | options[:output].puts "#{kind} #{term}" 115 | messages.each {|m| options[:output].puts " #{m}"} 116 | end 117 | end 118 | 119 | ## Command-Line interface 120 | The `rdf` command-line interface is extended with `entail` and `lint` commands. `Entail` can be used in combination, with `serialize` to generate an output graph representation including entailed triples. 121 | 122 | ## Dependencies 123 | 124 | * [Ruby](https://ruby-lang.org/) (>= 3.0) 125 | * [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.3) 126 | 127 | ## Change Log 128 | 129 | See [Release Notes on GitHub](https://github.com/ruby-rdf/rdf-reasoner/releases) 130 | 131 | ## Mailing List 132 | 133 | * 134 | 135 | ## Authors 136 | 137 | * [Gregg Kellogg](https://github.com/gkellogg) - 138 | 139 | ## Contributing 140 | 141 | * Do your best to adhere to the existing coding conventions and idioms. 142 | * Don't use hard tabs, and don't leave trailing whitespace on any line. 143 | Before committing, run `git diff --check` to make sure of this. 144 | * Do document every method you add using [YARD][] annotations. Read the 145 | [tutorial][YARD-GS] or just look at the existing code for examples. 146 | * Don't touch the `.gemspec`, `VERSION` or `AUTHORS` files. If you need to 147 | change them, do so on your private branch only. 148 | * Do feel free to add yourself to the `CREDITS` file and the corresponding 149 | list in the the `README`. Alphabetical order applies. 150 | * Do note that in order for us to merge any non-trivial changes (as a rule 151 | of thumb, additions larger than about 15 lines of code), we need an 152 | explicit [public domain dedication][PDD] on record from you, 153 | which you will be asked to agree to on the first commit to a repo within the organization. 154 | Note that the agreement applies to all repos in the [Ruby RDF](https://github.com/ruby-rdf/) organization. 155 | 156 | ## License 157 | 158 | This is free and unencumbered public domain software. For more information, 159 | see or the accompanying {file:UNLICENSE} file. 160 | 161 | [Ruby]: https://ruby-lang.org/ 162 | [RDF]: https://www.w3.org/RDF/ 163 | [YARD]: https://yardoc.org/ 164 | [YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md 165 | [PDD]: https://unlicense.org/#unlicensing-contributions 166 | [SPARQL]: https://en.wikipedia.org/wiki/SPARQL 167 | [SPARQL Query]: https://www.w3.org/TR/2013/REC-sparql11-query-20130321/ 168 | [SPARQL Entailment]:https://www.w3.org/TR/sparql11-entailment/ 169 | [RDF 1.1]: https://www.w3.org/TR/rdf11-concepts 170 | [RDF.rb]: https://ruby-rdf.github.io/rdf/ 171 | [RDF Schema]: https://www.w3.org/TR/rdf-schema/ 172 | [Rack]: https://rack.github.io/ 173 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'lib'))) 3 | require 'rubygems' 4 | 5 | namespace :gem do 6 | desc "Build the rdf-reasoner-#{File.read('VERSION').chomp}.gem file" 7 | task :build do 8 | sh "gem build rdf-reasoner.gemspec && mv rdf-reasoner-#{File.read('VERSION').chomp}.gem pkg/" 9 | end 10 | 11 | desc "Release the rdf-reasoner-#{File.read('VERSION').chomp}.gem file" 12 | task :release do 13 | sh "gem push pkg/rdf-reasoner-#{File.read('VERSION').chomp}.gem" 14 | end 15 | end 16 | 17 | require 'yard' 18 | namespace :doc do 19 | YARD::Rake::YardocTask.new 20 | end 21 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.9.0 2 | -------------------------------------------------------------------------------- /dependencyci.yml: -------------------------------------------------------------------------------- 1 | platform: 2 | Rubygems: 3 | rdf-isomorphic: 4 | tests: 5 | unmaintained: skip -------------------------------------------------------------------------------- /etc/doap.ttl: -------------------------------------------------------------------------------- 1 | @base . 2 | @prefix rdf: . 3 | @prefix rdfs: . 4 | @prefix dc: . 5 | @prefix earl: . 6 | @prefix foaf: . 7 | @prefix doap: . 8 | @prefix ex: . 9 | @prefix xsd: . 10 | 11 | <> a doap:Project, earl:TestSubject, earl:Software ; 12 | doap:name "RDF::Reasoner" ; 13 | doap:homepage ; 14 | doap:license ; 15 | doap:shortdesc "RDFS/OWL/Schema.org Reasoner for RDF.rb."@en ; 16 | doap:description """ 17 | Reasons over RDFS/OWL vocabularies to generate statements which are 18 | entailed based on base RDFS/OWL rules along with vocabulary information. It 19 | can also be used to ask specific questions, such as if a given object is 20 | consistent with the vocabulary ruleset. This can be used to implement 21 | SPARQL Entailment Regimes. 22 | """@en ; 23 | doap:created "2014-06-01"^^xsd:date ; 24 | doap:programming-language "Ruby" ; 25 | doap:implements , 26 | ; 27 | doap:category , 28 | ; 29 | doap:download-page <> ; 30 | doap:mailing-list ; 31 | doap:bug-database ; 32 | doap:blog ; 33 | doap:developer ; 34 | doap:maintainer ; 35 | doap:documenter ; 36 | foaf:maker ; 37 | dc:creator ; 38 | dc:isPartOf . 39 | 40 | a foaf:Person, foaf:Agent, dc:Agent; 41 | foaf:name "Gregg Kellogg". -------------------------------------------------------------------------------- /examples/entail_prof.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift(File.expand_path("../../lib", __FILE__)) 3 | require 'rubygems' 4 | require 'ruby-prof' 5 | require 'rdf/reasoner' 6 | 7 | output_dir = File.expand_path("../../doc/profiles/#{File.basename __FILE__, ".rb"}", __FILE__) 8 | FileUtils.mkdir_p(output_dir) 9 | 10 | RDF::Reasoner.apply(:rdfs) 11 | 12 | result = RubyProf.profile do 13 | RDF::Vocab::SCHEMA.Event.entail(:subClass).map(&:pname) 14 | end 15 | 16 | # Print a graph profile to text 17 | printer = RubyProf::MultiPrinter.new(result) 18 | printer.print(path: output_dir, profile: "profile") 19 | puts "output saved in #{output_dir}" 20 | -------------------------------------------------------------------------------- /examples/gs1-langString.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/gs1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | GS1 Denmark - GS1 Denmark 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 38 | 183 | 184 | 185 | 186 | 188 | 193 | 194 |
195 | 196 |
197 |
198 |
199 | 200 | 209 |
210 | 215 | 225 | 226 | 227 |
228 |
229 |
230 | 231 | 432 |
433 | 434 | 435 | 436 | 437 | 438 | 484 | 485 | 488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 | 498 | 508 | 509 |
510 |
511 |
512 |
513 |
514 |
515 | 516 |
517 |
518 | 535 |
536 |
537 |
538 |
539 | 577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 | 587 |
588 |

Sådan hjælper GS1 i sundhedssektoren

589 |

Se hvor denne korte film.

590 | 591 |
592 |
593 |
594 | 606 |
607 |
608 |
609 |
610 | 611 |
612 |
613 |
614 |
615 |
616 | 624 |
625 |
626 | 632 |
633 |
634 |
635 | 641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 | 653 | 654 | 655 | 656 | 657 |
658 |
659 |
660 |
661 | 662 | 663 |
664 | 748 |
749 |
750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | -------------------------------------------------------------------------------- /examples/no_class.ttl: -------------------------------------------------------------------------------- 1 | @prefix schema: . 2 | a schema:NoSuchClass . 3 | -------------------------------------------------------------------------------- /examples/ogp-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | The Rock (1996) 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ... 44 | 45 | -------------------------------------------------------------------------------- /lib/rdf/reasoner.rb: -------------------------------------------------------------------------------- 1 | require 'rdf' 2 | require 'rdf/reasoner/extensions' 3 | 4 | module RDF 5 | ## 6 | # RDFS/OWL reasonsing for RDF.rb. 7 | # 8 | # @see https://www.w3.org/TR/2013/REC-sparql11-entailment-20130321/ 9 | # @author [Gregg Kellogg](https://greggkellogg.net/) 10 | module Reasoner 11 | require 'rdf/reasoner/format' 12 | autoload :OWL, 'rdf/reasoner/owl' 13 | autoload :RDFS, 'rdf/reasoner/rdfs' 14 | autoload :Schema, 'rdf/reasoner/schema' 15 | autoload :VERSION, 'rdf/reasoner/version' 16 | 17 | # See https://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/ 18 | # 19 | # 20 | ISO_8601 = %r(^ 21 | # Year 22 | ([\+-]?\d{4}(?!\d{2}\b)) 23 | # Month 24 | ((-?)((0[1-9]|1[0-2]) 25 | (\3([12]\d|0[1-9]|3[01]))? 26 | | W([0-4]\d|5[0-2])(-?[1-7])? 27 | | (00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6]))) 28 | ([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00) 29 | ([\.,]\d+(?!:))?)? 30 | (\17[0-5]\d([\.,]\d+)?)? 31 | ([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)? 32 | )? 33 | )? 34 | $)x.freeze 35 | 36 | ## 37 | # Add entailment support for the specified regime 38 | # 39 | # @param [Array<:owl, :rdfs, :schema>] regime 40 | def apply(*regime) 41 | regime.each {|r| require "rdf/reasoner/#{r.to_s.downcase}"} 42 | end 43 | module_function :apply 44 | 45 | ## 46 | # Add all entailment regimes 47 | def apply_all 48 | apply(*%w(rdfs owl schema)) 49 | end 50 | module_function :apply_all 51 | 52 | ## 53 | # A reasoner error 54 | class Error < RuntimeError; end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/rdf/reasoner/extensions.rb: -------------------------------------------------------------------------------- 1 | # Extensions to RDF core classes to support reasoning 2 | require 'rdf' 3 | 4 | module RDF 5 | class URI 6 | class << self 7 | @@entailments = {} 8 | 9 | ## 10 | # Add an entailment method. The method accepts no arguments, and returns or yields an array of values associated with the particular entailment method 11 | # @param [Symbol] method 12 | # @param [Proc] proc 13 | def add_entailment(method, proc) 14 | @@entailments[method] = proc 15 | end 16 | end 17 | 18 | ## 19 | # Perform an entailment on this term. 20 | # 21 | # @param [Symbol] method A registered entailment method 22 | # @yield term 23 | # @yieldparam [Term] term 24 | # @return [Array] 25 | def entail(method, &block) 26 | self.send(@@entailments.fetch(method), &block) 27 | end 28 | 29 | ## 30 | # Determine if the domain of a property term is consistent with the specified resource in `queryable`. 31 | # 32 | # @param [RDF::Resource] resource 33 | # @param [RDF::Queryable] queryable 34 | # @param [Hash{Symbol => Object}] options ({}) 35 | # @option options [Array] :types 36 | # Fully entailed types of resource, if not provided, they are queried 37 | def domain_compatible?(resource, queryable, options = {}) 38 | %w(owl rdfs schema).map {|r| "domain_compatible_#{r}?".to_sym}.all? do |meth| 39 | !self.respond_to?(meth) || self.send(meth, resource, queryable, options) 40 | end 41 | end 42 | 43 | ## 44 | # Determine if the range of a property term is consistent with the specified resource in `queryable`. 45 | # 46 | # Specific entailment regimes should insert themselves before this to apply the appropriate semantic condition 47 | # 48 | # @param [RDF::Resource] resource 49 | # @param [RDF::Queryable] queryable 50 | # @param [Hash{Symbol => Object}] options ({}) 51 | # @option options [Array] :types 52 | # Fully entailed types of resource, if not provided, they are queried 53 | def range_compatible?(resource, queryable, options = {}) 54 | %w(owl rdfs schema).map {|r| "range_compatible_#{r}?".to_sym}.all? do |meth| 55 | !self.respond_to?(meth) || self.send(meth, resource, queryable, options) 56 | end 57 | end 58 | end 59 | 60 | class Node 61 | class << self 62 | @@entailments = {} 63 | 64 | ## 65 | # Add an entailment method. The method accepts no arguments, and returns or yields an array of values associated with the particular entailment method 66 | # @param [Symbol] method 67 | # @param [Proc] proc 68 | def add_entailment(method, proc) 69 | @@entailments[method] = proc 70 | end 71 | end 72 | 73 | ## 74 | # Perform an entailment on this term. 75 | # 76 | # @param [Symbol] method A registered entailment method 77 | # @yield term 78 | # @yieldparam [Term] term 79 | # @return [Array] 80 | def entail(method, &block) 81 | self.send(@@entailments.fetch(method), &block) 82 | end 83 | 84 | ## 85 | # Determine if the domain of a property term is consistent with the specified resource in `queryable`. 86 | # 87 | # @param [RDF::Resource] resource 88 | # @param [RDF::Queryable] queryable 89 | # @param [Hash{Symbol => Object}] options ({}) 90 | # @option options [Array] :types 91 | # Fully entailed types of resource, if not provided, they are queried 92 | def domain_compatible?(resource, queryable, options = {}) 93 | %w(owl rdfs schema).map {|r| "domain_compatible_#{r}?".to_sym}.all? do |meth| 94 | !self.respond_to?(meth) || self.send(meth, resource, queryable, **options) 95 | end 96 | end 97 | 98 | ## 99 | # Determine if the range of a property term is consistent with the specified resource in `queryable`. 100 | # 101 | # Specific entailment regimes should insert themselves before this to apply the appropriate semantic condition 102 | # 103 | # @param [RDF::Resource] resource 104 | # @param [RDF::Queryable] queryable 105 | # @param [Hash{Symbol => Object}] options ({}) 106 | # @option options [Array] :types 107 | # Fully entailed types of resource, if not provided, they are queried 108 | def range_compatible?(resource, queryable, options = {}) 109 | %w(owl rdfs schema).map {|r| "range_compatible_#{r}?".to_sym}.all? do |meth| 110 | !self.respond_to?(meth) || self.send(meth, resource, queryable, options) 111 | end 112 | end 113 | end 114 | 115 | class Statement 116 | class << self 117 | @@entailments = {} 118 | 119 | ## 120 | # Add an entailment method. The method accepts no arguments, and returns or yields an array of values associated with the particular entailment method 121 | # @param [Symbol] method 122 | # @param [Proc] proc 123 | def add_entailment(method, proc) 124 | @@entailments[method] = proc 125 | end 126 | end 127 | 128 | ## 129 | # Perform an entailment on this term. 130 | # 131 | # @param [Symbol] method A registered entailment method 132 | # @yield term 133 | # @yieldparam [Term] term 134 | # @return [Array] 135 | def entail(method, &block) 136 | self.send(@@entailments.fetch(method), &block) 137 | end 138 | end 139 | 140 | module Enumerable 141 | class << self 142 | @@entailments = {} 143 | 144 | ## 145 | # Add an entailment method. The method accepts no arguments, and returns or yields an array of values associated with the particular entailment method 146 | # @param [Symbol] method 147 | # @param [Proc] proc 148 | def add_entailment(method, proc) 149 | @@entailments[method] = proc 150 | end 151 | end 152 | 153 | ## 154 | # Perform entailments on this enumerable in a single pass, yielding entailed statements. 155 | # 156 | # For best results, either run rules separately expanding the enumberated graph, or run repeatedly until no new statements are added to the enumerable containing both original and entailed statements. As `:subClassOf` and `:subPropertyOf` entailments are implicitly recursive, this may not be necessary except for extreme cases. 157 | # 158 | # @overload entail 159 | # @param [Array] *rules 160 | # Registered entailment method(s). 161 | # 162 | # @yield statement 163 | # @yieldparam [RDF::Statement] statement 164 | # @return [void] 165 | # 166 | # @overload entail 167 | # @param [Array] *rules Registered entailment method(s) 168 | # @return [Enumerator] 169 | def entail(*rules, &block) 170 | if block_given? 171 | rules = %w(subClassOf subPropertyOf domain range).map(&:to_sym) if rules.empty? 172 | 173 | self.each do |statement| 174 | rules.each {|rule| statement.entail(rule, &block)} 175 | end 176 | else 177 | # Otherwise, return an Enumerator with the entailed statements 178 | this = self 179 | RDF::Queryable::Enumerator.new do |yielder| 180 | this.entail(*rules) {|y| yielder << y} 181 | end 182 | end 183 | end 184 | end 185 | 186 | module Mutable 187 | class << self 188 | @@entailments = {} 189 | 190 | ## 191 | # Add an entailment method. The method accepts no arguments, and returns or yields an array of values associated with the particular entailment method 192 | # @param [Symbol] method 193 | # @param [Proc] proc 194 | def add_entailment(method, proc) 195 | @@entailments[method] = proc 196 | end 197 | end 198 | 199 | # Return a new mutable, composed of original and entailed statements 200 | # 201 | # @param [Array] rules Registered entailment method(s) 202 | # @return [RDF::Mutable] 203 | # @see [RDF::Enumerable#entail] 204 | def entail(*rules, &block) 205 | self.dup.entail!(*rules) 206 | end 207 | 208 | # Add entailed statements to the mutable 209 | # 210 | # @param [Array] rules Registered entailment method(s) 211 | # @return [RDF::Mutable] 212 | # @see [RDF::Enumerable#entail] 213 | def entail!(*rules, &block) 214 | rules = %w(subClassOf subPropertyOf domain range).map(&:to_sym) if rules.empty? 215 | statements = [] 216 | 217 | self.each do |statement| 218 | rules.each do |rule| 219 | statement.entail(rule) do |st| 220 | statements << st 221 | end 222 | end 223 | end 224 | self.insert(*statements) 225 | self 226 | end 227 | end 228 | 229 | module Queryable 230 | # Lint a queryable, presuming that it has already had RDFS entailment expansion. 231 | # @return [Hash{Symbol => Hash{Symbol => Array}}] messages found for classes and properties by term 232 | def lint 233 | messages = {} 234 | 235 | # Check for defined classes in known vocabularies 236 | self.query({predicate: RDF.type}) do |stmt| 237 | vocab = RDF::Vocabulary.find(stmt.object) 238 | term = (RDF::Vocabulary.find_term(stmt.object) rescue nil) if vocab 239 | pname = term ? term.pname : stmt.object.pname 240 | 241 | # Must be a defined term, not in RDF or RDFS vocabularies 242 | if term && term.class? 243 | # Warn against using a deprecated term 244 | superseded = term.properties[:'http://schema.org/supersededBy'] 245 | superseded = superseded.pname if superseded.respond_to?(:pname) 246 | (messages[:class] ||= {})[pname] = ["Term is superseded by #{superseded}"] if superseded 247 | else 248 | (messages[:class] ||= {})[pname] = ["No class definition found"] unless vocab.nil? || [RDF::RDFV, RDF::RDFS].include?(vocab) 249 | end 250 | end 251 | 252 | # Check for defined predicates in known vocabularies and domain/range 253 | resource_types = {} 254 | self.each_statement do |stmt| 255 | vocab = RDF::Vocabulary.find(stmt.predicate) 256 | term = (RDF::Vocabulary.find_term(stmt.predicate) rescue nil) if vocab 257 | pname = term ? term.pname : stmt.predicate.pname 258 | 259 | # Must be a valid statement 260 | begin 261 | stmt.validate! 262 | rescue 263 | ((messages[:statement] ||= {})[pname] ||= []) << "Triple #{stmt.to_ntriples} is invalid" 264 | end 265 | 266 | # Must be a defined property 267 | if term.respond_to?(:property?) && term.property? 268 | # Warn against using a deprecated term 269 | superseded = term.properties[:'http://schema.org/supersededBy'] 270 | superseded = superseded.pname if superseded.respond_to?(:pname) 271 | (messages[:property] ||= {})[pname] = ["Term is superseded by #{superseded}"] if superseded 272 | else 273 | ((messages[:property] ||= {})[pname] ||= []) << "No property definition found" unless vocab.nil? 274 | next 275 | end 276 | 277 | # See if type of the subject is in the domain of this predicate 278 | resource_types[stmt.subject] ||= self.query({subject: stmt.subject, predicate: RDF.type}). 279 | map {|s| (t = (RDF::Vocabulary.find_term(s.object) rescue nil)) && t.entail(:subClassOf)}. 280 | flatten. 281 | uniq. 282 | compact 283 | 284 | unless term.domain_compatible?(stmt.subject, self, types: resource_types[stmt.subject]) 285 | ((messages[:property] ||= {})[pname] ||= []) << if !term.domain.empty? 286 | "Subject #{show_resource(stmt.subject)} not compatible with domain (#{Array(term.domain).map {|d| d.pname|| d}.join(',')})" 287 | else 288 | domains = Array(term.domainIncludes) + 289 | Array(term.properties[:'https://schema.org/domainIncludes']) 290 | "Subject #{show_resource(stmt.subject)} not compatible with domainIncludes (#{domains.map {|d| d.pname|| d}.join(',')})" 291 | end 292 | end 293 | 294 | # Make sure that if ranges are defined, the object has an appropriate type 295 | resource_types[stmt.object] ||= self.query({subject: stmt.object, predicate: RDF.type}). 296 | map {|s| (t = (RDF::Vocabulary.find_term(s.object) rescue nil)) && t.entail(:subClassOf)}. 297 | flatten. 298 | uniq. 299 | compact if stmt.object.resource? 300 | 301 | unless term.range_compatible?(stmt.object, self, types: resource_types[stmt.object]) 302 | ((messages[:property] ||= {})[pname] ||= []) << if !term.range.empty? 303 | "Object #{show_resource(stmt.object)} not compatible with range (#{Array(term.range).map {|d| d.pname|| d}.join(',')})" 304 | else 305 | ranges = Array(term.rangeIncludes) + 306 | Array(term.properties[:'https://schema.org/rangeIncludes']) 307 | "Object #{show_resource(stmt.object)} not compatible with rangeIncludes (#{ranges.map {|d| d.pname|| d}.join(',')})" 308 | end 309 | end 310 | end 311 | 312 | messages[:class].each {|k, v| messages[:class][k] = v.uniq} if messages[:class] 313 | messages[:property].each {|k, v| messages[:property][k] = v.uniq} if messages[:property] 314 | messages 315 | end 316 | 317 | private 318 | 319 | # Show resource in diagnostic output 320 | def show_resource(resource) 321 | if resource.node? 322 | resource.to_ntriples + '(' + 323 | self.query({subject: resource, predicate: RDF.type}). 324 | map {|s| s.object.uri? ? s.object.pname : s.object.to_ntriples} 325 | .join(',') + 326 | ')' 327 | else 328 | resource.to_ntriples 329 | end 330 | end 331 | end 332 | end -------------------------------------------------------------------------------- /lib/rdf/reasoner/format.rb: -------------------------------------------------------------------------------- 1 | module RDF::Reasoner 2 | ## 3 | # LD::Patch format specification. Note that this format does not define any readers or writers. 4 | # 5 | # @example Obtaining an LD Patch format class 6 | # RDF::Format.for(:reasoner) #=> RDF::Reasoner::Format 7 | # 8 | # @see https://www.w3.org/TR/ldpatch/ 9 | class Format < RDF::Format 10 | 11 | ## 12 | # Hash of CLI commands appropriate for this format 13 | # @return [Hash{Symbol => Lambda(Array, Hash)}] 14 | def self.cli_commands 15 | { 16 | entail: { 17 | description: "Add entailed triples to repository", 18 | help: "entail\nPerform RDFS, OWL and schema.org entailment to expand the repository based on referenced built-in vocabuaries", 19 | control: :button, # Treats this like a separate control in the HTML UI 20 | parse: true, 21 | lambda: ->(argv, opts) do 22 | RDF::Reasoner.apply(:rdfs, :owl, :schema) 23 | start, stmt_cnt = Time.now, RDF::CLI.repository.count 24 | RDF::CLI.repository.entail! 25 | secs, new_cnt = (Time.new - start), (RDF::CLI.repository.count - stmt_cnt) 26 | opts[:logger].info "Entailed #{new_cnt} new statements in #{secs} seconds." 27 | end 28 | }, 29 | lint: { 30 | description: "Lint the repository", 31 | help: "lint\nLint the repository using built-in vocabularies", 32 | parse: true, 33 | option_use: {output_format: :disabled}, 34 | lambda: ->(argv, opts) do 35 | RDF::Reasoner.apply(:rdfs, :owl, :schema) 36 | start = Time.now 37 | # Messages added to opts for appropriate display 38 | opts[:messages].merge!(RDF::CLI.repository.lint) 39 | opts[:output].puts "Linter responded with #{opts[:messages].empty? ? 'no' : ''} messages." 40 | secs = Time.new - start 41 | opts[:logger].info "Linted in #{secs} seconds." 42 | end 43 | } 44 | } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/rdf/reasoner/owl.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | module RDF::Reasoner 4 | ## 5 | # Rules for generating OWL entailment triples 6 | # 7 | # Extends `RDF::URI` and `RDF::Statement` with specific entailment capabilities 8 | module OWL 9 | ## 10 | # @return [RDF::Util::Cache] 11 | # @private 12 | def equivalentClass_cache 13 | @@subPropertyOf_cache ||= {} 14 | end 15 | ## 16 | # @return [RDF::Util::Cache] 17 | # @private 18 | def equivalentProperty_cache 19 | @@equivalentProperty_cache ||= {} 20 | end 21 | 22 | 23 | ## 24 | # For a Term: yield or return inferred equivalentClass relationships 25 | # For a Statement: if predicate is `rdf:types`, yield or return inferred statements having a equivalentClass relationship to the type of this statement 26 | # @private 27 | def _entail_equivalentClass 28 | case self 29 | when RDF::URI, RDF::Node 30 | unless class? 31 | yield self if block_given? 32 | return Array(self) 33 | end 34 | 35 | # Initialize @equivalentClass_cache by iterating over all defined property terms having an `owl:equivalentClass` attribute and adding the source class as an equivalent of the destination class 36 | if equivalentClass_cache.empty? 37 | RDF::Vocabulary.each do |v| 38 | v.each do |term| 39 | term.equivalentClass.each do |equiv| 40 | (equivalentClass_cache[equiv] ||= []) << term 41 | end if term.class? 42 | end 43 | end 44 | end 45 | terms = (self.equivalentClass + Array(equivalentClass_cache[self])).uniq 46 | terms.each {|t| yield t} if block_given? 47 | terms 48 | when RDF::Statement 49 | statements = [] 50 | if self.predicate == RDF.type 51 | if term = (RDF::Vocabulary.find_term(self.object) rescue nil) 52 | term._entail_equivalentClass do |t| 53 | statements << RDF::Statement(**self.to_h.merge(object: t, inferred: true)) 54 | end 55 | end 56 | end 57 | statements.each {|s| yield s} if block_given? 58 | statements 59 | else [] 60 | end 61 | end 62 | 63 | ## 64 | # For a Term: yield or return return inferred equivalentProperty relationships 65 | # For a Statement: yield or return inferred statements having a equivalentProperty relationship to predicate of this statement 66 | # @private 67 | def _entail_equivalentProperty 68 | case self 69 | when RDF::URI, RDF::Node 70 | unless property? 71 | yield self if block_given? 72 | return Array(self) 73 | end 74 | 75 | # Initialize equivalentProperty_cache by iterating over all defined property terms having an `owl:equivalentProperty` attribute and adding the source class as an equivalent of the destination class 76 | if equivalentProperty_cache.empty? 77 | RDF::Vocabulary.each do |v| 78 | v.each do |term| 79 | term.equivalentProperty.each do |equiv| 80 | (equivalentProperty_cache[equiv] ||= []) << term 81 | end if term.property? 82 | end 83 | end 84 | end 85 | terms = (self.equivalentProperty + Array(equivalentProperty_cache[self])).uniq 86 | terms.each {|t| yield t} if block_given? 87 | terms 88 | when RDF::Statement 89 | statements = [] 90 | if term = (RDF::Vocabulary.find_term(self.predicate) rescue nil) 91 | term._entail_equivalentProperty do |t| 92 | statements << RDF::Statement(**self.to_h.merge(predicate: t, inferred: true)) 93 | end 94 | end 95 | statements.each {|s| yield s} if block_given? 96 | statements 97 | else [] 98 | end 99 | end 100 | 101 | def self.included(mod) 102 | mod.add_entailment :equivalentClass, :_entail_equivalentClass 103 | mod.add_entailment :equivalentProperty, :_entail_equivalentProperty 104 | end 105 | end 106 | 107 | # Extend URI with these methods 108 | ::RDF::URI.send(:include, OWL) 109 | 110 | # Extend Statement with these methods 111 | ::RDF::Statement.send(:include, OWL) 112 | 113 | # Extend Enumerable with these methods 114 | ::RDF::Enumerable.send(:include, OWL) 115 | 116 | # Extend Mutable with these methods 117 | ::RDF::Mutable.send(:include, OWL) 118 | end -------------------------------------------------------------------------------- /lib/rdf/reasoner/rdfs.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | module RDF::Reasoner 4 | ## 5 | # Rules for generating RDFS entailment triples 6 | # 7 | # Extends `RDF::URI` and `RDF::Statement` with specific entailment capabilities 8 | module RDFS 9 | ## 10 | # @return [RDF::Util::Cache] 11 | # @private 12 | def subClassOf_cache 13 | @@subClassOf_cache ||= RDF::Util::Cache.new(-1) 14 | end 15 | 16 | ## 17 | # @return [RDF::Util::Cache] 18 | # @private 19 | def subClass_cache 20 | @@subClass_cache_cache ||= RDF::Util::Cache.new(-1) 21 | end 22 | 23 | ## 24 | # @return [RDF::Util::Cache] 25 | # @private 26 | def descendant_cache 27 | @@descendant_cache ||= RDF::Util::Cache.new(-1) 28 | end 29 | 30 | ## 31 | # @return [RDF::Util::Cache] 32 | # @private 33 | def subPropertyOf_cache 34 | @@subPropertyOf_cache ||= RDF::Util::Cache.new(-1) 35 | end 36 | 37 | ## 38 | # @return [RDF::Util::Cache] 39 | # @private 40 | def subProperty_cache 41 | @@subProperty_cache ||= RDF::Util::Cache.new(-1) 42 | end 43 | 44 | ## 45 | # @return [RDF::Util::Cache] 46 | # @private 47 | def descendant_property_cache 48 | @@descendant_property_cache ||= RDF::Util::Cache.new(-1) 49 | end 50 | 51 | ## 52 | # For a Term: yield or return inferred subClassOf relationships by recursively applying to named super classes to get a complete set of classes in the ancestor chain of this class 53 | # For a Statement: if predicate is `rdf:types`, yield or return inferred statements having a subClassOf relationship to the type of this statement 54 | # @todo Should be able to entail owl:Restriction, which is a BNode. This should be allowed, and also add BNode values of that node, recursively, similar to SPARQL concise_bounded_description.uu 55 | # @private 56 | def _entail_subClassOf 57 | case self 58 | when RDF::URI, RDF::Node 59 | unless class? 60 | yield self if block_given? 61 | return Array(self) 62 | end 63 | terms = subClassOf_cache[self] ||= ( 64 | Array(self.subClassOf). 65 | map {|c| c._entail_subClassOf rescue c}. 66 | flatten + 67 | Array(self) 68 | ).compact 69 | terms.each {|t| yield t} if block_given? 70 | terms 71 | when RDF::Statement 72 | statements = [] 73 | if self.predicate == RDF.type 74 | if term = (RDF::Vocabulary.find_term(self.object) rescue nil) 75 | term._entail_subClassOf do |t| 76 | next if t.node? # Don't entail BNodes 77 | statements << RDF::Statement(**self.to_h.merge(object: t, inferred: true)) 78 | end 79 | end 80 | #$stderr.puts("subClassf(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}") 81 | end 82 | statements.each {|s| yield s} if block_given? 83 | statements 84 | else [] 85 | end 86 | end 87 | 88 | ## 89 | # For a Term: yield or return inferred subClass relationships by recursively applying to named sub classes to get a complete set of classes in the descendant chain of this class 90 | # For a Statement: this is a no-op, as it's not useful in this context 91 | # @private 92 | def _entail_subClass 93 | case self 94 | when RDF::URI, RDF::Node 95 | unless class? 96 | yield self if block_given? 97 | return Array(self) 98 | end 99 | terms = descendant_cache[self] ||= ( 100 | Array(self.subClass). 101 | map {|c| c._entail_subClass rescue c}. 102 | flatten + 103 | Array(self) 104 | ).compact 105 | terms.each {|t| yield t} if block_given? 106 | terms 107 | else [] 108 | end 109 | end 110 | 111 | ## 112 | # Get the immediate subclasses of this class. 113 | # 114 | # This iterates over terms defined in the vocabulary of this term, as well as the vocabularies imported by this vocabulary. 115 | # @return [Array] 116 | def subClass 117 | raise RDF::Reasoner::Error, "#{self} Can't entail subClass" unless class? 118 | subClass_cache[self] ||= ([self.vocab] + self.vocab.imported_from).map do |v| 119 | Array(v.properties).select {|p| p.class? && Array(p.subClassOf).include?(self)} 120 | end.flatten.compact 121 | end 122 | 123 | ## 124 | # For a Term: yield or return inferred subPropertyOf relationships by recursively applying to named super classes to get a complete set of classes in the ancestor chain of this class 125 | # For a Statement: yield or return inferred statements having a subPropertyOf relationship to predicate of this statements 126 | # @private 127 | def _entail_subPropertyOf 128 | case self 129 | when RDF::URI, RDF::Node 130 | unless property? 131 | yield self if block_given? 132 | return Array(self) 133 | end 134 | terms = subPropertyOf_cache[self] ||= ( 135 | Array(self.subPropertyOf). 136 | map {|c| c._entail_subPropertyOf rescue c}. 137 | flatten + 138 | Array(self) 139 | ).compact 140 | terms.each {|t| yield t} if block_given? 141 | terms 142 | when RDF::Statement 143 | statements = [] 144 | if term = (RDF::Vocabulary.find_term(self.predicate) rescue nil) 145 | term._entail_subPropertyOf do |t| 146 | statements << RDF::Statement(**self.to_h.merge(predicate: t, inferred: true)) 147 | end 148 | #$stderr.puts("subPropertyOf(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}") 149 | end 150 | statements.each {|s| yield s} if block_given? 151 | statements 152 | else [] 153 | end 154 | end 155 | 156 | ## 157 | # For a Term: yield or return inferred subProperty relationships 158 | # by recursively applying to named subproperties to get a complete 159 | # set of properties in the descendant chain of this property 160 | # 161 | # For a Statement: this is a no-op, as it's not useful in this context 162 | # @private 163 | 164 | def _entail_subProperty 165 | case self 166 | when RDF::URI, RDF::Node 167 | unless property? 168 | yield self if block_given? 169 | return Array(self) 170 | end 171 | 172 | terms = descendant_property_cache[self] ||= ( 173 | Array(self.subProperty).map do |c| 174 | c._entail_subProperty rescue c 175 | end.flatten + Array(self)).compact 176 | 177 | terms.each {|t| yield t } if block_given? 178 | terms 179 | else [] 180 | end 181 | end 182 | 183 | ## 184 | # Get the immediate subproperties of this property. 185 | # 186 | # This iterates over terms defined in the vocabulary of this term, 187 | # as well as the vocabularies imported by this vocabulary. 188 | # @return [Array] 189 | def subProperty 190 | raise RDF::Reasoner::Error, 191 | "#{self} Can't entail subProperty" unless property? 192 | vocabs = [self.vocab] + self.vocab.imported_from 193 | subProperty_cache[self] ||= vocabs.map do |v| 194 | Array(v.properties).select do |p| 195 | p.property? && Array(p.subPropertyOf).include?(self) 196 | end 197 | end.flatten.compact 198 | end 199 | 200 | ## 201 | # For a Statement: yield or return inferred statements having an rdf:type of the domain of the statement predicate 202 | # @todo Should be able to entail owl:unionOf, which is a BNode. This should be allowed, and also add BNode values of that node, recursively, similar to SPARQL concise_bounded_description.uu 203 | # @private 204 | def _entail_domain 205 | case self 206 | when RDF::Statement 207 | statements = [] 208 | if term = (RDF::Vocabulary.find_term(self.predicate) rescue nil) 209 | term.domain.each do |t| 210 | next if t.node? # Don't entail BNodes 211 | statements << RDF::Statement(**self.to_h.merge(predicate: RDF.type, object: t, inferred: true)) 212 | end 213 | end 214 | #$stderr.puts("domain(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}}") 215 | statements.each {|s| yield s} if block_given? 216 | statements 217 | else [] 218 | end 219 | end 220 | 221 | ## 222 | # For a Statement: if object is a resource, yield or return inferred statements having an rdf:type of the range of the statement predicate 223 | # @todo Should be able to entail owl:unionOf, which is a BNode. This should be allowed, and also add BNode values of that node, recursively, similar to SPARQL concise_bounded_description.uu 224 | # @private 225 | def _entail_range 226 | case self 227 | when RDF::Statement 228 | statements = [] 229 | if object.resource? && term = (RDF::Vocabulary.find_term(self.predicate) rescue nil) 230 | term.range.each do |t| 231 | next if t.node? # Don't entail BNodes 232 | statements << RDF::Statement(**self.to_h.merge(subject: self.object, predicate: RDF.type, object: t, inferred: true)) 233 | end 234 | end 235 | #$stderr.puts("range(#{self.predicate.pname}): #{statements.map(&:object).map {|r| r.respond_to?(:pname) ? r.pname : r.to_ntriples}}") 236 | statements.each {|s| yield s} if block_given? 237 | statements 238 | else [] 239 | end 240 | end 241 | 242 | ## 243 | # RDFS requires that if the property has a domain, and the resource has a type that some type matches every domain. 244 | # 245 | # Note that this is different than standard entailment, which simply asserts that the resource has every type in the domain, but this is more useful to check if published data is consistent with the vocabulary definition. 246 | # 247 | # @param [RDF::Resource] resource 248 | # @param [RDF::Queryable] queryable 249 | # @param [Hash{Symbol => Object}] options ({}) 250 | # @option options [Array] :types 251 | # Fully entailed types of resource, if not provided, they are queried 252 | def domain_compatible_rdfs?(resource, queryable, options = {}) 253 | raise RDF::Reasoner::Error, "#{self} can't get domains" unless property? 254 | domains = Array(self.domain).reject(&:node?) - [RDF::OWL.Thing, RDF::RDFS.Resource] 255 | 256 | # Fully entailed types of the resource 257 | types = options.fetch(:types) do 258 | queryable.query({subject: resource, predicate: RDF.type}). 259 | map {|s| (t = (RDF::Vocabulary.find_term(s.object)) rescue nil) && t.entail(:subClassOf)}. 260 | flatten. 261 | uniq. 262 | compact 263 | end unless domains.empty? 264 | 265 | # Every domain must match some entailed type 266 | Array(types).empty? || domains.all? {|d| types.include?(d)} 267 | end 268 | 269 | ## 270 | # RDFS requires that if the property has a range, and the resource has a type that some type matches every range. If the resource is a datatyped Literal, and the range includes a datatype, the resource must be consistent with that. 271 | # 272 | # Note that this is different than standard entailment, which simply asserts that the resource has every type in the range, but this is more useful to check if published data is consistent with the vocabulary definition. 273 | # 274 | # @param [RDF::Resource] resource 275 | # @param [RDF::Queryable] queryable 276 | # @param [Hash{Symbol => Object}] options ({}) 277 | # @option options [Array] :types 278 | # Fully entailed types of resource, if not provided, they are queried 279 | def range_compatible_rdfs?(resource, queryable, options = {}) 280 | raise RDF::Reasoner::Error, "#{self} can't get ranges" unless property? 281 | if !(ranges = Array(self.range).reject(&:node?) - [RDF::OWL.Thing, RDF::RDFS.Resource]).empty? 282 | if resource.literal? 283 | ranges.all? do |range| 284 | if [RDF::RDFS.Literal, RDF.XMLLiteral, RDF.HTML].include?(range) 285 | true # Don't bother checking for validity 286 | elsif range == RDF.langString 287 | # Value must have a language 288 | resource.has_language? 289 | elsif range.start_with?(RDF::XSD) 290 | # XSD types are valid if the datatype matches, or they are plain and valid according to the grammar of the range 291 | resource.datatype == range || 292 | resource.plain? && RDF::Literal.new(resource.value, datatype: range).valid? 293 | elsif range.start_with?("http://ogp.me/ns/class#") 294 | case range 295 | when RDF::URI("http://ogp.me/ns/class#boolean_str") 296 | [RDF::URI("http://ogp.me/ns/class#boolean_str"), RDF::XSD.boolean].include?(resource.datatype) || 297 | resource.plain? && RDF::Literal::Boolean.new(resource.value).valid? 298 | when RDF::URI("http://ogp.me/ns/class#date_time_str") 299 | # Schema.org date based on ISO 8601, mapped to appropriate XSD types for validation 300 | case resource 301 | when RDF::Literal::Date, RDF::Literal::Time, RDF::Literal::DateTime, RDF::Literal::Duration 302 | resource.valid? 303 | else 304 | ISO_8601.match(resource.value) 305 | end 306 | when RDF::URI("http://ogp.me/ns/class#determiner_str") 307 | # The lexical space: "", "the", "a", "an", and "auto". 308 | resource.plain? && (%w(the a an auto) + [""]).include?(resource.value) 309 | when RDF::URI("http://ogp.me/ns/class#float_str") 310 | # A string representation of a 64-bit signed floating point number. Example lexical values include "1.234", "-1.234", "1.2e3", "-1.2e3", and "7E-10". 311 | [RDF::URI("http://ogp.me/ns/class#float_str"), RDF::Literal::Double, RDF::Literal::Float].include?(resource.datatype) || 312 | resource.plain? && RDF::Literal::Double.new(resource.value).valid? 313 | when RDF::URI("http://ogp.me/ns/class#integer_str") 314 | resource.is_a?(RDF::Literal::Integer) || 315 | [RDF::URI("http://ogp.me/ns/class#integer_str")].include?(resource.datatype) || 316 | resource.plain? && RDF::Literal::Integer.new(resource.value).valid? 317 | when RDF::URI("http://ogp.me/ns/class#mime_type_str") 318 | # Valid mime type strings \(e.g., "application/mp3"\). 319 | [RDF::URI("http://ogp.me/ns/class#mime_type_str")].include?(resource.datatype) || 320 | resource.plain? && resource.value =~ %r(^[\w\-\+]+/[\w\-\+]+$) 321 | when RDF::URI("http://ogp.me/ns/class#string") 322 | resource.plain? 323 | when RDF::URI("http://ogp.me/ns/class#url") 324 | # A string of Unicode characters forming a valid URL having the http or https scheme. 325 | u = RDF::URI(resource.value) 326 | resource.datatype == RDF::URI("http://ogp.me/ns/class#url") || 327 | resource.datatype == RDF::XSD.anyURI || 328 | resource.simple? && u.valid? && u.scheme.to_s =~ /^https?$/ 329 | else 330 | # Unknown datatype 331 | false 332 | end 333 | else 334 | false 335 | end 336 | end 337 | else 338 | # Fully entailed types of the resource 339 | types = options.fetch(:types) do 340 | queryable.query({subject: resource, predicate: RDF.type}). 341 | map {|s| (t = (RDF::Vocabulary.find_term(s.object) rescue nil)) && t.entail(:subClassOf)}. 342 | flatten. 343 | uniq. 344 | compact 345 | end 346 | 347 | # If any type is a class, add rdfs:Class 348 | if types.any? {|t| t.is_a?(RDF::Vocabulary::Term) && t.class?} && !types.include?(RDF::RDFS.Class) 349 | types << RDF::RDFS.Class 350 | end 351 | 352 | # Every range must match some entailed type 353 | Array(types).empty? || ranges.all? {|d| types.include?(d)} 354 | end 355 | else 356 | true 357 | end 358 | end 359 | 360 | def self.included(mod) 361 | mod.add_entailment :subClassOf, :_entail_subClassOf 362 | mod.add_entailment :subClass, :_entail_subClass 363 | mod.add_entailment :subPropertyOf, :_entail_subPropertyOf 364 | mod.add_entailment :subProperty, :_entail_subProperty 365 | mod.add_entailment :domain, :_entail_domain 366 | mod.add_entailment :range, :_entail_range 367 | end 368 | end 369 | 370 | # Extend URI with these methods 371 | ::RDF::URI.send(:include, RDFS) 372 | 373 | # Extend Statement with these methods 374 | ::RDF::Statement.send(:include, RDFS) 375 | 376 | # Extend Enumerable with these methods 377 | ::RDF::Enumerable.send(:include, RDFS) 378 | 379 | # Extend Mutable with these methods 380 | ::RDF::Mutable.send(:include, RDFS) 381 | end 382 | -------------------------------------------------------------------------------- /lib/rdf/reasoner/schema.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Also requires RDFS reasoner 4 | require 'rdf/reasoner/rdfs' 5 | 6 | module RDF::Reasoner 7 | ## 8 | # Rules for generating RDFS entailment triples 9 | # 10 | # Extends `RDF::URI` with specific entailment capabilities 11 | module Schema 12 | 13 | ## 14 | # Schema.org requires that if the property has a domain, and the resource has a type that some type matches some domain. 15 | # 16 | # Note that this is different than standard entailment, which simply asserts that the resource has every type in the domain, but this is more useful to check if published data is consistent with the vocabulary definition. 17 | # 18 | # If `resource` is of type `schema:Role`, `resource` is domain acceptable if any other resource references `resource` using this property. 19 | # 20 | # @param [RDF::Resource] resource 21 | # @param [RDF::Queryable] queryable 22 | # @param [Hash{Symbol => Object}] options 23 | # @option options [Array] :types 24 | # Fully entailed types of resource, if not provided, they are queried 25 | def domain_compatible_schema?(resource, queryable, options = {}) 26 | raise RDF::Reasoner::Error, "#{self} can't get domains" unless property? 27 | domains = Array(self.domainIncludes) + 28 | Array(self.properties[:'https://schema.org/domainIncludes']) - 29 | [RDF::OWL.Thing] 30 | 31 | # Fully entailed types of the resource 32 | types = entailed_types(resource, queryable, **options) unless domains.empty? 33 | 34 | # Every domain must match some entailed type 35 | resource_acceptable = Array(types).empty? || domains.any? {|d| types.include?(d)} 36 | 37 | # Resource may still be acceptable if types include schema:Role, and any any other resource references `resource` using this property 38 | resource_acceptable || 39 | (types.include?(RDF::URI("http://schema.org/Role")) || types.include?(RDF::URI("https://schema.org/Role"))) && 40 | !queryable.query({predicate: self, object: resource}).empty? 41 | end 42 | 43 | ## 44 | # Schema.org requires that if the property has a range, and the resource has a type that some type matches some range. If the resource is a datatyped Literal, and the range includes a datatype, the resource must be consistent with that. 45 | # 46 | # If `resource` is of type `schema:Role`, it is range acceptable if it has the same property with an acceptable value. 47 | # 48 | # If `resource` is of type `rdf:List` (must be previously entailed), it is range acceptable if all members of the list are otherwise range acceptable on the same property. 49 | # 50 | # Also, a plain literal (or schema:Text) is always compatible with an object range. 51 | # 52 | # @param [RDF::Resource] resource 53 | # @param [RDF::Queryable] queryable 54 | # @param [Hash{Symbol => Object}] options ({}) 55 | # @option options [Array] :types 56 | # Fully entailed types of resource, if not provided, they are queried 57 | def range_compatible_schema?(resource, queryable, options = {}) 58 | raise RDF::Reasoner::Error, "#{self} can't get ranges" unless property? 59 | if !(ranges = Array(self.rangeIncludes) + 60 | Array(self.properties[:'https://schema.org/rangeIncludes']) - 61 | [RDF::OWL.Thing]).empty? 62 | if resource.literal? 63 | ranges.any? do |range| 64 | case range 65 | when RDF::RDFS.Literal then true 66 | when RDF::URI("http://schema.org/Text"), RDF::URI("https://schema.org/Text") 67 | resource.plain? || resource.datatype == RDF::URI("http://schema.org/Text") 68 | when RDF::URI("http://schema.org/Boolean"), RDF::URI("https://schema.org/Boolean") 69 | [ 70 | RDF::URI("http://schema.org/Boolean"), 71 | RDF::URI("https://schema.org/Boolean"), 72 | RDF::XSD.boolean 73 | ].include?(resource.datatype) || 74 | resource.plain? && RDF::Literal::Boolean.new(resource.value).valid? 75 | when RDF::URI("http://schema.org/Date"), RDF::URI("https://schema.org/Date") 76 | # Schema.org date based on ISO 8601, mapped to appropriate XSD types for validation 77 | case resource 78 | when RDF::Literal::Date, RDF::Literal::Time, RDF::Literal::DateTime, RDF::Literal::Duration 79 | resource.valid? 80 | else 81 | ISO_8601.match(resource.value) 82 | end 83 | when RDF::URI("http://schema.org/DateTime"), RDF::URI("https://schema.org/DateTime") 84 | resource.datatype == RDF::URI("http://schema.org/DateTime") || 85 | resource.datatype == RDF::URI("https://schema.org/DateTime") || 86 | resource.is_a?(RDF::Literal::DateTime) || 87 | resource.plain? && RDF::Literal::DateTime.new(resource.value).valid? 88 | when RDF::URI("http://schema.org/Duration"), RDF::URI("https://schema.org/Duration") 89 | value = resource.value 90 | value = "P#{value}" unless value.start_with?("P") 91 | resource.datatype == RDF::URI("http://schema.org/Duration") || 92 | resource.datatype == RDF::URI("https://schema.org/Duration") || 93 | resource.is_a?(RDF::Literal::Duration) || 94 | resource.plain? && RDF::Literal::Duration.new(value).valid? 95 | when RDF::URI("http://schema.org/Time"), RDF::URI("https://schema.org/Time") 96 | resource.datatype == RDF::URI("http://schema.org/Time") || 97 | resource.datatype == RDF::URI("https://schema.org/Time") || 98 | resource.is_a?(RDF::Literal::Time) || 99 | resource.plain? && RDF::Literal::Time.new(resource.value).valid? 100 | when RDF::URI("http://schema.org/Number"), RDF::URI("https://schema.org/Number") 101 | resource.is_a?(RDF::Literal::Numeric) || 102 | [ 103 | RDF::URI("http://schema.org/Number"), 104 | RDF::URI("http://schema.org/Float"), 105 | RDF::URI("http://schema.org/Integer"), 106 | RDF::URI("https://schema.org/Number"), 107 | RDF::URI("https://schema.org/Float"), 108 | RDF::URI("https://schema.org/Integer"), 109 | ].include?(resource.datatype) || 110 | resource.plain? && RDF::Literal::Integer.new(resource.value).valid? || 111 | resource.plain? && RDF::Literal::Double.new(resource.value).valid? 112 | when RDF::URI("http://schema.org/Float"), RDF::URI("https://schema.org/Float") 113 | resource.is_a?(RDF::Literal::Double) || 114 | [ 115 | RDF::URI("http://schema.org/Number"), 116 | RDF::URI("http://schema.org/Float"), 117 | RDF::URI("https://schema.org/Number"), 118 | RDF::URI("https://schema.org/Float"), 119 | ].include?(resource.datatype) || 120 | resource.plain? && RDF::Literal::Double.new(resource.value).valid? 121 | when RDF::URI("http://schema.org/Integer"), RDF::URI("https://schema.org/Integer") 122 | resource.is_a?(RDF::Literal::Integer) || 123 | [ 124 | RDF::URI("http://schema.org/Number"), 125 | RDF::URI("http://schema.org/Integer"), 126 | RDF::URI("https://schema.org/Number"), 127 | RDF::URI("https://schema.org/Integer"), 128 | ].include?(resource.datatype) || 129 | resource.plain? && RDF::Literal::Integer.new(resource.value).valid? 130 | when RDF::URI("http://schema.org/URL"), RDF::URI("https://schema.org/URL") 131 | resource.datatype == RDF::URI("http://schema.org/URL") || 132 | resource.datatype == RDF::URI("https://schema.org/URL") || 133 | resource.datatype == RDF::XSD.anyURI || 134 | resource.plain? && RDF::Literal::AnyURI.new(resource.value).valid? 135 | else 136 | # If may be an XSD range, look for appropriate literal 137 | if range.start_with?(RDF::XSD.to_s) 138 | if resource.datatype == RDF::URI(range) 139 | true 140 | else 141 | # Valid if cast as datatype 142 | resource.plain? && RDF::Literal(resource.value, datatype: RDF::URI(range)).valid? 143 | end 144 | else 145 | # Otherwise, presume that the range refers to a typed resource. This is allowed if the value is a plain literal 146 | resource.plain? 147 | end 148 | end 149 | end 150 | elsif %w( 151 | http://schema.org/True 152 | http://schema.org/False 153 | https://schema.org/True 154 | https://schema.org/False 155 | ).include?(resource) && 156 | (ranges.include?(RDF::URI("http://schema.org/Boolean")) || ranges.include?(RDF::URI("https://schema.org/Boolean"))) 157 | true # Special case for schema boolean resources 158 | elsif (ranges.include?(RDF::URI("http://schema.org/URL")) || ranges.include?(RDF::URI("https://schema.org/URL"))) && 159 | resource.uri? 160 | true # schema:URL matches URI resources 161 | elsif ranges == [RDF::URI("http://schema.org/Text")] && resource.uri? 162 | # Allowed if resource is untyped 163 | entailed_types(resource, queryable, **options).empty? 164 | elsif ranges == [RDF::URI("https://schema.org/Text")] && resource.uri? 165 | # Allowed if resource is untyped 166 | entailed_types(resource, queryable, **options).empty? 167 | elsif literal_range?(ranges) 168 | false # If resource isn't literal, this is a range violation 169 | else 170 | # Fully entailed types of the resource 171 | types = entailed_types(resource, queryable, **options) 172 | 173 | # Every range must match some entailed type 174 | resource_acceptable = Array(types).empty? || ranges.any? {|d| types.include?(d)} 175 | 176 | # Resource may still be acceptable if it has the same property with an acceptable value 177 | resource_acceptable || 178 | 179 | # Resource also acceptable if it is a Role, and the Role object contains the same predicate having a compatible object 180 | (types.include?(RDF::URI("http://schema.org/Role")) || types.include?(RDF::URI("https://schema.org/Role"))) && 181 | queryable.query({subject: resource, predicate: self}).any? do |stmt| 182 | acc = self.range_compatible_schema?(stmt.object, queryable) 183 | acc 184 | end || 185 | # Resource also acceptable if it is a List, and every member of the list is range compatible with the predicate 186 | (list = RDF::List.new(subject: resource, graph: queryable)).valid? && list.all? do |member| 187 | self.range_compatible_schema?(member, queryable) 188 | end 189 | end 190 | else 191 | true 192 | end 193 | end 194 | 195 | # Are all ranges literal? 196 | # @param [Array] ranges 197 | # @return [Boolean] 198 | def literal_range?(ranges) 199 | ranges.all? do |range| 200 | case range 201 | when RDF::RDFS.Literal, 202 | RDF::URI("http://schema.org/Text"), 203 | RDF::URI("http://schema.org/Boolean"), 204 | RDF::URI("http://schema.org/Date"), 205 | RDF::URI("http://schema.org/DateTime"), 206 | RDF::URI("http://schema.org/Time"), 207 | RDF::URI("http://schema.org/URL"), 208 | RDF::URI("http://schema.org/Number"), 209 | RDF::URI("http://schema.org/Float"), 210 | RDF::URI("http://schema.org/Integer"), 211 | RDF::URI("https://schema.org/Text"), 212 | RDF::URI("https://schema.org/Boolean"), 213 | RDF::URI("https://schema.org/Date"), 214 | RDF::URI("https://schema.org/DateTime"), 215 | RDF::URI("https://schema.org/Time"), 216 | RDF::URI("https://schema.org/URL"), 217 | RDF::URI("https://schema.org/Number"), 218 | RDF::URI("https://schema.org/Float"), 219 | RDF::URI("https://schema.org/Integer") 220 | true 221 | else 222 | # If this is an XSD range, look for appropriate literal 223 | range.start_with?(RDF::XSD.to_s) 224 | end 225 | end 226 | end 227 | 228 | def self.included(mod) 229 | end 230 | 231 | private 232 | # Fully entailed types 233 | def entailed_types(resource, queryable, **options) 234 | options.fetch(:types) do 235 | queryable.query({subject: resource, predicate: RDF.type}). 236 | map {|s| (t = (RDF::Vocabulary.find_term(s.object) rescue nil)) && t.entail(:subClassOf)}. 237 | flatten. 238 | uniq. 239 | compact 240 | end 241 | end 242 | end 243 | 244 | # Extend URI with this methods 245 | ::RDF::URI.send(:include, Schema) 246 | end -------------------------------------------------------------------------------- /lib/rdf/reasoner/version.rb: -------------------------------------------------------------------------------- 1 | module RDF::Reasoner::VERSION 2 | VERSION_FILE = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "..", "VERSION") 3 | MAJOR, MINOR, TINY, EXTRA = File.read(VERSION_FILE).chop.split(".") 4 | 5 | STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.') 6 | 7 | ## 8 | # @return [String] 9 | def self.to_s() STRING end 10 | 11 | ## 12 | # @return [String] 13 | def self.to_str() STRING end 14 | 15 | ## 16 | # @return [Array(Integer, Integer, Integer)] 17 | def self.to_a() STRING.split(".") end 18 | end 19 | -------------------------------------------------------------------------------- /rdf-reasoner.gemspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -rubygems 2 | # -*- encoding: utf-8 -*- 3 | 4 | Gem::Specification.new do |gem| 5 | gem.version = File.read('VERSION').chomp 6 | gem.date = File.mtime('VERSION').strftime('%Y-%m-%d') 7 | 8 | gem.name = "rdf-reasoner" 9 | gem.homepage = "https://github.com/ruby-rdf/rdf-reasoner" 10 | gem.license = 'Unlicense' 11 | gem.summary = "RDFS/OWL Reasoner for RDF.rb" 12 | gem.metadata = { 13 | "documentation_uri" => "https://ruby-rdf.github.io/rdf-reasoner", 14 | "bug_tracker_uri" => "https://github.com/ruby-rdf/rdf-reasoner/issues", 15 | "homepage_uri" => "https://github.com/ruby-rdf/rdf-reasoner", 16 | "mailing_list_uri" => "https://lists.w3.org/Archives/Public/public-rdf-ruby/", 17 | "source_code_uri" => "https://github.com/ruby-rdf/rdf-reasoner", 18 | } 19 | 20 | gem.authors = ['Gregg Kellogg'] 21 | gem.email = 'public-rdf-ruby@w3.org' 22 | 23 | gem.platform = Gem::Platform::RUBY 24 | gem.files = %w(AUTHORS README.md UNLICENSE VERSION) + Dir.glob('lib/**/*.rb') 25 | gem.require_paths = %w(lib) 26 | gem.description = %(Reasons over RDFS/OWL vocabularies to generate statements 27 | which are entailed based on base RDFS/OWL rules along with 28 | vocabulary information. It can also be used to ask specific 29 | questions, such as if a given object is consistent with 30 | the vocabulary ruleset. This can be used to implement 31 | SPARQL Entailment Regimes.).gsub(/\s+/m, ' ') 32 | 33 | gem.required_ruby_version = '>= 3.0' 34 | gem.requirements = [] 35 | gem.add_runtime_dependency 'rdf', '~> 3.3' 36 | gem.add_runtime_dependency 'rdf-xsd', '~> 3.3' 37 | 38 | gem.add_development_dependency 'rdf-spec', '~> 3.3' 39 | gem.add_development_dependency 'rdf-vocab', '~> 3.3' 40 | gem.add_development_dependency 'rdf-turtle', '~> 3.3' 41 | gem.add_development_dependency 'json-ld', '~> 3.3' 42 | gem.add_development_dependency 'equivalent-xml', '~> 0.6' 43 | gem.add_development_dependency 'rspec', '~> 3.12' 44 | gem.add_development_dependency 'yard' , '~> 0.9' 45 | gem.post_install_message = nil 46 | end 47 | -------------------------------------------------------------------------------- /script/reason: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | $:.unshift(File.expand_path("../../lib", __FILE__)) 4 | require 'rdf/reasoner' 5 | %w(linkeddata rdf/turtle rdf/rdfa rdf/vocab).each do |req| 6 | begin 7 | require req 8 | rescue LoadError 9 | end 10 | end 11 | 12 | require 'getoptlong' 13 | require 'ruby-prof' 14 | 15 | def run(reader, file_name:, **options) 16 | if options[:profile] 17 | repo = RDF::Repository.new << reader 18 | 19 | output_dir = File.expand_path("../../doc/profiles/#{File.basename file_name, '.*'}", __FILE__) 20 | FileUtils.mkdir_p(output_dir) 21 | profile = RubyProf::Profile.new 22 | #profile.exclude_methods!(Array, :each, :map) 23 | profile.exclude_method!(Hamster::Hash, :each) 24 | profile.exclude_method!(Hamster::Trie, :each) 25 | #profile.exclude_method!(Kernel, :require) 26 | profile.exclude_method!(Object, :run) 27 | profile.exclude_common_methods! 28 | profile.start 29 | run(repo, file_name: file_name, **options.merge(profile: false)) 30 | result = profile.stop 31 | 32 | # Print a graph profile to text 33 | printer = RubyProf::MultiPrinter.new(result) 34 | printer.print(path: output_dir, profile: "profile") 35 | puts "output saved in #{output_dir}" 36 | return 37 | end 38 | 39 | repo = reader.is_a?(RDF::Queryable) ? reader : RDF::Repository.new << reader 40 | stmt_cnt = repo.count 41 | prefixes = reader.respond_to?(:prefixes) ? reader.prefixes : {} 42 | start = Time.new 43 | if options[:entail] 44 | repo.entail! 45 | secs = Time.new - start 46 | new_cnt = repo.count - stmt_cnt 47 | STDERR.puts "\nEntailed #{new_cnt} new statements in #{secs} seconds." unless options[:quiet] 48 | end 49 | 50 | if options[:validate] 51 | start = Time.new 52 | messages = repo.lint 53 | secs = Time.new - start 54 | STDERR.puts "\nLinted in #{secs} seconds." unless options[:quiet] 55 | messages.each do |kind, term_messages| 56 | term_messages.each do |term, messages| 57 | options[:output].puts "#{kind} #{term}" 58 | messages.each {|m| options[:output].puts " #{m}"} 59 | end 60 | end 61 | elsif !options[:output_format] 62 | # No output 63 | secs = Time.new - start 64 | STDERR.puts "\nReade #{repo.count} statements in #{secs} seconds" unless options[:quiet] 65 | else 66 | writer_options = options[:parser_options].merge(prefixes: prefixes, standard_prefixes: true) 67 | RDF::Writer.for(options[:output_format]).new(options[:output], writer_options) do |w| 68 | w << repo 69 | end 70 | end 71 | end 72 | 73 | RDF::Reasoner.apply_all 74 | 75 | parser_options = {base: nil} 76 | 77 | options = { 78 | parser_options: parser_options, 79 | output: STDOUT, 80 | output_format: nil, 81 | input_format: nil, 82 | } 83 | input = nil 84 | 85 | OPT_ARGS = [ 86 | ["--entail", GetoptLong::NO_ARGUMENT, "Run entailments on input graph"], 87 | ["--format", GetoptLong::REQUIRED_ARGUMENT,"Specify output format when converting to RDF"], 88 | ["--input-format", GetoptLong::REQUIRED_ARGUMENT,"Format of the input document, when converting from RDF."], 89 | ["--output", "-o", GetoptLong::REQUIRED_ARGUMENT,"Output to the specified file path"], 90 | ["--profile", GetoptLong::NO_ARGUMENT, "Run profiler with output to doc/profiles/"], 91 | ["--quiet", GetoptLong::NO_ARGUMENT, "Supress most output other than progress indicators"], 92 | ["--uri", GetoptLong::REQUIRED_ARGUMENT,"URI to be used as the document base"], 93 | ["--validate", GetoptLong::NO_ARGUMENT, "Validate input graph with reasoner"], 94 | ['--vocabs', GetoptLong::REQUIRED_ARGUMENT,"Comma-separated list of vocabulary identifiers over which to limit reasoning"], 95 | ["--help", "-?", GetoptLong::NO_ARGUMENT, "This message"], 96 | ] 97 | def usage 98 | STDERR.puts %{Usage: #{$0} [options] file ...} 99 | width = OPT_ARGS.map do |o| 100 | l = o.first.length 101 | l += o[1].length + 2 if o[1].is_a?(String) 102 | l 103 | end.max 104 | OPT_ARGS.each do |o| 105 | s = " %-*s " % [width, (o[1].is_a?(String) ? "#{o[0,2].join(', ')}" : o[0])] 106 | s += o.last 107 | STDERR.puts s 108 | end 109 | exit(1) 110 | end 111 | 112 | opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]}) 113 | 114 | opts.each do |opt, arg| 115 | case opt 116 | when '--entail' then options[:entail] = true 117 | when '--format' then options[:output_format] = arg.to_sym 118 | when '--input-format' then parser_options[:format] = arg.to_sym 119 | when '--output' then options[:output] = File.open(arg, "w") 120 | when '--profile' then options[:profile] = true 121 | when '--quiet' then options[:quiet] = true 122 | when '--uri' then parser_options[:base] = arg 123 | when '--validate' then options[:validate] = true 124 | when '--vocabs' then (options[:vocabs] ||= []).concat(arg.split(',').map(&:strip)) 125 | when '--help' then usage 126 | end 127 | end 128 | 129 | RDF::Vocabulary.limit_vocabs(*options[:vocabs]) if options[:vocabs] 130 | 131 | if ARGV.empty? 132 | s = input ? input : $stdin.read 133 | RDF::Reader.for(parser_options[:format] || :ntriples).new(input, file_name: 'stdin', **options) do |reader| 134 | run(reader, **options) 135 | end 136 | else 137 | ARGV.each do |file| 138 | RDF::Reader.open(file, **parser_options) do |reader| 139 | run(reader, file_name: file, **options) 140 | end 141 | end 142 | end 143 | puts 144 | -------------------------------------------------------------------------------- /spec/.gitignore: -------------------------------------------------------------------------------- 1 | /w3c-rdf 2 | -------------------------------------------------------------------------------- /spec/format_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | $:.unshift "." 3 | require 'spec_helper' 4 | require 'rdf/spec/format' 5 | 6 | describe RDF::Reasoner::Format do 7 | it_behaves_like 'an RDF::Format' do 8 | let(:format_class) {RDF::Reasoner::Format} 9 | end 10 | 11 | describe ".for" do 12 | formats = [ 13 | :reasoner, 14 | ].each do |arg| 15 | it "discovers with #{arg.inspect}" do 16 | expect(RDF::Format.for(arg)).to eq described_class 17 | end 18 | end 19 | end 20 | 21 | describe "#to_sym" do 22 | specify {expect(described_class.to_sym).to eq :reasoner} 23 | end 24 | 25 | describe ".cli_commands" do 26 | require 'rdf/cli' 27 | let(:ttl) {File.expand_path("../../etc/doap.ttl", __FILE__)} 28 | 29 | it "entails" do 30 | expect {RDF::CLI.exec(["entail", "serialize", ttl], format: :ttl)}.to write.to(:output) 31 | end 32 | 33 | it "lints" do 34 | expect {RDF::CLI.exec(["lint", ttl], format: :ttl)}.to write(/Linter responded with no messages/).to(:output) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/lint_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift "." 2 | require File.join(File.dirname(__FILE__), 'spec_helper') 3 | 4 | describe RDF::Queryable, "#lint" do 5 | before(:all) {RDF::Reasoner.apply(:rdfs, :schema)} 6 | 7 | context "detects undefined vocabulary items" do 8 | { 9 | "undefined class" => [ 10 | %( 11 | @prefix schema: . 12 | a schema:NoSuchClass . 13 | ), 14 | { 15 | class: {"schema:NoSuchClass" => ["No class definition found"]}, 16 | } 17 | ], 18 | "undefined property" => [ 19 | %( 20 | @prefix schema: . 21 | schema:noSuchProperty "bar" . 22 | ), 23 | { 24 | property: {"schema:noSuchProperty" => ["No property definition found"]}, 25 | } 26 | ], 27 | "undefined class from undefined vocabulary" => [ 28 | %( 29 | @prefix ex: . 30 | a ex:Foo . 31 | ), 32 | {} 33 | ], 34 | "undefined property from undefined vocabulary" => [ 35 | %( 36 | @prefix ex: . 37 | ex:shortTitle "bar" . 38 | ), 39 | {} 40 | ], 41 | }.each do |name, (input, errors)| 42 | it name do 43 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 44 | expect(graph.lint).to have_errors errors 45 | end 46 | end 47 | end 48 | 49 | context "detects domain violations" do 50 | { 51 | "type not defined" => [ 52 | %( 53 | @prefix schema: . 54 | a schema:Person; schema:acceptedOffer [a schema:Offer] . 55 | ), 56 | { 57 | property: {"schema:acceptedOffer" => [/Subject .* not compatible with domainIncludes \(schema:Order\)/]}, 58 | } 59 | ], 60 | }.each do |name, (input, errors)| 61 | it name do 62 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 63 | expect(graph.lint).to have_errors errors 64 | end 65 | end 66 | end 67 | 68 | context "detects range violations" do 69 | { 70 | "object of wrong type" => [ 71 | %( 72 | @prefix schema: . 73 | a schema:Order; schema:acceptedOffer [a schema:Thing] . 74 | ), 75 | { 76 | property: {"schema:acceptedOffer" => [/Object .* not compatible with rangeIncludes \(schema:Offer\)/]}, 77 | } 78 | ], 79 | #"object range with literal" => [ 80 | # %( 81 | # @prefix schema: . 82 | # a schema:Order; schema:acceptedOffer "foo" . 83 | # ), 84 | # { 85 | # property: {"schema:acceptedOffer" => [/Object .* not compatible with rangeIncludes \(schema:Offer\)/]}, 86 | # } 87 | #], 88 | "xsd:nonNegativeInteger expected with conforming plain literal" => [ 89 | %( 90 | @prefix sioc: . 91 | sioc:num_authors "bar" . 92 | ), 93 | { 94 | property: {"sioc:num_authors" => [/Object .* not compatible with range \(xsd:nonNegativeInteger\)/]}, 95 | } 96 | ], 97 | "xsd:nonNegativeInteger expected with non-equivalent datatyped literal" => [ 98 | %( 99 | @prefix sioc: . 100 | sioc:num_authors 1 . 101 | ), 102 | { 103 | property: {"sioc:num_authors" => [/Object .* not compatible with range \(xsd:nonNegativeInteger\)/]}, 104 | } 105 | ], 106 | "schema:Text with datatyped literal" => [ 107 | %( 108 | @prefix schema: . 109 | @prefix xsd: . 110 | a schema:Thing; schema:name "foo"^^xsd:token . 111 | ), 112 | { 113 | property: {"schema:name" => [/Object .* not compatible with rangeIncludes \(schema:Text\)/]}, 114 | } 115 | ], 116 | "schema:URL with non-conforming plain literal" => [ 117 | %( 118 | @prefix schema: . 119 | a schema:Thing; schema:url "foo" . 120 | ), 121 | { 122 | property: {"schema:url" => [/Object .* not compatible with rangeIncludes \(schema:URL\)/]}, 123 | } 124 | ], 125 | "schema:Boolean with non-conforming plain literal" => [ 126 | %( 127 | @prefix schema: . 128 | a schema:CreativeWork; schema:isFamilyFriendly "bar" . 129 | ), 130 | { 131 | property: {"schema:isFamilyFriendly" => [/Object .* not compatible with rangeIncludes \(schema:Boolean\)/]}, 132 | } 133 | ], 134 | }.each do |name, (input, errors)| 135 | it name do 136 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 137 | expect(graph.lint).to have_errors errors 138 | end 139 | end 140 | end 141 | 142 | context "detects superseded terms" do 143 | { 144 | "members superseded by member" => [ 145 | %( 146 | @prefix schema: . 147 | a schema:Organization; schema:members "Manny" . 148 | ), 149 | { 150 | property: {"schema:members" => ["Term is superseded by schema:member"]}, 151 | } 152 | ], 153 | }.each do |name, (input, errors)| 154 | it name do 155 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 156 | expect(graph.lint).to have_errors errors 157 | end 158 | end 159 | end 160 | 161 | context "accepts XSD equivalents for schema.org datatypes" do 162 | { 163 | "schema:Text with plain literal" => %( 164 | @prefix schema: . 165 | a schema:Thing; schema:name "bar" . 166 | ), 167 | "schema:Text with language-tagged literal" => %( 168 | @prefix schema: . 169 | a schema:Thing; schema:name "bar"@en . 170 | ), 171 | "schema:URL with matching plain literal" => %( 172 | @prefix schema: . 173 | a schema:Thing; schema:url "http://example/" . 174 | ), 175 | "schema:URL with anyURI" => %( 176 | @prefix schema: . 177 | @prefix xsd: . 178 | a schema:Thing; schema:url "http://example/"^^xsd:anyURI . 179 | ), 180 | "schema:Boolean with matching plain literal" => %( 181 | @prefix schema: . 182 | a schema:CreativeWork; schema:isFamilyFriendly "true" . 183 | ), 184 | "schema:Boolean with boolean" => %( 185 | @prefix schema: . 186 | a schema:CreativeWork; schema:isFamilyFriendly true . 187 | ), 188 | "schema:Boolean with schema:True" => %( 189 | @prefix schema: . 190 | a schema:CreativeWork; schema:isFamilyFriendly schema:True . 191 | ), 192 | "schema:Boolean with schema:False" => %( 193 | @prefix schema: . 194 | a schema:CreativeWork; schema:isFamilyFriendly schema:False . 195 | ), 196 | }.each do |name, input| 197 | it name do 198 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 199 | expect(graph.lint).to eq Hash.new 200 | end 201 | end 202 | end 203 | 204 | context "accepts alternates when any domainIncludes matches" do 205 | { 206 | "one type of several" => %( 207 | @prefix schema: . 208 | a schema:CreativeWork; schema:audience [a schema:Audience] . 209 | ) 210 | }.each do |name, input| 211 | it name do 212 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 213 | expect(graph.lint).to eq Hash.new 214 | end 215 | end 216 | end 217 | 218 | context "accepts alternates when any rangeIncludes matches" do 219 | { 220 | "one type of several" => %( 221 | @prefix schema: . 222 | a schema:Action; schema:agent [a schema:Person] . 223 | ), 224 | "xsd:nonNegativeInteger expected matching datatyped literal" => %( 225 | @prefix sioc: . 226 | @prefix xsd: . 227 | sioc:num_authors "1"^^xsd:nonNegativeInteger . 228 | ), 229 | "xsd:nonNegativeInteger expected with conforming plain literal" => %( 230 | @prefix sioc: . 231 | sioc:num_authors "1" . 232 | ), 233 | "schema:URL with language-tagged literal" => %( 234 | @prefix schema: . 235 | a schema:Thing; schema:url "http://example/"@en . 236 | ) 237 | }.each do |name, input| 238 | it name do 239 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 240 | expect(graph.lint).to eq Hash.new 241 | end 242 | end 243 | end 244 | 245 | context "Role intermediaries" do 246 | { 247 | "Cryptography Users" => { 248 | input: %( 249 | @prefix schema: . 250 | a schema:Organization; 251 | schema:name "Cryptography Users"; 252 | schema:member [ 253 | a schema:OrganizationRole; 254 | schema:member [ 255 | a schema:Person; 256 | schema:name "Alice" 257 | ]; 258 | schema:startDate "1977" 259 | ] . 260 | ), 261 | expected_errors: {} 262 | }, 263 | "Inconsistent properties" => { 264 | input: %( 265 | @prefix schema: . 266 | a schema:Organization; 267 | schema:name "Cryptography Users"; 268 | schema:member [ 269 | a schema:OrganizationRole; 270 | schema:alumni [ 271 | a schema:Person; 272 | schema:name "Alice" 273 | ]; 274 | schema:startDate "1977" 275 | ] . 276 | ), 277 | expected_errors: { 278 | property: { 279 | "schema:member" => [/Object .* not compatible with rangeIncludes \(schema:Organization,schema:Person\)/], 280 | "schema:alumni"=> [/Subject .* not compatible with domainIncludes \(schema:EducationalOrganization,schema:Organization\)/] 281 | } 282 | } 283 | }, 284 | }.each do |name, params| 285 | it name do 286 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(params[:input]) 287 | graph.entail! 288 | expect(graph.lint).to have_errors params[:expected_errors] 289 | end 290 | end 291 | end 292 | 293 | context "List intermediaries" do 294 | { 295 | "creators" => { 296 | input: %( 297 | @prefix schema: . 298 | 299 | a schema:Review; 300 | schema:creator ([a schema:Person; schema:name "John Doe"]) . 301 | ), 302 | expected_errors: {} 303 | }, 304 | }.each do |name, params| 305 | it name do 306 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(params[:input]) 307 | graph.entail! 308 | expect(graph.lint).to have_errors params[:expected_errors] 309 | end 310 | end 311 | end 312 | end 313 | -------------------------------------------------------------------------------- /spec/matchers.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'rdf/isomorphic' 3 | require 'json' 4 | JSON_STATE = JSON::State.new( 5 | indent: " ", 6 | space: " ", 7 | space_before: "", 8 | object_nl: "\n", 9 | array_nl: "\n" 10 | ) 11 | 12 | def normalize(graph) 13 | case graph 14 | when RDF::Queryable then graph 15 | when IO, StringIO 16 | RDF::Graph.new.load(graph, base_uri: @info.about) 17 | else 18 | # Figure out which parser to use 19 | g = RDF::Repository.new 20 | reader_class = detect_format(graph) 21 | reader_class.new(graph, base_uri: @info.about).each {|s| g << s} 22 | g 23 | end 24 | end 25 | 26 | Info = Struct.new(:about, :coment, :trace, :input, :result, :action, :expected) 27 | 28 | RSpec::Matchers.define :have_errors do |errors| 29 | match do |actual| 30 | return false unless actual.keys == errors.keys 31 | actual.each do |area_key, area_values| 32 | return false unless area_values.length == errors[area_key].length 33 | area_values.each do |term, values| 34 | return false unless values.length == errors[area_key][term].length 35 | values.each_with_index do |v, i| 36 | return false unless case m = errors[area_key][term][i] 37 | when Regexp then m.match v 38 | else m == v 39 | end 40 | end 41 | end 42 | end 43 | true 44 | end 45 | 46 | failure_message do |actual| 47 | "expected errors to match #{errors.to_json(JSON::LD::JSON_STATE)}\nwas #{actual.to_json(JSON::LD::JSON_STATE)}" 48 | end 49 | 50 | failure_message_when_negated do |actual| 51 | "expected errors not to match #{errors.to_json(JSON::LD::JSON_STATE)}" 52 | end 53 | end 54 | 55 | RSpec::Matchers.define :be_valid do |info| 56 | match do |actual| 57 | actual.valid? 58 | end 59 | 60 | failure_message do |actual| 61 | "Exprected Graph to be valid\n" + 62 | "Info:\n#{info.logger}" 63 | end 64 | 65 | failure_message_when_negated do |actual| 66 | "Exprected Graph not to be valid\n" + 67 | "Info:\n#{info.logger}" 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/owl_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | $:.unshift "." 3 | require 'spec_helper' 4 | require 'rdf/reasoner/owl' 5 | 6 | describe RDF::Reasoner::OWL do 7 | before(:all) {RDF::Reasoner.apply(:owl)} 8 | let(:ex) {RDF::URI("http://example/")} 9 | 10 | describe :equivalentClass do 11 | { 12 | RDF::Vocab::FOAF.Agent => [RDF::Vocab::DC.Agent], 13 | RDF::Vocab::DC.Agent => [RDF::Vocab::FOAF.Agent], 14 | RDF::Vocab::CERT.PGPCertificate => [RDF::Vocab::WOT.PubKey], 15 | RDF::Vocab::WOT.PubKey => [RDF::Vocab::CERT.PGPCertificate], 16 | }.each do |cls, entails| 17 | context cls.pname do 18 | describe RDF::Vocabulary::Term do 19 | specify {expect(cls.entail(:equivalentClass).map(&:pname)).to include(*entails.map(&:pname))} 20 | specify {expect {|b| cls.entail(:equivalentClass, &b)}.to yield_control.at_least(entails.length)} 21 | end 22 | 23 | describe RDF::Statement do 24 | subject {RDF::Statement(RDF::URI("a"), RDF.type, cls)} 25 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}} 26 | specify {expect(subject.entail(:equivalentClass)).to include(*results)} 27 | specify {expect(subject.entail(:equivalentClass)).to all(be_inferred)} 28 | specify {expect {|b| subject.entail(:equivalentClass, &b)}.to yield_control.at_least(entails.length)} 29 | end 30 | 31 | describe RDF::Enumerable do 32 | subject {[RDF::Statement(RDF::URI("a"), RDF.type, cls)].extend(RDF::Enumerable)} 33 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}} 34 | specify {expect(subject.entail(:equivalentClass)).to be_a(RDF::Enumerable)} 35 | specify {expect(subject.entail(:equivalentClass).to_a).to include(*results)} 36 | specify {expect {|b| subject.entail(:equivalentClass, &b)}.to yield_control.at_least(entails.length)} 37 | end 38 | 39 | describe RDF::Mutable do 40 | subject {RDF::Graph.new << RDF::Statement(RDF::URI("a"), RDF.type, cls)} 41 | let(:results) { 42 | subject.dup.insert(*entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}) 43 | } 44 | specify {expect(subject.entail(:equivalentClass)).to be_a(RDF::Graph)} 45 | specify {expect(subject.entail(:equivalentClass)).to be_equivalent_graph(results)} 46 | specify {expect(subject.entail!(:equivalentClass)).to equal subject} 47 | end 48 | end 49 | end 50 | end 51 | 52 | describe :equivalentProperty do 53 | { 54 | RDF::Vocab::DC.creator => [RDF::Vocab::FOAF.maker], 55 | RDF::Vocab::FOAF.maker => [RDF::Vocab::DC.creator], 56 | RDF::Vocab::SCHEMA.description => [RDF::Vocab::DC.description], 57 | RDF::Vocab::DC.description => [RDF::Vocab::SCHEMA.description], 58 | }.each do |prop, entails| 59 | context prop.pname do 60 | describe RDF::Vocabulary::Term do 61 | specify {expect(prop.entail(:equivalentProperty).map(&:pname)).to include(*entails.map(&:pname))} 62 | specify {expect {|b| prop.entail(:equivalentProperty, &b)}.to yield_control.at_least(entails.length)} 63 | end 64 | 65 | describe RDF::Statement do 66 | subject {RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 67 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), r, RDF::URI("b"))}} 68 | specify {expect(subject.entail(:equivalentProperty)).to include(*results)} 69 | specify {expect(subject.entail(:equivalentProperty)).to all(be_inferred)} 70 | specify {expect {|b| subject.entail(:equivalentProperty, &b)}.to yield_control.at_least(entails.length)} 71 | end 72 | 73 | describe RDF::Enumerable do 74 | subject {[RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))].extend(RDF::Enumerable)} 75 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), r, RDF::URI("b"))}} 76 | specify {expect(subject.entail(:equivalentProperty)).to be_a(RDF::Enumerable)} 77 | specify {expect(subject.entail(:equivalentProperty).to_a).to include(*results)} 78 | specify {expect {|b| subject.entail(:equivalentProperty, &b)}.to yield_control.at_least(entails.length)} 79 | end 80 | 81 | describe RDF::Mutable do 82 | subject {RDF::Graph.new << RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 83 | let(:results) { 84 | subject.dup.insert(*entails.map {|r| RDF::Statement(RDF::URI("a"), r, RDF::URI("b"))}) 85 | } 86 | specify {expect(subject.entail(:equivalentProperty)).to be_a(RDF::Graph)} 87 | specify {expect(subject.entail(:equivalentProperty)).to be_equivalent_graph(results)} 88 | specify {expect(subject.entail!(:equivalentProperty)).to equal subject} 89 | end 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/rdfs_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | $:.unshift "." 3 | require 'spec_helper' 4 | require 'rdf/reasoner/rdfs' 5 | require 'rdf/vocab' 6 | 7 | describe RDF::Reasoner::RDFS do 8 | before(:all) {RDF::Reasoner.apply(:rdfs)} 9 | let(:ex) {RDF::URI("http://example/")} 10 | 11 | describe :subClassOf do 12 | { 13 | RDF::Vocab::FOAF.Group => [RDF::Vocab::FOAF.Group, RDF::Vocab::FOAF.Agent], 14 | RDF::Vocab::CC.License => [RDF::Vocab::CC.License, RDF::Vocab::DC.LicenseDocument], 15 | RDF::Vocab::DC.Location => [RDF::Vocab::DC.Location, RDF::Vocab::DC.LocationPeriodOrJurisdiction], 16 | }.each do |cls, entails| 17 | context cls.pname do 18 | describe RDF::Vocabulary::Term do 19 | specify {expect(cls.entail(:subClassOf).map(&:pname)).to include(*entails.map(&:pname))} 20 | specify {expect {|b| cls.entail(:subClassOf, &b)}.to yield_control.at_least(entails.length)} 21 | end 22 | 23 | describe RDF::Statement do 24 | subject {RDF::Statement(RDF::URI("a"), RDF.type, cls)} 25 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}} 26 | specify {expect(subject.entail(:subClassOf)).to include(*results)} 27 | specify {expect(subject.entail(:subClassOf)).to all(be_inferred)} 28 | specify {expect {|b| subject.entail(:subClassOf, &b)}.to yield_control.at_least(entails.length)} 29 | end 30 | 31 | describe RDF::Enumerable do 32 | subject {[RDF::Statement(RDF::URI("a"), RDF.type, cls)].extend(RDF::Enumerable)} 33 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}} 34 | specify {expect(subject.entail(:subClassOf)).to be_a(RDF::Enumerable)} 35 | specify {expect(subject.entail(:subClassOf).to_a).to include(*results)} 36 | specify {expect {|b| subject.entail(:subClassOf, &b)}.to yield_control.at_least(entails.length)} 37 | end 38 | 39 | describe RDF::Mutable do 40 | subject {RDF::Graph.new << RDF::Statement(RDF::URI("a"), RDF.type, cls)} 41 | let(:results) { 42 | subject.dup.insert(*entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}) 43 | } 44 | specify {expect(subject.entail(:subClassOf)).to be_a(RDF::Graph)} 45 | specify {expect(subject.entail(:subClassOf)).to be_equivalent_graph(results)} 46 | specify {expect(subject.entail!(:subClassOf)).to equal subject} 47 | end 48 | end 49 | end 50 | end 51 | 52 | describe :subClass do 53 | { 54 | RDF::Vocab::FOAF.Group => [RDF::Vocab::FOAF.Group, RDF::Vocab::MO.MusicGroup], 55 | RDF::Vocab::FOAF.Agent => [RDF::Vocab::FOAF.Group, RDF::Vocab::MO.MusicGroup, RDF::Vocab::FOAF.Organization, RDF::Vocab::FOAF.Person, RDF::Vocab::FOAF.Agent], 56 | RDF::Vocab::CC.License => [RDF::Vocab::CC.License], 57 | RDF::Vocab::SCHEMA.Event => [RDF::Vocab::SCHEMA.Event, RDF::Vocab::SCHEMA.Festival, RDF::Vocab::SCHEMA.SportsEvent, RDF::Vocab::SCHEMA.UserLikes], 58 | }.each do |cls, entails| 59 | context cls.pname do 60 | describe RDF::Vocabulary::Term do 61 | specify {expect(cls.entail(:subClass).map(&:pname)).to include(*entails.map(&:pname))} 62 | specify {expect {|b| cls.entail(:subClass, &b)}.to yield_control.at_least(entails.length)} 63 | end 64 | 65 | describe RDF::Statement do 66 | subject {RDF::Statement(RDF::URI("a"), RDF.type, cls)} 67 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}} 68 | specify {expect(subject.entail(:subClass)).to be_empty} 69 | specify {expect(subject.entail(:subClass)).to all(be_inferred)} 70 | specify {expect {|b| subject.entail(:subClass, &b)}.not_to yield_control} 71 | end 72 | 73 | describe RDF::Enumerable do 74 | subject {[RDF::Statement(RDF::URI("a"), RDF.type, cls)].extend(RDF::Enumerable)} 75 | specify {expect(subject.entail(:subClass)).to be_a(RDF::Enumerable)} 76 | specify {expect(subject.entail(:subClass).to_a).to be_empty} 77 | specify {expect {|b| subject.entail(:subClass, &b)}.not_to yield_control} 78 | end 79 | 80 | describe RDF::Mutable do 81 | subject {RDF::Graph.new << RDF::Statement(RDF::URI("a"), RDF.type, cls)} 82 | let(:results) {subject.dup} 83 | specify {expect(subject.entail(:subClass)).to be_a(RDF::Graph)} 84 | specify {expect(subject.entail(:subClass)).to be_equivalent_graph(results)} 85 | specify {expect(subject.entail!(:subClass)).to equal subject} 86 | end 87 | end 88 | end 89 | 90 | it "does not entail a BNode" do 91 | s = RDF::Statement(RDF::URI("a"), RDF.type, RDF::Vocab::SKOSXL.Label) 92 | s.entail(:subClassOf) do |st| 93 | expect(st.object).not_to be_a_node 94 | end 95 | end 96 | end unless ENV['CI'] 97 | 98 | describe :subPropertyOf do 99 | { 100 | RDF::Vocab::FOAF.aimChatID => [RDF::Vocab::FOAF.aimChatID, RDF::Vocab::FOAF.nick], 101 | RDF::Vocab::FOAF.name => [RDF::Vocab::FOAF.name, RDF::RDFS.label], 102 | RDF::Vocab::CC.license => [RDF::Vocab::CC.license, RDF::Vocab::DC.license], 103 | }.each do |prop, entails| 104 | context prop.pname do 105 | describe RDF::Vocabulary::Term do 106 | specify {expect(prop.entail(:subPropertyOf).map(&:pname)).to include(*entails.map(&:pname))} 107 | specify {expect {|b| prop.entail(:subPropertyOf, &b)}.to yield_control.at_least(entails.length)} 108 | end 109 | 110 | describe RDF::Statement do 111 | subject {RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 112 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), r, RDF::URI("b"))}} 113 | specify {expect(subject.entail(:subPropertyOf)).to include(*results)} 114 | specify {expect(subject.entail(:subPropertyOf)).to all(be_inferred)} 115 | specify {expect {|b| subject.entail(:subPropertyOf, &b)}.to yield_control.at_least(entails.length)} 116 | end 117 | 118 | describe RDF::Enumerable do 119 | subject {[RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))].extend(RDF::Enumerable)} 120 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), r, RDF::URI("b"))}} 121 | specify {expect(subject.entail(:subPropertyOf)).to be_a(RDF::Enumerable)} 122 | specify {expect(subject.entail(:subPropertyOf).to_a).to include(*results)} 123 | specify {expect {|b| subject.entail(:subPropertyOf, &b)}.to yield_control.at_least(entails.length)} 124 | end 125 | 126 | describe RDF::Mutable do 127 | subject {RDF::Graph.new << RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 128 | let(:results) { 129 | subject.dup.insert(*entails.map {|r| RDF::Statement(RDF::URI("a"), r, RDF::URI("b"))}) 130 | } 131 | specify {expect(subject.entail(:subPropertyOf)).to be_a(RDF::Graph)} 132 | specify {expect(subject.entail(:subPropertyOf)).to be_equivalent_graph(results)} 133 | specify {expect(subject.entail!(:subPropertyOf)).to equal subject} 134 | end 135 | end 136 | end 137 | end 138 | 139 | # XXX this is cribbed from :subClass 140 | describe :subProperty do 141 | { 142 | RDF::Vocab::DC.source => %w(derived_from djmix_of mashup_of medley_of 143 | remaster_of remix_of sampled_version_of).map {|t| RDF::Vocab::MO[t] }, 144 | RDF::Vocab::SIOC.space_of => %w(host_of).map {|t| RDF::Vocab::SIOC[t] }, 145 | }.each do |prop, entails| 146 | context prop.pname do 147 | describe RDF::Vocabulary::Term do 148 | specify { 149 | expect(prop.entail(:subProperty).map(&:pname)).to include( 150 | *entails.map(&:pname))} 151 | specify { 152 | expect {|b| prop.entail(:subProperty, &b) 153 | }.to yield_control.at_least(entails.length)} 154 | end 155 | 156 | # XXX all of these can probably be rolled up too 157 | 158 | stmt = RDF::Statement(RDF::URI('a'), prop, RDF::Literal(true)) 159 | 160 | describe RDF::Statement do 161 | subject {stmt} 162 | let(:results) {entails.map {|r| 163 | RDF::Statement(RDF::URI("a"), r, RDF::Literal(true))}} 164 | specify {expect(subject.entail(:subProperty)).to be_empty} 165 | specify {expect(subject.entail(:subProperty)).to all(be_inferred)} 166 | specify {expect {|b| 167 | subject.entail(:subProperty, &b)}.not_to yield_control} 168 | end 169 | 170 | describe RDF::Enumerable do 171 | subject {[stmt].extend(RDF::Enumerable)} 172 | specify { 173 | expect(subject.entail(:subProperty)).to be_a(RDF::Enumerable)} 174 | specify {expect(subject.entail(:subProperty).to_a).to be_empty} 175 | specify { 176 | expect {|b| subject.entail(:subProperty, &b)}.not_to yield_control} 177 | end 178 | 179 | describe RDF::Mutable do 180 | subject {RDF::Graph.new << stmt} 181 | let(:results) {subject.dup} 182 | specify {expect(subject.entail(:subProperty)).to be_a(RDF::Graph)} 183 | specify {expect( 184 | subject.entail(:subProperty)).to be_equivalent_graph(results)} 185 | specify {expect(subject.entail!(:subProperty)).to equal subject} 186 | end 187 | end 188 | end 189 | end unless ENV['CI'] 190 | 191 | describe :domain do 192 | { 193 | RDF::Vocab::FOAF.account => [RDF::Vocab::FOAF.Agent], 194 | RDF::Vocab::DOAP.os => [RDF::Vocab::DOAP.Project, RDF::Vocab::DOAP.Version], 195 | RDF::Vocab::CC.attributionName => [RDF::Vocab::CC.Work], 196 | }.each do |prop, entails| 197 | context prop.pname do 198 | describe RDF::Vocabulary::Term do 199 | specify {expect(prop.entail(:domain)).to be_empty} 200 | specify {expect {|b| prop.entail(:domain, &b)}.not_to yield_control} 201 | end 202 | 203 | describe RDF::Statement do 204 | subject {RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 205 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}} 206 | specify {expect(subject.entail(:domain)).to include(*results)} 207 | specify {expect(subject.entail(:domain)).to all(be_inferred)} 208 | specify {expect {|b| subject.entail(:domain, &b)}.to yield_control.at_least(entails.length)} 209 | end 210 | 211 | describe RDF::Enumerable do 212 | subject {[RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))].extend(RDF::Enumerable)} 213 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}} 214 | specify {expect(subject.entail(:domain)).to be_a(RDF::Enumerable)} 215 | specify {expect(subject.entail(:domain).to_a).to include(*results)} 216 | specify {expect {|b| subject.entail(:domain, &b)}.to yield_control.at_least(entails.length)} 217 | end 218 | 219 | describe RDF::Mutable do 220 | subject {RDF::Graph.new << RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 221 | let(:results) { 222 | subject.dup.insert(*entails.map {|r| RDF::Statement(RDF::URI("a"), RDF.type, r)}) 223 | } 224 | specify {expect(subject.entail(:domain)).to be_a(RDF::Graph)} 225 | specify {expect(subject.entail(:domain)).to be_equivalent_graph(results)} 226 | specify {expect(subject.entail!(:domain)).to equal subject} 227 | end 228 | end 229 | end 230 | 231 | it "does not entail a BNode" do 232 | s = RDF::Statement(RDF::URI("a"), RDF::Vocab::V.currency, RDF::Literal("USD")) 233 | s.entail(:domain) do |st| 234 | expect(st.object).not_to be_a_node 235 | end 236 | end 237 | end 238 | 239 | describe :range do 240 | { 241 | RDF::Vocab::CC.jurisdiction => [RDF::Vocab::CC.Jurisdiction], 242 | RDF::Vocab::CERT.key => [RDF::Vocab::CERT.Key, RDF::Vocab::CERT.PublicKey], 243 | RDF::Vocab::DOAP.helper => [RDF::Vocab::FOAF.Person], 244 | }.each do |prop, entails| 245 | context prop.pname do 246 | describe RDF::Vocabulary::Term do 247 | specify {expect(prop.entail(:range)).to be_empty} 248 | specify {expect {|b| prop.entail(:range, &b)}.not_to yield_control} 249 | end 250 | 251 | describe RDF::Statement do 252 | subject {RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 253 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("b"), RDF.type, r)}} 254 | specify {expect(subject.entail(:range)).to include(*results)} 255 | specify {expect(subject.entail(:range)).to all(be_inferred)} 256 | specify {expect {|b| subject.entail(:range, &b)}.to yield_control.at_least(entails.length)} 257 | end 258 | 259 | describe RDF::Enumerable do 260 | subject {[RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))].extend(RDF::Enumerable)} 261 | let(:results) {entails.map {|r| RDF::Statement(RDF::URI("b"), RDF.type, r)}} 262 | specify {expect(subject.entail(:range)).to be_a(RDF::Enumerable)} 263 | specify {expect(subject.entail(:range).to_a).to include(*results)} 264 | specify {expect {|b| subject.entail(:range, &b)}.to yield_control.at_least(entails.length)} 265 | end 266 | 267 | describe RDF::Mutable do 268 | subject {RDF::Graph.new << RDF::Statement(RDF::URI("a"), prop, RDF::URI("b"))} 269 | let(:results) { 270 | subject.dup.insert(*entails.map {|r| RDF::Statement(RDF::URI("b"), RDF.type, r)}) 271 | } 272 | specify {expect(subject.entail(:range)).to be_a(RDF::Graph)} 273 | specify {expect(subject.entail(:range)).to be_equivalent_graph(results)} 274 | specify {expect(subject.entail!(:range)).to equal subject} 275 | end 276 | end 277 | end 278 | 279 | it "does not entail a BNode" do 280 | s = RDF::Statement(RDF::URI("a"), RDF::Vocab::V.affiliation, RDF::URI("http://example/ACMECorp")) 281 | s.entail(:range) do |st| 282 | expect(st.object).not_to be_a_node 283 | end 284 | end 285 | end 286 | 287 | describe :domain_compatible? do 288 | let!(:queryable) {RDF::Graph.new << RDF::Statement(ex+"a", RDF.type, RDF::Vocab::FOAF.Person)} 289 | 290 | context "domain and no provided types" do 291 | it "uses entailed types of resource" do 292 | expect(RDF::Vocab::FOAF.familyName).to be_domain_compatible(ex+"a", queryable) 293 | end 294 | end 295 | 296 | it "returns true with no domain and no type" do 297 | expect(RDF::Vocab::DC.date).to be_domain_compatible(ex+"b", queryable) 298 | end 299 | 300 | it "returns true with no domain and type" do 301 | expect(RDF::Vocab::DC.date).to be_domain_compatible(ex+"a", queryable) 302 | end 303 | 304 | it "uses supplied types" do 305 | expect(RDF::Vocab::FOAF.based_near).not_to be_domain_compatible(ex+"a", queryable, types: [RDF::Vocab::FOAF.Agent]) 306 | expect(RDF::Vocab::FOAF.based_near).to be_domain_compatible(ex+"a", queryable, types: [RDF::Vocab::GEO.SpatialThing]) 307 | expect(RDF.type).to be_domain_compatible(ex+"a", queryable, types: [RDF::Vocab::SCHEMA.Thing]) 308 | end 309 | 310 | context "domain violations" do 311 | { 312 | "subject of wrong type" => %( 313 | @prefix foaf: . 314 | a foaf:Person; foaf:depicts [a foaf:Image] . 315 | ), 316 | }.each do |name, input| 317 | it name do 318 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 319 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 320 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_domain_compatible(statement.subject, graph) 321 | end 322 | end 323 | end 324 | 325 | it "is compatible with a BNode domain" do 326 | expect(RDF::Vocab::V.address).to be_domain_compatible(ex+"a", queryable, types: [RDF::Vocab::SCHEMA.Thing]) 327 | end 328 | end 329 | 330 | describe :range_compatible? do 331 | let!(:queryable) {RDF::Graph.new << RDF::Statement(ex+"a", RDF.type, RDF::Vocab::FOAF.Person)} 332 | 333 | context "objects in range" do 334 | { 335 | "object of right type" => %( 336 | @prefix foaf: . 337 | a foaf:Image; foaf:depicts [a foaf:Person] . 338 | ), 339 | "xsd:anyURI with language-tagged literal" => %( 340 | @prefix ma: . 341 | a ma:MediaResource; ma:locator "http://example/"@en . 342 | ), 343 | }.each do |name, input| 344 | it name do 345 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 346 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 347 | expect(RDF::Vocabulary.find_term(statement.predicate)).to be_range_compatible(statement.object, graph) 348 | end 349 | end 350 | 351 | it "is compatible with a BNode range" do 352 | expect(RDF::Vocab::V.affiliation).to be_range_compatible(ex+"a", queryable, types: [RDF::Vocab::SCHEMA.Thing]) 353 | end 354 | 355 | context "OGP literal datatypes" do 356 | { 357 | #"boolean_str" => %( 358 | # a rdf:Property; rdfs:range; ogc:boolean_str 359 | # 360 | # "true"^^ogc:boolean_str, "false"^^ogc:boolean_str, 361 | # true, false 362 | # "true", "false", "1", "0", 363 | # "true"@en, "false"@en, "1"@en, "0"@en . 364 | #), 365 | #"date_time_str" => %( 366 | # a rdf:Property; rdfs:range; ogc:date_time_str 367 | # 368 | # "2009-12T12:34"^^ogc:date_time_str, 369 | # "2009-12T12:34", 370 | # "2009-12T12:34"^^xsd:dateTime . 371 | #), 372 | "determiner_str" => %( 373 | og:determiner "", "a", "the", "an", "auto" . 374 | ), 375 | #"float_str" => %( 376 | # a rdf:Property; rdfs:range; ogc:float_str . 377 | # 378 | # "1.1"^^xsd:float, "1.1e1"^^xsd:double, 379 | # "1.1", "1.1e1", 380 | # "1.1"@en, "1.1e1"@en . 381 | #), 382 | "integer_str" => %( 383 | og:image:height 1, "1", "1"@en . 384 | ), 385 | "image_type" => %( 386 | og:image:type "application1+a-b/2foo+bar-baz" . 387 | ), 388 | "string" => %( 389 | og:description "a", "b"@en . 390 | ), 391 | "url" => %( 392 | og:image "http://example.com", . 393 | ), 394 | }.each do |name, input| 395 | it name do 396 | input = %( 397 | @prefix rdf: . 398 | @prefix rdfs: . 399 | @prefix xsd: . 400 | @prefix og: . 401 | @prefix ogc: . 402 | ) + input 403 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 404 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 405 | expect(RDF::Vocabulary.find_term(statement.predicate)).to be_range_compatible(statement.object, graph) 406 | end 407 | end 408 | end 409 | 410 | context "GS1" do 411 | { 412 | "langString" => %( 413 | gs1:organizationName "GS1 Denmark"@en . 414 | ), 415 | }.each do |name, input| 416 | it name do 417 | input = %( 418 | @prefix rdf: . 419 | @prefix rdfs: . 420 | @prefix gs1: . 421 | ) + input 422 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 423 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 424 | expect(RDF::Vocabulary.find_term(statement.predicate)).to be_range_compatible(statement.object, graph) 425 | end 426 | end 427 | end 428 | end 429 | 430 | it "uses supplied types" do 431 | expect(RDF::Vocab::FOAF.based_near).not_to be_range_compatible(ex+"a", queryable, types: [RDF::Vocab::FOAF.Agent]) 432 | expect(RDF::Vocab::FOAF.based_near).to be_range_compatible(ex+"a", queryable, types: [RDF::Vocab::GEO.SpatialThing]) 433 | expect(RDF.type).to be_range_compatible(ex+"a", queryable, types: [RDF::Vocab::SCHEMA.Thing]) 434 | end 435 | 436 | context "object range violations" do 437 | { 438 | "object of wrong type" => %( 439 | @prefix foaf: . 440 | a foaf:Person; foaf:holdsAccount [a foaf:Image] . 441 | ), 442 | "object range with literal" => %( 443 | @prefix foaf: . 444 | a foaf:Person; foaf:homepage "Document" . 445 | ), 446 | }.each do |name, input| 447 | it name do 448 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 449 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 450 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_range_compatible(statement.object, graph) 451 | end 452 | end 453 | end 454 | 455 | context "literal range violations" do 456 | { 457 | "xsd:nonNegativeInteger expected with conforming plain literal" => %( 458 | @prefix sioc: . 459 | sioc:num_authors "bar" . 460 | ), 461 | "xsd:nonNegativeInteger expected with non-equivalent datatyped literal" => %( 462 | @prefix sioc: . 463 | sioc:num_authors 1 . 464 | ), 465 | "xsd:anyURI with non-conforming plain literal" => %( 466 | @prefix ma: . 467 | a ma:MediaResource; ma:locator "foo" . 468 | ), 469 | "xsd:boolean with non-conforming plain literal" => %( 470 | @prefix wrds: . 471 | a wrds:Document; wrds:certified "bar" . 472 | ), 473 | }.each do |name, input| 474 | it name do 475 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 476 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 477 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_range_compatible(statement.object, graph) 478 | end 479 | end 480 | 481 | context "OGP literal datatypes" do 482 | { 483 | #"boolean_str" => %( 484 | # a rdf:Property; rdfs:range; ogc:boolean_str 485 | # 486 | # "true"^^ogc:boolean_str, "false"^^ogc:boolean_str, 487 | # true, false 488 | # "true", "false", "1", "0", 489 | # "true"@en, "false"@en, "1"@en, "0"@en . 490 | #), 491 | #"date_time_str" => %( 492 | # a rdf:Property; rdfs:range; ogc:date_time_str 493 | # 494 | # "2009-12T12:34"^^ogc:date_time_str, 495 | # "2009-12T12:34", 496 | # "2009-12T12:34"^^xsd:dateTime . 497 | #), 498 | "determiner_str" => %( 499 | og:determiner "foo" . 500 | ), 501 | #"float_str" => %( 502 | # a rdf:Property; rdfs:range; ogc:float_str . 503 | # 504 | # "1.1"^^xsd:float, "1.1e1"^^xsd:double, 505 | # "1.1", "1.1e1", 506 | # "1.1"@en, "1.1e1"@en . 507 | #), 508 | "integer_str" => %( 509 | og:image:height 1.1, "1.1", "1.1"@en . 510 | ), 511 | "image_type" => %( 512 | og:image:type "application" . 513 | ), 514 | "string" => %( 515 | og:description 1 . 516 | ), 517 | "url" => %( 518 | og:image "foo" . 519 | ), 520 | }.each do |name, input| 521 | it name do 522 | input = %( 523 | @prefix rdf: . 524 | @prefix rdfs: . 525 | @prefix xsd: . 526 | @prefix og: . 527 | @prefix ogc: . 528 | ) + input 529 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 530 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 531 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_range_compatible(statement.object, graph) 532 | end 533 | end 534 | end 535 | end 536 | end 537 | end 538 | -------------------------------------------------------------------------------- /spec/readme_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | $:.unshift "." 3 | require 'spec_helper' 4 | require 'rdf/reasoner' 5 | 6 | describe RDF::Reasoner do 7 | describe "Examples" do 8 | it "Determine super-classes of a class" do 9 | RDF::Reasoner.apply(:rdfs) 10 | term = RDF::Vocabulary.find_term("http://xmlns.com/foaf/0.1/Person") 11 | expect(term.entail(:subClassOf)).to include *[ 12 | RDF::Vocab::FOAF.Agent, 13 | RDF::URI("http://www.w3.org/2000/10/swap/pim/contact#Person"), 14 | RDF::Vocab::GEO.SpatialThing, 15 | RDF::Vocab::FOAF.Person 16 | ] 17 | end 18 | 19 | it "Determine sub-classes of a class" do 20 | RDF::Reasoner.apply(:rdfs) 21 | term = RDF::Vocab::FOAF.Person 22 | expect(term.entail(:subClass)).to include *[RDF::Vocab::FOAF.Person, RDF::Vocab::MO.SoloMusicArtist] 23 | end 24 | 25 | it "Determine if a resource is compatible with the domains of a property" do 26 | RDF::Reasoner.apply(:rdfs) 27 | graph = RDF::Graph.load("etc/doap.ttl") 28 | subj = RDF::URI("https://rubygems.org/gems/rdf-reasoner") 29 | expect(RDF::Vocab::DOAP.name).to be_domain_compatible(subj, graph) 30 | end 31 | 32 | it "Determine if a resource is compatible with the ranges of a property" do 33 | RDF::Reasoner.apply(:rdfs) 34 | graph = RDF::Graph.load("etc/doap.ttl") 35 | obj = RDF::Literal(Date.new) 36 | expect(RDF::Vocab::DOAP.created).to be_range_compatible(obj, graph) 37 | end 38 | 39 | it "Perform equivalentClass entailment on a graph" do 40 | RDF::Reasoner.apply(:owl) 41 | graph = RDF::Graph.load("etc/doap.ttl") 42 | graph.entail!(:equivalentClass) 43 | expect(graph).to have_statement(RDF::Statement(RDF::URI("https://greggkellogg.net/foaf#me"), RDF.type, RDF::Vocab::DC.Agent)) 44 | end 45 | 46 | it "Yield all entailed statements for all entailment methods" do 47 | RDF::Reasoner.apply(:rdfs, :owl) 48 | graph = RDF::Graph.load("etc/doap.ttl") 49 | enumerable = graph.enum_statement 50 | entailed = enumerable.entail 51 | expect(entailed.count).to be > 1 52 | end 53 | 54 | it "Lints a graph" do 55 | RDF::Reasoner.apply(:rdfs, :owl) 56 | graph = RDF::Graph.load("etc/doap.ttl") 57 | graph.entail! 58 | messages = graph.lint 59 | expect(messages).to be_empty 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/schema_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | $:.unshift "." 3 | require 'spec_helper' 4 | require 'rdf/reasoner/schema' 5 | 6 | describe RDF::Reasoner::Schema do 7 | before(:all) {RDF::Reasoner.apply(:schema, :rdfs)} 8 | let(:ex) {RDF::URI("http://example/")} 9 | 10 | describe :domainIncludes do 11 | { 12 | RDF::Vocab::SCHEMA.about => [RDF::Vocab::SCHEMA.CreativeWork].map(&:pname), 13 | }.each do |cls, entails| 14 | describe cls.pname do 15 | specify {expect(cls.domain_includes.map(&:pname)).to include(*entails)} 16 | specify {expect(cls.domainIncludes.map(&:pname)).to include(*entails)} 17 | end 18 | end 19 | 20 | { 21 | RDF::Vocab::SCHEMAS.about => [RDF::Vocab::SCHEMAS.CreativeWork].map(&:pname), 22 | }.each do |cls, entails| 23 | describe cls.pname do 24 | specify {expect(cls.properties[RDF::Vocab::SCHEMAS.domainIncludes].map(&:pname)).to include(*entails)} 25 | end 26 | end 27 | end 28 | 29 | describe :rangeIncludes do 30 | { 31 | RDF::Vocab::SCHEMA.about => [RDF::Vocab::SCHEMA.Thing].map(&:pname), 32 | RDF::Vocab::SCHEMA.event => [RDF::Vocab::SCHEMA.Event].map(&:pname), 33 | }.each do |cls, entails| 34 | describe cls.pname do 35 | specify {expect(cls.range_includes.map(&:pname)).to include(*entails)} 36 | specify {expect(cls.rangeIncludes.map(&:pname)).to include(*entails)} 37 | end 38 | end 39 | 40 | { 41 | RDF::Vocab::SCHEMAS.about => [RDF::Vocab::SCHEMAS.Thing].map(&:pname), 42 | RDF::Vocab::SCHEMAS.event => [RDF::Vocab::SCHEMAS.Event].map(&:pname), 43 | }.each do |cls, entails| 44 | describe cls.pname do 45 | specify {expect(Array(cls.properties[RDF::Vocab::SCHEMAS.rangeIncludes]).map(&:pname)).to include(*entails)} 46 | end 47 | end 48 | end 49 | 50 | describe :domain_compatible? do 51 | let!(:queryable) { 52 | RDF::Graph.new do |g| 53 | g << RDF::Statement(ex+"a", RDF.type, RDF::Vocab::SCHEMA.Person) 54 | g << RDF::Statement(ex+"a", RDF.type, RDF::Vocab::SCHEMAS.Person) 55 | end 56 | } 57 | context "domain and no provided types" do 58 | it "uses entailed types of resource" do 59 | expect(RDF::Vocab::SCHEMA.familyName).to be_domain_compatible(ex+"a", queryable) 60 | end 61 | 62 | it "uses entailed types of resource (https)" do 63 | expect(RDF::Vocab::SCHEMAS.familyName).to be_domain_compatible(ex+"a", queryable) 64 | end 65 | end 66 | 67 | it "returns true with no domain and no type" do 68 | expect(RDF::Vocab::SCHEMA.dateCreated).to be_domain_compatible(ex+"b", queryable) 69 | end 70 | 71 | it "returns true with no domain and no type (https)" do 72 | expect(RDF::Vocab::SCHEMAS.dateCreated).to be_domain_compatible(ex+"b", queryable) 73 | end 74 | 75 | it "uses supplied types" do 76 | expect(RDF::Vocab::SCHEMA.dateCreated).not_to be_domain_compatible(ex+"a", queryable) 77 | expect(RDF::Vocab::SCHEMA.dateCreated).to be_domain_compatible(ex+"a", queryable, types: [RDF::Vocab::SCHEMA.CreativeWork]) 78 | end 79 | 80 | it "uses supplied types (https)" do 81 | expect(RDF::Vocab::SCHEMAS.dateCreated).not_to be_domain_compatible(ex+"a", queryable) 82 | expect(RDF::Vocab::SCHEMAS.dateCreated).to be_domain_compatible(ex+"a", queryable, types: [RDF::Vocab::SCHEMAS.CreativeWork]) 83 | end 84 | 85 | context "domain violations" do 86 | { 87 | "subject of wrong type" => %( 88 | @prefix schema: . 89 | a schema:Person; schema:acceptedOffer [a schema:Offer] . 90 | ), 91 | "subject of wrong type (https)" => %( 92 | @prefix schemas: . 93 | a schemas:Person; schemas:acceptedOffer [a schemas:Offer] . 94 | ), 95 | }.each do |name, input| 96 | it name do 97 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 98 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 99 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_domain_compatible(statement.object, graph) 100 | end 101 | end 102 | end 103 | end 104 | 105 | describe :range_compatible? do 106 | context "objects in range" do 107 | { 108 | "object of right type" => %( 109 | @prefix schema: . 110 | a schema:Order; schema:acceptedOffer [a schema:Offer] . 111 | ), 112 | "object range with plain literal" => %( 113 | @prefix schema: . 114 | a schema:Order; schema:acceptedOffer "foo" . 115 | ), 116 | "schema:URL with language-tagged literal" => %( 117 | @prefix schema: . 118 | a schema:Thing; schema:url "http://example/"@en . 119 | ), 120 | "schema:URL with an untyped URI resource" => %( 121 | @prefix schema: . 122 | a schema:Thing; schema:url . 123 | ), 124 | "schema:URL with a typed URI resource" => %( 125 | @prefix schema: . 126 | a schema:Thing; schema:url . a schema:Organization . 127 | ), 128 | "schema:Text with an untyped URI resource" => %( 129 | @prefix schema: . 130 | a schema:Thing; schema:name . 131 | ), 132 | "schema:Height with anonymous structured value" => %( 133 | @prefix schema: . 134 | schema:height [ a schema:Distance; schema:name "20 3/4 inches" ] . 135 | ), 136 | "schema:Height with identified structured value" => %( 137 | @prefix schema: . 138 | schema:height . a schema:Distance; schema:name "20 3/4 inches" . 139 | ), 140 | "schema:CreativeWork with itemListElement (IRI)" => %( 141 | @prefix schema: . 142 | schema:itemListElement . a schema:CreativeWork . 143 | ), 144 | "schema:CreativeWork with itemListElement (BNode)" => %( 145 | @prefix schema: . 146 | schema:itemListElement [ a schema:CreativeWork ] . 147 | ), 148 | "text literal with itemListElement" => %( 149 | @prefix schema: . 150 | schema:itemListElement "Foo" . 151 | ), 152 | }.each do |name, input| 153 | it name do 154 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 155 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 156 | expect(RDF::Vocabulary.find_term(statement.predicate)).to be_range_compatible(statement.object, graph) 157 | end 158 | 159 | it "#{name.sub('schema:', 'schemas:')} (https)" do 160 | input = input. 161 | gsub('http://schema.org', 'https://schema.org'). 162 | gsub('schema:', 'schemas:') 163 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 164 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 165 | expect(RDF::Vocabulary.find_term(statement.predicate)).to be_range_compatible(statement.object, graph) 166 | end 167 | end 168 | 169 | context "ISO 8601" do 170 | %w( 171 | 2009-12T12:34 172 | 2009 173 | 2009-05-19 174 | 2009-05-19 175 | 20090519 176 | 2009123 177 | 2009-05 178 | 2009-123 179 | 2009-222 180 | 2009-001 181 | 2009-W01-1 182 | 2009-W51-1 183 | 2009-W511 184 | 2009-W33 185 | 2009W511 186 | 2009-05-19 187 | 2009-05-19_00:00 188 | 2009-05-19_14 189 | 2009-05-19_14:31 190 | 2009-05-19_14:39:22 191 | 2009-05-19T14:39Z 192 | 2009-W21-2 193 | 2009-W21-2T01:22 194 | 2009-139 195 | 2009-05-19_14:39:22-06:00 196 | 2009-05-19_14:39:22+0600 197 | 2009-05-19_14:39:22-01 198 | 20090621T0545Z 199 | 2007-04-06T00:00 200 | 2007-04-05T24:00 201 | 202 | 2010-02-18T16:23:48.5 203 | 2010-02-18T16:23:48,444 204 | 2010-02-18T16:23:48,3-06:00 205 | 2010-02-18T16:23.4 206 | 2010-02-18T16:23,25 207 | 2010-02-18T16:23.33+0600 208 | 2010-02-18T16.23334444 209 | 2010-02-18T16,2283 210 | 2009-05-19_143922.500 211 | 2009-05-19_1439,55 212 | ).each do |date| 213 | it "recognizes #{date.sub('_', ' ')}" do 214 | expect(RDF::Vocab::SCHEMA.startDate).to be_range_compatible(RDF::Literal(date.sub('_', ' ')), []) 215 | expect(RDF::Vocab::SCHEMAS.startDate).to be_range_compatible(RDF::Literal(date.sub('_', ' ')), []) 216 | end 217 | end 218 | 219 | %w( 220 | 200905 221 | 2009367 222 | 2009- 223 | 2007-04-05T24:50 224 | 2009-000 225 | 2009-M511 226 | 2009M511 227 | 2009-05-19T14a39r 228 | 2009-05-19T14:3924 229 | 2009-0519 230 | 2009-05-1914:39 231 | 2009-05-19_14: 232 | 2009-05-19r14:39 233 | 2009-05-19_14a39a22 234 | 200912-01 235 | 2009-05-19_14:39:22+06a00 236 | 237 | 2009-05-19_146922.500 238 | 2010-02-18T16.5:23.35:48 239 | 2010-02-18T16:23.35:48 240 | 2010-02-18T16:23.35:48.45 241 | 2009-05-19_14.5.44 242 | 2010-02-18T16:23.33.600 243 | 2010-02-18T16,25:23:48,444 244 | ).each do |date| 245 | it "does not recognize #{date.sub('_', ' ')}" do 246 | expect(RDF::Vocab::SCHEMA.startDate).not_to be_range_compatible(RDF::Literal(date.sub('_', ' ')), []) 247 | expect(RDF::Vocab::SCHEMAS.startDate).not_to be_range_compatible(RDF::Literal(date.sub('_', ' ')), []) 248 | end 249 | end 250 | end 251 | end 252 | 253 | context "object range violations" do 254 | { 255 | "object of wrong type" => %( 256 | @prefix schema: . 257 | a schema:Order; schema:acceptedOffer [a schema:Thing] . 258 | ), 259 | "object range with typed literal" => %( 260 | @prefix schema: . 261 | a schema:Order; schema:acceptedOffer "foo"^^schema:URL . 262 | ), 263 | "literal range with BNode" => %( 264 | @prefix schema: . 265 | schema:name _:bar . 266 | ), 267 | "literal range with URI (not schema:URL or schema:Text)" => %( 268 | @prefix schema: . 269 | schema:startDate . 270 | ), 271 | "schema:Text with a typed URI resource" => %( 272 | @prefix schema: . 273 | a schema:Thing; schema:name . a schema:Person . 274 | ), 275 | }.each do |name, input| 276 | it name do 277 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 278 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 279 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_range_compatible(statement.object, graph) 280 | end 281 | 282 | it "#{name.sub('schema:', 'schemas:')} (https)" do 283 | input = input. 284 | gsub('http://schema.org', 'https://schema.org'). 285 | gsub('schema:', 'schemas:') 286 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 287 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 288 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_range_compatible(statement.object, graph) 289 | end 290 | end 291 | end 292 | 293 | context "literal range violations" do 294 | { 295 | "schema:Number expected with conforming plain literal" => %( 296 | @prefix schema: . 297 | schema:amountOfThisGood "bar" . 298 | ), 299 | "schema:Integer expected with conforming plain literal" => %( 300 | @prefix schema: . 301 | schema:answerCount "bar" . 302 | ), 303 | "schema:Date expected with conforming plain literal" => %( 304 | @prefix schema: . 305 | schema:birthDate "bar" . 306 | ), 307 | "schema:DateTime expected with conforming plain literal" => %( 308 | @prefix schema: . 309 | schema:checkinTime "bar" . 310 | ), 311 | "schema:Text with datatyped literal" => %( 312 | @prefix schema: . 313 | @prefix xsd: . 314 | a schema:Thing; schema:recipeIngredient "foo"^^xsd:token . 315 | ), 316 | "schema:URL with non-conforming plain literal" => %( 317 | @prefix schema: . 318 | a schema:Thing; schema:url "foo" . 319 | ), 320 | "schema:Boolean with non-conforming plain literal" => %( 321 | @prefix schema: . 322 | a schema:CreativeWork; schema:isFamilyFriendly "bar" . 323 | ), 324 | "date with itemListElement" => %( 325 | @prefix schema: . 326 | schema:itemListElement "2016-08-22"^^schema:Date . 327 | ), 328 | }.each do |name, input| 329 | it name do 330 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 331 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 332 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_range_compatible(statement.object, graph) 333 | end 334 | 335 | it "#{name.sub('schema:', 'schemas:')} (https)" do 336 | input = input. 337 | gsub('http://schema.org', 'https://schema.org'). 338 | gsub('schema:', 'schemas:') 339 | graph = RDF::Graph.new << RDF::Turtle::Reader.new(input) 340 | statement = graph.to_a.reject {|s| s.predicate == RDF.type}.first 341 | expect(RDF::Vocabulary.find_term(statement.predicate)).not_to be_range_compatible(statement.object, graph) 342 | end 343 | end 344 | end 345 | end 346 | 347 | 348 | describe "Roles" do 349 | { 350 | "Cryptography Users" => { 351 | input: %( 352 | @prefix schema: . 353 | a schema:Organization; 354 | schema:name "Cryptography Users"; 355 | schema:member [ 356 | a schema:OrganizationRole, schema:Role; 357 | schema:member [ 358 | a schema:Person; 359 | schema:name "Alice" 360 | ]; 361 | schema:startDate "1977" 362 | ] . 363 | ), 364 | predicate: RDF::Vocab::SCHEMA.member, 365 | result: :domain_range 366 | }, 367 | "Cryptography Users (not domain)" => { 368 | input: %( 369 | @prefix schema: . 370 | a schema:Organization; 371 | schema:name "Cryptography Users"; 372 | schema:alumni [ 373 | a schema:OrganizationRole, schema:Role; 374 | schema:member [ 375 | a schema:Person; 376 | schema:name "Alice" 377 | ]; 378 | schema:startDate "1977" 379 | ] . 380 | ), 381 | predicate: RDF::Vocab::SCHEMA.member, 382 | result: :not_domain 383 | }, 384 | "Cryptography Users (not range)" => { 385 | input: %( 386 | @prefix schema: . 387 | a schema:Organization; 388 | schema:name "Cryptography Users"; 389 | schema:alumni [ 390 | a schema:OrganizationRole, schema:Role; 391 | schema:member [ 392 | a schema:Person; 393 | schema:name "Alice" 394 | ]; 395 | schema:startDate "1977" 396 | ] . 397 | ), 398 | predicate: RDF::Vocab::SCHEMA.alumni, 399 | result: :not_range 400 | }, 401 | "University of Cambridge" => { 402 | input: %( 403 | @prefix schema: . 404 | a schema:CollegeOrUniversity; 405 | schema:name "University of Cambridge"; 406 | schema:sameAs ; 407 | schema:alumni [ 408 | a schema:OrganizationRole, schema:Role; 409 | schema:alumni [ 410 | a schema:Person; 411 | schema:name "Delia Derbyshire"; 412 | schema:sameAs 413 | ]; 414 | schema:startDate "1957" 415 | ] . 416 | ), 417 | predicate: RDF::Vocab::SCHEMA.alumni, 418 | result: :domain_range 419 | }, 420 | "Delia Derbyshire" => { 421 | input: %( 422 | @prefix schema: . 423 | a schema:Person; 424 | schema:name "Delia Derbyshire"; 425 | schema:sameAs ; 426 | schema:alumniOf [ 427 | a schema:OrganizationRole, schema:Role; 428 | schema:alumniOf [ 429 | a schema:CollegeOrUniversity; 430 | schema:name "University of Cambridge"; 431 | schema:sameAs 432 | ]; 433 | schema:startDate "1957" 434 | ] . 435 | ), 436 | predicate: RDF::Vocab::SCHEMA.alumniOf, 437 | result: :domain_range 438 | }, 439 | "San Francisco 49ers" => { 440 | input: %( 441 | @prefix schema: . 442 | a schema:SportsTeam; 443 | schema:name "San Francisco 49ers"; 444 | schema:member [ 445 | a schema:PerformanceRole, schema:Role; 446 | schema:member [ 447 | a schema:Person; 448 | schema:name "Joe Montana" 449 | ]; 450 | schema:startDate "1979"; 451 | schema:endDate "1992"; 452 | schema:namedPosition "Quarterback" 453 | ] . 454 | ), 455 | predicate: RDF::Vocab::SCHEMA.member, 456 | result: :domain_range 457 | }, 458 | }.each do |name, params| 459 | context name do 460 | let(:graph) {RDF::Graph.new << RDF::Turtle::Reader.new(params[:input])} 461 | let(:resource) {graph.first_subject(predicate: RDF.type, object: RDF::Vocab::SCHEMA.Role)} 462 | 463 | it "allows role in domain", if: params[:result] == :domain_range do 464 | expect(params[:predicate]).to be_domain_compatible(resource, graph) 465 | end 466 | 467 | it "allows role in range", if: params[:result] == :domain_range do 468 | expect(params[:predicate]).to be_range_compatible(resource, graph) 469 | end 470 | 471 | it "does not allow role in domain", if: params[:result] == :not_domain do 472 | expect(params[:predicate]).not_to be_domain_compatible(resource, graph) 473 | end 474 | 475 | it "does not allow role in range", if: params[:result] == :not_range do 476 | expect(params[:predicate]).not_to be_range_compatible(resource, graph) 477 | end 478 | end 479 | end 480 | end 481 | 482 | describe "Lists" do 483 | { 484 | "Creator list" => { 485 | input: %( 486 | @prefix rdf: . 487 | @prefix schema: . 488 | a schema:Review; 489 | schema:creator [ 490 | a rdf:List; 491 | rdf:first [a schema:Person; schema:name "John Doe"]; 492 | rdf:rest rdf:nil 493 | ] . 494 | ), 495 | resource: RDF::URI("http://example/Review"), 496 | predicate: RDF::Vocab::SCHEMA.creator, 497 | result: :range 498 | }, 499 | "Creator list (https)" => { 500 | input: %( 501 | @prefix rdf: . 502 | @prefix schema: . 503 | a schema:Review; 504 | schema:creator [ 505 | a rdf:List; 506 | rdf:first [a schema:Person; schema:name "John Doe"]; 507 | rdf:rest rdf:nil 508 | ] . 509 | ), 510 | resource: RDF::URI("http://example/Review"), 511 | predicate: RDF::Vocab::SCHEMAS.creator, 512 | result: :range 513 | }, 514 | "Creator list with string value" => { 515 | input: %( 516 | @prefix rdf: . 517 | @prefix schema: . 518 | a schema:Review; 519 | schema:creator [ 520 | a rdf:List; 521 | rdf:first "John Doe"; 522 | rdf:rest rdf:nil 523 | ] . 524 | ), 525 | resource: RDF::URI("http://example/Review"), 526 | predicate: RDF::Vocab::SCHEMA.creator, 527 | result: :range 528 | }, 529 | "Creator list with string value (https)" => { 530 | input: %( 531 | @prefix rdf: . 532 | @prefix schema: . 533 | a schema:Review; 534 | schema:creator [ 535 | a rdf:List; 536 | rdf:first "John Doe"; 537 | rdf:rest rdf:nil 538 | ] . 539 | ), 540 | resource: RDF::URI("http://example/Review"), 541 | predicate: RDF::Vocab::SCHEMAS.creator, 542 | result: :range 543 | }, 544 | "Creator list (single invalid value)" => { 545 | input: %( 546 | @prefix rdf: . 547 | @prefix schema: . 548 | a schema:Review; 549 | schema:creator [ 550 | a rdf:List; 551 | rdf:first [a schema:CreativeWork; schema:name "Website"]; 552 | rdf:rest rdf:nil 553 | ] . 554 | ), 555 | resource: RDF::URI("http://example/Review"), 556 | predicate: RDF::Vocab::SCHEMA.creator, 557 | result: :not_range 558 | }, 559 | "Creator list (single invalid value) (https)" => { 560 | input: %( 561 | @prefix rdf: . 562 | @prefix schema: . 563 | a schema:Review; 564 | schema:creator [ 565 | a rdf:List; 566 | rdf:first [a schema:CreativeWork; schema:name "Website"]; 567 | rdf:rest rdf:nil 568 | ] . 569 | ), 570 | resource: RDF::URI("http://example/Review"), 571 | predicate: RDF::Vocab::SCHEMAS.creator, 572 | result: :not_range 573 | }, 574 | "Creator list (mixed valid/invalid)" => { 575 | input: %( 576 | @prefix rdf: . 577 | @prefix schema: . 578 | a schema:Review; 579 | schema:creator [ 580 | a rdf:List; 581 | rdf:first [a schema:Person; schema:name "John Doe";]; 582 | rdf:rest [ 583 | a rdf:List; 584 | rdf:first [a schema:CreativeWork; schema:name "Website"]; 585 | rdf:rest rdf:nil 586 | ] 587 | ] . 588 | ), 589 | resource: RDF::URI("http://example/Review"), 590 | predicate: RDF::Vocab::SCHEMA.creator, 591 | result: :not_range 592 | }, 593 | "Creator list (mixed valid/invalid) (https)" => { 594 | input: %( 595 | @prefix rdf: . 596 | @prefix schema: . 597 | a schema:Review; 598 | schema:creator [ 599 | a rdf:List; 600 | rdf:first [a schema:Person; schema:name "John Doe";]; 601 | rdf:rest [ 602 | a rdf:List; 603 | rdf:first [a schema:CreativeWork; schema:name "Website"]; 604 | rdf:rest rdf:nil 605 | ] 606 | ] . 607 | ), 608 | resource: RDF::URI("http://example/Review"), 609 | predicate: RDF::Vocab::SCHEMAS.creator, 610 | result: :not_range 611 | }, 612 | }.each do |name, params| 613 | context name do 614 | let(:graph) {RDF::Graph.new << RDF::Turtle::Reader.new(params[:input])} 615 | let(:resource) {params[:resource]} 616 | let(:predicate) {params[:predicate]} 617 | let(:list) {graph.first_object(subject: resource, predicate: predicate)} 618 | 619 | it "allows list in range", if: params[:result] == :range do 620 | expect(predicate).to be_range_compatible(list, graph) 621 | end 622 | 623 | it "does not allow list in range", if: params[:result] == :not_range do 624 | expect(predicate).not_to be_range_compatible(list, graph) 625 | end 626 | end 627 | end 628 | end 629 | 630 | end 631 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $:.unshift File.dirname(__FILE__) 3 | 4 | require "bundler/setup" 5 | require 'rspec' 6 | require 'matchers' 7 | require 'rdf/spec/matchers' 8 | require 'json/ld' 9 | require 'rdf/reasoner' 10 | require 'rdf/turtle' 11 | require 'rdf/vocab' 12 | require 'rdf/xsd' 13 | 14 | begin 15 | require 'simplecov' 16 | require 'simplecov-lcov' 17 | 18 | SimpleCov::Formatter::LcovFormatter.config do |config| 19 | #Coveralls is coverage by default/lcov. Send info results 20 | config.report_with_single_file = true 21 | config.single_report_path = 'coverage/lcov.info' 22 | end 23 | 24 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 25 | SimpleCov::Formatter::HTMLFormatter, 26 | SimpleCov::Formatter::LcovFormatter 27 | ]) 28 | SimpleCov.start do 29 | add_filter "/spec/" 30 | end 31 | rescue LoadError 32 | end 33 | 34 | ::RSpec.configure do |c| 35 | c.filter_run focus: true 36 | c.run_all_when_everything_filtered = true 37 | c.exclusion_filter = { 38 | ruby: lambda { |version| !(RUBY_VERSION.to_s =~ /^#{version.to_s}/) }, 39 | } 40 | end 41 | 42 | # Remove vocabulary from RDF::Vocabulary 43 | class RDF::Vocabulary 44 | class << self 45 | def remove(vocab) 46 | @@subclasses.delete_if {|klass| klass = vocab} 47 | @@uris.delete(vocab) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/suite_helper.rb: -------------------------------------------------------------------------------- 1 | # Spira class for manipulating test-manifest style test suites. 2 | # Used for Turtle tests 3 | require 'rdf/turtle' 4 | require 'json/ld' 5 | 6 | # For now, override RDF::Utils::File.open_file to look for the file locally before attempting to retrieve it 7 | module RDF::Util 8 | module File 9 | REMOTE_PATH = "https://www.w3.org/2013/rdf-mt-tests/" 10 | LOCAL_PATH = ::File.expand_path("../w3c-rdf/rdf-mt", __FILE__) + '/' 11 | 12 | class << self 13 | alias_method :original_open_file, :open_file 14 | end 15 | 16 | ## 17 | # Override to use Patron for http and https, Kernel.open otherwise. 18 | # 19 | # @param [String] filename_or_url to open 20 | # @param [Hash{Symbol => Object}] options 21 | # @option options [Array, String] :headers 22 | # HTTP Request headers. 23 | # @return [IO] File stream 24 | # @yield [IO] File stream 25 | def self.open_file(filename_or_url, **options, &block) 26 | case 27 | when filename_or_url.to_s =~ /^file:/ 28 | path = filename_or_url[5..-1] 29 | Kernel.open(path.to_s, options, &block) 30 | when (filename_or_url.to_s =~ %r{^#{REMOTE_PATH}} && Dir.exist?(LOCAL_PATH)) 31 | #puts "attempt to open #{filename_or_url} locally" 32 | localpath = filename_or_url.to_s.sub(REMOTE_PATH, LOCAL_PATH) 33 | response = begin 34 | ::File.open(localpath) 35 | rescue Errno::ENOENT => e 36 | raise IOError, e.message 37 | end 38 | document_options = { 39 | base_uri: RDF::URI(filename_or_url), 40 | charset: Encoding::UTF_8, 41 | code: 200, 42 | headers: {} 43 | } 44 | #puts "use #{filename_or_url} locally" 45 | document_options[:headers][:content_type] = case filename_or_url.to_s 46 | when /\.ttl$/ then 'text/turtle' 47 | when /\.nt$/ then 'application/n-triples' 48 | when /\.jsonld$/ then 'application/ld+json' 49 | else 'unknown' 50 | end 51 | 52 | document_options[:headers][:content_type] = response.content_type if response.respond_to?(:content_type) 53 | # For overriding content type from test data 54 | document_options[:headers][:content_type] = options[:contentType] if options[:contentType] 55 | 56 | remote_document = RDF::Util::File::RemoteDocument.new(response.read, document_options) 57 | if block_given? 58 | yield remote_document 59 | else 60 | remote_document 61 | end 62 | else 63 | original_open_file(filename_or_url, **options, &block) 64 | end 65 | end 66 | end 67 | end 68 | 69 | module Fixtures 70 | module SuiteTest 71 | BASE = "http://www.w3.org/2013/rdf-mt-tests/" 72 | FRAME = JSON.parse(%q({ 73 | "@context": { 74 | "@vocab": "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#", 75 | "xsd": "http://www.w3.org/2001/XMLSchema#", 76 | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", 77 | "mf": "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#", 78 | "mq": "http://www.w3.org/2001/sw/DataAccess/tests/test-query#", 79 | "rdft": "http://www.w3.org/ns/rdftest#", 80 | 81 | "comment": "rdfs:comment", 82 | "entries": {"@id": "mf:entries", "@container": "@list"}, 83 | "name": "mf:name", 84 | "action": {"@type": "@id"}, 85 | "result": {"@type": "@id"}, 86 | "result_bool": {"@id": "result", "@type": "xsd:boolean"}, 87 | "recognizedDatatypes": {"@type": "@id", "@container": "@list"}, 88 | "unrecognizedDatatypes": {"@type": "@id", "@container": "@list"}, 89 | "approval": {"@id": "rdft:approval", "@type": "@vocab"} 90 | }, 91 | "@type": "mf:Manifest", 92 | "entries": { 93 | "@type": [ 94 | "mf:PositiveEntailmentTest", 95 | "mf:NegativeEntailmentTest" 96 | ] 97 | } 98 | })) 99 | 100 | class Manifest < JSON::LD::Resource 101 | def self.open(file) 102 | #puts "open: #{file}" 103 | g = RDF::Repository.load(file, format: :ttl) 104 | JSON::LD::API.fromRDF(g) do |expanded| 105 | JSON::LD::API.frame(expanded, FRAME) do |framed| 106 | yield Manifest.new(framed) 107 | end 108 | end 109 | end 110 | 111 | # @param [Hash] json framed JSON-LD 112 | # @return [Array] 113 | def self.from_jsonld(json) 114 | json['@graph'].map {|e| Manifest.new(e)} 115 | end 116 | 117 | def entries 118 | # Map entries to resources 119 | attributes['entries'].map {|e| Entry.new(e)} 120 | end 121 | end 122 | 123 | class Entry < JSON::LD::Resource 124 | attr_accessor :logger 125 | 126 | def base 127 | BASE + action.split('/').last 128 | end 129 | 130 | # Alias data and query 131 | def input 132 | @input ||= RDF::Util::File.open_file(action) {|f| f.read} 133 | end 134 | 135 | def result_bool 136 | attributes['result_bool'] == "true" 137 | end 138 | 139 | def result 140 | attributes['result'] || result_bool 141 | end 142 | 143 | def expected 144 | @expected ||= RDF::Util::File.open_file(result) {|f| f.read} if result.is_a?(String) 145 | end 146 | 147 | def entailment? 148 | !!Array(attributes['@type']).join(" ").match(/Entailment/) 149 | end 150 | 151 | def positive_test? 152 | !Array(attributes['@type']).join(" ").match(/Negative/) 153 | end 154 | 155 | def negative_test? 156 | !positive_test? 157 | end 158 | 159 | def inspect 160 | super.sub('>', "\n" + 161 | " positive?: #{positive_test?.inspect}\n" + 162 | " entailment?: #{entailment?.inspect}\n" + 163 | ">" 164 | ) 165 | end 166 | end 167 | end 168 | end -------------------------------------------------------------------------------- /spec/suite_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift "." 2 | require 'spec_helper' 3 | require 'rdf/spec' 4 | 5 | describe RDF::Reasoner do 6 | # W3C RDF Semantics Test suite from https://dvcs.w3.org/hg/rdf/file/default/rdf-mt/tests/ 7 | describe "w3c reasoner tests" do 8 | before(:all) {RDF::Reasoner.apply(:rdfs)} 9 | require 'suite_helper' 10 | 11 | %w(manifest.ttl).each do |man| 12 | Fixtures::SuiteTest::Manifest.open(Fixtures::SuiteTest::BASE + man) do |m| 13 | describe m.comment do 14 | m.entries.each_with_index do |t, ndx| 15 | specify "#{t.name}: #{Array(t.comment).join(' - ')}" do 16 | t.logger = RDF::Spec.logger 17 | t.logger.info "action:\n#{t.input}" if t.input 18 | t.logger.info "expected:\n#{t.expected}" if t.expected 19 | 20 | action_graph = t.action ? RDF::Repository.load(t.action, base_uri: t.base) : false 21 | result_graph = t.result.is_a?(String) ? RDF::Repository.load(t.result, base_uri: t.base) : false 22 | 23 | # Extract any triples 24 | if vocab = extract_vocab(action_graph, ndx) 25 | t.logger.info vocab.inspect 26 | end 27 | 28 | case t.name 29 | when 'datatypes-semantic-equivalence-within-type-1', 30 | 'datatypes-semantic-equivalence-within-type-2', 31 | 'datatypes-semantic-equivalence-between-datatypes' 32 | pending "Datatype Entailment" 33 | when *%w(rdfs-subPropertyOf-semantics-test001) 34 | pending 'subProperty inheritance' 35 | when *%w(tex-01-language-tag-case-1 tex-01-language-tag-case-2) 36 | pending 'language tag case insensitivity' 37 | when 'datatypes-test008' 38 | pending 'rdfD1' 39 | when /rdfms-seq/ 40 | skip 'No rdf:Seq entailment' 41 | end 42 | begin 43 | if t.positive_test? 44 | action_graph.entail! 45 | case result_graph 46 | when RDF::Enumerable 47 | # Add source triples to result to use equivalence 48 | # FIXME: entailment test should be subgraph, considering BNode equivalence. 49 | # Could be implemented in N3 as {G2 . {G1} => log:Success} or {G2} log:includes {G1} 50 | action_graph.each {|s| result_graph << s} 51 | expect(action_graph).to be_equivalent_graph(result_graph, t) 52 | when false 53 | expect(action_graph.lint.to_s).not_to produce('{}', t) 54 | end 55 | else 56 | skip "NegativeEntailment" 57 | end 58 | #rescue 59 | # if t.action == false 60 | # fail "don't know how to deal with false premise" 61 | # elsif t.result == false 62 | # fail "don't know how to deal with false result" 63 | # else 64 | # raise 65 | # end 66 | ensure 67 | RDF::Vocabulary.remove(vocab) if vocab 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end unless ENV['CI'] 76 | 77 | # Figure out if there's a graph, and what URI to give it, then create an RDF::Vocabulary subclass 78 | def extract_vocab(graph, ndx) 79 | vocab_stmt = graph.statements.detect do |s| 80 | %w(subClassOf subPropertyOf domain range). 81 | map {|t| RDF::RDFS[t.to_sym]}. 82 | include?(s.predicate) 83 | end 84 | 85 | if vocab_stmt 86 | vocab_subject = vocab_stmt.subject 87 | base = if vocab_subject.fragment 88 | vocab_subject = vocab_subject.dup 89 | vocab_subject.fragment = "" 90 | vocab_subject 91 | else 92 | vocab_subject.join("") 93 | end 94 | RDF::Vocabulary.from_graph(graph, url: base, class_name: "RDFMT#{ndx}") 95 | end 96 | end --------------------------------------------------------------------------------