├── CREDITS ├── .gitmodules ├── VERSION ├── spec ├── .gitignore ├── version_spec.rb ├── extension_spec.rb ├── spec_helper.rb ├── format_spec.rb ├── shex_spec.rb ├── matchers.rb ├── algebra_spec.rb └── suite_helper.rb ├── _config.yml ├── AUTHORS ├── .coveralls.yml ├── .rspec ├── examples ├── lang_stem.shex ├── inclusion-example.shex └── disjunction-example-5.shex ├── lib ├── shex │ ├── algebra │ │ ├── import.rb │ │ ├── language.rb │ │ ├── external.rb │ │ ├── annotation.rb │ │ ├── value.rb │ │ ├── start.rb │ │ ├── shape_expression.rb │ │ ├── not.rb │ │ ├── stem.rb │ │ ├── and.rb │ │ ├── triple_expression.rb │ │ ├── or.rb │ │ ├── one_of.rb │ │ ├── each_of.rb │ │ ├── semact.rb │ │ ├── triple_constraint.rb │ │ ├── stem_range.rb │ │ ├── shape.rb │ │ ├── node_constraint.rb │ │ └── schema.rb │ ├── version.rb │ ├── extensions │ │ ├── test.rb │ │ └── extension.rb │ ├── algebra.rb │ ├── terminals.rb │ ├── format.rb │ └── shex_context.rb └── shex.rb ├── .gitignore ├── .yardopts ├── etc ├── README ├── doap.shex ├── .earl ├── doap.ttl ├── doap.json ├── specNotes.md ├── shex.sxp ├── template.haml ├── shex.ebnf └── shex.peg.sxp ├── .github └── workflows │ ├── generate-docs.yml │ └── ci.yml ├── LICENSE ├── Gemfile ├── Rakefile ├── shex.gemspec ├── CONTRIBUTING.md ├── script ├── run └── tc └── README.md /CREDITS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.8.1 2 | -------------------------------------------------------------------------------- /spec/.gitignore: -------------------------------------------------------------------------------- 1 | /shexTest 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Gregg Kellogg 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: z1Oxb0VtSEfdJIXLgX022Pt8QSDKCRr14 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | # Skip isomorphic tests 2 | --tag ~isomorphic 3 | --tag ~shexr 4 | -------------------------------------------------------------------------------- /examples/lang_stem.shex: -------------------------------------------------------------------------------- 1 | PREFIX my: 2 | 3 | my:IssueShape { 4 | my:status [@en~ @fr]; 5 | } 6 | -------------------------------------------------------------------------------- /lib/shex/algebra/import.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Import < Operator::Unary 4 | NAME = :import 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .tmp 3 | .yardoc 4 | pkg 5 | tmp 6 | /*.gem 7 | /.rbx/ 8 | /doc/ 9 | Gemfile.lock 10 | .bundle/ 11 | *.sw? 12 | benchmark/ 13 | /.byebug_history 14 | /coverage/ 15 | /doc/ 16 | -------------------------------------------------------------------------------- /examples/inclusion-example.shex: -------------------------------------------------------------------------------- 1 | PREFIX ex: 2 | PREFIX foaf: 3 | ex:PersonShape { 4 | foaf:name . 5 | } 6 | ex:EmployeeShape { 7 | &ex:PersonShape ; 8 | ex:employeeNumber . 9 | } 10 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title "ShEx: Shape Expression language for Ruby" 2 | --output-dir doc/yard 3 | --protected 4 | --no-private 5 | --hide-void-return 6 | --markup markdown 7 | --readme README.md 8 | - 9 | AUTHORS 10 | VERSION 11 | LICENSE 12 | etc/shex.html 13 | etc/earl.html 14 | -------------------------------------------------------------------------------- /spec/version_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'spec_helper') 2 | 3 | describe ShEx::VERSION do 4 | it "should match the VERSION file" do 5 | expect(ShEx::VERSION.to_s).to eq File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).chomp 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /examples/disjunction-example-5.shex: -------------------------------------------------------------------------------- 1 | PREFIX ex: 2 | PREFIX foaf: 3 | ex:UserShape closed extra foaf:familyName { 4 | ( # extra ()s to clarify alignment with ShExJ 5 | foaf:name LITERAL | 6 | ( # extra ()s to clarify alignment with ShExJ 7 | foaf:givenName LITERAL+ ; 8 | foaf:familyName LITERAL 9 | ) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /etc/README: -------------------------------------------------------------------------------- 1 | This is a collection of individual EARL reports for 2 | test subjects claiming Turtle processor conformance. 3 | 4 | The consolodated report is saved to index.html generated 5 | using the earl-report Ruby gem. Run it as follows: 6 | 7 | gem install earl-report 8 | 9 | earl-report --format json -o earl.jsonld earl.ttl 10 | earl-report --json --format html --template template.haml -o earl.html earl.jsonld 11 | -------------------------------------------------------------------------------- /lib/shex/version.rb: -------------------------------------------------------------------------------- 1 | module ShEx 2 | module VERSION 3 | FILE = File.expand_path('../../../VERSION', __FILE__) 4 | MAJOR, MINOR, TINY, EXTRA = File.read(FILE).chomp.split('.') 5 | STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.').freeze 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(String, String, String, String)] 17 | def self.to_a() [MAJOR, MINOR, TINY, EXTRA].compact end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/shex/algebra/language.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Language < Operator::Unary 4 | NAME = :language 5 | 6 | ## 7 | # matches any literal having a language tag that matches value 8 | def match?(value, depth: 0) 9 | status "", depth: depth 10 | if case expr = operands.first 11 | when RDF::Literal then value.language == expr.to_s.to_sym 12 | else false 13 | end 14 | status "matched #{value}", depth: depth 15 | true 16 | else 17 | status "not matched #{value}", depth: depth 18 | false 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/extension_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../..", __FILE__) 2 | require 'spec_helper' 3 | 4 | describe ShEx::Extension do 5 | describe ".each" do 6 | it "inumerates pre-defined extensions" do 7 | expect {|b| ShEx::Extension.each(&b)}.to yield_control.at_least(1).times 8 | expect(ShEx::Extension.each.to_a).to include(ShEx::Test) 9 | end 10 | end 11 | 12 | describe ".find" do 13 | it "finds Test" do 14 | expect(ShEx::Extension.find("http://shex.io/extensions/Test/")).to eq ShEx::Test 15 | end 16 | end 17 | end 18 | 19 | describe ShEx::Test do 20 | specify do 21 | expect(ShEx::Test.name).to eq "http://shex.io/extensions/Test/" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /etc/doap.shex: -------------------------------------------------------------------------------- 1 | BASE 2 | PREFIX doap: 3 | PREFIX dc: 4 | 5 | start=@ 6 | 7 | EXTRA a { 8 | a [doap:Project]; 9 | 10 | # May have either or both of doap:name/doap:description or dc:title/dc:description 11 | ( doap:name Literal; 12 | doap:description Literal 13 | | dc:title Literal; 14 | dc:description Literal)+; 15 | 16 | # Good idea to use a category for what the project relates to 17 | doap:category IRI*; 18 | 19 | # There must be at least one developer 20 | doap:developer IRI+; 21 | 22 | # For our purposes, it MUST implement the ShEx specification. 23 | doap:implements [] 24 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /lib/shex/algebra/external.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class External < Operator 4 | include ShapeExpression 5 | NAME = :external 6 | 7 | # 8 | # S is a ShapeRef and the Schema's shapes maps reference to a shape expression se2 and satisfies(n, se2, G, m). 9 | def satisfies?(focus, depth: 0) 10 | extern_shape = nil 11 | 12 | # Find the id for this external 13 | not_satisfied("Can't find id for this extern", depth: depth) unless id 14 | 15 | schema.external_schemas.each do |schema| 16 | extern_shape ||= schema.shapes.detect {|s| s.id == id} 17 | end 18 | 19 | not_satisfied("External not configured for this shape", depth: depth) unless extern_shape 20 | extern_shape.satisfies?(focus, depth: depth + 1) 21 | end 22 | 23 | def json_type 24 | "ShapeExternal" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/shex/algebra/annotation.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Annotation < Operator 4 | NAME = :annotation 5 | 6 | ## 7 | # Creates an operator instance from a parsed ShExJ representation 8 | # @param (see Operator#from_shexj) 9 | # @return [Operator] 10 | def self.from_shexj(operator, **options) 11 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "Annotation" 12 | raise ArgumentError, "missing predicate in #{operator.inspect}" unless operator.has_key?('predicate') 13 | raise ArgumentError, "missing object in #{operator.inspect}" unless operator.has_key?('object') 14 | super 15 | end 16 | 17 | def to_h 18 | { 19 | 'type' => json_type, 20 | 'predicate' => operands.first.last.to_s, 21 | 'object' => serialize_value(operands.last) 22 | } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/shex/extensions/test.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Test extension. 3 | # 4 | # Default implementation of http://shex.io/extensions/Test/ 5 | # 6 | # @see http://shex.io/extensions/Test/ 7 | require 'shex' 8 | 9 | module ShEx 10 | Test = Class.new(ShEx::Extension("http://shex.io/extensions/Test/")) do 11 | # (see ShEx::Extension#visit) 12 | def visit(code: nil, matched: nil, depth: 0, **options) 13 | str = if md = /^ *(fail|print) *\( *(?:(\"(?:[^\\"]|\\")*\")|([spo])) *\) *$/.match(code.to_s) 14 | md[2] || case md[3] 15 | when 's' then matched.subject 16 | when 'p' then matched.predicate 17 | when 'o' then matched.object 18 | else matched.to_sxp 19 | end.to_s 20 | else 21 | matched ? matched.to_sxp : 'no statement' 22 | end 23 | 24 | $stdout.puts str 25 | return !md || md[1] == 'print' 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /etc/.earl: -------------------------------------------------------------------------------- 1 | --- 2 | :format: :json 3 | :manifest: 4 | - https://raw.githubusercontent.com/shexSpec/shexTest/master/schemas/manifest.ttl 5 | - https://raw.githubusercontent.com/shexSpec/shexTest/master/negativeSyntax/manifest.ttl 6 | - https://raw.githubusercontent.com/shexSpec/shexTest/master/negativeStructure/manifest.ttl 7 | - https://raw.githubusercontent.com/shexSpec/shexTest/master/validation/manifest.ttl 8 | :bibRef: ! '[[shex]]' 9 | :name: Shape Expressions Language 10 | :query: > 11 | PREFIX mf: 12 | PREFIX rdf: 13 | PREFIX sx: 14 | 15 | SELECT ?uri ?testAction ?manUri 16 | WHERE { 17 | ?uri mf:action | sx:shex ?testAction . 18 | OPTIONAL { 19 | ?manUri a mf:Manifest; mf:entries ?lh . 20 | ?lh rdf:first ?uri . 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require 'rspec/its' 3 | require 'rdf/spec' 4 | require 'rdf/spec/matchers' 5 | require 'matchers' 6 | require 'rdf/turtle' 7 | 8 | begin 9 | require 'simplecov' 10 | require 'simplecov-lcov' 11 | 12 | SimpleCov::Formatter::LcovFormatter.config do |config| 13 | #Coveralls is coverage by default/lcov. Send info results 14 | config.report_with_single_file = true 15 | config.single_report_path = 'coverage/lcov.info' 16 | end 17 | 18 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 19 | SimpleCov::Formatter::HTMLFormatter, 20 | SimpleCov::Formatter::LcovFormatter 21 | ]) 22 | SimpleCov.start do 23 | add_filter "/spec/" 24 | end 25 | rescue LoadError 26 | end 27 | 28 | require 'shex' 29 | require 'shex/algebra' 30 | 31 | RSpec.configure do |config| 32 | config.filter_run focus: true 33 | config.run_all_when_everything_filtered = true 34 | end 35 | -------------------------------------------------------------------------------- /.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: ruby --version; bundle install --jobs 4 --retry 3 32 | - name: Run tests 33 | run: bundle exec rspec spec || $ALLOW_FAILURES 34 | -------------------------------------------------------------------------------- /lib/shex/algebra/value.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Value < Operator::Unary 4 | NAME = :value 5 | 6 | ## 7 | # For a node n and constraint value v, nodeSatisfies(n, v) if n matches some valueSetValue vsv in v. A term matches a valueSetValue if: 8 | # 9 | # * vsv is an objectValue and n = vsv. 10 | # * vsv is a Stem with stem st and nodeIn(n, st). 11 | # * vsv is a StemRange with stem st and exclusions excls and nodeIn(n, st) and there is no x in excls such that nodeIn(n, excl). 12 | # * vsv is a Wildcard with exclusions excls and there is no x in excls such that nodeIn(n, excl). 13 | def match?(value, depth: 0) 14 | status "", depth: depth 15 | if case expr = operands.first 16 | when RDF::Value then value.eql?(expr) 17 | when Language, Stem, StemRange then expr.match?(value, depth: depth + 1) 18 | else false 19 | end 20 | status "matched #{value}", depth: depth 21 | true 22 | else 23 | status "not matched #{value}", depth: depth 24 | false 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /lib/shex/algebra/start.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Start < Operator::Unary 4 | include ShapeExpression 5 | NAME = :start 6 | 7 | # 8 | # @param (see ShapeExpression#satisfies?) 9 | # @return (see ShapeExpression#satisfies?) 10 | # @raise (see ShapeExpression#satisfies?) 11 | def satisfies?(focus, depth: 0) 12 | status "", depth: depth 13 | matched_op = case expression 14 | when RDF::Resource 15 | schema.enter_shape(expression, focus) do |shape| 16 | if shape 17 | shape.satisfies?(focus, depth: depth + 1) 18 | else 19 | status "Satisfy as #{expression} was re-entered for #{focus}", depth: depth 20 | nil 21 | end 22 | end 23 | when ShapeExpression 24 | expression.satisfies?(focus, depth: depth + 1) 25 | end 26 | satisfy focus: focus, satisfied: matched_op, depth: depth 27 | rescue ShEx::NotSatisfied => e 28 | not_satisfied e.message, focus: focus, unsatisfied: e.expression, depth: depth 29 | raise 30 | end 31 | 32 | ## 33 | # expressions must be ShapeExpressions or references to ShapeExpressions 34 | # 35 | # @return [Operator] `self` 36 | # @raise [ShEx::StructureError] if the value is invalid 37 | def validate! 38 | validate_expressions! 39 | super 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rdf', git: "https://github.com/ruby-rdf/rdf", branch: "develop" 6 | gem 'json-ld', git: "https://github.com/ruby-rdf/json-ld", branch: "develop" 7 | gem 'json-ld-preloaded',git: "https://github.com/ruby-rdf/json-ld-preloaded", branch: "develop" 8 | 9 | group :development, :test do 10 | gem 'ebnf', git: "https://github.com/dryruby/ebnf", branch: "develop" 11 | gem 'rdf-aggregate-repo', git: "https://github.com/ruby-rdf/rdf-aggregate-repo", branch: "develop" 12 | gem 'rdf-rdfa', git: "https://github.com/ruby-rdf/rdf-rdfa", branch: "develop" 13 | gem 'rdf-isomorphic', git: "https://github.com/ruby-rdf/rdf-isomorphic", branch: "develop" 14 | gem 'rdf-turtle', git: "https://github.com/ruby-rdf/rdf-turtle", branch: "develop" 15 | gem 'rdf-vocab', git: "https://github.com/ruby-rdf/rdf-vocab", branch: "develop" 16 | gem 'rdf-xsd', git: "https://github.com/ruby-rdf/rdf-xsd", branch: "develop" 17 | gem 'rdf-spec', git: "https://github.com/ruby-rdf/rdf-spec", branch: "develop" 18 | gem 'sparql', git: "https://github.com/ruby-rdf/sparql", branch: "develop" 19 | gem 'sparql-client', git: "https://github.com/ruby-rdf/sparql-client", branch: "develop" 20 | gem 'sxp', git: "https://github.com/dryruby/sxp.rb", branch: "develop" 21 | gem 'simplecov', '~> 0.22', platforms: :mri 22 | gem 'simplecov-lcov', '~> 0.8', platforms: :mri 23 | end 24 | 25 | group :debug do 26 | gem "byebug", platform: :mri 27 | end 28 | -------------------------------------------------------------------------------- /spec/format_spec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | $:.unshift "." 3 | require 'spec_helper' 4 | require 'rdf/spec/format' 5 | 6 | describe ShEx::Format do 7 | it_behaves_like 'an RDF::Format' do 8 | let(:format_class) {ShEx::Format} 9 | end 10 | 11 | describe ".for" do 12 | [ 13 | :shex, 14 | "etc/doap.shex", 15 | {file_name: 'etc/doap.shex'}, 16 | {file_extension: 'shex'}, 17 | {content_type: 'application/shex'}, 18 | ].each do |arg| 19 | it "discovers with #{arg.inspect}" do 20 | expect(RDF::Format.for(arg)).to eq described_class 21 | end 22 | end 23 | end 24 | 25 | describe "#to_sym" do 26 | specify {expect(described_class.to_sym).to eq :shex} 27 | end 28 | 29 | describe ".cli_commands" do 30 | require 'rdf/cli' 31 | let(:ttl) {File.expand_path("../../etc/doap.ttl", __FILE__)} 32 | let(:schema) {File.expand_path("../../etc/doap.shex", __FILE__)} 33 | let(:schema_input) {File.read(schema)} # Not encoded, since decode done in option parsing 34 | let(:focus) {"https://rubygems.org/gems/shex"} 35 | let(:messages) {Hash.new} 36 | 37 | describe "#shex" do 38 | it "matches from file" do 39 | expect {RDF::CLI.exec(["shex", ttl], focus: focus, schema: schema, messages: messages)}.not_to write.to(:output) 40 | expect(messages).not_to be_empty 41 | end 42 | it "patches from StringIO" do 43 | expect {RDF::CLI.exec(["shex", ttl], focus: focus, schema: StringIO.new(schema_input), messages: messages)}.not_to write.to(:output) 44 | expect(messages).not_to be_empty 45 | end 46 | it "patches from argument" do 47 | expect {RDF::CLI.exec(["shex", ttl], focus: focus, schema_input: schema_input, messages: messages)}.not_to write.to(:output) 48 | expect(messages).not_to be_empty 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /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 "ShEx" ; 13 | doap:homepage ; 14 | doap:license ; 15 | doap:shortdesc "ShEx is a Shape Expression engine for Ruby RDF.rb."@en ; 16 | doap:description "ShEx is an Shape Expression engine for the Ruby RDF.rb library suite."@en ; 17 | doap:created "2016-12-09"^^xsd:date ; 18 | doap:programming-language "Ruby" ; 19 | doap:implements ; 20 | doap:category , 21 | ; 22 | doap:download-page ; 23 | doap:mailing-list ; 24 | doap:bug-database ; 25 | doap:blog ; 26 | doap:developer ; 27 | doap:maintainer ; 28 | doap:documenter ; 29 | foaf:maker ; 30 | dc:date "2016-12-09"^^xsd:date ; 31 | dc:creator ; 32 | dc:isPartOf . 33 | -------------------------------------------------------------------------------- /lib/shex/algebra/shape_expression.rb: -------------------------------------------------------------------------------- 1 | require 'sparql/algebra' 2 | 3 | module ShEx::Algebra 4 | # Implements `satisfies?` and `not_satisfies?` 5 | module ShapeExpression 6 | ## 7 | # Satisfies method 8 | # @param [RDF::Resource] focus 9 | # @param [Integer] depth for logging 10 | # @param [Hash{Symbol => Object}] options 11 | # Other, operand-specific options 12 | # @return [ShapeExpression] with `matched` and `satisfied` accessors for matched triples and sub-expressions 13 | # @raise [ShEx::NotMatched] with `expression` accessor to access `matched` and `unmatched` statements along with `satisfied` and `unsatisfied` operations. 14 | # @see [http://shex.io/shex-semantics/#shape-expression-semantics] 15 | def satisfies?(focus, depth: 0, **options) 16 | raise NotImplementedError, "#satisfies? Not implemented in #{self.class}" 17 | end 18 | 19 | ## 20 | # expressions must be ShapeExpressions or references. 21 | # 22 | # @raise [ShEx::StructureError] if the value is invalid 23 | def validate_expressions! 24 | expressions.each do |op| 25 | case op 26 | when ShapeExpression 27 | when RDF::Resource 28 | ref = schema.find(op) 29 | ref.is_a?(ShapeExpression) || 30 | structure_error("#{json_type} must reference a ShapeExpression: #{ref}") 31 | else 32 | structure_error("#{json_type} must be a ShapeExpression or reference: #{op.to_sxp}") 33 | end 34 | end 35 | end 36 | 37 | ## 38 | # An Operator with a label must contain a reference to itself. 39 | # 40 | # @raise [ShEx::StructureError] if the shape is invalid 41 | def validate_self_references! 42 | return # FIXME: needs to stop at a TripleConstraint 43 | each_descendant do |op| 44 | structure_error("#{json_type} must not reference itself (#{id}): #{op.to_sxp}") if op.references.include?(id) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), 'lib'))) 3 | require 'rubygems' 4 | require 'yard' 5 | require 'rspec/core/rake_task' 6 | 7 | namespace :gem do 8 | desc "Build the shex-#{File.read('VERSION').chomp}.gem file" 9 | task :build do 10 | sh "gem build shex.gemspec && mv shex-#{File.read('VERSION').chomp}.gem pkg/" 11 | end 12 | 13 | desc "Release the shex-#{File.read('VERSION').chomp}.gem file" 14 | task :release do 15 | sh "gem push pkg/shex-#{File.read('VERSION').chomp}.gem" 16 | end 17 | end 18 | 19 | desc 'Default: run specs.' 20 | task default: :spec 21 | 22 | RSpec::Core::RakeTask.new(:spec) 23 | 24 | desc 'Create versions of ebnf files in etc' 25 | task etc: %w{etc/shex.sxp etc/shex.html etc/shex.peg.sxp} 26 | 27 | desc 'Build first, follow and branch tables' 28 | task meta: "lib/shex/meta.rb" 29 | 30 | file "lib/shex/meta.rb" => "etc/shex.ebnf" do |t| 31 | sh %{ 32 | ebnf --peg --format rb \ 33 | --input-format native \ 34 | --mod-name ShEx::Meta \ 35 | --output lib/shex/meta.rb \ 36 | etc/shex.ebnf 37 | } 38 | end 39 | 40 | file "etc/shex.peg.sxp" => "etc/shex.ebnf" do |t| 41 | sh %{ 42 | ebnf --peg --format sxp \ 43 | --input-format native \ 44 | --output etc/shex.peg.sxp \ 45 | etc/shex.ebnf 46 | } 47 | end 48 | 49 | file "etc/shex.sxp" => "etc/shex.ebnf" do |t| 50 | sh %{ 51 | ebnf --input-format native --format sxp \ 52 | --output etc/shex.sxp \ 53 | etc/shex.ebnf 54 | } 55 | end 56 | 57 | file "etc/shex.html" => "etc/shex.ebnf" do |t| 58 | sh %{ 59 | ebnf --input-format native --format html \ 60 | --output etc/shex.html \ 61 | etc/shex.ebnf 62 | } 63 | end 64 | 65 | desc "Build shex JSON-LD context cache" 66 | task context: "lib/shex/shex_context.rb" 67 | file "lib/shex/shex_context.rb" do 68 | require 'json/ld' 69 | File.open("lib/shex/shex_context.rb", "w") do |f| 70 | c = JSON::LD::Context.new().parse("http://www.w3.org/ns/shex.jsonld") 71 | f.write c.to_rb 72 | end 73 | end -------------------------------------------------------------------------------- /lib/shex/algebra/not.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Not < Operator::Unary 4 | include ShapeExpression 5 | NAME = :not 6 | 7 | ## 8 | # Creates an operator instance from a parsed ShExJ representation 9 | # @param (see Operator#from_shexj) 10 | # @return [Operator] 11 | def self.from_shexj(operator, **options) 12 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'ShapeNot' 13 | raise ArgumentError, "missing shapeExpr in #{operator.inspect}" unless operator.has_key?('shapeExpr') 14 | super 15 | end 16 | 17 | # 18 | # S is a ShapeNot and for the shape expression se2 at shapeExpr, notSatisfies(n, se2, G, m). 19 | # @param (see ShapeExpression#satisfies?) 20 | # @return (see ShapeExpression#satisfies?) 21 | # @raise (see ShapeExpression#satisfies?) 22 | # @see [http://shex.io/shex-semantics/#shape-expression-semantics] 23 | def satisfies?(focus, depth: 0) 24 | status "" 25 | op = expressions.last 26 | satisfied_op = begin 27 | case op 28 | when RDF::Resource 29 | schema.enter_shape(op, focus) do |shape| 30 | if shape 31 | shape.satisfies?(focus, depth: depth + 1) 32 | else 33 | status "Satisfy as #{op} was re-entered for #{focus}", depth: depth 34 | shape 35 | end 36 | end 37 | when ShapeExpression 38 | op.satisfies?(focus, depth: depth + 1) 39 | end 40 | rescue ShEx::NotSatisfied => e 41 | return satisfy focus: focus, satisfied: e.expression.unsatisfied, depth: depth 42 | end 43 | not_satisfied "Expression should not have matched", 44 | focus: focus, unsatisfied: satisfied_op, depth: depth 45 | end 46 | 47 | ## 48 | # expressions must be ShapeExpressions or references to ShapeExpressions and must not reference itself recursively. 49 | # 50 | # @return [Operator] `self` 51 | # @raise [ShEx::StructureError] if the value is invalid 52 | def validate! 53 | validate_expressions! 54 | validate_self_references! 55 | super 56 | end 57 | 58 | def json_type 59 | "ShapeNot" 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /shex.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 = 'shex' 9 | gem.homepage = 'https://github.com/ruby-rdf/shex' 10 | gem.license = 'Unlicense' 11 | gem.summary = 'Implementation of Shape Expressions (ShEx) for RDF.rb' 12 | gem.description = 'Implements ShExC and ShEx JSON.' 13 | gem.metadata = { 14 | "documentation_uri" => "https://ruby-rdf.github.io/shex", 15 | "bug_tracker_uri" => "https://github.com/ruby-rdf/shex/issues", 16 | "homepage_uri" => "https://github.com/ruby-rdf/shex", 17 | "mailing_list_uri" => "https://lists.w3.org/Archives/Public/public-rdf-ruby/", 18 | "source_code_uri" => "https://github.com/ruby-rdf/shex", 19 | } 20 | 21 | gem.authors = ['Gregg Kellogg'] 22 | gem.email = 'public-rdf-ruby@w3.org' 23 | 24 | gem.platform = Gem::Platform::RUBY 25 | gem.files = %w(AUTHORS CREDITS README.md LICENSE VERSION etc/doap.ttl) + Dir.glob('lib/**/*.rb') 26 | gem.require_paths = %w(lib) 27 | gem.metadata["yard.run"] = "yri" # use "yard" to build full HTML docs. 28 | 29 | gem.required_ruby_version = '>= 3.0' 30 | gem.requirements = [] 31 | gem.add_runtime_dependency 'rdf', '~> 3.3' 32 | gem.add_runtime_dependency 'json-ld', '~> 3.3' 33 | gem.add_runtime_dependency 'json-ld-preloaded','~> 3.3' 34 | gem.add_runtime_dependency 'ebnf', '~> 2.5' 35 | gem.add_runtime_dependency 'sxp', '~> 2.0' 36 | gem.add_runtime_dependency 'rdf-xsd', '~> 3.3' 37 | gem.add_runtime_dependency 'sparql', '~> 3.3' 38 | gem.add_runtime_dependency 'htmlentities','~> 4.3' 39 | gem.add_development_dependency 'erubis', '~> 2.7' 40 | 41 | gem.add_development_dependency 'getoptlong', '~> 0.2' 42 | gem.add_development_dependency 'rdf-spec', '~> 3.3' 43 | gem.add_development_dependency 'rdf-turtle', '~> 3.3' 44 | gem.add_development_dependency 'rspec', '~> 3.13' 45 | gem.add_development_dependency 'rspec-its', '~> 1.3' 46 | gem.add_development_dependency 'yard', '~> 0.9' 47 | 48 | gem.post_install_message = nil 49 | end 50 | -------------------------------------------------------------------------------- /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/issues) 10 | * Fork and clone the repo: 11 | `git clone git@github.com:your-username/rdf.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 | -------------------------------------------------------------------------------- /lib/shex/algebra/stem.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Stem < Operator::Unary 4 | NAME = :stem 5 | 6 | ## 7 | # Creates an operator instance from a parsed ShExJ representation 8 | # @param (see Operator#from_shexj) 9 | # @return [Operator] 10 | def self.from_shexj(operator, **options) 11 | raise ArgumentError unless operator.is_a?(Hash) && %w(IriStem LiteralStem LanguageStem).include?(operator['type']) 12 | raise ArgumentError, "missing stem in #{operator.inspect}" unless operator.has_key?('stem') 13 | super 14 | end 15 | 16 | ## 17 | # For a node n and constraint value v, nodeSatisfies(n, v) if n matches some valueSetValue vsv in v. A term matches a valueSetValue if: 18 | # 19 | # * vsv is a Stem with stem st and nodeIn(n, st). 20 | def match?(value, depth: 0) 21 | if value.start_with?(operands.first) 22 | status "matched #{value}", depth: depth 23 | true 24 | else 25 | status "not matched #{value}", depth: depth 26 | false 27 | end 28 | end 29 | 30 | def json_type 31 | # FIXME: This is funky, due to oddities in normative shexj 32 | t = self.class.name.split('::').last 33 | #parent.is_a?(Value) ? "#{t}Range" : t 34 | end 35 | end 36 | 37 | class IriStem < Stem 38 | NAME = :iriStem 39 | 40 | # (see Stem#match?) 41 | def match?(value, depth: 0) 42 | if value.iri? 43 | super 44 | else 45 | status "not matched #{value.inspect} if wrong type", depth: depth 46 | false 47 | end 48 | end 49 | end 50 | 51 | class LiteralStem < Stem 52 | NAME = :literalStem 53 | 54 | # (see Stem#match?) 55 | def match?(value, depth: 0) 56 | if value.literal? 57 | super 58 | else 59 | status "not matched #{value.inspect} if wrong type", depth: depth 60 | false 61 | end 62 | end 63 | end 64 | 65 | class LanguageStem < Stem 66 | NAME = :languageStem 67 | 68 | # (see Stem#match?) 69 | # If the operand is empty, than any language will do, 70 | # otherwise, it matches the substring up to that first '-', if any. 71 | def match?(value, depth: 0) 72 | if value.literal? && 73 | value.language? && 74 | (operands.first.to_s.empty? || value.language.to_s.match?(%r(^#{operands.first}((-.*)?)$))) 75 | status "matched #{value}", depth: depth 76 | true 77 | else 78 | status "not matched #{value}", depth: depth 79 | false 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/shex/algebra/and.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class And < Operator 4 | include ShapeExpression 5 | NAME = :and 6 | 7 | def initialize(*args, **options) 8 | case 9 | when args.length < 2 10 | raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 2..)" 11 | end 12 | 13 | # All arguments must be ShapeExpression 14 | raise ArgumentError, "All operands must be Shape operands or resource" unless args.all? {|o| o.is_a?(ShapeExpression) || o.is_a?(RDF::Resource)} 15 | super 16 | end 17 | 18 | ## 19 | # Creates an operator instance from a parsed ShExJ representation 20 | # @param (see Operator#from_shexj) 21 | # @return [Operator] 22 | def self.from_shexj(operator, **options) 23 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'ShapeAnd' 24 | raise ArgumentError, "missing shapeExprs in #{operator.inspect}" unless operator.has_key?('shapeExprs') 25 | super 26 | end 27 | 28 | # 29 | # S is a ShapeAnd and for every shape expression se2 in shapeExprs, satisfies(n, se2, G, m). 30 | # @param (see ShapeExpression#satisfies?) 31 | # @return (see ShapeExpression#satisfies?) 32 | # @raise (see ShapeExpression#satisfies?) 33 | def satisfies?(focus, depth: 0) 34 | status "" 35 | satisfied = [] 36 | unsatisfied = expressions.dup 37 | 38 | # Operand raises NotSatisfied, so no need to check here. 39 | expressions.each do |op| 40 | satisfied << case op 41 | when RDF::Resource 42 | schema.enter_shape(op, focus) do |shape| 43 | if shape 44 | shape.satisfies?(focus, depth: depth + 1) 45 | else 46 | status "Satisfy as #{op} was re-entered for #{focus}", depth: depth 47 | end 48 | end 49 | when ShapeExpression 50 | op.satisfies?(focus, depth: depth + 1) 51 | end 52 | unsatisfied.shift 53 | end 54 | satisfy focus: focus, satisfied: satisfied, depth: depth 55 | rescue ShEx::NotSatisfied => e 56 | not_satisfied e.message, 57 | focus: focus, 58 | satisfied: satisfied, 59 | unsatisfied: unsatisfied, 60 | depth: depth 61 | end 62 | 63 | ## 64 | # expressions must be ShapeExpressions or references to ShapeExpressions 65 | # 66 | # @return [Operator] `self` 67 | # @raise [ShEx::StructureError] if the value is invalid 68 | def validate! 69 | validate_expressions! 70 | validate_self_references! 71 | super 72 | end 73 | 74 | def json_type 75 | "ShapeAnd" 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/shex/algebra/triple_expression.rb: -------------------------------------------------------------------------------- 1 | require 'sparql/algebra' 2 | 3 | module ShEx::Algebra 4 | # Implements `neigh`, `arcs_out`, `args_in` and `matches` 5 | module TripleExpression 6 | ## 7 | # `matches`: asserts that a triple expression is matched by a set of triples that come from the neighbourhood of a node in an RDF graph. The expression `matches(T, expr, m)` indicates that a set of triples `T` can satisfy these rules... 8 | # 9 | # Behavior should be overridden in subclasses, which end by calling this through `super`. 10 | # 11 | # @param [Array] arcs_in 12 | # @param [Array] arcs_out 13 | # @return [TripleExpression] with `matched` accessor for matched triples 14 | # @raise [ShEx::NotMatched] with `expression` accessor to access `matched` and `unmatched` statements along with `satisfied` and `unsatisfied` operations. 15 | def matches(arcs_in, arcs_out, depth: 0) 16 | raise NotImplementedError, "#matches Not implemented in #{self.class}" 17 | end 18 | 19 | ## 20 | # expressions must be TripleExpressions or references to TripleExpressions 21 | # 22 | # @raise [ShEx::StructureError] if the value is invalid 23 | def validate_expressions! 24 | expressions.each do |op| 25 | case op 26 | when TripleExpression 27 | when RDF::Resource 28 | ref = schema.find(op) 29 | ref.is_a?(TripleExpression) || 30 | structure_error("#{json_type} must reference a TripleExpression: #{ref}") 31 | else 32 | structure_error("#{json_type} must be a TripleExpression or reference: #{op.to_sxp}") 33 | end 34 | end 35 | end 36 | 37 | ## 38 | # Included TripleConstraints 39 | # @return [Array] 40 | def triple_constraints 41 | @triple_contraints ||= operands.select do |o| 42 | o.is_a?(TripleExpression) 43 | end. 44 | map(&:triple_constraints). 45 | flatten. 46 | uniq 47 | end 48 | 49 | ## 50 | # Minimum constraint (defaults to 1) 51 | # @return [Integer] 52 | def minimum 53 | @minimum ||= begin 54 | op = operands.detect {|o| o.is_a?(Array) && o.first == :min} || [:min, 1] 55 | op[1] 56 | end 57 | end 58 | 59 | ## 60 | # Maximum constraint (defaults to 1) 61 | # @return [Integer, Float::INFINITY] 62 | def maximum 63 | @maximum ||= begin 64 | op = operands.detect {|o| o.is_a?(Array) && o.first == :max} || [:max, 1] 65 | op[1] == '*' ? Float::INFINITY : op[1] 66 | end 67 | end 68 | 69 | # This operator includes TripleExpression 70 | def triple_expression?; true; end 71 | end 72 | 73 | module ReferencedStatement 74 | # @return [ShEx::Algebra::ShapeExpression] referenced operand which satisfied some of this statement 75 | attr_accessor :referenced 76 | 77 | def to_sxp_bin 78 | referenced ? super + [referenced.to_sxp_bin] : super 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/shex/algebra/or.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Or < Operator 4 | include ShapeExpression 5 | NAME = :or 6 | 7 | def initialize(*args, **options) 8 | case 9 | when args.length < 2 10 | raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 2..)" 11 | end 12 | 13 | # All arguments must be ShapeExpression 14 | raise ArgumentError, "All operands must be Shape operands or resource" unless args.all? {|o| o.is_a?(ShapeExpression) || o.is_a?(RDF::Resource)} 15 | super 16 | end 17 | 18 | ## 19 | # Creates an operator instance from a parsed ShExJ representation 20 | # @param (see Operator#from_shexj) 21 | # @return [Operator] 22 | def self.from_shexj(operator, **options) 23 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'ShapeOr' 24 | raise ArgumentError, "missing shapeExprs in #{operator.inspect}" unless operator.is_a?(Hash) && operator.has_key?('shapeExprs') 25 | super 26 | end 27 | 28 | # 29 | # S is a ShapeOr and there is some shape expression se2 in shapeExprs such that satisfies(n, se2, G, m). 30 | # @param (see ShapeExpression#satisfies?) 31 | # @return (see ShapeExpression#satisfies?) 32 | # @raise (see ShapeExpression#satisfies?) 33 | def satisfies?(focus, depth: 0) 34 | status "", depth: depth 35 | unsatisfied = [] 36 | expressions.any? do |op| 37 | begin 38 | matched_op = case op 39 | when RDF::Resource 40 | schema.enter_shape(op, focus) do |shape| 41 | if shape 42 | shape.satisfies?(focus, depth: depth + 1) 43 | else 44 | status "Satisfy as #{op} was re-entered for #{focus}", depth: depth 45 | shape 46 | end 47 | end 48 | when ShapeExpression 49 | op.satisfies?(focus, depth: depth + 1) 50 | end 51 | return satisfy focus: focus, satisfied: matched_op, depth: depth 52 | rescue ShEx::NotSatisfied => e 53 | status "unsatisfied #{focus}", depth: depth 54 | op = op.dup 55 | if op.respond_to?(:satisfied) 56 | op.satisfied = e.expression.satisfied 57 | op.unsatisfied = e.expression.unsatisfied 58 | end 59 | unsatisfied << op 60 | status "unsatisfied: #{e.message}", depth: depth 61 | false 62 | end 63 | end 64 | 65 | not_satisfied "Expected some expression to be satisfied", 66 | focus: focus, unsatisfied: unsatisfied, depth: depth 67 | end 68 | 69 | ## 70 | # expressions must be ShapeExpressions or references to ShapeExpressions 71 | # 72 | # @return [Operator] `self` 73 | # @raise [ShEx::StructureError] if the value is invalid 74 | def validate! 75 | validate_expressions! 76 | validate_self_references! 77 | super 78 | end 79 | 80 | def json_type 81 | "ShapeOr" 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/shex/algebra/one_of.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class OneOf < Operator 4 | include TripleExpression 5 | NAME = :oneOf 6 | 7 | ## 8 | # Creates an operator instance from a parsed ShExJ representation 9 | # @param (see Operator#from_shexj) 10 | # @return [Operator] 11 | def self.from_shexj(operator, **options) 12 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'OneOf' 13 | raise ArgumentError, "missing expressions in #{operator.inspect}" unless operator.has_key?('expressions') 14 | super 15 | end 16 | 17 | ## 18 | # `expr` is a OneOf and there is some shape expression `se2` in shapeExprs such that a `matches(T, se2, m)`... 19 | # 20 | # @param (see TripleExpression#matches) 21 | # @return (see TripleExpression#matches) 22 | # @raise (see TripleExpression#matches) 23 | def matches(arcs_in, arcs_out, depth: 0) 24 | results, satisfied, unsatisfied = [], [], [] 25 | num_iters, max = 0, maximum 26 | 27 | # OneOf is greedy, and consumes triples from every sub-expression, although only one is requred it succeed. Cardinality is somewhat complicated, as if two expressions match, this works for either a cardinality of one or two. Or two passes with just one match on each pass. 28 | status "" 29 | while num_iters < max 30 | matched_something = expressions.select {|o| o.is_a?(TripleExpression) || o.is_a?(RDF::Resource)}.any? do |op| 31 | begin 32 | op = schema.find(op) if op.is_a?(RDF::Resource) 33 | matched_op = op.matches(arcs_in, arcs_out, depth: depth + 1) 34 | satisfied << matched_op 35 | results += matched_op.matched 36 | arcs_in -= matched_op.matched 37 | arcs_out -= matched_op.matched 38 | status "matched #{matched_op.matched.to_sxp}", depth: depth 39 | rescue ShEx::NotMatched => e 40 | status "not matched: #{e.message}", depth: depth 41 | unsatisfied << e.expression 42 | false 43 | end 44 | end 45 | break unless matched_something 46 | num_iters += 1 47 | status "matched #{results.length} statements after #{num_iters} iterations", depth: depth 48 | end 49 | 50 | # Max violations handled in Shape 51 | if num_iters < minimum 52 | raise ShEx::NotMatched, "Minimum Cardinality Violation: #{results.length} < #{minimum}" 53 | end 54 | 55 | # Last, evaluate semantic acts 56 | semantic_actions.each do |op| 57 | op.satisfies?(matched: results, depth: depth + 1) 58 | end unless results.empty? 59 | 60 | satisfy matched: results, satisfied: satisfied, depth: depth 61 | rescue ShEx::NotMatched, ShEx::NotSatisfied => e 62 | not_matched e.message, 63 | matched: results, unmatched: ((arcs_in + arcs_out).uniq - results), 64 | satisfied: satisfied, unsatisfied: unsatisfied, depth: depth 65 | end 66 | 67 | ## 68 | # expressions must be TripleExpressions or references to TripleExpressions 69 | # 70 | # @return [Operator] `self` 71 | # @raise [ShEx::StructureError] if the value is invalid 72 | def validate! 73 | validate_expressions! 74 | super 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/shex/algebra/each_of.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class EachOf < Operator 4 | include TripleExpression 5 | NAME = :eachOf 6 | 7 | ## 8 | # Creates an operator instance from a parsed ShExJ representation 9 | # @param (see Operator#from_shexj) 10 | # @return [Operator] 11 | def self.from_shexj(operator, **options) 12 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'EachOf' 13 | raise ArgumentError, "missing expressions in #{operator.inspect}" unless operator.has_key?('expressions') 14 | super 15 | end 16 | 17 | ## 18 | # expr is an EachOf and there is some partition of T into T1, T2,… such that for every expression expr1, expr2,… in shapeExprs, matches(Tn, exprn, m)... 19 | # 20 | # @param (see TripleExpression#matches) 21 | # @return (see TripleExpression#matches) 22 | # @raise (see TripleExpression#matches) 23 | def matches(arcs_in, arcs_out, depth: 0) 24 | status "", depth: depth 25 | results, satisfied, unsatisfied = [], [], [] 26 | num_iters, max = 0, maximum 27 | 28 | # enter semantic acts 29 | semantic_actions.each {|op| op.enter(arcs_in: arcs_in, arcs_out: arcs_out, depth: depth + 1)} 30 | 31 | while num_iters < max 32 | begin 33 | matched_this_iter = [] 34 | expressions.select {|o| o.is_a?(TripleExpression) || o.is_a?(RDF::Resource)}.all? do |op| 35 | begin 36 | op = schema.find(op) if op.is_a?(RDF::Resource) 37 | matched_op = op.matches(arcs_in - matched_this_iter, arcs_out - matched_this_iter, depth: depth + 1) 38 | satisfied << matched_op 39 | matched_this_iter += matched_op.matched 40 | rescue ShEx::NotMatched => e 41 | status "not matched: #{e.message}", depth: depth 42 | unsatisfied << e.expression 43 | raise 44 | end 45 | end 46 | results += matched_this_iter 47 | arcs_in -= matched_this_iter 48 | arcs_out -= matched_this_iter 49 | num_iters += 1 50 | status "matched #{results.length} statements after #{num_iters} iterations", depth: depth 51 | rescue ShEx::NotMatched => e 52 | status "no match after #{num_iters} iterations (ignored)", depth: depth 53 | break 54 | end 55 | end 56 | 57 | # Max violations handled in Shape 58 | if num_iters < minimum 59 | raise ShEx::NotMatched, "Minimum Cardinality Violation: #{results.length} < #{minimum}" 60 | end 61 | 62 | # Last, evaluate semantic acts 63 | semantic_actions.each {|op| op.satisfies?(nil, matched: results, depth: depth + 1)} 64 | 65 | satisfy matched: results, satisfied: satisfied, depth: depth 66 | rescue ShEx::NotMatched, ShEx::NotSatisfied => e 67 | not_matched e.message, 68 | matched: results, unmatched: ((arcs_in + arcs_out).uniq - results), 69 | satisfied: satisfied, unsatisfied: unsatisfied, 70 | depth: depth 71 | ensure 72 | semantic_actions.each {|op| op.exit(matched: matched, depth: depth + 1)} 73 | end 74 | 75 | ## 76 | # expressions must be TripleExpressions or references to TripleExpressions 77 | # 78 | # @return [Operator] `self` 79 | # @raise [ShEx::StructureError] if the value is invalid 80 | def validate! 81 | validate_expressions! 82 | super 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /etc/doap.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://www.w3.org/ns/shex.jsonld", 3 | "type": "Schema", 4 | "start": "https://rubygems.org/gems/shex/DOAP", 5 | "shapes": [ 6 | { 7 | "id": "https://rubygems.org/gems/shex/DOAP", 8 | "type": "Shape", 9 | "extra": [ 10 | "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" 11 | ], 12 | "expression": { 13 | "type": "EachOf", 14 | "expressions": [ 15 | { 16 | "type": "TripleConstraint", 17 | "predicate": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", 18 | "valueExpr": { 19 | "type": "NodeConstraint", 20 | "values": [ 21 | "http://usefulinc.com/ns/doap#Project" 22 | ] 23 | } 24 | }, 25 | { 26 | "type": "OneOf", 27 | "expressions": [ 28 | { 29 | "type": "EachOf", 30 | "expressions": [ 31 | { 32 | "type": "TripleConstraint", 33 | "predicate": "http://usefulinc.com/ns/doap#name", 34 | "valueExpr": { 35 | "type": "NodeConstraint", 36 | "nodeKind": "literal" 37 | } 38 | }, 39 | { 40 | "type": "TripleConstraint", 41 | "predicate": "http://usefulinc.com/ns/doap#description", 42 | "valueExpr": { 43 | "type": "NodeConstraint", 44 | "nodeKind": "literal" 45 | } 46 | } 47 | ] 48 | }, 49 | { 50 | "type": "EachOf", 51 | "expressions": [ 52 | { 53 | "type": "TripleConstraint", 54 | "predicate": "http://purl.org/dc/terms/title", 55 | "valueExpr": { 56 | "type": "NodeConstraint", 57 | "nodeKind": "literal" 58 | } 59 | }, 60 | { 61 | "type": "TripleConstraint", 62 | "predicate": "http://purl.org/dc/terms/description", 63 | "valueExpr": { 64 | "type": "NodeConstraint", 65 | "nodeKind": "literal" 66 | } 67 | } 68 | ] 69 | } 70 | ], 71 | "min": 1, 72 | "max": -1 73 | }, 74 | { 75 | "type": "TripleConstraint", 76 | "predicate": "http://usefulinc.com/ns/doap#category", 77 | "valueExpr": { 78 | "type": "NodeConstraint", 79 | "nodeKind": "iri" 80 | }, 81 | "min": 0, 82 | "max": -1 83 | }, 84 | { 85 | "type": "TripleConstraint", 86 | "predicate": "http://usefulinc.com/ns/doap#developer", 87 | "valueExpr": { 88 | "type": "NodeConstraint", 89 | "nodeKind": "iri" 90 | }, 91 | "min": 1, 92 | "max": -1 93 | }, 94 | { 95 | "type": "TripleConstraint", 96 | "predicate": "http://usefulinc.com/ns/doap#implements", 97 | "valueExpr": { 98 | "type": "NodeConstraint", 99 | "values": [ 100 | "http://shex.io/shex-semantics/" 101 | ] 102 | } 103 | } 104 | ] 105 | } 106 | } 107 | ] 108 | } -------------------------------------------------------------------------------- /lib/shex/algebra.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path("../..", __FILE__)) 2 | require 'sparql/algebra' 3 | 4 | module ShEx 5 | # Based on the SPARQL Algebra, operators for executing a patch 6 | # 7 | # @author [Gregg Kellogg](https://greggkellogg.net/) 8 | module Algebra 9 | autoload :And, 'shex/algebra/and' 10 | autoload :Annotation, 'shex/algebra/annotation' 11 | autoload :EachOf, 'shex/algebra/each_of' 12 | autoload :External, 'shex/algebra/external' 13 | autoload :IriStem, 'shex/algebra/stem' 14 | autoload :IriStemRange, 'shex/algebra/stem_range' 15 | autoload :Language, 'shex/algebra/language' 16 | autoload :LanguageStem, 'shex/algebra/stem' 17 | autoload :LanguageStemRange,'shex/algebra/stem_range' 18 | autoload :LiteralStem, 'shex/algebra/stem' 19 | autoload :LiteralStemRange, 'shex/algebra/stem_range' 20 | autoload :NodeConstraint, 'shex/algebra/node_constraint' 21 | autoload :Not, 'shex/algebra/not' 22 | autoload :OneOf, 'shex/algebra/one_of' 23 | autoload :Operator, 'shex/algebra/operator' 24 | autoload :Or, 'shex/algebra/or' 25 | autoload :Schema, 'shex/algebra/schema' 26 | autoload :SemAct, 'shex/algebra/semact' 27 | autoload :Shape, 'shex/algebra/shape' 28 | autoload :ShapeExpression, 'shex/algebra/shape_expression' 29 | autoload :Start, 'shex/algebra/start' 30 | autoload :Stem, 'shex/algebra/stem' 31 | autoload :StemRange, 'shex/algebra/stem_range' 32 | autoload :TripleConstraint, 'shex/algebra/triple_constraint' 33 | autoload :TripleExpression, 'shex/algebra/triple_expression' 34 | autoload :Value, 'shex/algebra/value' 35 | 36 | 37 | ## 38 | # Creates an operator instance from a parsed ShExJ representation 39 | # 40 | # @example Simple TripleConstraint 41 | # rep = JSON.parse(%({ 42 | # "type": "TripleConstraint", 43 | # "predicate": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" 44 | # } 45 | # )) 46 | # TripleConstraint.from(rep) #=> (tripleConstraint a) 47 | # @param [Hash] operator 48 | # @param [Hash] options ({}) 49 | # @option options [RDF::URI] :base 50 | # @option options [Hash{String => RDF::URI}] :prefixes 51 | # @return [Operator] 52 | def self.from_shexj(operator, **options) 53 | raise ArgumentError unless operator.is_a?(Hash) 54 | klass = case operator['type'] 55 | when 'Annotation' then Annotation 56 | when 'EachOf' then EachOf 57 | when 'IriStem' then IriStem 58 | when 'IriStemRange' then IriStemRange 59 | when 'Language' then Language 60 | when 'LanguageStem' then LanguageStem 61 | when 'LanguageStemRange' then LanguageStemRange 62 | when 'LiteralStem' then LiteralStem 63 | when 'LiteralStemRange' then LiteralStemRange 64 | when 'NodeConstraint' then NodeConstraint 65 | when 'OneOf' then OneOf 66 | when 'Schema' then Schema 67 | when 'SemAct' then SemAct 68 | when 'Shape' then Shape 69 | when 'ShapeAnd' then And 70 | when 'ShapeExternal' then External 71 | when 'ShapeNot' then Not 72 | when 'ShapeOr' then Or 73 | when 'TripleConstraint' then TripleConstraint 74 | when 'Wildcard' then StemRange 75 | else raise ArgumentError, "unknown type #{operator['type'].inspect}" 76 | end 77 | 78 | klass.from_shexj(operator, **options) 79 | end 80 | end 81 | end 82 | 83 | 84 | -------------------------------------------------------------------------------- /lib/shex/algebra/semact.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class SemAct < Operator 4 | NAME = :semact 5 | 6 | ## 7 | # Creates an operator instance from a parsed ShExJ representation 8 | # @param (see Operator#from_shexj) 9 | # @return [Operator] 10 | def self.from_shexj(operator, **options) 11 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "SemAct" 12 | raise ArgumentError, "missing name in #{operator.inspect}" unless operator.has_key?('name') 13 | code = operator.delete('code') 14 | operator['code'] = code if code # Reorders operands appropriately 15 | super 16 | end 17 | 18 | ## 19 | # Called on entry 20 | # 21 | # @overload enter(code, arcs_in, arcs_out, logging) 22 | # @param [String] code 23 | # @param [Array] arcs_in available statements to be matched having `focus` as an object 24 | # @param [Array] arcs_out available statements to be matched having `focus` as a subject 25 | # @param [Integer] depth for logging 26 | # @param [Hash{Symbol => Object}] options 27 | # Other, operand-specific options 28 | # @return [Boolean] Returning `false` results in {ShEx::NotSatisfied} exception 29 | def enter(**options) 30 | if implementation = schema.extensions[operands.first.to_s] 31 | implementation.enter(code: operands[0], expression: parent, **options) 32 | end 33 | end 34 | 35 | # 36 | # The evaluation semActsSatisfied on a list of SemActs returns success or failure. The evaluation of an individual SemAct is implementation-dependent. 37 | # 38 | # In addition to standard arguments `satsisfies` arguments, the current `matched` and `unmatched` statements may be passed. Additionally, all sub-classes of `Operator` have available `parent`, and `schema` accessors, which allows access to the operands of the parent, for example. 39 | # 40 | # @param [Object] focus (ignored) 41 | # @param [Array] matched matched statements 42 | # @param [Array] unmatched unmatched statements 43 | # @return [Boolean] `true` if satisfied, `false` if it does not apply 44 | # @raise [ShEx::NotSatisfied] if not satisfied 45 | def satisfies?(focus, matched: [], unmatched: [], depth: 0) 46 | if implementation = schema.extensions[operands.first.to_s] 47 | if matched.empty? 48 | implementation.visit(code: operands[1], 49 | expression: parent, 50 | depth: depth) || 51 | not_satisfied("SemAct failed", unmatched: unmatched) 52 | end 53 | matched.all? do |statement| 54 | implementation.visit(code: operands[1], 55 | matched: statement, 56 | expression: parent, 57 | depth: depth) 58 | end || not_satisfied("SemAct failed", matched: matched, unmatched: unmatched) 59 | else 60 | status("unknown SemAct name #{operands.first}", depth: depth) {"expression: #{self.to_sxp}"} 61 | false 62 | end 63 | end 64 | 65 | ## 66 | # Called on exit from containing {ShEx::TripleExpression} 67 | # 68 | # @param [String] code 69 | # @param [Array] matched statements matched by this expression 70 | # @param [Array] unmatched statements considered, but not matched by this expression 71 | # @param [Integer] depth for logging 72 | # @param [Hash{Symbol => Object}] options 73 | # Other, operand-specific options 74 | # @return [void] 75 | def exit(code: nil, matched: [], unmatched: [], depth: 0, **options) 76 | if implementation = schema.extensions[operands.first.to_s] 77 | implementation.exit(code: operands[1], 78 | matched: matched, 79 | unmatched: unmatched, 80 | expresssion: parent, 81 | depth: depth) 82 | end 83 | end 84 | 85 | # Does This operator is SemAct 86 | def semact?; true; end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /etc/specNotes.md: -------------------------------------------------------------------------------- 1 | ## 4.2 Mapping from Nodes to Shapes 2 | 3 | * The description of `ShapeMap` should clarify that the node in the map represents a node within the graph being satisfied, not a label within the schema. The distinction between a `shapeLabel` and a `node` in a graph can be elusive. 4 | * The use of BNodes for identifying nodes in a graph is not conformant with the meaning of a blank node in RDF Concepts. Even though some systems may allow you to get a node within a graph using the bnode label contained within a serialization from which the graph is created, this is problematic. It may be better to identify nodes in a `ShapeMap` using something like a [SPARQL Property Path](https://www.w3.org/TR/sparql11-query/#propertypath-syntaxforms) or [Path Expression](https://www.w3.org/TR/ldpatch/#path-expression) as defined in [LD Patch](https://www.w3.org/TR/ldpatch/). For example: 5 | 6 | ``` 7 | { 8 | "http://inst.example/#Issue1": "http://schema.example/IssueShape, 9 | "http://inst.example/#Issue1 / http://ex.example/#reportedBy", "_:UserShape", 10 | "http://inst.example/#Issue1 / http://ex.example/#reportedBy", "http://schema.example/EmployeeShape" 11 | } 12 | ``` 13 | 14 | This might instead borrow syntax from ShExC, but that won't help much for ShExJ. 15 | 16 | ## 4.3.2 Semantics (Shape Expressions) 17 | 18 | * `S` is used consistently in describing how `satisfies` operates, but it is not defined what `S` represents. Presumably this is `se` from `satisfies(n, se, G, m)`. 19 | * Presumably, `satisfies` also requires that the shape schema be used, as it is required for looking up shape references. Perhaps the schema is intended to be in scope. 20 | * It should be clarified that `n` represents a node in `G`, and also a key in `m`. And that `se` represents a Shape Expression identified by the value of `n` in `m`. 21 | * `S is a Shape and satisfies(n, se) as defined below in Shapes and Triple Expressions.` Note that `satisfies` here takes just two arguments, but elsewhere, and in the reference, it takes four arguments. 22 | 23 | ## 4.4.1 Semantics (Node Constraints) 24 | * `satisfies2(n, nc)` takes two arguments. This is fine if it reads that `n` is the object of some triple, for which the constraint is checked (`G` may still be necessary, though). However, the _NODE KIND EXMPLE 1_ uses `issue1`, `issue2`, and `issue3`, which are subjects, and clearly the `IRI` constraint is checked on the object of the triples. It seems that the semantics of _TripleConstraint_ might handle this, but it's not clear how this works, as `value` is not in `m` in that description. 25 | 26 | ## 4.4.3 Datatype Constraints 27 | * shape should use `xsd:date`, not `xsd:dateTime`. (In PR) 28 | 29 | ### 4.4.6 Values Constraint 30 | * Example 2 data `` should be `` to be false. (in PR) 31 | * Also, note that **VALUES CONSTRAINT EXAMPLE 2** appears later in **6. Parsing ShEx Compact syntax** as a totally different shape. 32 | 33 | ## 4.5 Shapes and Triple Expressions 34 | * Shape looks like there is zero or one extra IRI, but grammar has `predicate+` 35 | * Description of `expr is a TripleConstraint`: 36 | * uses `givenName` and `author`, should both be `givenName`. 37 | * use `value` instead of `n2`? 38 | * What is Shape has an expression with cardinality 0; in this case, it would be an error if `matched` was not empty. But, this might not happen unless cardinality is specified on the shape itself. 39 | 40 | ## 4.6.1 Inclusion Requirement 41 | * ShExJ version of example should use `EachOf` instead of `ShapeAnd`, as it's wrapped in `Shape`. 42 | 43 | ## 4.7 Semantic Actions 44 | * Shape uses `ex:p1`, but data uses ``. 45 | * What do to for Test action with no argument? 46 | 47 | ## 4.8 Annotations 48 | * Not clear how `object` is serialized. It seems to be N-Triples for Literals, but URIs are serialized as strings without surrounding angle brackets. 49 | * Example in spec is at odds with test-suite for ShExC vs ShExJ formats. 50 | * Using JSON-LD Value Objects will make this unambiguous. 51 | 52 | ## 4.9.1 Simple Examples (Validation Examples) 53 | * The third example fails because `nonmatchables` includes ` ex:shoeSize 30 .` 54 | * The third example also incorrectly places `extra` inside of TripleConstraint. -------------------------------------------------------------------------------- /lib/shex/terminals.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'ebnf/ll1/lexer' 3 | 4 | module ShEx 5 | module Terminals 6 | # Definitions of token regular expressions used for lexical analysis 7 | 8 | ## 9 | # Unicode regular expressions for Ruby 1.9+ with the Oniguruma engine. 10 | U_CHARS1 = Regexp.compile(<<-EOS.gsub(/\s+/, '')) 11 | [\\u00C0-\\u00D6]|[\\u00D8-\\u00F6]|[\\u00F8-\\u02FF]| 12 | [\\u0370-\\u037D]|[\\u037F-\\u1FFF]|[\\u200C-\\u200D]| 13 | [\\u2070-\\u218F]|[\\u2C00-\\u2FEF]|[\\u3001-\\uD7FF]| 14 | [\\uF900-\\uFDCF]|[\\uFDF0-\\uFFFD]|[\\u{10000}-\\u{EFFFF}] 15 | EOS 16 | U_CHARS2 = Regexp.compile("\\u00B7|[\\u0300-\\u036F]|[\\u203F-\\u2040]").freeze 17 | IRI_RANGE = Regexp.compile("[[^<>\"{}|^`\\\\]&&[^\\x00-\\x20]]").freeze 18 | 19 | # 87 20 | UCHAR4 = /\\u([0-9A-Fa-f]{4,4})/.freeze 21 | UCHAR8 = /\\U([0-9A-Fa-f]{8,8})/.freeze 22 | UCHAR = Regexp.union(UCHAR4, UCHAR8).freeze 23 | # 171s 24 | PERCENT = /%\h\h/.freeze 25 | # 173s 26 | PN_LOCAL_ESC = /\\[_~\.\-\!$\&'\(\)\*\+,;=\/\?\#@%]/.freeze 27 | # 170s 28 | PLX = /#{PERCENT}|#{PN_LOCAL_ESC}/.freeze.freeze 29 | # 164s 30 | PN_CHARS_BASE = /[A-Za-z]|#{U_CHARS1}/.freeze 31 | # 165s 32 | PN_CHARS_U = /_|#{PN_CHARS_BASE}/.freeze 33 | # 167s 34 | PN_CHARS = /[\d-]|#{PN_CHARS_U}|#{U_CHARS2}/.freeze 35 | PN_LOCAL_BODY = /(?:(?:\.|:|#{PN_CHARS}|#{PLX})*(?:#{PN_CHARS}|:|#{PLX}))?/.freeze 36 | PN_CHARS_BODY = /(?:(?:\.|#{PN_CHARS})*#{PN_CHARS})?/.freeze 37 | # 168s 38 | PN_PREFIX = /#{PN_CHARS_BASE}#{PN_CHARS_BODY}/.freeze 39 | # 169s 40 | PN_LOCAL = /(?:[\d|]|#{PN_CHARS_U}|#{PLX})#{PN_LOCAL_BODY}/.freeze 41 | # 155s 42 | EXPONENT = /[eE][+-]?\d+/ 43 | # 160s 44 | ECHAR = /\\[tbnrf\\"']/ 45 | 46 | WS = %r(( 47 | \s 48 | | (?:\#[^\n\r]*) 49 | | (?:/\*(?:(?:\*[^/])|[^*])*\*/) 50 | )+)xmu.freeze 51 | 52 | # 69 53 | RDF_TYPE = /a/.freeze 54 | # 18t 55 | IRIREF = /<(?:#{IRI_RANGE}|#{UCHAR})*>/.freeze 56 | # 73 57 | PNAME_NS = /#{PN_PREFIX}?:/.freeze 58 | # 74 59 | PNAME_LN = /#{PNAME_NS}#{PN_LOCAL}/.freeze 60 | # 75 61 | ATPNAME_NS = /@#{WS}*#{PN_PREFIX}?:/m.freeze 62 | # 76 63 | ATPNAME_LN = /@#{WS}*#{PNAME_NS}#{PN_LOCAL}/m.freeze 64 | # 77 65 | BLANK_NODE_LABEL = /_:(?:\d|#{PN_CHARS_U})(?:(?:#{PN_CHARS}|\.)*#{PN_CHARS})?/.freeze 66 | # 78 67 | LANGTAG = /@[a-zA-Z]+(?:-[a-zA-Z0-9]+)*/.freeze 68 | # 79 69 | INTEGER = /[+-]?\d+/.freeze 70 | # 80 71 | DECIMAL = /[+-]?(?:\d*\.\d+)/.freeze 72 | # 81 73 | DOUBLE = /[+-]?(?:\d+\.\d*#{EXPONENT}|\.?\d+#{EXPONENT})/.freeze 74 | # 83 75 | STRING_LITERAL1 = /'(?:[^\'\\\n\r]|#{ECHAR}|#{UCHAR})*'/.freeze 76 | # 84 77 | STRING_LITERAL2 = /"(?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*"/.freeze 78 | # 85 79 | STRING_LITERAL_LONG1 = /'''(?:(?:'|'')?(?:[^'\\]|#{ECHAR}|#{UCHAR}))*'''/m.freeze 80 | # 86 81 | STRING_LITERAL_LONG2 = /"""(?:(?:"|"")?(?:[^"\\]|#{ECHAR}|#{UCHAR}))*"""/m.freeze 82 | 83 | # 83l 84 | LANG_STRING_LITERAL1 = /'(?:[^\'\\\n\r]|#{ECHAR}|#{UCHAR})*'#{LANGTAG}/.freeze 85 | # 84l 86 | LANG_STRING_LITERAL2 = /"(?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*"#{LANGTAG}/.freeze 87 | # 85l 88 | LANG_STRING_LITERAL_LONG1 = /'''(?:(?:'|'')?(?:[^'\\]|#{ECHAR}|#{UCHAR}))*'''#{LANGTAG}/m.freeze 89 | # 86l 90 | LANG_STRING_LITERAL_LONG2 = /"""(?:(?:"|"")?(?:[^"\\]|#{ECHAR}|#{UCHAR}))*"""#{LANGTAG}/m.freeze 91 | 92 | # XX 93 | REGEXP = %r(/(?:[^/\\\n\r]|\\[nrt\\|.?*+(){}$-\[\]^/]|#{UCHAR})+/[smix]*).freeze 94 | 95 | # 68 96 | CODE = /\{(?:[^%\\]|\\[%\\]|#{UCHAR})*%#{WS}*\}/m.freeze 97 | # 70 98 | REPEAT_RANGE = /\{\s*#{INTEGER}(?:,#{WS}*(?:#{INTEGER}|\*)?)?#{WS}*\}/.freeze 99 | 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/shex/algebra/triple_constraint.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class TripleConstraint < Operator 4 | include TripleExpression 5 | NAME = :tripleConstraint 6 | 7 | ## 8 | # Creates an operator instance from a parsed ShExJ representation 9 | # @param (see Operator#from_shexj) 10 | # @return [Operator] 11 | def self.from_shexj(operator, **options) 12 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'TripleConstraint' 13 | raise ArgumentError unless operator.has_key?('predicate') 14 | super 15 | end 16 | 17 | ## 18 | # In this case, we accept an array of statements, and match based on cardinality. 19 | # 20 | # @param (see TripleExpression#matches) 21 | # @return (see TripleExpression#matches) 22 | # @raise (see TripleExpression#matches) 23 | def matches(arcs_in, arcs_out, depth: 0) 24 | status "predicate #{predicate}", depth: depth 25 | results, unmatched, satisfied, unsatisfied = [], [], [], [] 26 | num_iters, max = 0, maximum 27 | 28 | statements = inverse? ? arcs_in : arcs_out 29 | statements.select {|st| st.predicate == predicate}.each do |statement| 30 | break if num_iters == max # matched enough 31 | 32 | focus = inverse? ? statement.subject : statement.object 33 | 34 | begin 35 | matched_shape = if expression.is_a?(RDF::Resource) 36 | schema.enter_shape(expression, focus) do |shape| 37 | if shape 38 | shape.satisfies?(focus, depth: depth + 1) 39 | else 40 | status "Satisfy as #{expression} was re-entered for #{focus}", depth: depth 41 | nil 42 | end 43 | end 44 | elsif expression 45 | expression.satisfies?(focus, depth: depth + 1) 46 | end 47 | status "matched #{statement.to_sxp}", depth: depth 48 | if matched_shape 49 | matched_shape.matched = [statement] 50 | statement = statement.dup.extend(ReferencedStatement) 51 | statement.referenced = matched_shape 52 | satisfied << matched_shape 53 | end 54 | results << statement 55 | num_iters += 1 56 | rescue ShEx::NotSatisfied => e 57 | status "not satisfied: #{e.message}", depth: depth 58 | unsatisfied << e.expression 59 | statement = statement.dup.extend(ReferencedStatement) 60 | statement.referenced = expression 61 | unmatched << statement 62 | end 63 | end 64 | 65 | # Max violations handled in Shape 66 | if results.length < minimum 67 | raise ShEx::NotMatched, "Minimum Cardinality Violation: #{results.length} < #{minimum}" 68 | end 69 | 70 | # Last, evaluate semantic acts 71 | semantic_actions.each do |op| 72 | op.satisfies?(results, matched: results, depth: depth + 1) 73 | end unless results.empty? 74 | 75 | satisfy matched: results, unmatched: unmatched, 76 | satisfied: satisfied, unsatisfied: unsatisfied, depth: depth 77 | rescue ShEx::NotMatched, ShEx::NotSatisfied => e 78 | not_matched e.message, 79 | matched: results, unmatched: unmatched, 80 | satisfied: satisfied, unsatisfied: unsatisfied, depth: depth 81 | end 82 | 83 | def predicate 84 | @predicate ||= operands.detect {|o| o.is_a?(Array) && o.first == :predicate}.last 85 | end 86 | 87 | ## 88 | # expression must be a ShapeExpression 89 | # 90 | # @return [Operator] `self` 91 | # @raise [ShEx::StructureError] if the value is invalid 92 | def validate! 93 | case expression 94 | when nil, ShapeExpression 95 | when RDF::Resource 96 | ref = schema.find(expression) 97 | ref.is_a?(ShapeExpression) || 98 | structure_error("#{json_type} must reference a ShapeExpression: #{ref}") 99 | else 100 | structure_error("#{json_type} must be a ShapeExpression or reference: #{expresson.to_sxp}") 101 | end 102 | super 103 | end 104 | 105 | ## 106 | # Included TripleConstraints 107 | # @return [Array] 108 | def triple_constraints 109 | [self] 110 | end 111 | 112 | def inverse? 113 | operands.include?(:inverse) 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/shex_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../..", __FILE__) 2 | require 'spec_helper' 3 | require 'rdf/util/file' 4 | 5 | describe ShEx do 6 | let(:input) {%( {})} 7 | describe ".parse" do 8 | specify do 9 | expect(described_class.parse(input)).to be_a(ShEx::Algebra::Schema) 10 | end 11 | 12 | it "detects bad format" do 13 | expect {described_class.parse(input, format: :foo)}.to raise_error(/Unknown expression format/) 14 | end 15 | end 16 | 17 | describe ".open" do 18 | specify do 19 | expect(RDF::Util::File).to receive(:open_file).and_yield(StringIO.new(input)) 20 | expect(described_class.open("foo")).to be_a(ShEx::Algebra::Schema) 21 | end 22 | 23 | it "detects bad format" do 24 | expect(RDF::Util::File).to receive(:open_file).and_yield(StringIO.new(input)) 25 | expect {described_class.open("foo", format: :foo)}.to raise_error(/Unknown expression format/) 26 | end 27 | end 28 | 29 | describe ".execute" do 30 | specify do 31 | expect(described_class.execute(input, nil, {RDF::URI("http://example/foo") => RDF::URI("http://a.example/S1")})).to be_a(Hash) 32 | expect(described_class.execute(input, nil, {RDF::URI("http://example/foo") => RDF::URI("http://a.example/S1")}).values.flatten).to all(be_a(ShEx::Algebra::ShapeResult)) 33 | end 34 | end 35 | 36 | describe ".satisfy?" do 37 | specify do 38 | expect(described_class.satisfies?(input, nil, {RDF::URI("http://example/foo") => RDF::URI("http://a.example/S1")})).to be_truthy 39 | end 40 | end 41 | 42 | context "README" do 43 | let(:doap_shex) {File.expand_path("../../etc/doap.shex", __FILE__)} 44 | let(:doap_json) {File.expand_path("../../etc/doap.json", __FILE__)} 45 | let(:doap_ttl) {File.expand_path("../../etc/doap.ttl", __FILE__)} 46 | let(:doap_subj) {RDF::URI("https://rubygems.org/gems/shex")} 47 | let(:doap_shape) {RDF::URI("https://rubygems.org/gems/shex/DOAP")} 48 | let(:doap_graph) {RDF::Graph.load(doap_ttl)} 49 | let(:doap_sxp) {%{(schema 50 | (base ) 51 | (prefix (("doap" ) ("dc" ))) 52 | (start ) 53 | (shapes 54 | (shape 55 | (id ) 56 | (extra a) 57 | (eachOf 58 | (tripleConstraint 59 | (predicate a) 60 | (nodeConstraint (value ))) 61 | (oneOf 62 | (eachOf 63 | (tripleConstraint 64 | (predicate ) 65 | (nodeConstraint literal)) 66 | (tripleConstraint 67 | (predicate ) 68 | (nodeConstraint literal)) ) 69 | (eachOf 70 | (tripleConstraint 71 | (predicate ) 72 | (nodeConstraint literal)) 73 | (tripleConstraint 74 | (predicate ) 75 | (nodeConstraint literal)) ) 76 | (min 1) 77 | (max "*")) 78 | (tripleConstraint 79 | (predicate ) 80 | (nodeConstraint iri) 81 | (min 0) 82 | (max "*")) 83 | (tripleConstraint 84 | (predicate ) 85 | (nodeConstraint iri) 86 | (min 1) 87 | (max "*")) 88 | (tripleConstraint 89 | (predicate ) 90 | (nodeConstraint (value ))) )) ))}.gsub(/^ /m, '') 91 | } 92 | 93 | it "parses doap.shex" do 94 | expect(File.read(doap_shex)).to generate(doap_sxp) 95 | end 96 | 97 | it "parses doap.json", skip: "base not in ShExJ" do 98 | sxp = doap_sxp.split("\n").reject {|l| l =~ /\(prefix/}.join("\n") 99 | expect(File.read(doap_json)).to generate(sxp, format: :shexj) 100 | end 101 | 102 | it "validates doap.ttl from shexc" do 103 | schema = ShEx.open(doap_shex) 104 | expect(schema).to satisfy(doap_graph, File.read(doap_ttl), {doap_subj => doap_shape}, logger: RDF::Spec.logger) 105 | end 106 | 107 | it "validates doap.ttl from shexj" do 108 | schema = ShEx.open(doap_json, format: :shexj) 109 | expect(schema).to satisfy(doap_graph, File.read(doap_ttl), {doap_subj => doap_shape}, logger: RDF::Spec.logger) 110 | end 111 | end 112 | end 113 | 114 | -------------------------------------------------------------------------------- /lib/shex/algebra/stem_range.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class StemRange < Operator::Binary 4 | NAME = :stemRange 5 | 6 | ## 7 | # Creates an operator instance from a parsed ShExJ representation 8 | # @param (see Operator#from_shexj) 9 | # @return [Operator] 10 | def self.from_shexj(operator, **options) 11 | raise ArgumentError unless operator.is_a?(Hash) && %w(IriStemRange LiteralStemRange LanguageStemRange).include?(operator['type']) 12 | raise ArgumentError, "missing stem in #{operator.inspect}" unless operator.has_key?('stem') 13 | 14 | # Normalize wildcard representation 15 | operator['stem'] = :wildcard if operator['stem'] =={'type' => 'Wildcard'} 16 | 17 | # Note that the type may be StemRange, but if there's no exclusions, it's really just a Stem 18 | if operator.has_key?('exclusions') 19 | super 20 | else 21 | # Remove "Range" from type 22 | case operator['type'] 23 | when 'IriStemRange' 24 | IriStem.from_shexj(operator.merge('type' => 'IriStem'), **options) 25 | when 'LiteralStemRange' 26 | LiteralStem.from_shexj(operator.merge('type' => 'LiteralStem'), **options) 27 | when 'LanguageStemRange' 28 | LanguageStem.from_shexj(operator.merge('type' => 'LanguageStem'), **options) 29 | end 30 | end 31 | end 32 | 33 | ## 34 | # For a node n and constraint value v, nodeSatisfies(n, v) if n matches some valueSetValue vsv in v. A term matches a valueSetValue if: 35 | # 36 | # * vsv is a StemRange with stem st and exclusions excls and nodeIn(n, st) and there is no x in excls such that nodeIn(n, excl). 37 | # * vsv is a Wildcard with exclusions excls and there is no x in excls such that nodeIn(n, excl). 38 | def match?(value, depth: 0) 39 | initial_match = case operands.first 40 | when :wildcard then true 41 | when RDF::Value then value.start_with?(operands.first) 42 | else false 43 | end 44 | 45 | unless initial_match 46 | status "#{value} does not match #{operands.first}", depth: depth 47 | return false 48 | end 49 | 50 | if exclusions.any? do |exclusion| 51 | case exclusion 52 | when RDF::Value then value == exclusion 53 | when Stem then exclusion.match?(value, depth: depth + 1) 54 | else false 55 | end 56 | end 57 | status "#{value} excluded", depth: depth 58 | return false 59 | end 60 | 61 | status "matched #{value}", depth: depth 62 | true 63 | end 64 | 65 | def exclusions 66 | (operands.last.is_a?(Array) && operands.last.first == :exclusions) ? operands.last[1..-1] : [] 67 | end 68 | end 69 | 70 | class IriStemRange < StemRange 71 | NAME = :iriStemRange 72 | 73 | # (see StemRange#match?) 74 | def match?(value, depth: 0) 75 | if value.uri? 76 | super 77 | else 78 | status "not matched #{value.inspect} if wrong type", depth: depth 79 | false 80 | end 81 | end 82 | end 83 | 84 | class LiteralStemRange < StemRange 85 | NAME = :literalStemRange 86 | 87 | # (see StemRange#match?) 88 | def match?(value, depth: 0) 89 | if value.literal? 90 | super 91 | else 92 | status "not matched #{value.inspect} if wrong type", depth: depth 93 | false 94 | end 95 | end 96 | end 97 | 98 | class LanguageStemRange < StemRange 99 | NAME = :languageStemRange 100 | 101 | # (see StemRange#match?) 102 | def match?(value, depth: 0) 103 | initial_match = case operands.first 104 | when :wildcard then true 105 | when RDF::Literal 106 | value.language? && 107 | (operands.first.to_s.empty? || value.language.to_s.match?(%r(^#{operands.first}((-.*)?)$))) 108 | else false 109 | end 110 | 111 | unless initial_match 112 | status "#{value} does not match #{operands.first}", depth: depth 113 | return false 114 | end 115 | 116 | if exclusions.any? do |exclusion| 117 | case exclusion 118 | when RDF::Literal, String then value.language.to_s == exclusion 119 | when Stem then exclusion.match?(value, depth: depth + 1) 120 | else false 121 | end 122 | end 123 | status "#{value} excluded", depth: depth 124 | return false 125 | end 126 | 127 | status "matched #{value}", depth: depth 128 | true 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/shex/format.rb: -------------------------------------------------------------------------------- 1 | require 'rdf/format' 2 | 3 | module ShEx 4 | ## 5 | # ShEx format specification. Note that this format does not define any readers or writers. 6 | # 7 | # @example Obtaining an ShEx format class 8 | # RDF::Format.for(:shex) #=> ShEx::Format 9 | # RDF::Format.for("etc/foaf.shex") 10 | # RDF::Format.for(file_name: "etc/foaf.shex") 11 | # RDF::Format.for(file_extension: "shex") 12 | # RDF::Format.for(content_type: "application/shex") 13 | class Format < RDF::Format 14 | content_type 'application/shex', extension: :shex 15 | content_encoding 'utf-8' 16 | 17 | ## 18 | # Hash of CLI commands appropriate for this format 19 | # @return [Hash{Symbol => Lambda(Array, Hash)}] 20 | def self.cli_commands 21 | { 22 | shex: { 23 | description: "Validate repository given shape", 24 | help: "shex [--shape Resource] [--focus Resource] [--schema-input STRING] [--schema STRING] file", 25 | parse: true, 26 | lambda: -> (argv, **options) do 27 | options[:schema_input] ||= case options[:schema] 28 | when IO, StringIO then options[:schema] 29 | else RDF::Util::File.open_file(options[:schema]) {|f| f.read} 30 | end 31 | raise ArgumentError, "Shape matching requires a schema or reference to schema resource" unless options[:schema_input] 32 | raise ArgumentError, "Shape matching requires a focus node" unless options[:focus] 33 | format = options[:schema].to_s.end_with?('json') ? 'shexj' : 'shexc' 34 | shex = ShEx.parse(options[:schema_input], format: format, **options) 35 | 36 | if options[:to_sxp] || options[:to_json] 37 | options[:messages][:shex] = {} 38 | options[:messages][:shex].merge!({"S-Expression": [SXP::Generator.string(shex.to_sxp_bin)]}) if options[:to_sxp] 39 | options[:messages][:shex].merge!({ShExJ: [shex.to_json(JSON::LD::JSON_STATE)]}) if options[:to_json] 40 | else 41 | focus = options.delete(:focus) 42 | shape = options.delete(:shape) 43 | map = shape ? {focus => shape} : {} 44 | begin 45 | res = shex.execute(RDF::CLI.repository, map, focus: focus, **options) 46 | options[:messages][:shex] = { 47 | result: ["Satisfied shape."], 48 | detail: [SXP::Generator.string(res.to_sxp_bin)] 49 | } 50 | rescue ShEx::NotSatisfied => e 51 | options[:logger].error e.to_s 52 | options[:messages][:shex] = { 53 | result: ["Did not satisfied shape."], 54 | detail: [SXP::Generator.stringe.expression] 55 | } 56 | raise 57 | end 58 | end 59 | end, 60 | options: [ 61 | RDF::CLI::Option.new( 62 | symbol: :focus, 63 | datatype: String, 64 | control: :text, 65 | use: :required, 66 | on: ["--focus Resource"], 67 | description: "Focus node within repository" 68 | ) {|v| RDF::URI(v)}, 69 | RDF::CLI::Option.new( 70 | symbol: :shape, 71 | datatype: String, 72 | control: :text, 73 | use: :optional, 74 | on: ["--shape URI"], 75 | description: "Shape identifier within ShEx schema" 76 | ) {|v| RDF::URI(v)}, 77 | RDF::CLI::Option.new( 78 | symbol: :schema_input, 79 | datatype: String, 80 | control: :none, 81 | on: ["--schema-input STRING"], 82 | description: "ShEx schema in URI encoded format" 83 | ) {|v| URI.decode(v)}, 84 | RDF::CLI::Option.new( 85 | symbol: :schema, 86 | datatype: String, 87 | control: :url2, 88 | on: ["--schema URI"], 89 | description: "ShEx schema location" 90 | ) {|v| RDF::URI(v)}, 91 | RDF::CLI::Option.new( 92 | symbol: :to_json, 93 | datatype: String, 94 | control: :checkbox, 95 | on: ["--to-json"], 96 | description: "Display parsed schema as ShExJ" 97 | ), 98 | RDF::CLI::Option.new( 99 | symbol: :to_sxp, 100 | datatype: String, 101 | control: :checkbox, 102 | on: ["--to-sxp"], 103 | description: "Display parsed schema as an S-Expression" 104 | ), 105 | ] 106 | } 107 | } 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /script/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | $:.unshift(File.expand_path('../../lib', __FILE__)) 4 | require "bundler/setup" 5 | require 'logger' 6 | require 'shex' 7 | begin 8 | require 'linkeddata' 9 | rescue LoadError 10 | require 'rdf/ntriples' 11 | require 'rdf/turtle' 12 | end 13 | require 'getoptlong' 14 | require 'amazing_print' 15 | 16 | def run(graph, focus: nil, shape: nil, **options) 17 | if options[:verbose] 18 | STDERR.puts "target graph:\n#{graph.dump(:ttl, standard_prefixes: true)}\n" 19 | STDERR.puts "shex:\n#{options[:shex]}\n" 20 | end 21 | 22 | if options[:verbose] 23 | STDERR.puts ("\nshex:\n" + options[:shex]) 24 | end 25 | 26 | if options[:parse_only] 27 | ShEx::Parser.new.peg_parse( 28 | options[:shex], 29 | :shexDoc, 30 | ShEx::Meta::RULES, 31 | whitespace: ShEx::Terminals::WS, 32 | **options) 33 | return 34 | end 35 | 36 | shex = ShEx.parse(options[:shex], **options) 37 | 38 | STDERR.puts ("\nSXP:\n" + SXP::Generator.string(shex.to_sxp_bin)) if options[:verbose] 39 | 40 | if options[:to_sxp] 41 | SXP::Generator.print(shex.to_sxp_bin) 42 | elsif options[:to_json] 43 | puts shex.to_json(JSON::LD::JSON_STATE) 44 | else 45 | map = {focus => shape} if focus && shape 46 | require 'byebug'; byebug 47 | res = shex.execute(graph, map, focus: (map ? nil : focus), **options) 48 | puts SXP::Generator.string(res.to_sxp_bin) 49 | end 50 | rescue ShEx::NotSatisfied => e 51 | STDERR.puts e 52 | STDERR.puts SXP::Generator.string(e.expression.to_sxp_bin) 53 | rescue 54 | STDERR.puts $! 55 | STDERR.puts $!.backtrace 56 | end 57 | 58 | OPT_ARGS = [ 59 | ["--base", GetoptLong::REQUIRED_ARGUMENT, "Base URI of target graph, if different from graph location"], 60 | ["--debug", GetoptLong::NO_ARGUMENT, "Debug shape matching"], 61 | ["--execute", "-e", GetoptLong::REQUIRED_ARGUMENT, "Use option argument as the patch input"], 62 | ["--focus", GetoptLong::REQUIRED_ARGUMENT, "Starting point"], 63 | ["--shape", GetoptLong::REQUIRED_ARGUMENT, "Shape to start with"], 64 | ["--shex", GetoptLong::REQUIRED_ARGUMENT, "Location of ShEx document"], 65 | ["--parse-only", GetoptLong::NO_ARGUMENT, "No processing"], 66 | ["--progress", GetoptLong::NO_ARGUMENT, "Display parse tree"], 67 | ["--to-json", GetoptLong::NO_ARGUMENT, "Generate JSON for schema instead of validating graph"], 68 | ["--to-sxp", GetoptLong::NO_ARGUMENT, "Generate SXP for schema instead of validating graph"], 69 | ["--validate", GetoptLong::NO_ARGUMENT, "Validate schema document"], 70 | ["--verbose", GetoptLong::NO_ARGUMENT, "Display details of processing"], 71 | ["--help", "-?", GetoptLong::NO_ARGUMENT, "This message"] 72 | ] 73 | def usage 74 | STDERR.puts %{Usage: #{$0} [options] file ...} 75 | width = OPT_ARGS.map do |o| 76 | l = o.first.length 77 | l += o[1].length + 2 if o[1].is_a?(String) 78 | l 79 | end.max 80 | OPT_ARGS.each do |o| 81 | s = " %-*s " % [width, (o[1].is_a?(String) ? "#{o[0,2].join(', ')}" : o[0])] 82 | s += o.last 83 | STDERR.puts s 84 | end 85 | exit(1) 86 | end 87 | 88 | opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]}) 89 | 90 | options = {} 91 | 92 | opts.each do |opt, arg| 93 | case opt 94 | when '--base' then options[:base_uri] = arg 95 | when '--debug' 96 | logger = Logger.new(STDERR) 97 | logger.level = Logger::DEBUG 98 | logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity} #{msg}\n"} 99 | options[:logger] = logger 100 | options[:debug] = true 101 | when '--execute' then options[:shex] = arg 102 | when '--focus' then options[:focus] = RDF::URI(arg) 103 | when '--shape' then options[:shape] = RDF::URI(arg) 104 | when '--shex' 105 | options[:shex] = RDF::Util::File.open_file(arg).read 106 | options[:format] = :shexj if arg.end_with?(".json") 107 | when '--parse-only' then options[:parse_only] = true 108 | when '--progress' 109 | logger = Logger.new(STDERR) 110 | logger.level = Logger::INFO 111 | logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity} #{msg}\n"} 112 | options[:logger] = logger 113 | options[:progress] = true 114 | when '--to-json' then options[:to_json] = true 115 | when '--to-sxp' then options[:to_sxp] = true 116 | when '--validate' then options[:validate] = true 117 | when '--verbose' then options[:verbose] = true 118 | when "--help" then usage 119 | end 120 | end 121 | 122 | raise "No expression defined" unless options[:shex] 123 | if ARGV.empty? 124 | run(RDF::Graph.new, **options) 125 | else 126 | ARGV.each do |test_file| 127 | puts "shex #{test_file}" 128 | run(RDF::Graph.load(test_file), base_uri: RDF::URI(test_file), **options) 129 | end 130 | end 131 | puts -------------------------------------------------------------------------------- /spec/matchers.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'json' 3 | JSON_STATE = JSON::State.new( 4 | indent: " ", 5 | space: " ", 6 | space_before: "", 7 | object_nl: "\n", 8 | array_nl: "\n" 9 | ) 10 | 11 | def parser(**options) 12 | Proc.new do |input| 13 | case options[:format] 14 | when :shexj 15 | ShEx::Algebra.from_shexj(JSON.parse input) 16 | else 17 | parser = ShEx::Parser.new(input, **options) 18 | options[:production] ? parser.parse(options[:production]) : parser.parse 19 | end 20 | end 21 | end 22 | 23 | def normalize(obj) 24 | if obj.is_a?(String) 25 | obj.gsub(/\s+/m, ' '). 26 | gsub(/\s+\)/m, ')'). 27 | gsub(/\(\s+/m, '('). 28 | strip 29 | else 30 | obj 31 | end 32 | end 33 | 34 | RSpec::Matchers.define :generate do |expected, **options| 35 | match do |input| 36 | @input = input 37 | begin 38 | case 39 | when [ShEx::ParseError, ShEx::StructureError, ArgumentError, StandardError].include?(expected) 40 | begin 41 | @actual = parser(**options).call(input) 42 | false 43 | rescue expected 44 | true 45 | end 46 | when expected.is_a?(Regexp) 47 | @actual = parser(**options).call(input) 48 | expected.match(@actual.to_sxp) 49 | when expected.is_a?(String) 50 | @actual = parser(**options).call(input) 51 | normalize(@actual.to_sxp) == normalize(expected) 52 | else 53 | @actual = parser(**options).call(input) 54 | @actual == expected 55 | end 56 | rescue 57 | @actual = $!.message 58 | options[:logger].info "Backtrace:\n#{$!.backtrace.join("\n")}" if options[:logger] 59 | false 60 | end 61 | end 62 | 63 | failure_message do |input| 64 | "Input : #{@input}\n" + 65 | case expected 66 | when String 67 | "Expected : #{expected}\n" 68 | else 69 | "Expected : #{expected.inspect}\n" + 70 | "Expected(sxp): #{SXP::Generator.string(expected.to_sxp_bin)}\n" 71 | end + 72 | "Actual : #{actual.inspect}\n" + 73 | "Actual(sxp) : #{SXP::Generator.string(actual.to_sxp_bin)}\n" + 74 | (options[:logger] ? "Trace :\n#{options[:logger].to_s}" : "") 75 | end 76 | 77 | failure_message_when_negated do |input| 78 | "Input : #{@input}\n" + 79 | case expected 80 | when String 81 | "Expected : #{expected}\n" 82 | else 83 | "Expected : #{expected.inspect}\n" + 84 | "Expected(sxp): #{SXP::Generator.string(expected.to_sxp_bin)}\n" 85 | end + 86 | "Actual : #{actual.inspect}\n" + 87 | "Actual(sxp) : #{SXP::Generator.string(actual.to_sxp_bin)}\n" + 88 | (options[:logger] ? "Trace :\n#{options[:logger].to_s}" : "") 89 | end 90 | end 91 | 92 | RSpec::Matchers.define :satisfy do |graph, data, map, focus: nil, expected: nil, logger: nil, expected_results: nil, **options| 93 | match do |input| 94 | shape_results = nil 95 | 96 | res = case 97 | when [ShEx::NotSatisfied, ShEx::StructureError].include?(expected) 98 | begin 99 | shape_results = input.execute(graph, map, focus: focus, logger: logger, **options) 100 | false 101 | rescue expected => e 102 | shape_results = e.expression if e.respond_to?(:expression) && e.expression.is_a?(Hash) 103 | true 104 | end 105 | else 106 | begin 107 | shape_results = input.execute(graph, map, focus: focus, logger: logger, **options) 108 | true 109 | rescue ShEx::NotSatisfied => e 110 | @exception = e 111 | shape_results = e.expression 112 | false 113 | end 114 | end 115 | 116 | @results = (shape_results || {}).inject({}) do |memo, (k, vv)| 117 | memo.merge(k.to_s => vv.map {|v| {"shape" => v.shape.to_s, "result" => v.result}}) 118 | end 119 | 120 | res # && (expected_results.nil? || results == @results) # FIXME work on result representation 121 | end 122 | 123 | failure_message do |input| 124 | (expected == ShEx::NotSatisfied ? "Unexpected match\n" : "Shape did not match: #{@exception && @exception.message}\n") + 125 | "Input(sxp): #{SXP::Generator.string(input.to_sxp_bin)}\n" + 126 | "Data : #{data}\n" + 127 | "Focus : #{focus}\n" + 128 | "Expected : #{(expected_results || expected).inspect}\n" + 129 | "Results : #{@results.inspect if @results}\n" + 130 | (logger ? "Trace :\n#{logger.to_s}" : "") 131 | end 132 | 133 | failure_message_when_negated do |input| 134 | "Unexpected match\n" + 135 | "Input(sxp): #{SXP::Generator.string(input.to_sxp_bin)}\n" + 136 | "Data : #{data}\n" + 137 | "Focus : #{focus}\n" + + 138 | "Expected : #{(expected_results || expected).inspect}\n" + 139 | "Results : #{@results.inspect if @results}\n" + 140 | (logger ? "Trace :\n#{logger.to_s}" : "") 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/shex/extensions/extension.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path("../..", __FILE__)) 2 | require 'sparql/algebra' 3 | 4 | ## 5 | # Abstract class of ShEx [Extension](http://shex.io/shex-semantics/#semantic-actions) extensions. 6 | # 7 | # Extensions are registered automatically when they are required by subclassing this class. 8 | # 9 | # Implementations may provide an initializer which is called once for a given semantic action. Additionally, `enter` and `exit` methods are invoked when beginning any Triple Expression containing this Semantic Action. The `visit` method is invoked once for each matched triple within that Triple Expression. 10 | # 11 | # @example Test extension 12 | # class Test < ShEx::Extension("http://shex.io/extensions/Test/") 13 | # 14 | # # Called to initialize module before evaluating shape 15 | # def initialize(schema: nil, depth: 0, logger: nil, **options) 16 | # end 17 | # 18 | # # Called on entry to containing Triple Expression 19 | # def enter(code: nil, arcs_in: nil, arcs_out: nil, depth: 0, **options) 20 | # end 21 | # 22 | # # Called once for each matched statement 23 | # def visit(code: nil, matched: nil, depth: 0, **options) 24 | # end 25 | # 26 | # # Called on entry to containing Triple Expression 27 | # def exit(code: nil, matched: [], unmatched: [], depth: 0, **options) 28 | # end 29 | # 30 | # # Called after shape completes on success or failure 31 | # def close(schema: nil, depth: 0, **options) 32 | # end 33 | # 34 | # Subclasses **must** define at least `visit`. 35 | # 36 | # @see http://shex.io/shex-semantics/#semantic-actions 37 | class ShEx::Extension 38 | extend ::Enumerable 39 | 40 | class << self 41 | ## 42 | # The "name" of this class is a URI used to uniquely identify it. 43 | # @return [String] 44 | def name 45 | @@subclasses.invert[self] 46 | end 47 | 48 | ## 49 | # Enumerates known Semantic Action classes. 50 | # 51 | # @yield [klass] 52 | # @yieldparam [Class] klass 53 | # @return [Enumerator] 54 | def each(&block) 55 | if self.equal?(ShEx::Extension) 56 | # This is needed since all Semantic Action classes are defined using 57 | # Ruby's autoloading facility, meaning that `@@subclasses` will be 58 | # empty until each subclass has been touched or require'd. 59 | @@subclasses.values.each(&block) 60 | else 61 | block.call(self) 62 | end 63 | end 64 | 65 | ## 66 | # Return the SemanticAction associated with a URI. 67 | # 68 | # @param [#to_s] name 69 | # @return [SemanticAction] 70 | def find(name) 71 | @@subclasses.fetch(name.to_s, nil) 72 | end 73 | 74 | private 75 | @@subclasses = {} # @private 76 | @@uri = nil # @private 77 | 78 | def create(uri) # @private 79 | @@uri = uri 80 | self 81 | end 82 | 83 | def inherited(subclass) # @private 84 | unless @@uri.nil? 85 | @@subclasses[@@uri.to_s] = subclass 86 | @@uri = nil 87 | end 88 | super 89 | end 90 | 91 | ShEx::EXTENSIONS.each { |v| require "shex/extensions/#{v}" } 92 | end 93 | 94 | ## 95 | # Initializer for a given instance. Implementations _may_ define this for instance and/or class 96 | # @param [ShEx::Algebra::Schema] schema top level of the shape expression 97 | # @param [RDF::Util::Logger] logger 98 | # @param [Integer] depth for logging 99 | # @param [Hash{Symbol => Object}] options from shape initialization 100 | # @return [self] 101 | def initialize(schema: nil, logger: nil, depth: 0, **options) 102 | @logger = logger 103 | @options = options 104 | self 105 | end 106 | 107 | ## 108 | # Called on entry to containing {ShEx::TripleExpression} 109 | # 110 | # @param [String] code 111 | # @param [Array] arcs_in available statements to be matched 112 | # @param [Array] arcs_out available statements to be matched 113 | # @param [ShEx::Algebra::TripleExpression] expression containing this semantic act 114 | # @param [Integer] depth for logging 115 | # @param [Hash{Symbol => Object}] options 116 | # Other, operand-specific options 117 | # @return [Boolean] Returning `false` results in {ShEx::NotSatisfied} exception 118 | def enter(code: nil, arcs_in: nil, arcs_out: nil, expression: nil, depth: 0, **options) 119 | true 120 | end 121 | 122 | ## 123 | # Called after a {ShEx::TripleExpression} has matched zero or more statements 124 | # 125 | # @param [String] code 126 | # @param [RDF::Statement] matched statement 127 | # @param [Integer] depth for logging 128 | # @param [ShEx::Algebra::TripleExpression] expression containing this semantic act 129 | # @param [Hash{Symbol => Object}] options 130 | # Other, operand-specific options 131 | # @return [Boolean] Returning `false` results in {ShEx::NotSatisfied} 132 | def visit(code: nil, matched: nil, expression: nil, depth: 0, **options) 133 | raise NotImplementedError 134 | end 135 | 136 | ## 137 | # Called on exit from containing {ShEx::TripleExpression} 138 | # 139 | # @param [String] code 140 | # @param [Array] matched statements matched by this expression 141 | # @param [Array] unmatched statements considered, but not matched by this expression 142 | # @param [ShEx::Algebra::TripleExpression] expression containing this semantic act 143 | # @param [Integer] depth for logging 144 | # @param [Hash{Symbol => Object}] options 145 | # Other, operand-specific options 146 | # @return [self] 147 | def exit(code: nil, matched: [], unmatched: [], expression: nil, depth: 0, **options) 148 | self 149 | end 150 | 151 | # Called after shape completes on success or failure 152 | # @param [ShEx::Algebra::Schema] schema top level of the shape expression 153 | # @param [Integer] depth for logging 154 | # @param [Hash{Symbol => Object}] options 155 | # Other, operand-specific options 156 | # @return [self] 157 | def close(schema: nil, depth: 0, **options) 158 | self 159 | end 160 | end -------------------------------------------------------------------------------- /lib/shex/algebra/shape.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Shape < Operator 4 | include ShapeExpression 5 | NAME = :shape 6 | 7 | ## 8 | # Let `outs` be the `arcsOut` in `remainder`: `outs = remainder ∩ arcsOut(G, n)`. 9 | # @return [Array] 10 | attr_accessor :outs 11 | 12 | ## 13 | # Let `matchables` be the triples in `outs` whose predicate appears in a {TripleConstraint} in `expression`. If `expression` is absent, `matchables = Ø` (the empty set). 14 | # @return [Array] 15 | attr_accessor :matchables 16 | 17 | ## 18 | # Let `unmatchables` be the triples in `outs` which are not in `matchables`. `matchables ∪ unmatchables = outs.` 19 | # @return [Array] 20 | attr_accessor :unmatchables 21 | 22 | ## 23 | # Creates an operator instance from a parsed ShExJ representation 24 | # @param (see Operator#from_shexj) 25 | # @return [Operator] 26 | def self.from_shexj(operator, **options) 27 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "Shape" 28 | super 29 | end 30 | 31 | # The `satisfies` semantics for a `Shape` depend on a matches function defined below. For a node `n`, shape `S`, graph `G`, and shapeMap `m`, `satisfies(n, S, G, m)`. 32 | # @param (see ShapeExpression#satisfies?) 33 | # @return (see ShapeExpression#satisfies?) 34 | # @raise (see ShapeExpression#satisfies?) 35 | def satisfies?(focus, depth: 0) 36 | # neigh(G, n) is the neighbourhood of the node n in the graph G. 37 | # 38 | # neigh(G, n) = arcsOut(G, n) ∪ arcsIn(G, n) 39 | arcs_in = schema.graph.query({object: focus}).to_a.sort_by(&:to_sxp) 40 | arcs_out = schema.graph.query({subject: focus}).to_a.sort_by(&:to_sxp) 41 | neigh = (arcs_in + arcs_out).uniq 42 | 43 | # `matched` is the subset of statements which match `expression`. 44 | status("arcsIn: #{arcs_in.count}, arcsOut: #{arcs_out.count}", depth: depth) 45 | matched_expression = case expression 46 | when RDF::Resource 47 | ref.matches(arcs_in, arcs_out, depth: depth + 1) 48 | when TripleExpression 49 | expression.matches(arcs_in, arcs_out, depth: depth + 1) 50 | end 51 | matched = Array(matched_expression && matched_expression.matched) 52 | 53 | # `remainder` is the set of unmatched statements 54 | remainder = neigh - matched 55 | 56 | # Let `outs` be the `arcsOut` in `remainder`: `outs = remainder ∩ arcsOut(G, n)`. 57 | @outs = remainder.select {|s| s.subject == focus} 58 | 59 | # Let `matchables` be the triples in `outs` whose predicate appears in a `TripleConstraint` in `expression`. If `expression` is absent, `matchables = Ø` (the empty set). 60 | predicates = expression ? expression.triple_constraints.map(&:predicate).uniq : [] 61 | @matchables = outs.select {|s| predicates.include?(s.predicate)} 62 | 63 | # Let `unmatchables` be the triples in `outs` which are not in `matchables`. 64 | @unmatchables = outs - matchables 65 | 66 | # No matchable can be matched by any TripleConstraint in expression 67 | unmatched = matchables.select do |statement| 68 | expression.triple_constraints.any? do |expr| 69 | begin 70 | statement.predicate == expr.predicate && expr.matches([], [statement], depth: depth + 1) 71 | rescue ShEx::NotMatched 72 | false # Expected not to match 73 | end 74 | end if expression 75 | end 76 | unless unmatched.empty? 77 | not_satisfied "Statements remain matching TripleConstraints", 78 | matched: matched, 79 | unmatched: unmatched, 80 | satisfied: expression, 81 | depth: depth 82 | end 83 | 84 | # There is no triple in matchables whose predicate does not appear in extra. 85 | unmatched = matchables.reject {|st| extra.include?(st.predicate)} 86 | unless unmatched.empty? 87 | not_satisfied "Statements remains with predicate #{unmatched.map(&:predicate).compact.join(',')} not in extra", 88 | matched: matched, 89 | unmatched: unmatched, 90 | satisfied: expression, 91 | depth: depth 92 | end 93 | 94 | # closed is false or unmatchables is empty. 95 | not_satisfied "Unmatchables remain on a closed shape", depth: depth unless !closed? || unmatchables.empty? 96 | 97 | # Presumably, to be satisfied, there must be some triples in matches 98 | semantic_actions.each do |op| 99 | op.satisfies?(matched, matched: matched, depth: depth + 1) 100 | end unless matched.empty? 101 | 102 | # FIXME: also record matchables, outs and others? 103 | satisfy focus: focus, matched: matched, depth: depth 104 | rescue ShEx::NotMatched => e 105 | not_satisfied e.message, focus: focus, unsatisfied: e.expression, depth: depth 106 | end 107 | 108 | ## 109 | # expression must be a TripleExpression and must not reference itself recursively. 110 | # 111 | # @return [Operator] `self` 112 | # @raise [ShEx::StructureError] if the value is invalid 113 | def validate! 114 | case expression 115 | when nil, TripleExpression 116 | when RDF::Resource 117 | ref = schema.find(expression) 118 | ref.is_a?(TripleExpression) || 119 | structure_error("#{json_type} must reference a TripleExpression: #{ref}") 120 | else 121 | structure_error("#{json_type} must be a TripleExpression or reference: #{expression.to_sxp}") 122 | end 123 | # FIXME: this runs afoul of otherwise legitamate self-references, through a TripleExpression. 124 | #!validate_self_references! 125 | super 126 | end 127 | 128 | private 129 | # There may be multiple extra operands 130 | def extra 131 | operands.select {|op| op.is_a?(Array) && op.first == :extra}.inject([]) do |memo, ary| 132 | memo + Array(ary[1..-1]) 133 | end.uniq 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /spec/algebra_spec.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../..", __FILE__) 2 | require 'spec_helper' 3 | 4 | describe ShEx::Algebra do 5 | before(:each) {$stderr = StringIO.new} 6 | after(:each) {$stderr = STDERR} 7 | let(:logger) {RDF::Spec.logger} 8 | let(:schema) {double("Schema", graph: RDF::Graph.new)} 9 | before(:each) {allow_any_instance_of(ShEx::Algebra::Operator).to receive(:schema).and_return(schema)} 10 | 11 | describe ShEx::Algebra::And do 12 | subject {described_class.new(ShEx::Algebra::Shape.new, ShEx::Algebra::NodeConstraint.new(:iri))} 13 | it {expect {described_class.new}.to raise_error(ArgumentError, /wrong number of arguments/)} 14 | it {expect {described_class.new(ShEx::Algebra::Shape.new)}.to raise_error(ArgumentError, /wrong number of arguments/)} 15 | it {expect {described_class.new(nil, nil)}.to raise_error(ArgumentError, /All operands must be Shape/)} 16 | it {expect {described_class.new(ShEx::Algebra::Shape.new, ShEx::Algebra::TripleConstraint.new)}.to raise_error(ArgumentError, /All operands must be Shape/)} 17 | it {expect {subject}.not_to raise_error} 18 | 19 | it "raises NotSatisfied if any operand does not satisfy" do 20 | expect {subject.satisfies?(RDF::Literal("foo")).to raise_error(ShEx::NotSatisfied)} 21 | end 22 | 23 | it "returns true if all operands satisfy" do 24 | expect(subject.satisfies?(RDF::URI("foo"))).to be_truthy 25 | end 26 | end 27 | 28 | describe ShEx::Algebra::Not do 29 | subject {described_class.new(ShEx::Algebra::NodeConstraint.new(:literal))} 30 | it {expect {described_class.new}.to raise_error(ArgumentError, /wrong number of arguments/)} 31 | it {expect {described_class.new(ShEx::Algebra::Shape.new, ShEx::Algebra::Shape.new)}.to raise_error(ArgumentError, /wrong number of arguments/)} 32 | it {expect {described_class.new(ShEx::Parser.new)}.to raise_error(TypeError, /invalid ShEx::Algebra::Operator/)} 33 | it {expect {subject}.not_to raise_error} 34 | 35 | it "raises NotSatisfied if operand satisfies" do 36 | expect {subject.satisfies?(RDF::Literal("foo")).to raise_error(ShEx::NotSatisfied)} 37 | end 38 | 39 | it "returns true if operands does not satisfy" do 40 | expect(subject.satisfies?(RDF::URI("foo"))).to be_truthy 41 | end 42 | end 43 | 44 | describe ShEx::Algebra::Or do 45 | subject {described_class.new(ShEx::Algebra::NodeConstraint.new(:literal), ShEx::Algebra::NodeConstraint.new(:iri))} 46 | it {expect {described_class.new}.to raise_error(ArgumentError, /wrong number of arguments/)} 47 | it {expect {described_class.new(ShEx::Algebra::Shape.new)}.to raise_error(ArgumentError, /wrong number of arguments/)} 48 | it {expect {described_class.new(nil, nil)}.to raise_error(ArgumentError, /All operands must be Shape/)} 49 | it {expect {described_class.new(ShEx::Algebra::Shape.new, ShEx::Parser.new)}.to raise_error(ArgumentError, /All operands must be Shape/)} 50 | it {expect {subject}.not_to raise_error} 51 | 52 | it "raises NotSatisfied if all operands do not satisfy" do 53 | expect {subject.satisfies?(RDF::Node.new).to raise_error(ShEx::NotSatisfied)} 54 | end 55 | 56 | it "returns true if any operands satisfy" do 57 | expect(subject.satisfies?(RDF::Literal("foo"))).to be_truthy 58 | expect(subject.satisfies?(RDF::URI("foo"))).to be_truthy 59 | end 60 | end 61 | 62 | describe ShEx::Algebra::SemAct do 63 | subject {described_class.new(RDF::URI("http://example/TestAct"), "foo")} 64 | let(:implementation) {double("implementation")} 65 | let(:schema) {double("schema", extensions: {"http://example/TestAct" => implementation})} 66 | before {allow(subject).to receive(:schema).and_return(schema)} 67 | 68 | describe "#enter" do 69 | it "enters implementation" do 70 | expect(implementation).to receive(:enter) 71 | subject.enter 72 | end 73 | end 74 | 75 | describe "#exit" do 76 | it "exits implementation" do 77 | expect(implementation).to receive(:exit) 78 | subject.exit 79 | end 80 | end 81 | 82 | describe "#satisfies?" do 83 | it "visits implementation with nothing matched" do 84 | expect(implementation).to receive(:visit).with(code: "foo", expression: nil, depth: 0).and_return(true) 85 | subject.satisfies?(nil, matched: []) 86 | end 87 | 88 | it "visits implementation and raises error with nothing matched" do 89 | expect(implementation).to receive(:visit).with(code: "foo", expression: nil, depth: 0).and_return(false) 90 | expect {subject.satisfies?(nil, matched: [])}.to raise_error(ShEx::NotSatisfied) 91 | end 92 | 93 | it "visits implementation all matched statements" do 94 | expect(implementation).to receive(:visit).with(code: "foo", matched: anything, expression: nil, depth: 0).and_return(true, true) 95 | subject.satisfies?(nil, matched: %w(a b)) 96 | end 97 | 98 | it "visits implementation all matched statements and raises error" do 99 | expect(implementation).to receive(:visit).with(code: "foo", matched: anything, expression: nil, depth: 0).and_return(true, false) 100 | expect {subject.satisfies?(nil, matched: %w(a b))}.to raise_error(ShEx::NotSatisfied) 101 | end 102 | end 103 | end 104 | 105 | subject {described_class.new(RDF.type)} 106 | 107 | describe ".from_shexj" do 108 | { 109 | "0" => { 110 | shexj: %({"type": "Shape"}), 111 | shexc: %{(shape)} 112 | }, 113 | "1Adot" => { 114 | shexj: %({ "type": "TripleConstraint", "predicate": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"}), 115 | shexc: %{(tripleConstraint (predicate a))} 116 | }, 117 | "1length" => { 118 | shexj: %({ 119 | "type": "TripleConstraint", 120 | "predicate": "http://a.example/p1", 121 | "valueExpr": { "type": "NodeConstraint", "length": 5 } 122 | }), 123 | shexc: %{(tripleConstraint (predicate ) (nodeConstraint (length 5)))} 124 | } 125 | }.each do |name, params| 126 | it name do 127 | expect(params[:shexj]).to generate(params[:shexc], format: :shexj) 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/suite_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rdf/spec' 2 | require 'rdf/turtle' 3 | require 'json/ld' 4 | 5 | # For now, override RDF::Utils::File.open_file to look for the file locally before attempting to retrieve it 6 | module RDF::Util 7 | module File 8 | REMOTE_PATH = "https://raw.githubusercontent.com/shexSpec/shexTest/master/" 9 | LOCAL_PATH = ::File.expand_path("../shexTest", __FILE__) + '/' 10 | 11 | class << self 12 | alias_method :original_open_file, :open_file 13 | end 14 | 15 | ## 16 | # Override to use Patron for http and https, Kernel.open otherwise. 17 | # 18 | # @param [String] filename_or_url to open 19 | # @param [Hash{Symbol => Object}] options 20 | # @option options [Array, String] :headers 21 | # HTTP Request headers. 22 | # @return [IO] File stream 23 | # @yield [IO] File stream 24 | def self.open_file(filename_or_url, **options, &block) 25 | case 26 | when filename_or_url.to_s =~ /^file:/ 27 | path = filename_or_url[5..-1] 28 | Kernel.open(path.to_s, options, &block) 29 | when (filename_or_url.to_s =~ %r{^#{REMOTE_PATH}} && Dir.exist?(LOCAL_PATH)) 30 | #puts "attempt to open #{filename_or_url} locally" 31 | localpath = filename_or_url.to_s.sub(REMOTE_PATH, LOCAL_PATH) 32 | response = begin 33 | ::File.open(localpath) 34 | rescue Errno::ENOENT => e 35 | raise IOError, e.message 36 | end 37 | document_options = { 38 | base_uri: RDF::URI(filename_or_url), 39 | charset: Encoding::UTF_8, 40 | code: 200, 41 | headers: {} 42 | } 43 | #puts "use #{filename_or_url} locally" 44 | document_options[:headers][:content_type] = case filename_or_url.to_s 45 | when /\.ttl$/ then 'text/turtle' 46 | when /\.nt$/ then 'application/n-triples' 47 | when /\.jsonld$/ then 'application/ld+json' 48 | else 'unknown' 49 | end 50 | 51 | document_options[:headers][:content_type] = response.content_type if response.respond_to?(:content_type) 52 | # For overriding content type from test data 53 | document_options[:headers][:content_type] = options[:contentType] if options[:contentType] 54 | 55 | remote_document = RDF::Util::File::RemoteDocument.new(response.read, **document_options) 56 | if block_given? 57 | yield remote_document 58 | else 59 | remote_document 60 | end 61 | else 62 | original_open_file(filename_or_url, **options, &block) 63 | end 64 | end 65 | end 66 | end 67 | 68 | module Fixtures 69 | module SuiteTest 70 | BASE = "https://raw.githubusercontent.com/shexSpec/shexTest/master/" 71 | 72 | class Manifest < JSON::LD::Resource 73 | attr_accessor :file 74 | 75 | def self.open(file) 76 | #puts "open: #{file}" 77 | @file = file 78 | 79 | # Create a manifest, if `file` doesn't exist 80 | json = JSON.parse(RDF::Util::File.open_file(file.to_s).read) 81 | man = Manifest.new(json['@graph'].first, json: json, context: {base: file.to_s}) 82 | man.instance_variable_set(:@json, json) 83 | yield man 84 | end 85 | 86 | def entries 87 | # Map entries to resources 88 | ents = attributes['entries'].map {|e| Entry.new(e, context: context)} 89 | ents 90 | end 91 | end 92 | 93 | class Entry < JSON::LD::Resource 94 | attr_accessor :debug 95 | 96 | def base 97 | RDF::URI(context[:base]) 98 | end 99 | 100 | def schema 101 | base.join(action.is_a?(Hash) && action["schema"] ? action["schema"] : shex) 102 | end 103 | 104 | def json 105 | sch = action["schema"].to_s.sub('.shex', '.json') if action.is_a?(Hash) && action["schema"] 106 | base.join(attributes.fetch('json', sch)) 107 | end 108 | 109 | def data 110 | action.is_a?(Hash) && action["data"] && base.join(action["data"]) 111 | end 112 | 113 | def ttl 114 | attributes["ttl"] && base.join(attributes["ttl"]) 115 | end 116 | 117 | def shapeExterns 118 | action.is_a?(Hash) && action["shapeExterns"] && [base.join(action["shapeExterns"])] 119 | end 120 | 121 | def result 122 | base.join(attributes['result']) 123 | end 124 | 125 | def shape 126 | action.is_a?(Hash) && action["shape"] 127 | end 128 | 129 | def focus 130 | action.is_a?(Hash) && action["focus"] 131 | end 132 | 133 | def trait 134 | Array(attributes["trait"]) 135 | end 136 | 137 | def map 138 | action.is_a?(Hash) && action["map"] && base.join(action["map"]) 139 | end 140 | 141 | def shape_map 142 | @shape_map ||= JSON.parse(RDF::Util::File.open_file(map, &:read)) 143 | end 144 | 145 | def graph 146 | @graph ||= RDF::Graph.load(data, base_uri: base) 147 | end 148 | 149 | def schema_source 150 | @schema_source ||= RDF::Util::File.open_file(schema, &:read) 151 | end 152 | 153 | def schema_json 154 | @schema_json ||= RDF::Util::File.open_file(json, &:read) 155 | end 156 | 157 | def data_source 158 | @data_source ||= RDF::Util::File.open_file(data, &:read) 159 | end 160 | 161 | def turtle_source 162 | @turtle_source ||= RDF::Util::File.open_file(ttl, &:read) 163 | end 164 | 165 | def results 166 | @results ||= (JSON.parse(RDF::Util::File.open_file(result, &:read)) if attributes['result']) 167 | end 168 | 169 | def structure_test? 170 | !!Array(attributes['@type']).join(" ").match(/Structure/) 171 | end 172 | 173 | def syntax_test? 174 | !!Array(attributes['@type']).join(" ").match(/Syntax/) 175 | end 176 | 177 | def validation_test? 178 | !!Array(attributes['@type']).join(" ").match(/Validation/) 179 | end 180 | 181 | def positive_test? 182 | !negative_test? 183 | end 184 | 185 | def negative_test? 186 | !!Array(attributes['@type']).join(" ").match(/Negative|Failure/) 187 | end 188 | 189 | # Create a logger initialized with the content of `debug` 190 | def logger 191 | @logger ||= RDF::Spec.logger 192 | end 193 | 194 | def inspect 195 | "" 202 | end 203 | end 204 | end 205 | end -------------------------------------------------------------------------------- /lib/shex.rb: -------------------------------------------------------------------------------- 1 | require 'rdf' 2 | require 'sxp' 3 | require 'shex/format' 4 | 5 | ## 6 | # A ShEx runtime for RDF.rb. 7 | # 8 | # @see https://shex.io/shex-semantics/#shexc 9 | module ShEx 10 | autoload :Algebra, 'shex/algebra' 11 | autoload :Meta, 'shex/meta' 12 | autoload :Parser, 'shex/parser' 13 | autoload :Extension, 'shex/extensions/extension' 14 | autoload :Terminals, 'shex/terminals' 15 | autoload :VERSION, 'shex/version' 16 | 17 | # Location of the ShEx JSON-LD context 18 | CONTEXT = "http://www.w3.org/ns/shex.jsonld" 19 | 20 | # Extensions defined in this gem 21 | EXTENSIONS = %w{test} 22 | 23 | ## 24 | # Parse the given ShEx `query` string. 25 | # 26 | # @example parsing a ShExC schema 27 | # schema = ShEx.parse(%( 28 | # PREFIX ex: ex:IssueShape {ex:state IRI} 29 | # ).parse 30 | # 31 | # @param [IO, StringIO, String, #to_s] expression (ShExC or ShExJ) 32 | # @param ['shexc', 'shexj', 'sxp'] format ('shexc') 33 | # @param [Hash{Symbol => Object}] options 34 | # @option (see ShEx::Parser#initialize) 35 | # @return (see ShEx::Parser#parse) 36 | # @raise (see ShEx::Parser#parse) 37 | def self.parse(expression, format: 'shexc', **options) 38 | case format.to_s 39 | when 'shexc' then Parser.new(expression, **options).parse 40 | when 'shexj' 41 | expression = expression.read if expression.respond_to?(:read) 42 | Algebra.from_shexj(JSON.parse(expression), **options) 43 | when 'sxp' 44 | expression = expression.read if expression.respond_to?(:read) 45 | Algebra.from_sxp(expression, **options) 46 | else raise "Unknown expression format: #{format.inspect}" 47 | end 48 | end 49 | 50 | ## 51 | # Parses input from the given file name or URL. 52 | # 53 | # @example parsing a ShExC schema 54 | # schema = ShEx.parse('foo.shex').parse 55 | # 56 | # @param [String, #to_s] filename 57 | # @param (see parse) 58 | # @option (see ShEx::Parser#initialize) 59 | # @return (see ShEx::Parser#parse) 60 | # @raise (see ShEx::Parser#parse) 61 | def self.open(filename, format: 'shexc', **options, &block) 62 | RDF::Util::File.open_file(filename, **options) do |file| 63 | self.parse(file, format: format, **options) 64 | end 65 | end 66 | 67 | ## 68 | # Parse and validate the given ShEx `expression` string against `queriable`. 69 | # 70 | # @example executing a ShExC schema 71 | # graph = RDF::Graph.load("etc/doap.ttl") 72 | # ShEx.execute('etc/doap.shex', graph, "https://rubygems.org/gems/shex", "") 73 | # 74 | # @param [IO, StringIO, String, #to_s] expression (ShExC or ShExJ) 75 | # @param (see ShEx::Algebra::Schema#execute) 76 | # @return (see ShEx::Algebra::Schema#execute) 77 | # @raise (see ShEx::Algebra::Schema#execute) 78 | def self.execute(expression, queryable, map, format: 'shexc', **options) 79 | shex = self.parse(expression, format: format, **options) 80 | queryable = queryable || RDF::Graph.new 81 | 82 | shex.execute(queryable, map, **options) 83 | end 84 | 85 | ## 86 | # Parse and validate the given ShEx `expression` string against `queriable`. 87 | # 88 | # @example executing a ShExC schema 89 | # graph = RDF::Graph.load("etc/doap.ttl") 90 | # ShEx.execute('etc/doap.shex', graph, "https://rubygems.org/gems/shex", "") 91 | # 92 | # @param [IO, StringIO, String, #to_s] expression (ShExC or ShExJ) 93 | # @param (see ShEx::Algebra::Schema#satisfies?) 94 | # @return (see ShEx::Algebra::Schema#satisfies?) 95 | # @raise (see ShEx::Algebra::Schema#satisfies?) 96 | def self.satisfies?(expression, queryable, map, format: 'shexc', **options) 97 | shex = self.parse(expression, format: format, **options) 98 | queryable = queryable || RDF::Graph.new 99 | 100 | shex.satisfies?(queryable, map, **options) 101 | end 102 | 103 | ## 104 | # Alias for `ShEx::Extension.create`. 105 | # 106 | # @param (see ShEx::Extension#create) 107 | # @return [Class] 108 | def self.Extension(uri) 109 | Extension.send(:create, uri) 110 | end 111 | 112 | class Error < StandardError 113 | # The status code associated with this error 114 | attr_reader :code 115 | 116 | ## 117 | # Initializes a new patch error instance. 118 | # 119 | # @param [String, #to_s] message 120 | # @param [Hash{Symbol => Object}] options 121 | # @option options [Integer] :code (422) 122 | def initialize(message, **options) 123 | @code = options.fetch(:status_code, 422) 124 | super(message.to_s) 125 | end 126 | end 127 | 128 | 129 | # Shape expectation not satisfied 130 | class StructureError < Error; end 131 | 132 | # Shape expectation not satisfied 133 | class NotSatisfied < Error 134 | ## 135 | # The expression which was not satified 136 | # @return [ShEx::Algebra::ShapeExpression] 137 | attr_reader :expression 138 | 139 | ## 140 | # Initializes a new parser error instance. 141 | # 142 | # @param [String, #to_s] message 143 | # @param [ShEx::Algebra::ShapeExpression] expression 144 | def initialize(message, expression: self) 145 | @expression = expression 146 | super(message.to_s) 147 | end 148 | 149 | def inspect 150 | super + (expression ? SXP::Generator.string(expression.to_sxp_bin) : '') 151 | end 152 | end 153 | 154 | # TripleExpression did not match 155 | class NotMatched < ShEx::Error 156 | ## 157 | # The expression which was not satified 158 | # @return [ShEx::Algebra::TripleExpression] 159 | attr_reader :expression 160 | 161 | ## 162 | # Initializes a new parser error instance. 163 | # 164 | # @param [String, #to_s] message 165 | # @param [ShEx::Algebra::TripleExpression] expression 166 | def initialize(message, expression: self) 167 | @expression = expression 168 | super(message.to_s) 169 | end 170 | 171 | def inspect 172 | super + (expression ? SXP::Generator.string(expression.to_sxp_bin) : '') 173 | end 174 | end 175 | 176 | # Indicates bad syntax found in LD Patch document 177 | class ParseError < Error 178 | ## 179 | # The invalid token which triggered the error. 180 | # 181 | # @return [String] 182 | attr_reader :token 183 | 184 | ## 185 | # The line number where the error occurred. 186 | # 187 | # @return [Integer] 188 | attr_reader :lineno 189 | 190 | ## 191 | # ParseError includes `token` and `lineno` associated with the expression. 192 | # 193 | # @param [String, #to_s] message 194 | # @param [String] token (nil) 195 | # @param [Integer] lineno (nil) 196 | def initialize(message, token: nil, lineno: nil) 197 | @token = token 198 | @lineno = lineno || (@token.lineno if @token.respond_to?(:lineno)) 199 | super(message.to_s) 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/shex/algebra/node_constraint.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | module ShEx::Algebra 3 | ## 4 | class NodeConstraint < Operator 5 | include ShapeExpression 6 | NAME = :nodeConstraint 7 | 8 | ## 9 | # Creates an operator instance from a parsed ShExJ representation 10 | # @param (see Operator#from_shexj) 11 | # @return [Operator] 12 | def self.from_shexj(operator, **options) 13 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == 'NodeConstraint' 14 | super 15 | end 16 | 17 | # 18 | # S is a NodeConstraint and satisfies2(focus, se) as described below in Node Constraints. Note that testing if a node satisfies a node constraint does not require a graph or shapeMap. 19 | # @param (see ShapeExpression#satisfies?) 20 | # @return (see ShapeExpression#satisfies?) 21 | # @raise (see ShapeExpression#satisfies?) 22 | def satisfies?(focus, depth: 0) 23 | status "", depth: depth 24 | satisfies_node_kind?(focus, depth: depth + 1) && 25 | satisfies_datatype?(focus, depth: depth + 1) && 26 | satisfies_string_facet?(focus, depth: depth + 1) && 27 | satisfies_numeric_facet?(focus, depth: depth + 1) && 28 | satisfies_values?(focus, depth: depth + 1) && 29 | satisfy(depth: depth) 30 | end 31 | 32 | private 33 | 34 | ## 35 | # Satisfies Node Kind Constraint 36 | # @return [Boolean] `true` if satisfied, `false` if it does not apply 37 | # @raise [ShEx::NotSatisfied] if not satisfied 38 | def satisfies_node_kind?(value, depth: 0) 39 | kind = case operands.detect {|o| o.is_a?(Symbol)} 40 | when :iri then RDF::URI 41 | when :bnode then RDF::Node 42 | when :literal then RDF::Literal 43 | when :nonliteral then RDF::Resource 44 | else return true 45 | end 46 | 47 | not_satisfied "Node was #{value.inspect} expected kind #{kind}", depth: depth unless 48 | value.is_a?(kind) 49 | status "right kind: #{value}: #{kind}", depth: depth 50 | true 51 | end 52 | 53 | ## 54 | # Datatype Constraint 55 | # @return [Boolean] `true` if satisfied, `false` if it does not apply 56 | # @raise [ShEx::NotSatisfied] if not satisfied 57 | def satisfies_datatype?(value, depth: 0) 58 | dt = op_fetch(:datatype) 59 | return true unless dt 60 | 61 | not_satisfied "Node was #{value.inspect}, expected datatype #{dt}", depth: depth unless 62 | value.is_a?(RDF::Literal) && value.datatype == RDF::URI(dt) && value.valid? 63 | status "right datatype: #{value}: #{dt}", depth: depth 64 | true 65 | end 66 | 67 | ## 68 | # String Facet Constraint 69 | # Checks all length/minlength/maxlength/pattern facets against the string representation of the value. 70 | # @return [Boolean] `true` if satisfied, `false` if it does not apply 71 | # @raise [ShEx::NotSatisfied] if not satisfied 72 | # @todo using the XPath regexp engine supports additional flags "s" and "q" 73 | def satisfies_string_facet?(value, depth: 0) 74 | length = op_fetch(:length) 75 | minlength = op_fetch(:minlength) 76 | maxlength = op_fetch(:maxlength) 77 | pat = (operands.detect {|op| op.is_a?(Array) && op[0] == :pattern} || []) 78 | pattern = pat[1] 79 | 80 | flags = 0 81 | flags |= Regexp::EXTENDED if pat[2].to_s.include?("x") 82 | flags |= Regexp::IGNORECASE if pat[2].to_s.include?("i") 83 | flags |= Regexp::MULTILINE if pat[2].to_s.include?("m") 84 | 85 | return true if (length || minlength || maxlength || pattern).nil? 86 | 87 | v_s = case value 88 | when RDF::Node then value.id 89 | else value.to_s 90 | end 91 | 92 | not_satisfied "Node #{v_s.inspect} length not #{length}", depth: depth if 93 | length && v_s.length != length.to_i 94 | not_satisfied"Node #{v_s.inspect} length < #{minlength}", depth: depth if 95 | minlength && v_s.length < minlength.to_i 96 | not_satisfied "Node #{v_s.inspect} length > #{maxlength}", depth: depth if 97 | maxlength && v_s.length > maxlength.to_i 98 | not_satisfied "Node #{v_s.inspect} does not match #{pattern}", depth: depth if 99 | pattern && !Regexp.new(pattern, flags).match(v_s) 100 | status "right string facet: #{value}", depth: depth 101 | true 102 | end 103 | 104 | ## 105 | # Numeric Facet Constraint 106 | # Checks all numeric facets against the value. 107 | # @return [Boolean] `true` if satisfied, `false` if it does not apply 108 | # @raise [ShEx::NotSatisfied] if not satisfied 109 | def satisfies_numeric_facet?(value, depth: 0) 110 | mininclusive = op_fetch(:mininclusive) 111 | minexclusive = op_fetch(:minexclusive) 112 | maxinclusive = op_fetch(:maxinclusive) 113 | maxexclusive = op_fetch(:maxexclusive) 114 | totaldigits = op_fetch(:totaldigits) 115 | fractiondigits = op_fetch(:fractiondigits) 116 | 117 | return true if (mininclusive || minexclusive || maxinclusive || maxexclusive || totaldigits || fractiondigits).nil? 118 | 119 | not_satisfied "Node #{value.inspect} not numeric", depth: depth unless 120 | value.is_a?(RDF::Literal::Numeric) 121 | 122 | not_satisfied "Node #{value.inspect} not decimal", depth: depth if 123 | (totaldigits || fractiondigits) && (!value.is_a?(RDF::Literal::Decimal) || value.invalid?) 124 | 125 | numeric_value = value.object 126 | case 127 | when !mininclusive.nil? && numeric_value < mininclusive.object then not_satisfied("Node #{value.inspect} < #{mininclusive.object}", depth: depth) 128 | when !minexclusive.nil? && numeric_value <= minexclusive.object then not_satisfied("Node #{value.inspect} not <= #{minexclusive.object}", depth: depth) 129 | when !maxinclusive.nil? && numeric_value > maxinclusive.object then not_satisfied("Node #{value.inspect} > #{maxinclusive.object}", depth: depth) 130 | when !maxexclusive.nil? && numeric_value >= maxexclusive.object then not_satisfied("Node #{value.inspect} >= #{maxexclusive.object}", depth: depth) 131 | when !totaldigits.nil? 132 | md = value.canonicalize.to_s.match(/([1-9]\d*|0)?(?:\.(\d+)(?!0))?/) 133 | digits = md ? (md[1].to_s + md[2].to_s) : "" 134 | if digits.length > totaldigits.to_i 135 | not_satisfied "Node #{value.inspect} total digits != #{totaldigits}", depth: depth 136 | end 137 | when !fractiondigits.nil? 138 | md = value.canonicalize.to_s.match(/\.(\d+)(?!0)?/) 139 | num = md ? md[1].to_s : "" 140 | if num.length > fractiondigits.to_i 141 | not_satisfied "Node #{value.inspect} fractional digits != #{fractiondigits}", depth: depth 142 | end 143 | end 144 | status "right numeric facet: #{value}", depth: depth 145 | true 146 | end 147 | 148 | ## 149 | # Value Constraint 150 | # Checks all numeric facets against the value. 151 | # @return [Boolean] `true` if satisfied, `false` if it does not apply 152 | # @raise [ShEx::NotSatisfied] if not satisfied 153 | def satisfies_values?(value, depth: 0) 154 | values = operands.select {|op| op.is_a?(Value)} 155 | return true if values.empty? 156 | matched_value = values.detect {|v| v.match?(value, depth: depth + 1)} 157 | not_satisfied "Value #{value.to_sxp} not expected, wanted #{values.to_sxp}", depth: depth unless matched_value 158 | status "right value: #{value}", depth: depth 159 | true 160 | end 161 | 162 | # Returns the value of a particular facet 163 | def op_fetch(which) 164 | (operands.detect {|op| op.is_a?(Array) && op[0] == which} || [])[1] 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /script/tc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require "bundler/setup" 4 | $:.unshift(File.expand_path("../../lib", __FILE__)) 5 | $:.unshift(File.expand_path("../../spec", __FILE__)) 6 | require 'logger' 7 | require 'rdf' 8 | require 'rdf/isomorphic' 9 | require 'rspec' 10 | require 'shex' 11 | require 'suite_helper' 12 | require 'getoptlong' 13 | 14 | ASSERTOR = "https://greggkellogg.net/foaf#me" 15 | RUN_TIME = Time.now 16 | TEST_BASE = 'https://raw.githubusercontent.com/shexSpec/shexTest/master/' 17 | 18 | def earl_preamble(options) 19 | options[:output].write File.read(File.expand_path("../../etc/doap.ttl", __FILE__)) 20 | options[:output].puts %( 21 | <> foaf:primaryTopic ; 22 | dc:issued "#{RUN_TIME.xmlschema}"^^xsd:dateTime ; 23 | foaf:maker <#{ASSERTOR}> . 24 | 25 | <#{ASSERTOR}> a foaf:Person, earl:Assertor; 26 | foaf:name "Gregg Kellogg"; 27 | foaf:title "Implementor"; 28 | foaf:homepage . 29 | ) 30 | end 31 | 32 | def run_tc(tc, **options) 33 | STDERR.write "run #{tc.name}" unless options[:quiet] 34 | result = "untested" 35 | 36 | begin 37 | if options[:verbose] 38 | puts "\nTestCase: #{tc.inspect}" 39 | puts "\nSchema:\n" + tc.schema_source 40 | #puts "\nExpected:\n" + tc.expected 41 | end 42 | 43 | tc.logger.level = options[:level] 44 | tc.logger.formatter = lambda {|severity, datetime, progname, msg| "%5s %s\n" % [severity, msg]} 45 | 46 | validate = case tc.name 47 | when '_all', 'kitchenSink' then false 48 | else true 49 | end 50 | schema = ShEx.parse(tc.schema_source, 51 | base_uri: tc.base, 52 | logger: tc.logger) 53 | 54 | puts "\nsxp: " + SXP::Generator.string(schema.to_sxp_bin) if options[:verbose] 55 | schema.validate! if validate 56 | 57 | focus = ShEx::Algebra::Operator.value(tc.focus, base_uri: tc.base) 58 | map = if tc.map 59 | tc.shape_map.inject({}) do |memo, (k,v)| 60 | memo.merge(ShEx::Algebra::Operator.value(k, base_uri: tc.base) => ShEx::Algebra::Operator.iri(v, base_uri: tc.base)) 61 | end 62 | elsif tc.shape 63 | {focus => ShEx::Algebra::Operator.iri(tc.shape, base_uri: tc.base)} 64 | else 65 | {} 66 | end 67 | focus = nil unless map.empty? 68 | 69 | if tc.positive_test? 70 | if tc.validation_test? 71 | r = schema.execute(tc.graph, map, focus: focus, logger: tc.logger, shapeExterns: tc.shapeExterns) 72 | puts "\nresult: " + SXP::Generator.string(r.to_sxp_bin) if options[:verbose] 73 | result = "passed" 74 | else 75 | result = schema.is_a?(ShEx::Algebra::Schema) ? "passed" : "failed" 76 | end 77 | else 78 | if tc.validation_test? 79 | r = schema.execute(tc.graph, map, focus: focus, logger: tc.logger, shapeExterns: tc.shapeExterns) 80 | puts "\nresult: " + SXP::Generator.string(r.to_sxp_bin) if options[:verbose] 81 | result = "failed" 82 | else 83 | result = "failed" 84 | end 85 | end 86 | rescue ShEx::ParseError, ShEx::StructureError, ArgumentError => e 87 | puts "\nexception: " + e.inspect if options[:verbose] 88 | result = if tc.negative_test? && (tc.syntax_test? || tc.structure_test?) 89 | "passed" 90 | else 91 | "failed" 92 | end 93 | rescue ShEx::NotSatisfied => e 94 | puts "\nexception: " + e.inspect if options[:verbose] 95 | result = if tc.negative_test? && tc.validation_test? 96 | "passed" 97 | else 98 | "failed" 99 | end 100 | rescue Interrupt 101 | exit(1) 102 | rescue Exception => e 103 | result = "failed" 104 | end 105 | 106 | if !tc.logger.to_s.empty? && options[:verbose] 107 | puts "\nlog: " + tc.logger.to_s 108 | end 109 | 110 | if options[:earl] 111 | test = TEST_BASE + tc.base.join(tc.id).to_s.sub('.jsonld', '').split('/')[-2..-1].join("/") 112 | options[:output].puts %{ 113 | [ a earl:Assertion; 114 | earl:assertedBy <#{ASSERTOR}>; 115 | earl:subject ; 116 | earl:test <#{test}>; 117 | earl:result [ 118 | a earl:TestResult; 119 | earl:outcome earl:#{result}; 120 | dc:date "#{RUN_TIME.xmlschema}"^^xsd:dateTime]; 121 | earl:mode earl:automatic ] . 122 | } 123 | end 124 | 125 | options[:result_count][result] ||= 0 126 | options[:result_count][result] += 1 127 | 128 | if options[:quiet] 129 | STDERR.write(result == "passed" ? '.' : 'F') 130 | else 131 | STDERR.puts " #{result}" 132 | end 133 | end 134 | 135 | options = { 136 | output: STDOUT, 137 | level: Logger::WARN, 138 | validate: true, 139 | verbose: false, 140 | } 141 | 142 | OPT_ARGS = [ 143 | ["--debug", GetoptLong::NO_ARGUMENT, "Debug shape matching"], 144 | ["--earl", GetoptLong::NO_ARGUMENT, "Generate EARL report"], 145 | ["--help", "-?", GetoptLong::NO_ARGUMENT, "This message"], 146 | ["--mancache", GetoptLong::NO_ARGUMENT, "Creates an N-Triples representation of the combined manifests"], 147 | ["--output", "-o", GetoptLong::REQUIRED_ARGUMENT, "Output to specified file"], 148 | ["--progress", GetoptLong::NO_ARGUMENT, "Display parse tree"], 149 | ["--quiet", "-q", GetoptLong::NO_ARGUMENT, "Minimal output"], 150 | ["--validate", GetoptLong::NO_ARGUMENT, "Validate schema document"], 151 | ["--verbose", GetoptLong::NO_ARGUMENT, "Display details of processing"], 152 | ] 153 | def usage 154 | STDERR.puts %{Usage: #{$0} [options] file ...} 155 | width = OPT_ARGS.map do |o| 156 | l = o.first.length 157 | l += o[1].length + 2 if o[1].is_a?(String) 158 | l 159 | end.max 160 | OPT_ARGS.each do |o| 161 | s = " %-*s " % [width, (o[1].is_a?(String) ? "#{o[0,2].join(', ')}" : o[0])] 162 | s += o.last 163 | STDERR.puts s 164 | end 165 | exit(1) 166 | end 167 | 168 | opts = GetoptLong.new(*OPT_ARGS.map {|o| o[0..-2]}) 169 | 170 | opts.each do |opt, arg| 171 | case opt 172 | when '--earl' then options[:quiet] = options[:earl] = true 173 | when '--debug' then options[:level] = Logger::DEBUG 174 | when '--mancache' then options[:mancache] = true 175 | when '--output' then options[:output] = File.open(arg, "w") 176 | when '--progress' then options[:level] = Logger::INFO 177 | when '--quiet' 178 | options[:quiet] = true 179 | options[:level] = Logger::FATAL 180 | when '--verbose' then options[:verbose] = true 181 | end 182 | end 183 | 184 | earl_preamble(options) if options[:earl] 185 | result_count = {} 186 | man_graph = RDF::Graph.new 187 | 188 | %w(schemas/manifest.jsonld negativeSyntax/manifest.jsonld negativeStructure/manifest.jsonld validation/manifest.jsonld).each do |variant| 189 | manifest = Fixtures::SuiteTest::BASE + variant 190 | 191 | Fixtures::SuiteTest::Manifest.open(manifest) do |m| 192 | if options[:mancache] 193 | # Output N-Triples for this manifest 194 | puts m.id 195 | JSON::LD::API.toRdf(m.instance_variable_get(:@json), base: "#{TEST_BASE}#{variant.sub('.jsonld', '')}") {|s| 196 | man_graph << s 197 | } 198 | next 199 | end 200 | 201 | m.entries.each do |tc| 202 | next unless ARGV.empty? || ARGV.any? do |n| 203 | tc.id.include?(n) || 204 | tc.schema.to_s.include?(n) || 205 | tc.name.match?(/#{n}/) 206 | end 207 | run_tc(tc, result_count: result_count, **options) 208 | end 209 | end 210 | end 211 | 212 | if options[:mancache] 213 | require 'rdf/turtle' 214 | options[:output].write(man_graph.dump(:ttl, standard_prefixes: true, 215 | base_uri: TEST_BASE, 216 | prefixes: { 217 | mf: "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#", 218 | sht: "http://www.w3.org/ns/shacl/test-suite#", 219 | sx: "https://shexspec.github.io/shexTest/ns#" 220 | })) 221 | else 222 | STDERR.puts "" if options[:quiet] 223 | 224 | result_count.each do |result, count| 225 | puts "#{result}: #{count}" 226 | end 227 | end -------------------------------------------------------------------------------- /lib/shex/shex_context.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # frozen_string_literal: true 3 | # This file generated automatically from http://www.w3.org/ns/shex.jsonld 4 | require 'json/ld' 5 | class JSON::LD::Context 6 | add_preloaded("http://www.w3.org/ns/shex.jsonld") do 7 | new(processingMode: "json-ld-1.0", term_definitions: { 8 | "Annotation" => TermDefinition.new("Annotation", id: "http://www.w3.org/ns/shex#Annotation", simple: true), 9 | "EachOf" => TermDefinition.new("EachOf", id: "http://www.w3.org/ns/shex#EachOf", simple: true), 10 | "IriStem" => TermDefinition.new("IriStem", id: "http://www.w3.org/ns/shex#IriStem", simple: true), 11 | "IriStemRange" => TermDefinition.new("IriStemRange", id: "http://www.w3.org/ns/shex#IriStemRange", simple: true), 12 | "LanguageStem" => TermDefinition.new("LanguageStem", id: "http://www.w3.org/ns/shex#LanguageStem", simple: true), 13 | "LanguageStemRange" => TermDefinition.new("LanguageStemRange", id: "http://www.w3.org/ns/shex#LanguageStemRange", simple: true), 14 | "LiteralStem" => TermDefinition.new("LiteralStem", id: "http://www.w3.org/ns/shex#LiteralStem", simple: true), 15 | "LiteralStemRange" => TermDefinition.new("LiteralStemRange", id: "http://www.w3.org/ns/shex#LiteralStemRange", simple: true), 16 | "NodeConstraint" => TermDefinition.new("NodeConstraint", id: "http://www.w3.org/ns/shex#NodeConstraint", simple: true), 17 | "OneOf" => TermDefinition.new("OneOf", id: "http://www.w3.org/ns/shex#OneOf", simple: true), 18 | "Schema" => TermDefinition.new("Schema", id: "http://www.w3.org/ns/shex#Schema", simple: true), 19 | "SemAct" => TermDefinition.new("SemAct", id: "http://www.w3.org/ns/shex#SemAct", simple: true), 20 | "Shape" => TermDefinition.new("Shape", id: "http://www.w3.org/ns/shex#Shape", simple: true), 21 | "ShapeAnd" => TermDefinition.new("ShapeAnd", id: "http://www.w3.org/ns/shex#ShapeAnd", simple: true), 22 | "ShapeExternal" => TermDefinition.new("ShapeExternal", id: "http://www.w3.org/ns/shex#ShapeExternal", simple: true), 23 | "ShapeNot" => TermDefinition.new("ShapeNot", id: "http://www.w3.org/ns/shex#ShapeNot", simple: true), 24 | "ShapeOr" => TermDefinition.new("ShapeOr", id: "http://www.w3.org/ns/shex#ShapeOr", simple: true), 25 | "Stem" => TermDefinition.new("Stem", id: "http://www.w3.org/ns/shex#Stem", simple: true), 26 | "StemRange" => TermDefinition.new("StemRange", id: "http://www.w3.org/ns/shex#StemRange", simple: true), 27 | "TripleConstraint" => TermDefinition.new("TripleConstraint", id: "http://www.w3.org/ns/shex#TripleConstraint", simple: true), 28 | "Wildcard" => TermDefinition.new("Wildcard", id: "http://www.w3.org/ns/shex#Wildcard", simple: true), 29 | "annotations" => TermDefinition.new("annotations", id: "http://www.w3.org/ns/shex#annotation", type_mapping: "@id", container_mapping: "@list"), 30 | "bnode" => TermDefinition.new("bnode", id: "http://www.w3.org/ns/shex#bnode", simple: true), 31 | "closed" => TermDefinition.new("closed", id: "http://www.w3.org/ns/shex#closed", type_mapping: "http://www.w3.org/2001/XMLSchema#boolean"), 32 | "code" => TermDefinition.new("code", id: "http://www.w3.org/ns/shex#code"), 33 | "datatype" => TermDefinition.new("datatype", id: "http://www.w3.org/ns/shex#datatype", type_mapping: "@id"), 34 | "exclusions" => TermDefinition.new("exclusions", id: "http://www.w3.org/ns/shex#exclusion", type_mapping: "@id", container_mapping: "@list"), 35 | "expression" => TermDefinition.new("expression", id: "http://www.w3.org/ns/shex#expression", type_mapping: "@id"), 36 | "expressions" => TermDefinition.new("expressions", id: "http://www.w3.org/ns/shex#expressions", type_mapping: "@id", container_mapping: "@list"), 37 | "extra" => TermDefinition.new("extra", id: "http://www.w3.org/ns/shex#extra", type_mapping: "@id"), 38 | "flags" => TermDefinition.new("flags", id: "http://www.w3.org/ns/shex#flags"), 39 | "fractiondigits" => TermDefinition.new("fractiondigits", id: "http://www.w3.org/ns/shex#fractiondigits", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 40 | "id" => TermDefinition.new("id", id: "@id", simple: true), 41 | "inverse" => TermDefinition.new("inverse", id: "http://www.w3.org/ns/shex#inverse", type_mapping: "http://www.w3.org/2001/XMLSchema#boolean"), 42 | "iri" => TermDefinition.new("iri", id: "http://www.w3.org/ns/shex#iri", simple: true), 43 | "language" => TermDefinition.new("language", id: "@language", simple: true), 44 | "length" => TermDefinition.new("length", id: "http://www.w3.org/ns/shex#length", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 45 | "literal" => TermDefinition.new("literal", id: "http://www.w3.org/ns/shex#literal", simple: true), 46 | "max" => TermDefinition.new("max", id: "http://www.w3.org/ns/shex#max", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 47 | "maxexclusive" => TermDefinition.new("maxexclusive", id: "http://www.w3.org/ns/shex#maxexclusive", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 48 | "maxinclusive" => TermDefinition.new("maxinclusive", id: "http://www.w3.org/ns/shex#maxinclusive", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 49 | "maxlength" => TermDefinition.new("maxlength", id: "http://www.w3.org/ns/shex#maxlength", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 50 | "min" => TermDefinition.new("min", id: "http://www.w3.org/ns/shex#min", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 51 | "minexclusive" => TermDefinition.new("minexclusive", id: "http://www.w3.org/ns/shex#minexclusive", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 52 | "mininclusive" => TermDefinition.new("mininclusive", id: "http://www.w3.org/ns/shex#mininclusive", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 53 | "minlength" => TermDefinition.new("minlength", id: "http://www.w3.org/ns/shex#minlength", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 54 | "name" => TermDefinition.new("name", id: "http://www.w3.org/ns/shex#name", type_mapping: "@id"), 55 | "nodeKind" => TermDefinition.new("nodeKind", id: "http://www.w3.org/ns/shex#nodeKind", type_mapping: "@vocab"), 56 | "nonliteral" => TermDefinition.new("nonliteral", id: "http://www.w3.org/ns/shex#nonliteral", simple: true), 57 | "object" => TermDefinition.new("object", id: "http://www.w3.org/ns/shex#object", type_mapping: "@id"), 58 | "pattern" => TermDefinition.new("pattern", id: "http://www.w3.org/ns/shex#pattern"), 59 | "predicate" => TermDefinition.new("predicate", id: "http://www.w3.org/ns/shex#predicate", type_mapping: "@id"), 60 | "rdf" => TermDefinition.new("rdf", id: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", simple: true), 61 | "rdfs" => TermDefinition.new("rdfs", id: "http://www.w3.org/2000/01/rdf-schema#", simple: true), 62 | "semActs" => TermDefinition.new("semActs", id: "http://www.w3.org/ns/shex#semActs", type_mapping: "@id", container_mapping: "@list"), 63 | "shapeExpr" => TermDefinition.new("shapeExpr", id: "http://www.w3.org/ns/shex#shapeExpr", type_mapping: "@id"), 64 | "shapeExprs" => TermDefinition.new("shapeExprs", id: "http://www.w3.org/ns/shex#shapeExprs", type_mapping: "@id", container_mapping: "@list"), 65 | "shapes" => TermDefinition.new("shapes", id: "http://www.w3.org/ns/shex#shapes", type_mapping: "@id"), 66 | "shex" => TermDefinition.new("shex", id: "http://www.w3.org/ns/shex#", simple: true), 67 | "start" => TermDefinition.new("start", id: "http://www.w3.org/ns/shex#start", type_mapping: "@id"), 68 | "startActs" => TermDefinition.new("startActs", id: "http://www.w3.org/ns/shex#startActs", type_mapping: "@id", container_mapping: "@list"), 69 | "stem" => TermDefinition.new("stem", id: "http://www.w3.org/ns/shex#stem", type_mapping: "http://www.w3.org/2001/XMLSchema#string"), 70 | "totaldigits" => TermDefinition.new("totaldigits", id: "http://www.w3.org/ns/shex#totaldigits", type_mapping: "http://www.w3.org/2001/XMLSchema#integer"), 71 | "type" => TermDefinition.new("type", id: "@type", simple: true), 72 | "value" => TermDefinition.new("value", id: "@value", simple: true), 73 | "valueExpr" => TermDefinition.new("valueExpr", id: "http://www.w3.org/ns/shex#valueExpr", type_mapping: "@id"), 74 | "values" => TermDefinition.new("values", id: "http://www.w3.org/ns/shex#values", type_mapping: "@id", container_mapping: "@list"), 75 | "xsd" => TermDefinition.new("xsd", id: "http://www.w3.org/2001/XMLSchema#", simple: true) 76 | }) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/shex/algebra/schema.rb: -------------------------------------------------------------------------------- 1 | module ShEx::Algebra 2 | ## 3 | class Schema < Operator 4 | NAME = :schema 5 | 6 | # Graph to validate 7 | # @return [RDF::Queryable] 8 | attr_accessor :graph 9 | 10 | # Map of nodes to shapes 11 | # @return [Hash{RDF::Resource => RDF::Resource}] 12 | attr_reader :map 13 | 14 | # Map of Semantic Action instances 15 | # @return [Hash{String => ShEx::Extension}] 16 | attr_reader :extensions 17 | 18 | ## 19 | # Creates an operator instance from a parsed ShExJ representation 20 | # @param (see Operator#from_shexj) 21 | # @return [Operator] 22 | def self.from_shexj(operator, **options) 23 | raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "Schema" 24 | super 25 | end 26 | 27 | # (see Operator#initialize) 28 | def initialize(*operands, **options) 29 | super 30 | schema = self 31 | each_descendant do |op| 32 | # Set schema everywhere 33 | op.schema = self 34 | end 35 | end 36 | 37 | ## 38 | # Match on schema. Finds appropriate shape for node, and matches that shape. 39 | # 40 | # @param [RDF::Queryable] graph 41 | # @param [Hash{RDF::Term => }, Array] map 42 | # A set of (`term`, `resource`) pairs where `term` is a node within `graph`, and `resource` identifies a shape 43 | # @param [Array] focus ([]) 44 | # One or more nodes within `graph` for which to run the start expression. 45 | # @param [Array] shapeExterns ([]) 46 | # One or more schemas, or paths to ShEx schema resources used for finding external shapes. 47 | # @return [Hash{RDF::Term => Array}] Returns _ShapeResults_, a hash of graph nodes to the results of their associated shapes 48 | # @param [Hash{Symbol => Object}] options 49 | # @option options [String] :base_uri (for resolving focus) 50 | # @raise [ShEx::NotSatisfied] along with individual shape results 51 | def execute(graph, map, focus: [], shapeExterns: [], depth: 0, **options) 52 | @graph, @shapes_entered, results = graph, {}, {} 53 | @external_schemas = shapeExterns 54 | @extensions = {} 55 | focus = Array(focus).map {|f| value(f, **options)} 56 | 57 | logger = options[:logger] || @options[:logger] 58 | each_descendant do |op| 59 | # Set logging everywhere 60 | op.logger = logger 61 | end 62 | 63 | # Initialize Extensions 64 | each_descendant do |op| 65 | next unless op.is_a?(SemAct) 66 | name = op.operands.first.to_s 67 | if ext_class = ShEx::Extension.find(name) 68 | @extensions[name] ||= ext_class.new(schema: self, depth: depth, **options) 69 | end 70 | end 71 | 72 | # If `n` is a Blank Node, we won't find it through normal matching, find an equivalent node in the graph having the same id 73 | @map = case map 74 | when Hash 75 | map.inject({}) do |memo, (node, shapes)| 76 | gnode = graph.enum_term.detect {|t| t.node? && t.id == node.id} if node.is_a?(RDF::Node) 77 | node = gnode if gnode 78 | memo.merge(node => Array(shapes)) 79 | end 80 | when Array 81 | map.inject({}) do |memo, (node, shape)| 82 | gnode = graph.enum_term.detect {|t| t.node? && t.id == node.id} if node.is_a?(RDF::Node) 83 | node = gnode if gnode 84 | (memo[node] ||= []).concat(Array(shape)) 85 | memo 86 | end 87 | when nil then {} 88 | else 89 | structure_error "Unrecognized shape map: #{map.inspect}" 90 | end 91 | 92 | # First, evaluate semantic acts 93 | semantic_actions.all? do |op| 94 | op.satisfies?([], depth: depth + 1) 95 | end 96 | 97 | # Next run any start expression 98 | if !focus.empty? 99 | if start 100 | focus.each do |node| 101 | node = graph.enum_term.detect {|t| t.node? && t.id == node.id} if node.is_a?(RDF::Node) 102 | sr = ShapeResult.new(RDF::URI("http://www.w3.org/ns/shex#Start")) 103 | (results[node] ||= []) << sr 104 | begin 105 | sr.expression = start.satisfies?(node, depth: depth + 1) 106 | sr.result = true 107 | rescue ShEx::NotSatisfied => e 108 | sr.expression = e.expression 109 | sr.result = false 110 | end 111 | end 112 | else 113 | structure_error "Focus nodes with no start" 114 | end 115 | end 116 | 117 | # Match against all shapes associated with the ids for focus 118 | @map.each do |node, shapes| 119 | results[node] ||= [] 120 | shapes.each do |id| 121 | enter_shape(id, node) do |shape| 122 | sr = ShapeResult.new(id) 123 | results[node] << sr 124 | begin 125 | sr.expression = shape.satisfies?(node, depth: depth + 1) 126 | sr.result = true 127 | rescue ShEx::NotSatisfied => e 128 | sr.expression = e.expression 129 | sr.result = false 130 | end 131 | end 132 | end 133 | end 134 | 135 | if results.values.flatten.all? {|sr| sr.result} 136 | status "schema satisfied", depth: depth 137 | results 138 | else 139 | raise ShEx::NotSatisfied.new("Graph does not conform to schema", expression: results) 140 | end 141 | ensure 142 | # Close Semantic Action extensions 143 | @extensions.values.each {|ext| ext.close(schema: self, depth: depth, **options)} 144 | end 145 | 146 | ## 147 | # Match on schema. Finds appropriate shape for node, and matches that shape. 148 | # 149 | # @param (see ShEx::Algebra::Schema#execute) 150 | # @param [Hash{Symbol => Object}] options 151 | # @option options [String] :base_uri 152 | # @return [Boolean] 153 | def satisfies?(graph, map, **options) 154 | execute(graph, map, **options) 155 | rescue ShEx::NotSatisfied 156 | false 157 | end 158 | 159 | ## 160 | # Shapes as a hash 161 | # @return [Array] 162 | def shapes 163 | @shapes ||= begin 164 | shapes = Array(operands.detect {|op| op.is_a?(Array) && op.first == :shapes}) 165 | Array(shapes[1..-1]) 166 | end 167 | end 168 | 169 | ## 170 | # Indicate that a shape has been entered with a specific focus node. Any future attempt to enter the same shape with the same node raises an exception. 171 | # @param [RDF::Resource] id 172 | # @param [RDF::Resource] node 173 | # @yield :shape 174 | # @yieldparam [ShapeExpression] shape, or `nil` if shape already entered 175 | # @return (see ShapeExpression#satisfies?) 176 | # @raise (see ShapeExpression#satisfies?) 177 | def enter_shape(id, node, &block) 178 | shape = shapes.detect {|s| s.id == id} 179 | structure_error("No shape found for #{id}") unless shape 180 | @shapes_entered[id] ||= {} 181 | if @shapes_entered[id][node] 182 | block.call(false) 183 | else 184 | @shapes_entered[id][node] = self 185 | begin 186 | block.call(shape) 187 | ensure 188 | @shapes_entered[id].delete(node) 189 | end 190 | end 191 | end 192 | 193 | ## 194 | # Externally loaded schemas, lazily evaluated 195 | # @return [Array] 196 | def external_schemas 197 | @external_schemas = Array(@external_schemas).map do |extern| 198 | schema = case extern 199 | when Schema then extern 200 | else 201 | status "Load extern #{extern}" 202 | ShEx.open(extern, logger: options[:logger]) 203 | end 204 | schema.graph = graph 205 | schema 206 | end 207 | end 208 | 209 | ## 210 | # Start action, if any 211 | def start 212 | @start ||= operands.detect {|op| op.is_a?(Start)} 213 | end 214 | 215 | ## 216 | # Validate shapes, in addition to other operands 217 | # @return [Operator] `self` 218 | # @raise [ArgumentError] if the value is invalid 219 | def validate! 220 | shapes.each do |op| 221 | op.validate! if op.respond_to?(:validate!) 222 | if op.is_a?(RDF::Resource) 223 | ref = find(op) 224 | structure_error("Missing reference: #{op}") if ref.nil? 225 | end 226 | end 227 | super 228 | end 229 | end 230 | 231 | # A shape result 232 | class ShapeResult 233 | # The label of the shape within the schema, or a URI indicating a start shape 234 | # @return [RDF::Resource] 235 | attr_reader :shape 236 | 237 | # Does the node conform to the shape 238 | # @return [Boolean] 239 | attr_accessor :result 240 | 241 | # The annotated {Operator} indicating processing results 242 | # @return [ShEx::Algebra::Operator] 243 | attr_accessor :expression 244 | 245 | # Holds the result of processing a shape 246 | # @param [RDF::Resource] shape 247 | # @return [ShapeResult] 248 | def initialize(shape) 249 | @shape = shape 250 | end 251 | 252 | # The SXP of {#expression} 253 | # @return [String] 254 | def reason 255 | SXP::Generator.string(expression.to_sxp_bin) 256 | end 257 | 258 | ## 259 | # Returns the binary S-Expression (SXP) representation of this result. 260 | # 261 | # @return [Array] 262 | # @see https://en.wikipedia.org/wiki/S-expression 263 | def to_sxp_bin 264 | [:ShapeResult, shape, result, expression].map(&:to_sxp_bin) 265 | end 266 | end 267 | end 268 | -------------------------------------------------------------------------------- /etc/shex.sxp: -------------------------------------------------------------------------------- 1 | ( 2 | (rule shexDoc "1" 3 | (seq (star directive) (opt (seq (alt notStartAction startActions) (star statement))))) 4 | (rule directive "2" (alt baseDecl prefixDecl importDecl)) 5 | (rule baseDecl "3" (seq "BASE" IRIREF)) 6 | (rule prefixDecl "4" (seq "PREFIX" PNAME_NS IRIREF)) 7 | (rule importDecl "4.5" (seq "IMPORT" IRIREF)) 8 | (rule notStartAction "5" (alt start shapeExprDecl)) 9 | (rule start "6" (seq "START" "=" inlineShapeExpression)) 10 | (rule startActions "7" (plus codeDecl)) 11 | (rule statement "8" (alt directive notStartAction)) 12 | (rule shapeExprDecl "9" (seq shapeExprLabel (alt shapeExpression "EXTERNAL"))) 13 | (rule shapeExpression "10" (seq shapeOr)) 14 | (rule inlineShapeExpression "11" (seq inlineShapeOr)) 15 | (rule shapeOr "12" (seq shapeAnd (star (seq "OR" shapeAnd)))) 16 | (rule inlineShapeOr "13" (seq inlineShapeAnd (star (seq "OR" inlineShapeAnd)))) 17 | (rule shapeAnd "14" (seq shapeNot (star (seq "AND" shapeNot)))) 18 | (rule inlineShapeAnd "15" (seq inlineShapeNot (star (seq "AND" inlineShapeNot)))) 19 | (rule shapeNot "16" (seq (opt "NOT") shapeAtom)) 20 | (rule inlineShapeNot "17" (seq (opt "NOT") inlineShapeAtom)) 21 | (rule shapeAtom "18" 22 | (alt 23 | (seq nonLitNodeConstraint (opt shapeOrRef)) litNodeConstraint 24 | (seq shapeOrRef (opt nonLitNodeConstraint)) 25 | (seq "(" shapeExpression ")") "." )) 26 | (rule shapeAtomNoRef "19" 27 | (alt 28 | (seq nonLitNodeConstraint (opt shapeOrRef)) litNodeConstraint 29 | (seq shapeDefinition (opt nonLitNodeConstraint)) 30 | (seq "(" shapeExpression ")") "." )) 31 | (rule inlineShapeAtom "20" 32 | (alt 33 | (seq nonLitNodeConstraint (opt inlineShapeOrRef)) litNodeConstraint 34 | (seq inlineShapeOrRef (opt nonLitNodeConstraint)) 35 | (seq "(" shapeExpression ")") "." )) 36 | (rule shapeOrRef "21" (alt shapeDefinition shapeRef)) 37 | (rule inlineShapeOrRef "22" (alt inlineShapeDefinition shapeRef)) 38 | (rule shapeRef "23" (alt ATPNAME_LN ATPNAME_NS (seq "@" shapeExprLabel))) 39 | (rule litNodeConstraint "24" 40 | (alt 41 | (seq "LITERAL" (star xsFacet)) 42 | (seq datatype (star xsFacet)) 43 | (seq valueSet (star xsFacet)) 44 | (plus numericFacet)) ) 45 | (rule nonLitNodeConstraint "25" 46 | (alt (seq nonLiteralKind (star stringFacet)) (plus stringFacet))) 47 | (rule nonLiteralKind "26" (alt "IRI" "BNODE" "NONLITERAL")) 48 | (rule xsFacet "27" (alt stringFacet numericFacet)) 49 | (rule stringFacet "28" (alt (seq stringLength INTEGER) REGEXP)) 50 | (rule stringLength "29" (alt "LENGTH" "MINLENGTH" "MAXLENGTH")) 51 | (rule numericFacet "30" 52 | (alt (seq numericRange numericLiteral) (seq numericLength INTEGER))) 53 | (rule numericRange "31" 54 | (alt "MININCLUSIVE" "MINEXCLUSIVE" "MAXINCLUSIVE" "MAXEXCLUSIVE")) 55 | (rule numericLength "32" (alt "TOTALDIGITS" "FRACTIONDIGITS")) 56 | (rule shapeDefinition "33" 57 | (seq 58 | (star (alt extraPropertySet "CLOSED")) "{" 59 | (opt tripleExpression) "}" 60 | (star annotation) semanticActions )) 61 | (rule inlineShapeDefinition "34" 62 | (seq (star (alt extraPropertySet "CLOSED")) "{" (opt tripleExpression) "}")) 63 | (rule extraPropertySet "35" (seq "EXTRA" (plus predicate))) 64 | (rule tripleExpression "36" (seq oneOfTripleExpr)) 65 | (rule oneOfTripleExpr "37" (seq groupTripleExpr (star (seq "|" groupTripleExpr)))) 66 | (rule groupTripleExpr "40" (seq unaryTripleExpr (star (seq ";" (opt unaryTripleExpr))))) 67 | (rule unaryTripleExpr "43" 68 | (alt 69 | (seq (opt (seq "$" tripleExprLabel)) (alt tripleConstraint bracketedTripleExpr)) 70 | include )) 71 | (rule bracketedTripleExpr "44" 72 | (seq "(" tripleExpression ")" (opt cardinality) (star annotation) semanticActions)) 73 | (rule tripleConstraint "45" 74 | (seq 75 | (opt senseFlags) predicate inlineShapeExpression 76 | (opt cardinality) 77 | (star annotation) semanticActions )) 78 | (rule cardinality "46" (alt "*" "+" "?" REPEAT_RANGE)) 79 | (rule senseFlags "47" (seq "^")) 80 | (rule valueSet "48" (seq "[" (star valueSetValue) "]")) 81 | (rule valueSetValue "49" 82 | (alt iriRange literalRange languageRange (seq "." (plus exclusion)))) 83 | (rule exclusion "50" (seq "-" (alt iri literal LANGTAG) (opt "~"))) 84 | (rule iriRange "51" (seq iri (opt (seq "~" (star iriExclusion))))) 85 | (rule iriExclusion "52" (seq "-" iri (opt "~"))) 86 | (rule literalRange "53" (seq literal (opt (seq "~" (star literalExclusion))))) 87 | (rule literalExclusion "54" (seq "-" literal (opt "~"))) 88 | (rule languageRange "55" 89 | (alt 90 | (seq LANGTAG (opt (seq "~" (star languageExclusion)))) 91 | (seq "@" "~" (star languageExclusion))) ) 92 | (rule languageExclusion "56" (seq "-" LANGTAG (opt "~"))) 93 | (rule include "57" (seq "&" tripleExprLabel)) 94 | (rule annotation "58" (seq "//" predicate (alt iri literal))) 95 | (rule semanticActions "59" (star codeDecl)) 96 | (rule codeDecl "60" (seq "%" iri (alt CODE "%"))) 97 | (rule literal "13t" (alt rdfLiteral numericLiteral booleanLiteral)) 98 | (rule predicate "61" (alt iri RDF_TYPE)) 99 | (rule datatype "62" (seq iri)) 100 | (rule shapeExprLabel "63" (alt iri blankNode)) 101 | (rule tripleExprLabel "64" (alt iri blankNode)) 102 | (rule numericLiteral "16t" (alt DOUBLE DECIMAL INTEGER)) 103 | (rule rdfLiteral "65" (alt langString (seq string (opt (seq "^^" datatype))))) 104 | (rule booleanLiteral "134s" (alt "true" "false")) 105 | (rule string "135s" 106 | (alt STRING_LITERAL_LONG1 STRING_LITERAL_LONG2 STRING_LITERAL1 STRING_LITERAL2)) 107 | (rule langString "66" 108 | (alt LANG_STRING_LITERAL1 LANG_STRING_LITERAL_LONG1 LANG_STRING_LITERAL2 109 | LANG_STRING_LITERAL_LONG2 )) 110 | (rule iri "136s" (alt IRIREF prefixedName)) 111 | (rule prefixedName "137s" (alt PNAME_LN PNAME_NS)) 112 | (rule blankNode "138s" (seq BLANK_NODE_LABEL)) 113 | (terminals _terminals (seq)) 114 | (terminal CODE "67" (seq "{" (range "^%\\] | '\\'[%\\] | UCHAR)* '%''}'"))) 115 | (terminal REPEAT_RANGE "68" (seq "{" INTEGER (opt (seq "," (opt (alt INTEGER "*")))) "}")) 116 | (terminal RDF_TYPE "69" (seq "a")) 117 | (terminal IRIREF "18t" 118 | (seq "<" 119 | (range 120 | "^#x00-#x20<>\"{}|^`\\] | UCHAR)* '>' /* #x00=NULL #01-#x1F=control codes #x20=space */" 121 | )) ) 122 | (terminal PNAME_NS "140s" (seq (opt PN_PREFIX) ":")) 123 | (terminal PNAME_LN "141s" (seq PNAME_NS PN_LOCAL)) 124 | (terminal ATPNAME_NS "70" (seq "@" (opt PN_PREFIX) ":")) 125 | (terminal ATPNAME_LN "71" (seq "@" PNAME_NS PN_LOCAL)) 126 | (terminal REGEXP "72" 127 | (seq "/" 128 | (plus (alt (range "^/\\\n\r") (seq "\\" (range "nrt\\|.?*+(){}$-[]^/")) UCHAR)) 129 | "/" 130 | (star (range "smix"))) ) 131 | (terminal BLANK_NODE_LABEL "142s" 132 | (seq "_:" (alt PN_CHARS_U (range "0-9")) (opt (seq (star (alt PN_CHARS ".")) PN_CHARS)))) 133 | (terminal LANGTAG "145s" 134 | (seq "@" (plus (range "a-zA-Z")) (star (seq "-" (plus (range "a-zA-Z0-9")))))) 135 | (terminal INTEGER "19t" (seq (opt (range "+-")) (plus (range "0-9")))) 136 | (terminal DECIMAL "20t" (seq (opt (range "+-")) (star (range "0-9")) "." (plus (range "0-9")))) 137 | (terminal DOUBLE "21t" 138 | (seq 139 | (opt (range "+-")) 140 | (alt 141 | (seq (plus (range "0-9")) "." (star (range "0-9")) EXPONENT) 142 | (seq (opt ".") (plus (range "0-9")) EXPONENT)) )) 143 | (terminal EXPONENT "155s" (seq (range "eE") (opt (range "+-")) (plus (range "0-9")))) 144 | (terminal STRING_LITERAL1 "156s" 145 | (seq "'" (star (alt (range "^#x27#x5C#xA#xD") ECHAR UCHAR)) "'")) 146 | (terminal STRING_LITERAL2 "157s" 147 | (seq "\"" (star (alt (range "^#x22#x5C#xA#xD") ECHAR UCHAR)) "\"")) 148 | (terminal STRING_LITERAL_LONG1 "158s" 149 | (seq "'''" (seq (opt (alt "'" "''")) (range "^'\\] | ECHAR | UCHAR))* \"'''\"")))) 150 | (terminal STRING_LITERAL_LONG2 "159s" 151 | (seq "\"\"\"" (seq (opt (alt "\"" "\"\"")) (range "^\"\\] | ECHAR | UCHAR))* '\"\"\"'")))) 152 | (terminal LANG_STRING_LITERAL1 "73" 153 | (seq "'" (star (alt (range "^#x27#x5C#xA#xD") ECHAR UCHAR)) "'" LANGTAG)) 154 | (terminal LANG_STRING_LITERAL2 "74" 155 | (seq "\"" (star (alt (range "^#x22#x5C#xA#xD") ECHAR UCHAR)) "\"" LANGTAG)) 156 | (terminal LANG_STRING_LITERAL_LONG1 "75" 157 | (seq "'''" (seq (opt (alt "'" "''")) (range "^'\\] | ECHAR | UCHAR))* \"'''\" LANGTAG")))) 158 | (terminal LANG_STRING_LITERAL_LONG2 "76" 159 | (seq "\"\"\"" 160 | (seq (opt (alt "\"" "\"\"")) (range "^\"\\] | ECHAR | UCHAR))* '\"\"\"' LANGTAG"))) ) 161 | (terminal UCHAR "26t" 162 | (alt (seq "\\u" HEX HEX HEX HEX) (seq "\\U" HEX HEX HEX HEX HEX HEX HEX HEX))) 163 | (terminal ECHAR "160s" (seq "\\" (range "tbnrf\\\"'"))) 164 | (terminal PN_CHARS_BASE "164s" 165 | (alt 166 | (range "A-Z") 167 | (range "a-z") 168 | (range "#x00C0-#x00D6") 169 | (range "#x00D8-#x00F6") 170 | (range "#x00F8-#x02FF") 171 | (range "#x0370-#x037D") 172 | (range "#x037F-#x1FFF") 173 | (range "#x200C-#x200D") 174 | (range "#x2070-#x218F") 175 | (range "#x2C00-#x2FEF") 176 | (range "#x3001-#xD7FF") 177 | (range "#xF900-#xFDCF") 178 | (range "#xFDF0-#xFFFD") 179 | (range "#x10000-#xEFFFF")) ) 180 | (terminal PN_CHARS_U "165s" (alt PN_CHARS_BASE "_")) 181 | (terminal PN_CHARS "167s" 182 | (alt PN_CHARS_U "-" 183 | (range "0-9") 184 | (range "#x00B7") 185 | (range "#x0300-#x036F") 186 | (range "#x203F-#x2040")) ) 187 | (terminal PN_PREFIX "168s" 188 | (seq PN_CHARS_BASE (opt (seq (star (alt PN_CHARS ".")) PN_CHARS)))) 189 | (terminal PN_LOCAL "169s" 190 | (seq 191 | (alt PN_CHARS_U ":" (range "0-9") PLX) 192 | (opt (seq (star (alt PN_CHARS "." ":" PLX)) (alt PN_CHARS ":" PLX)))) ) 193 | (terminal PLX "170s" (alt PERCENT PN_LOCAL_ESC)) 194 | (terminal PERCENT "171s" (seq "%" HEX HEX)) 195 | (terminal HEX "172s" (alt (range "0-9") (range "A-F") (range "a-f"))) 196 | (terminal PN_LOCAL_ESC "173s" 197 | (seq "\\" 198 | (alt "_" "~" "." "-" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" "/" "?" "#" 199 | "@" "%" )) ) 200 | (pass _pass 201 | (alt 202 | (plus (range " \t\r\n")) 203 | (seq "#" (star (range "^\r\n"))) 204 | (seq "/*" (star (alt (range "^*") (seq "*" (alt (range "^/") "\\/")))) "*/")) )) 205 | -------------------------------------------------------------------------------- /etc/template.haml: -------------------------------------------------------------------------------- 1 | -# This template is used for generating a rollup EARL report. It expects to be 2 | -# called with a single _tests_ local with the following structure 3 | - require 'cgi' 4 | - require 'digest' 5 | 6 | !!! 5 7 | %html{prefix: "earl: http://www.w3.org/ns/earl# doap: http://usefulinc.com/ns/doap# mf: http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#"} 8 | - subjects = tests['testSubjects'] 9 | %head 10 | %meta{"http-equiv" => "Content-Type", content: "text/html;charset=utf-8"} 11 | %meta{name: "viewport", content: "width=device-width, initial-scale=1.0"} 12 | %link{rel: "stylesheet", type: "text/css", href: "https://www.w3.org/StyleSheets/TR/base"} 13 | %title 14 | = tests['name'] 15 | Implementation Report 16 | :css 17 | span[property='dc:description'] { display: none; } 18 | td.PASS { color: green; } 19 | td.FAIL { color: red; } 20 | table.report { 21 | border-width: 1px; 22 | border-spacing: 2px; 23 | border-style: outset; 24 | border-color: gray; 25 | border-collapse: separate; 26 | background-color: white; 27 | } 28 | table.report th { 29 | border-width: 1px; 30 | padding: 1px; 31 | border-style: inset; 32 | border-color: gray; 33 | background-color: white; 34 | -moz-border-radius: ; 35 | } 36 | table.report td { 37 | border-width: 1px; 38 | padding: 1px; 39 | border-style: inset; 40 | border-color: gray; 41 | background-color: white; 42 | -moz-border-radius: ; 43 | } 44 | tr.summary {font-weight: bold;} 45 | td.passed-all {color: green;} 46 | td.passed-most {color: darkorange;} 47 | td.passed-some {color: red;} 48 | td.passed-none {color: gray;} 49 | em.rfc2119 { 50 | text-transform: lowercase; 51 | font-variant: small-caps; 52 | font-style: normal; 53 | color: #900; 54 | } 55 | a.testlink { 56 | color: inherit; 57 | text-decoration: none; 58 | } 59 | a.testlink:hover { 60 | text-decoration: underline; 61 | } 62 | %body 63 | - subject_refs = {} 64 | - tests['entries'].each {|m| m['title'] ||= m['rdfs:label'] || m['description']} 65 | %section{about: tests['@id'], typeof: Array(tests['@type']).join(" ")} 66 | %h2 67 | Ruby ShEx gem test results 68 | %p 69 | This document reports conformance for for 70 | %a{property: "doap:name", href: "http://shex.io/shex-semantics/"}="Shape Expressions Language" 71 | %dl 72 | - subjects.each_with_index do |subject, index| 73 | - subject_refs[subject['@id']] = "subj_#{index}" 74 | %dt{id: subject_refs[subject['@id']]} 75 | %a{href: subject['@id']} 76 | %span{about: subject['@id'], property: "doap:name"}<= subject['name'] 77 | %dd{property: "earl:testSubjects", resource: subject['@id'], typeof: Array(subject['@type']).join(" ")} 78 | %dl 79 | - if subject['doapDesc'] 80 | %dt= "Description" 81 | %dd{property: "doap:description", lang: 'en'}< 82 | ~ CGI.escapeHTML subject['doapDesc'].to_s 83 | - if subject['language'] 84 | %dt= "Programming Language" 85 | %dd{property: "doap:programming-language"}< 86 | ~ CGI.escapeHTML subject['language'].to_s 87 | - if subject['homepage'] 88 | %dt= "Home Page" 89 | %dd{property: "doap:homepage"} 90 | %a{href: subject['homepage']} 91 | ~ CGI.escapeHTML subject['homepage'].to_s 92 | - if subject['developer'] 93 | %dt= "Developer" 94 | %dd{rel: "doap:developer"} 95 | - subject['developer'].each do |dev| 96 | %div{resource: dev['@id'], typeof: Array(dev['@type']).join(" ")} 97 | - if dev.has_key?('@id') 98 | %a{href: dev['@id']} 99 | %span{property: "foaf:name"}< 100 | ~ CGI.escapeHTML dev['foaf:name'].to_s 101 | - else 102 | %span{property: "foaf:name"}< 103 | ~ CGI.escapeHTML dev['foaf:name'].to_s 104 | - if dev['foaf:homepage'] 105 | %a{property: "foaf:homepage", href: dev['foaf:homepage']} 106 | ~ CGI.escapeHTML dev['foaf:homepage'].to_s 107 | %dt 108 | Test Suite Compliance 109 | %dd 110 | %table.report 111 | %tbody 112 | - tests['entries'].sort_by {|m| m['title'].to_s.downcase}.each do |manifest| 113 | - passed = manifest['entries'].select {|t| t['assertions'][index]['result']['outcome'] == 'earl:passed' }.length 114 | - total = manifest['entries'].length 115 | - pct = (passed * 100.0) / total 116 | - cls = (pct == 100.0 ? 'passed-all' : (pct >= 85.0) ? 'passed-most' : (pct == 0.0 ? 'passed-none' : 'passed-some')) 117 | %tr 118 | %td 119 | %a{href: "##{manifest['rdfs:comment']}"} 120 | ~ manifest['rdfs:comment'] 121 | %td{class: cls} 122 | = pct == 0.0 ? "Untested" : "#{passed}/#{total} (#{'%.1f' % pct}%)" 123 | %section 124 | %h2 125 | Individual Test Results 126 | - tests['entries'].sort_by {|m| m['title'].to_s.downcase}.each do |manifest| 127 | - test_cases = manifest['entries'] 128 | %section{id: manifest['rdfs:comment'], typeof: manifest['@type'].join(" "), resource: manifest['@id']} 129 | %h2{property: "rdfs:comment mf:name"}<=(manifest['rdfs:comment']) 130 | - Array(manifest['description']).each do |desc| 131 | %p{property: "rdfs:comment"}< 132 | ~ CGI.escapeHTML desc.to_s 133 | %table.report 134 | - skip_subject = {} 135 | - passed_tests = [] 136 | %tr 137 | %th 138 | Test 139 | - subjects.each_with_index do |subject, index| 140 | - subject_refs[subject['@id']] = "subj_#{index}" 141 | -# If subject is untested for every test in this manifest, skip it 142 | - skip_subject[subject['@id']] = manifest['entries'].all? {|t| t['assertions'][index]['result']['outcome'] == 'earl:untested'} 143 | - unless skip_subject[subject['@id']] 144 | %th 145 | %a{href: '#' + subject_refs[subject['@id']]}<=subject['name'] 146 | - test_cases.each do |test| 147 | - test['title'] ||= test['rdfs:label'] 148 | - test['title'] = Array(test['title']).first 149 | %tr{rel: "mf:entries", typeof: test['@type'].join(" "), resource: test['@id'], inlist: true} 150 | %td 151 | = "Test #{test['@id'].split("#").last}: #{CGI.escapeHTML test['title'].to_s}" 152 | - test['assertions'].each_with_index do |assertion, ndx| 153 | - next if skip_subject[assertion['subject']] 154 | - pass_fail = assertion['result']['outcome'].split(':').last.upcase.sub(/(PASS|FAIL)ED$/, '\1') 155 | - passed_tests[ndx] = (passed_tests[ndx] || 0) + (pass_fail == 'PASS' ? 1 : 0) 156 | %td{class: pass_fail, property: "earl:assertions", typeof: assertion['@type']} 157 | - if assertion['assertedBy'] 158 | %link{property: "earl:assertedBy", href: assertion['assertedBy']} 159 | %link{property: "earl:test", href: assertion['test']} 160 | %link{property: "earl:subject", href: assertion['subject']} 161 | - if assertion['mode'] 162 | %link{property: 'earl:mode', href: assertion['mode']} 163 | %span{property: "earl:result", typeof: assertion['result']['@type']} 164 | %span{property: 'earl:outcome', resource: assertion['result']['outcome']} 165 | = pass_fail 166 | %tr.summary 167 | %td 168 | = "Percentage passed out of #{manifest['entries'].length} Tests" 169 | - passed_tests.compact.each do |r| 170 | - pct = (r * 100.0) / manifest['entries'].length 171 | %td{class: (pct == 100.0 ? 'passed-all' : (pct >= 95.0 ? 'passed-most' : 'passed-some'))} 172 | = "#{'%.1f' % pct}%" 173 | %section#appendix{property: "earl:generatedBy", resource: tests['generatedBy']['@id'], typeof: tests['generatedBy']['@type']} 174 | %h2 175 | Report Generation Software 176 | - doap = tests['generatedBy'] 177 | - rel = doap['release'] 178 | %p 179 | This report generated by 180 | %span{property: "doap:name"}< 181 | %a{href: tests['generatedBy']['@id']}< 182 | = doap['name'] 183 | %meta{property: "doap:shortdesc", content: doap['shortdesc'], lang: 'en'} 184 | %meta{property: "doap:description", content: doap['doapDesc'], lang: 'en'} 185 | version 186 | %span{property: "doap:release", resource: rel['@id'], typeof: 'doap:Version'} 187 | %span{property: "doap:revision"}<=rel['revision'] 188 | %meta{property: "doap:name", content: rel['name']} 189 | %meta{property: "doap:created", content: rel['created'], datatype: "xsd:date"} 190 | an 191 | %a{property: "doap:license", href: doap['license']}<="Unlicensed" 192 | %span{property: "doap:programming-language"}<="Ruby" 193 | application. More information is available at 194 | %a{property: "doap:homepage", href: doap['homepage']}<=doap['homepage'] 195 | = "." 196 | -------------------------------------------------------------------------------- /etc/shex.ebnf: -------------------------------------------------------------------------------- 1 | # Notation: 2 | # in-line terminals in ""s are case-insensitive 3 | # production numbers ending in t or s are from Turtle or SPARQL. 4 | 5 | # leading CODE is captured in startActions 6 | [1] shexDoc ::= directive* ((notStartAction | startActions) statement*)? 7 | [2] directive ::= baseDecl | prefixDecl | importDecl 8 | [3] baseDecl ::= "BASE" IRIREF 9 | [4] prefixDecl ::= "PREFIX" PNAME_NS IRIREF 10 | [4.5] importDecl ::= "IMPORT" IRIREF 11 | 12 | [5] notStartAction ::= start | shapeExprDecl 13 | # "START" easier for parser than "start" 14 | [6] start ::= "START" '=' inlineShapeExpression 15 | [7] startActions ::= codeDecl+ 16 | 17 | [8] statement ::= directive | notStartAction 18 | 19 | [9] shapeExprDecl ::= shapeExprLabel (shapeExpression | "EXTERNAL") 20 | [10] shapeExpression ::= shapeOr 21 | [11] inlineShapeExpression ::= inlineShapeOr 22 | [12] shapeOr ::= shapeAnd ("OR" shapeAnd)* 23 | [13] inlineShapeOr ::= inlineShapeAnd ("OR" inlineShapeAnd)* 24 | [14] shapeAnd ::= shapeNot ("AND" shapeNot)* 25 | [15] inlineShapeAnd ::= inlineShapeNot ("AND" inlineShapeNot)* 26 | [16] shapeNot ::= "NOT"? shapeAtom 27 | [17] inlineShapeNot ::= "NOT"? inlineShapeAtom 28 | [18] shapeAtom ::= nonLitNodeConstraint shapeOrRef? 29 | | litNodeConstraint 30 | | shapeOrRef nonLitNodeConstraint? 31 | | "(" shapeExpression ")" 32 | | '.' # no constraint 33 | [19] shapeAtomNoRef ::= nonLitNodeConstraint shapeOrRef? 34 | | litNodeConstraint 35 | | shapeDefinition nonLitNodeConstraint? 36 | | "(" shapeExpression ")" 37 | | '.' # no constraint 38 | [20] inlineShapeAtom ::= nonLitNodeConstraint inlineShapeOrRef? 39 | | litNodeConstraint 40 | | inlineShapeOrRef nonLitNodeConstraint? 41 | | "(" shapeExpression ")" 42 | | '.' # no constraint 43 | 44 | [21] shapeOrRef ::= shapeDefinition | shapeRef 45 | [22] inlineShapeOrRef ::= inlineShapeDefinition | shapeRef 46 | [23] shapeRef ::= ATPNAME_LN | ATPNAME_NS | '@' shapeExprLabel 47 | 48 | [24] litNodeConstraint ::= "LITERAL" xsFacet* 49 | | datatype xsFacet* 50 | | valueSet xsFacet* 51 | | numericFacet+ 52 | [25] nonLitNodeConstraint ::= nonLiteralKind stringFacet* 53 | | stringFacet+ 54 | [26] nonLiteralKind ::= "IRI" | "BNODE" | "NONLITERAL" 55 | [27] xsFacet ::= stringFacet | numericFacet 56 | [28] stringFacet ::= stringLength INTEGER 57 | | REGEXP 58 | [29] stringLength ::= "LENGTH" | "MINLENGTH" | "MAXLENGTH" 59 | [30] numericFacet ::= numericRange numericLiteral 60 | | numericLength INTEGER 61 | [31] numericRange ::= "MININCLUSIVE" | "MINEXCLUSIVE" | "MAXINCLUSIVE" | "MAXEXCLUSIVE" 62 | [32] numericLength ::= "TOTALDIGITS" | "FRACTIONDIGITS" 63 | 64 | [33] shapeDefinition ::= (extraPropertySet | "CLOSED")* '{' tripleExpression? '}' annotation* semanticActions 65 | [34] inlineShapeDefinition ::= (extraPropertySet | "CLOSED")* '{' tripleExpression? '}' 66 | [35] extraPropertySet ::= "EXTRA" predicate+ 67 | 68 | [36] tripleExpression ::= oneOfTripleExpr 69 | 70 | # oneOfTripleExpr and multiElementOneOf both start with groupTripleExpr 71 | #[37] oneOfTripleExpr ::= groupTripleExpr | multiElementOneOf 72 | #[38] multiElementOneOf ::= groupTripleExpr ('|' groupTripleExpr)+ 73 | #[39] innerTripleExpr ::= multiElementGroup | multiElementOneOf 74 | [37] oneOfTripleExpr ::= groupTripleExpr ('|' groupTripleExpr)* 75 | 76 | # singleElementGroup and multiElementGroup both start with unaryTripleExpr 77 | #[40] groupTripleExpr ::= singleElementGroup | multiElementGroup 78 | #[41] singleElementGroup ::= unaryTripleExpr ';'? 79 | #[42] multiElementGroup ::= unaryTripleExpr (';' unaryTripleExpr)+ ';'? 80 | [40] groupTripleExpr ::= unaryTripleExpr (';' unaryTripleExpr?)* 81 | 82 | [43] unaryTripleExpr ::= ('$' tripleExprLabel)? (tripleConstraint | bracketedTripleExpr) | include 83 | 84 | # Use oneOfTripleExpr instead of innerTripleExpr 85 | [44] bracketedTripleExpr ::= '(' tripleExpression ')' cardinality? annotation* semanticActions 86 | 87 | [45] tripleConstraint ::= senseFlags? predicate inlineShapeExpression cardinality? annotation* semanticActions 88 | [46] cardinality ::= '*' | '+' | '?' | REPEAT_RANGE 89 | [47] senseFlags ::= '^' 90 | 91 | [48] valueSet ::= '[' valueSetValue* ']' 92 | 93 | [49] valueSetValue ::= iriRange | literalRange | languageRange | '.' exclusion+ 94 | 95 | [50] exclusion ::= '-' (iri | literal | LANGTAG) '~'? 96 | [51] iriRange ::= iri ('~' iriExclusion*)? 97 | [52] iriExclusion ::= '-' iri '~'? 98 | [53] literalRange ::= literal ('~' literalExclusion*)? 99 | [54] literalExclusion ::= '-' literal '~'? 100 | [55] languageRange ::= LANGTAG ('~' languageExclusion*)? 101 | | '@' '~' languageExclusion* 102 | [56] languageExclusion ::= '-' LANGTAG '~'? 103 | 104 | [57] include ::= '&' tripleExprLabel 105 | 106 | [58] annotation ::= '//' predicate (iri | literal) 107 | [59] semanticActions ::= codeDecl* 108 | [60] codeDecl ::= '%' iri (CODE | "%") 109 | 110 | [13t] literal ::= rdfLiteral | numericLiteral | booleanLiteral 111 | [61] predicate ::= iri | RDF_TYPE 112 | [62] datatype ::= iri 113 | [63] shapeExprLabel ::= iri | blankNode 114 | [64] tripleExprLabel ::= iri | blankNode 115 | 116 | [16t] numericLiteral ::= DOUBLE | DECIMAL | INTEGER 117 | [65] rdfLiteral ::= langString | string ('^^' datatype)? 118 | [134s] booleanLiteral ::= "true" | "false" 119 | [135s] string ::= STRING_LITERAL_LONG1 | STRING_LITERAL_LONG2 120 | | STRING_LITERAL1 | STRING_LITERAL2 121 | [66] langString ::= LANG_STRING_LITERAL1 | LANG_STRING_LITERAL_LONG1 122 | | LANG_STRING_LITERAL2 | LANG_STRING_LITERAL_LONG2 123 | [136s] iri ::= IRIREF | prefixedName 124 | [137s] prefixedName ::= PNAME_LN | PNAME_NS 125 | [138s] blankNode ::= BLANK_NODE_LABEL 126 | 127 | @terminals 128 | 129 | [67] CODE ::= '{' ([^%\\] | '\\' [%\\] | UCHAR)* '%' '}' 130 | [68] REPEAT_RANGE ::= '{' INTEGER (',' (INTEGER | '*')?)? '}' 131 | [69] RDF_TYPE ::= 'a' 132 | [18t] IRIREF ::= '<' ([^#x00-#x20<>\"{}|^`\\] | UCHAR)* '>' /* #x00=NULL #01-#x1F=control codes #x20=space */ 133 | [140s] PNAME_NS ::= PN_PREFIX? ':' 134 | [141s] PNAME_LN ::= PNAME_NS PN_LOCAL 135 | [70] ATPNAME_NS ::= '@' PN_PREFIX? ':' 136 | [71] ATPNAME_LN ::= '@' PNAME_NS PN_LOCAL 137 | [72] REGEXP ::= '/' ([^/\\\n\r] | '\\' [nrt\\|.?*+(){}$-\[\]^/] | UCHAR)+ '/' [smix]* 138 | 139 | [142s] BLANK_NODE_LABEL ::= '_:' (PN_CHARS_U | [0-9]) ((PN_CHARS | '.')* PN_CHARS)? 140 | [145s] LANGTAG ::= '@' [a-zA-Z]+ ('-' [a-zA-Z0-9]+)* 141 | [19t] INTEGER ::= [+-]? [0-9]+ 142 | [20t] DECIMAL ::= [+-]? [0-9]* '.' [0-9]+ 143 | [21t] DOUBLE ::= [+-]? ([0-9]+ '.' [0-9]* EXPONENT | '.'? [0-9]+ EXPONENT) 144 | [155s] EXPONENT ::= [eE] [+-]? [0-9]+ 145 | [156s] STRING_LITERAL1 ::= "'" ([^#x27#x5C#xA#xD] | ECHAR | UCHAR)* "'" /* #x27=' #x5C=\ #xA=new line #xD=carriage return */ 146 | [157s] STRING_LITERAL2 ::= '"' ([^#x22#x5C#xA#xD] | ECHAR | UCHAR)* '"' /* #x22=" #x5C=\ #xA=new line #xD=carriage return */ 147 | [158s] STRING_LITERAL_LONG1 ::= "'''" (("'" | "''")? ([^\'\\] | ECHAR | UCHAR))* "'''" 148 | [159s] STRING_LITERAL_LONG2 ::= '"""' (('"' | '""')? ([^\"\\] | ECHAR | UCHAR))* '"""' 149 | [73] LANG_STRING_LITERAL1 ::= "'" ([^#x27#x5C#xA#xD] | ECHAR | UCHAR)* "'" LANGTAG 150 | [74] LANG_STRING_LITERAL2 ::= '"' ([^#x22#x5C#xA#xD] | ECHAR | UCHAR)* '"' LANGTAG 151 | [75] LANG_STRING_LITERAL_LONG1 ::= "'''" (("'" | "''")? ([^\'\\] | ECHAR | UCHAR))* "'''" LANGTAG 152 | [76] LANG_STRING_LITERAL_LONG2 ::= '"""' (('"' | '""')? ([^\"\\] | ECHAR | UCHAR))* '"""' LANGTAG 153 | [26t] UCHAR ::= '\\u' HEX HEX HEX HEX 154 | | '\\U' HEX HEX HEX HEX HEX HEX HEX HEX 155 | [160s] ECHAR ::= '\\' [tbnrf\\\"\'] 156 | [164s] PN_CHARS_BASE ::= [A-Z] | [a-z] 157 | | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x02FF] 158 | | [#x0370-#x037D] | [#x037F-#x1FFF] 159 | | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] 160 | | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] 161 | | [#x10000-#xEFFFF] 162 | [165s] PN_CHARS_U ::= PN_CHARS_BASE | '_' 163 | [167s] PN_CHARS ::= PN_CHARS_U | '-' | [0-9] 164 | | [#x00B7] | [#x0300-#x036F] | [#x203F-#x2040] 165 | [168s] PN_PREFIX ::= PN_CHARS_BASE ((PN_CHARS | '.')* PN_CHARS)? 166 | [169s] PN_LOCAL ::= (PN_CHARS_U | ':' | [0-9] | PLX) ((PN_CHARS | '.' | ':' | PLX)* (PN_CHARS | ':' | PLX))? 167 | [170s] PLX ::= PERCENT | PN_LOCAL_ESC 168 | [171s] PERCENT ::= '%' HEX HEX 169 | [172s] HEX ::= [0-9] | [A-F] | [a-f] 170 | [173s] PN_LOCAL_ESC ::= '\\' ('_' | '~' | '.' | '-' | '!' | '$' | '&' | "'" | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '/' | '?' | '#' | '@' | '%') 171 | 172 | @pass [ \t\r\n]+ | "#" [^\r\n]* | "/*" ([^*] | '*' ([^/] | '\\/'))* "*/" 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShEx: Shape Expression language for Ruby 2 | 3 | This is a pure-Ruby library for working with the [Shape Expressions Language][ShExSpec] to validate the shape of [RDF][] graphs. 4 | 5 | [![Gem Version](https://badge.fury.io/rb/shex.svg)](https://badge.fury.io/rb/shex) 6 | [![Build Status](https://github.com/ruby-rdf/shex/workflows/CI/badge.svg?branch=develop)](https://github.com/ruby-rdf/shex/actions?query=workflow%3ACI) 7 | [![Coverage Status](https://coveralls.io/repos/ruby-rdf/shex/badge.svg?branch=develop)](https://coveralls.io/github/ruby-rdf/shex?branch=develop) 8 | [![Gitter chat](https://badges.gitter.im/ruby-rdf/rdf.png)](https://gitter.im/ruby-rdf/rdf) 9 | [![DOI](https://zenodo.org/badge/74419330.svg)](https://zenodo.org/badge/latestdoi/74419330) 10 | 11 | ## Features 12 | 13 | * 100% pure Ruby with minimal dependencies and no bloat. 14 | * Fully compatible with [ShEx][ShExSpec] specifications. 15 | * 100% free and unencumbered [public domain](https://unlicense.org/) software. 16 | 17 | ## Description 18 | 19 | The ShEx gem implements a [ShEx][ShExSpec] Shape Expression engine version 2.0. 20 | 21 | * `ShEx::Parser` parses ShExC and ShExJ formatted documents generating executable operators which can be serialized as [S-Expressions][]. 22 | * `ShEx::Algebra` executes operators against Any `RDF::Graph`, including compliant [RDF.rb][]. 23 | * [Implementation Report](file.earl.html) 24 | 25 | ## Examples 26 | ### Validating a node using ShExC 27 | 28 | require 'rdf/turtle' 29 | require 'shex' 30 | 31 | shexc = %( 32 | PREFIX doap: 33 | PREFIX dc: 34 | PREFIX ex: 35 | 36 | ex:TestShape EXTRA a { 37 | a [doap:Project]; 38 | ( doap:name Literal; 39 | doap:description Literal 40 | | dc:title Literal; 41 | dc:description Literal)+; 42 | doap:category IRI*; 43 | doap:developer IRI+; 44 | doap:implements [] 45 | } 46 | ) 47 | graph = RDF::Graph.load("etc/doap.ttl") 48 | schema = ShEx.parse(shexc) 49 | map = { 50 | RDF::URI("https://rubygems.org/gems/shex") => RDF::URI("http://example.com/TestShape") 51 | } 52 | schema.satisfies?(graph, map) 53 | # => true 54 | ### Validating a node using ShExJ 55 | 56 | require 'rubygems' 57 | require 'rdf/turtle' 58 | require 'shex' 59 | 60 | shexj = %({ 61 | "@context": "http://www.w3.org/ns/shex.jsonld", 62 | "type": "Schema", 63 | "shapes": [{ 64 | "id": "http://example.com/TestShape", 65 | "type": "Shape", 66 | "extra": ["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"], 67 | "expression": { 68 | "type": "EachOf", 69 | "expressions": [{ 70 | "type": "TripleConstraint", 71 | "predicate": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", 72 | "valueExpr": { 73 | "type": "NodeConstraint", 74 | "values": ["http://usefulinc.com/ns/doap#Project"] 75 | } 76 | }, { 77 | "type": "OneOf", 78 | "expressions": [{ 79 | "type": "EachOf", 80 | "expressions": [{ 81 | "type": "TripleConstraint", 82 | "predicate": "http://usefulinc.com/ns/doap#name", 83 | "valueExpr": { 84 | "type": "NodeConstraint", 85 | "nodeKind": "literal" 86 | } 87 | }, { 88 | "type": "TripleConstraint", 89 | "predicate": "http://usefulinc.com/ns/doap#description", 90 | "valueExpr": { 91 | "type": "NodeConstraint", 92 | "nodeKind": "literal" 93 | } 94 | }] 95 | }, { 96 | "type": "EachOf", 97 | "expressions": [{ 98 | "type": "TripleConstraint", 99 | "predicate": "http://purl.org/dc/terms/title", 100 | "valueExpr": { 101 | "type": "NodeConstraint", 102 | "nodeKind": "literal" 103 | } 104 | }, { 105 | "type": "TripleConstraint", 106 | "predicate": "http://purl.org/dc/terms/description", 107 | "valueExpr": { 108 | "type": "NodeConstraint", 109 | "nodeKind": "literal" 110 | } 111 | }] 112 | }], 113 | "min": 1, 114 | "max": -1 115 | }, { 116 | "type": "TripleConstraint", 117 | "predicate": "http://usefulinc.com/ns/doap#category", 118 | "valueExpr": { 119 | "type": "NodeConstraint", 120 | "nodeKind": "iri" 121 | }, 122 | "min": 0, 123 | "max": -1 124 | }, { 125 | "type": "TripleConstraint", 126 | "predicate": "http://usefulinc.com/ns/doap#developer", 127 | "valueExpr": { 128 | "type": "NodeConstraint", 129 | "nodeKind": "iri" 130 | }, 131 | "min": 1, 132 | "max": -1 133 | }, { 134 | "type": "TripleConstraint", 135 | "predicate": "http://usefulinc.com/ns/doap#implements", 136 | "valueExpr": { 137 | "type": "NodeConstraint", 138 | "values": [ 139 | "http://shex.io/shex-semantics/" 140 | ] 141 | } 142 | } 143 | ] 144 | } 145 | } 146 | ]}) 147 | graph = RDF::Graph.load("etc/doap.ttl") 148 | schema = ShEx.parse(shexj, format: :shexj) 149 | map = { 150 | RDF::URI("https://rubygems.org/gems/shex") => RDF::URI("http://example.com/TestShape") 151 | } 152 | schema.satisfies?(graph, map) 153 | # => true 154 | 155 | ## Extensions 156 | ShEx has an extension mechanism using [Semantic Actions](http://shex.io/shex-semantics/#semantic-actions). Extensions may be implemented in Ruby ShEx by sub-classing {ShEx::Extension} and implementing {ShEx::Extension#visit} and possibly {ShEx::Extension#initialize}, {ShEx::Extension#enter}, {ShEx::Extension#exit}, and {ShEx::Extension#close}. The `#visit` method will be called as part of the `#satisfies?` operation. 157 | 158 | require 'shex' 159 | class ShEx::Test < ShEx::Extension("http://shex.io/extensions/Test/") 160 | # (see ShEx::Extension#initialize) 161 | def initialize(schema: nil, logger: nil, depth: 0, **options) 162 | ... 163 | end 164 | 165 | # (see ShEx::Extension#visit) 166 | def visit(code: nil, matched: nil, expression: nil, depth: 0, **options) 167 | ... 168 | end 169 | end 170 | 171 | The `#enter` method will be called on any {ShEx::Algebra::TripleExpression} that includes a {ShEx::Algebra::SemAct} referencing the extension, while the `#exit` method will be called on exit, even if not satisfied. 172 | 173 | The `#initialize` method is called when {ShEx::Algebra::Schema#execute} starts and `#close` called on exit, even if not satisfied. 174 | 175 | To make sure your extension is found, make sure to require it before the shape is executed. 176 | 177 | ## Command Line 178 | When the `linkeddata` gem is installed, RDF.rb includes a `rdf` executable which acts as a wrapper to perform a number of different 179 | operations on RDF files, including ShEx. The commands specific to ShEx is 180 | 181 | * `shex`: Validate repository given shape 182 | 183 | Using this command requires either a `shex-input` where the ShEx schema is URI encoded, or `shex`, which references a URI or file path to the schema. Other required options are `shape` and `focus`. 184 | 185 | Example usage: 186 | 187 | rdf shex https://raw.githubusercontent.com/ruby-rdf/shex/develop/etc/doap.ttl \ 188 | --schema https://raw.githubusercontent.com/ruby-rdf/shex/develop/etc/doap.shex \ 189 | --focus https://rubygems.org/gems/shex 190 | 191 | ## Documentation 192 | 193 | 194 | 195 | 196 | ## Implementation Notes 197 | The ShExC parser uses the [EBNF][] gem to generate a [PEG][] parser. 198 | 199 | The parser uses the executable [S-Expressions][] generated from the EBNF ShExC grammar to create a set of executable {ShEx::Algebra} Operators which are directly executed to perform shape validation. 200 | 201 | ## Change Log 202 | 203 | See [Release Notes on GitHub](https://github.com/ruby-rdf/shex/releases) 204 | 205 | ## Dependencies 206 | 207 | * [Ruby](https://ruby-lang.org/) (>= 3.0) 208 | * [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.3) 209 | * [SPARQL gem](https://rubygems.org/gems/sparql) (~> 3.3) 210 | 211 | ## Installation 212 | 213 | The recommended installation method is via [RubyGems](https://rubygems.org/). 214 | To install the latest official release of RDF.rb, do: 215 | 216 | % [sudo] gem install shex 217 | 218 | ## Download 219 | 220 | To get a local working copy of the development repository, do: 221 | 222 | % git clone git://github.com/ruby-rdf/shex.git 223 | 224 | Alternatively, download the latest development version as a tarball as 225 | follows: 226 | 227 | % wget https://github.com/ruby-rdf/shex/tarball/master 228 | 229 | ## Resources 230 | 231 | * 232 | * 233 | * 234 | 235 | ## Mailing List 236 | 237 | * 238 | 239 | ## Author 240 | 241 | * [Gregg Kellogg](https://github.com/gkellogg) - 242 | 243 | ## Contributing 244 | 245 | This repository uses [Git Flow](https://github.com/nvie/gitflow) to mange development and release activity. All submissions _must_ be on a feature branch based on the _develop_ branch to ease staging and integration. 246 | 247 | * Do your best to adhere to the existing coding conventions and idioms. 248 | * Don't use hard tabs, and don't leave trailing whitespace on any line. 249 | Before committing, run `git diff --check` to make sure of this. 250 | * Do document every method you add using [YARD][] annotations. Read the 251 | [tutorial][YARD-GS] or just look at the existing code for examples. 252 | * Don't touch the `.gemspec` or `VERSION` files. If you need to change them, 253 | do so on your private branch only. 254 | * Do feel free to add yourself to the `CREDITS` file and the 255 | corresponding list in the the `README`. Alphabetical order applies. 256 | * Don't touch the `AUTHORS` file. If your contributions are significant 257 | enough, be assured we will eventually add you in there. 258 | * Do note that in order for us to merge any non-trivial changes (as a rule 259 | of thumb, additions larger than about 15 lines of code), we need an 260 | explicit [public domain dedication][PDD] on record from you, 261 | which you will be asked to agree to on the first commit to a repo within the organization. 262 | Note that the agreement applies to all repos in the [Ruby RDF](https://github.com/ruby-rdf/) organization. 263 | 264 | ## License 265 | 266 | This is free and unencumbered public domain software. For more information, 267 | see or the accompanying {file:LICENSE} file. 268 | 269 | [ShExSpec]: http://shex.io/shex-semantics-20170713/ 270 | [RDF]: https://www.w3.org/RDF/ 271 | [RDF.rb]: https://ruby-rdf.github.io/rdf 272 | [EBNF]: https://rubygems.org/gems/ebnf 273 | [YARD]: https://yardoc.org/ 274 | [YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md 275 | [PDD]: https://unlicense.org/#unlicensing-contributions 276 | [PEG]: https://en.wikipedia.org/wiki/Parsing_expression_grammar "Parsing Expression Grammar" 277 | [S-Expression]: https://en.wikipedia.org/wiki/S-expression 278 | -------------------------------------------------------------------------------- /etc/shex.peg.sxp: -------------------------------------------------------------------------------- 1 | ( 2 | (rule shexDoc "1" (seq _shexDoc_1 _shexDoc_2)) 3 | (rule _shexDoc_1 "1.1" (star directive)) 4 | (rule _shexDoc_2 "1.2" (opt _shexDoc_3)) 5 | (rule _shexDoc_3 "1.3" (seq _shexDoc_4 _shexDoc_5)) 6 | (rule _shexDoc_4 "1.4" (alt notStartAction startActions)) 7 | (rule _shexDoc_5 "1.5" (star statement)) 8 | (rule directive "2" (alt baseDecl prefixDecl importDecl)) 9 | (rule baseDecl "3" (seq "BASE" IRIREF)) 10 | (rule prefixDecl "4" (seq "PREFIX" PNAME_NS IRIREF)) 11 | (rule importDecl "4.5" (seq "IMPORT" IRIREF)) 12 | (rule notStartAction "5" (alt start shapeExprDecl)) 13 | (rule start "6" (seq "START" "=" inlineShapeExpression)) 14 | (rule startActions "7" (plus codeDecl)) 15 | (rule statement "8" (alt directive notStartAction)) 16 | (rule shapeExprDecl "9" (seq shapeExprLabel _shapeExprDecl_1)) 17 | (rule _shapeExprDecl_1 "9.1" (alt shapeExpression "EXTERNAL")) 18 | (rule shapeExpression "10" (seq shapeOr)) 19 | (rule inlineShapeExpression "11" (seq inlineShapeOr)) 20 | (rule shapeOr "12" (seq shapeAnd _shapeOr_1)) 21 | (rule _shapeOr_1 "12.1" (star _shapeOr_2)) 22 | (rule _shapeOr_2 "12.2" (seq "OR" shapeAnd)) 23 | (rule inlineShapeOr "13" (seq inlineShapeAnd _inlineShapeOr_1)) 24 | (rule _inlineShapeOr_1 "13.1" (star _inlineShapeOr_2)) 25 | (rule _inlineShapeOr_2 "13.2" (seq "OR" inlineShapeAnd)) 26 | (rule shapeAnd "14" (seq shapeNot _shapeAnd_1)) 27 | (rule _shapeAnd_1 "14.1" (star _shapeAnd_2)) 28 | (rule _shapeAnd_2 "14.2" (seq "AND" shapeNot)) 29 | (rule inlineShapeAnd "15" (seq inlineShapeNot _inlineShapeAnd_1)) 30 | (rule _inlineShapeAnd_1 "15.1" (star _inlineShapeAnd_2)) 31 | (rule _inlineShapeAnd_2 "15.2" (seq "AND" inlineShapeNot)) 32 | (rule shapeNot "16" (seq _shapeNot_1 shapeAtom)) 33 | (rule _shapeNot_1 "16.1" (opt "NOT")) 34 | (rule inlineShapeNot "17" (seq _inlineShapeNot_1 inlineShapeAtom)) 35 | (rule _inlineShapeNot_1 "17.1" (opt "NOT")) 36 | (rule shapeAtom "18" 37 | (alt _shapeAtom_1 litNodeConstraint _shapeAtom_2 _shapeAtom_3 ".")) 38 | (rule _shapeAtom_1 "18.1" (seq nonLitNodeConstraint _shapeAtom_4)) 39 | (rule _shapeAtom_4 "18.4" (opt shapeOrRef)) 40 | (rule _shapeAtom_2 "18.2" (seq shapeOrRef _shapeAtom_5)) 41 | (rule _shapeAtom_5 "18.5" (opt nonLitNodeConstraint)) 42 | (rule _shapeAtom_3 "18.3" (seq "(" shapeExpression ")")) 43 | (rule shapeAtomNoRef "19" 44 | (alt _shapeAtomNoRef_1 litNodeConstraint _shapeAtomNoRef_2 _shapeAtomNoRef_3 ".")) 45 | (rule _shapeAtomNoRef_1 "19.1" (seq nonLitNodeConstraint _shapeAtomNoRef_4)) 46 | (rule _shapeAtomNoRef_4 "19.4" (opt shapeOrRef)) 47 | (rule _shapeAtomNoRef_2 "19.2" (seq shapeDefinition _shapeAtomNoRef_5)) 48 | (rule _shapeAtomNoRef_5 "19.5" (opt nonLitNodeConstraint)) 49 | (rule _shapeAtomNoRef_3 "19.3" (seq "(" shapeExpression ")")) 50 | (rule inlineShapeAtom "20" 51 | (alt _inlineShapeAtom_1 litNodeConstraint _inlineShapeAtom_2 52 | _inlineShapeAtom_3 "." )) 53 | (rule _inlineShapeAtom_1 "20.1" (seq nonLitNodeConstraint _inlineShapeAtom_4)) 54 | (rule _inlineShapeAtom_4 "20.4" (opt inlineShapeOrRef)) 55 | (rule _inlineShapeAtom_2 "20.2" (seq inlineShapeOrRef _inlineShapeAtom_5)) 56 | (rule _inlineShapeAtom_5 "20.5" (opt nonLitNodeConstraint)) 57 | (rule _inlineShapeAtom_3 "20.3" (seq "(" shapeExpression ")")) 58 | (rule shapeOrRef "21" (alt shapeDefinition shapeRef)) 59 | (rule inlineShapeOrRef "22" (alt inlineShapeDefinition shapeRef)) 60 | (rule shapeRef "23" (alt ATPNAME_LN ATPNAME_NS _shapeRef_1)) 61 | (rule _shapeRef_1 "23.1" (seq "@" shapeExprLabel)) 62 | (rule litNodeConstraint "24" 63 | (alt _litNodeConstraint_1 _litNodeConstraint_2 _litNodeConstraint_3 64 | _litNodeConstraint_4 )) 65 | (rule _litNodeConstraint_1 "24.1" (seq "LITERAL" _litNodeConstraint_5)) 66 | (rule _litNodeConstraint_5 "24.5" (star xsFacet)) 67 | (rule _litNodeConstraint_2 "24.2" (seq datatype _litNodeConstraint_6)) 68 | (rule _litNodeConstraint_6 "24.6" (star xsFacet)) 69 | (rule _litNodeConstraint_3 "24.3" (seq valueSet _litNodeConstraint_7)) 70 | (rule _litNodeConstraint_7 "24.7" (star xsFacet)) 71 | (rule _litNodeConstraint_4 "24.4" (plus numericFacet)) 72 | (rule nonLitNodeConstraint "25" 73 | (alt _nonLitNodeConstraint_1 _nonLitNodeConstraint_2)) 74 | (rule _nonLitNodeConstraint_1 "25.1" (seq nonLiteralKind _nonLitNodeConstraint_3)) 75 | (rule _nonLitNodeConstraint_3 "25.3" (star stringFacet)) 76 | (rule _nonLitNodeConstraint_2 "25.2" (plus stringFacet)) 77 | (rule nonLiteralKind "26" (alt "IRI" "BNODE" "NONLITERAL")) 78 | (rule xsFacet "27" (alt stringFacet numericFacet)) 79 | (rule stringFacet "28" (alt _stringFacet_1 REGEXP)) 80 | (rule _stringFacet_1 "28.1" (seq stringLength INTEGER)) 81 | (rule stringLength "29" (alt "LENGTH" "MINLENGTH" "MAXLENGTH")) 82 | (rule numericFacet "30" (alt _numericFacet_1 _numericFacet_2)) 83 | (rule _numericFacet_1 "30.1" (seq numericRange numericLiteral)) 84 | (rule _numericFacet_2 "30.2" (seq numericLength INTEGER)) 85 | (rule numericRange "31" 86 | (alt "MININCLUSIVE" "MINEXCLUSIVE" "MAXINCLUSIVE" "MAXEXCLUSIVE")) 87 | (rule numericLength "32" (alt "TOTALDIGITS" "FRACTIONDIGITS")) 88 | (rule shapeDefinition "33" 89 | (seq _shapeDefinition_1 "{" _shapeDefinition_2 "}" _shapeDefinition_3 90 | semanticActions )) 91 | (rule _shapeDefinition_1 "33.1" (star _shapeDefinition_4)) 92 | (rule _shapeDefinition_4 "33.4" (alt extraPropertySet "CLOSED")) 93 | (rule _shapeDefinition_2 "33.2" (opt tripleExpression)) 94 | (rule _shapeDefinition_3 "33.3" (star annotation)) 95 | (rule inlineShapeDefinition "34" 96 | (seq _inlineShapeDefinition_1 "{" _inlineShapeDefinition_2 "}")) 97 | (rule _inlineShapeDefinition_1 "34.1" (star _inlineShapeDefinition_3)) 98 | (rule _inlineShapeDefinition_3 "34.3" (alt extraPropertySet "CLOSED")) 99 | (rule _inlineShapeDefinition_2 "34.2" (opt tripleExpression)) 100 | (rule extraPropertySet "35" (seq "EXTRA" _extraPropertySet_1)) 101 | (rule _extraPropertySet_1 "35.1" (plus predicate)) 102 | (rule tripleExpression "36" (seq oneOfTripleExpr)) 103 | (rule oneOfTripleExpr "37" (seq groupTripleExpr _oneOfTripleExpr_1)) 104 | (rule _oneOfTripleExpr_1 "37.1" (star _oneOfTripleExpr_2)) 105 | (rule _oneOfTripleExpr_2 "37.2" (seq "|" groupTripleExpr)) 106 | (rule groupTripleExpr "40" (seq unaryTripleExpr _groupTripleExpr_1)) 107 | (rule _groupTripleExpr_1 "40.1" (star _groupTripleExpr_2)) 108 | (rule _groupTripleExpr_2 "40.2" (seq ";" _groupTripleExpr_3)) 109 | (rule _groupTripleExpr_3 "40.3" (opt unaryTripleExpr)) 110 | (rule unaryTripleExpr "43" (alt _unaryTripleExpr_1 include)) 111 | (rule _unaryTripleExpr_1 "43.1" (seq _unaryTripleExpr_2 _unaryTripleExpr_3)) 112 | (rule _unaryTripleExpr_2 "43.2" (opt _unaryTripleExpr_4)) 113 | (rule _unaryTripleExpr_4 "43.4" (seq "$" tripleExprLabel)) 114 | (rule _unaryTripleExpr_3 "43.3" (alt tripleConstraint bracketedTripleExpr)) 115 | (rule bracketedTripleExpr "44" 116 | (seq "(" tripleExpression ")" _bracketedTripleExpr_1 _bracketedTripleExpr_2 117 | semanticActions )) 118 | (rule _bracketedTripleExpr_1 "44.1" (opt cardinality)) 119 | (rule _bracketedTripleExpr_2 "44.2" (star annotation)) 120 | (rule tripleConstraint "45" 121 | (seq _tripleConstraint_1 predicate inlineShapeExpression _tripleConstraint_2 122 | _tripleConstraint_3 semanticActions )) 123 | (rule _tripleConstraint_1 "45.1" (opt senseFlags)) 124 | (rule _tripleConstraint_2 "45.2" (opt cardinality)) 125 | (rule _tripleConstraint_3 "45.3" (star annotation)) 126 | (rule cardinality "46" (alt "*" "+" "?" REPEAT_RANGE)) 127 | (rule senseFlags "47" (seq "^")) 128 | (rule valueSet "48" (seq "[" _valueSet_1 "]")) 129 | (rule _valueSet_1 "48.1" (star valueSetValue)) 130 | (rule valueSetValue "49" (alt iriRange literalRange languageRange _valueSetValue_1)) 131 | (rule _valueSetValue_1 "49.1" (seq "." _valueSetValue_2)) 132 | (rule _valueSetValue_2 "49.2" (plus exclusion)) 133 | (rule exclusion "50" (seq "-" _exclusion_1 _exclusion_2)) 134 | (rule _exclusion_1 "50.1" (alt iri literal LANGTAG)) 135 | (rule _exclusion_2 "50.2" (opt "~")) 136 | (rule iriRange "51" (seq iri _iriRange_1)) 137 | (rule _iriRange_1 "51.1" (opt _iriRange_2)) 138 | (rule _iriRange_2 "51.2" (seq "~" _iriRange_3)) 139 | (rule _iriRange_3 "51.3" (star iriExclusion)) 140 | (rule iriExclusion "52" (seq "-" iri _iriExclusion_1)) 141 | (rule _iriExclusion_1 "52.1" (opt "~")) 142 | (rule literalRange "53" (seq literal _literalRange_1)) 143 | (rule _literalRange_1 "53.1" (opt _literalRange_2)) 144 | (rule _literalRange_2 "53.2" (seq "~" _literalRange_3)) 145 | (rule _literalRange_3 "53.3" (star literalExclusion)) 146 | (rule literalExclusion "54" (seq "-" literal _literalExclusion_1)) 147 | (rule _literalExclusion_1 "54.1" (opt "~")) 148 | (rule languageRange "55" (alt _languageRange_1 _languageRange_2)) 149 | (rule _languageRange_1 "55.1" (seq LANGTAG _languageRange_3)) 150 | (rule _languageRange_3 "55.3" (opt _languageRange_4)) 151 | (rule _languageRange_4 "55.4" (seq "~" _languageRange_5)) 152 | (rule _languageRange_5 "55.5" (star languageExclusion)) 153 | (rule _languageRange_2 "55.2" (seq "@" "~" _languageRange_6)) 154 | (rule _languageRange_6 "55.6" (star languageExclusion)) 155 | (rule languageExclusion "56" (seq "-" LANGTAG _languageExclusion_1)) 156 | (rule _languageExclusion_1 "56.1" (opt "~")) 157 | (rule include "57" (seq "&" tripleExprLabel)) 158 | (rule annotation "58" (seq "//" predicate _annotation_1)) 159 | (rule _annotation_1 "58.1" (alt iri literal)) 160 | (rule semanticActions "59" (star codeDecl)) 161 | (rule codeDecl "60" (seq "%" iri _codeDecl_1)) 162 | (rule _codeDecl_1 "60.1" (alt CODE "%")) 163 | (rule literal "13t" (alt rdfLiteral numericLiteral booleanLiteral)) 164 | (rule predicate "61" (alt iri RDF_TYPE)) 165 | (rule datatype "62" (seq iri)) 166 | (rule shapeExprLabel "63" (alt iri blankNode)) 167 | (rule tripleExprLabel "64" (alt iri blankNode)) 168 | (rule numericLiteral "16t" (alt DOUBLE DECIMAL INTEGER)) 169 | (rule rdfLiteral "65" (alt langString _rdfLiteral_1)) 170 | (rule _rdfLiteral_1 "65.1" (seq string _rdfLiteral_2)) 171 | (rule _rdfLiteral_2 "65.2" (opt _rdfLiteral_3)) 172 | (rule _rdfLiteral_3 "65.3" (seq "^^" datatype)) 173 | (rule booleanLiteral "134s" (alt "true" "false")) 174 | (rule string "135s" 175 | (alt STRING_LITERAL_LONG1 STRING_LITERAL_LONG2 STRING_LITERAL1 STRING_LITERAL2)) 176 | (rule langString "66" 177 | (alt LANG_STRING_LITERAL1 LANG_STRING_LITERAL_LONG1 LANG_STRING_LITERAL2 178 | LANG_STRING_LITERAL_LONG2 )) 179 | (rule iri "136s" (alt IRIREF prefixedName)) 180 | (rule prefixedName "137s" (alt PNAME_LN PNAME_NS)) 181 | (rule blankNode "138s" (seq BLANK_NODE_LABEL)) 182 | (terminals _terminals (seq)) 183 | (terminal CODE "67" (seq "{" _CODE_1)) 184 | (terminal _CODE_1 "67.1" (range "^%\\] | '\\'[%\\] | UCHAR)* '%''}'")) 185 | (terminal REPEAT_RANGE "68" (seq "{" INTEGER _REPEAT_RANGE_1 "}")) 186 | (terminal _REPEAT_RANGE_1 "68.1" (opt _REPEAT_RANGE_2)) 187 | (terminal _REPEAT_RANGE_2 "68.2" (seq "," _REPEAT_RANGE_3)) 188 | (terminal _REPEAT_RANGE_3 "68.3" (opt _REPEAT_RANGE_4)) 189 | (terminal _REPEAT_RANGE_4 "68.4" (alt INTEGER "*")) 190 | (terminal RDF_TYPE "69" (seq "a")) 191 | (terminal IRIREF "18t" (seq "<" _IRIREF_1)) 192 | (terminal _IRIREF_1 "18t.1" 193 | (range 194 | "^#x00-#x20<>\"{}|^`\\] | UCHAR)* '>' /* #x00=NULL #01-#x1F=control codes #x20=space */" 195 | )) 196 | (terminal PNAME_NS "140s" (seq _PNAME_NS_1 ":")) 197 | (terminal _PNAME_NS_1 "140s.1" (opt PN_PREFIX)) 198 | (terminal PNAME_LN "141s" (seq PNAME_NS PN_LOCAL)) 199 | (terminal ATPNAME_NS "70" (seq "@" _ATPNAME_NS_1 ":")) 200 | (terminal _ATPNAME_NS_1 "70.1" (opt PN_PREFIX)) 201 | (terminal ATPNAME_LN "71" (seq "@" PNAME_NS PN_LOCAL)) 202 | (terminal REGEXP "72" (seq "/" _REGEXP_1 "/" _REGEXP_2)) 203 | (terminal _REGEXP_1 "72.1" (plus _REGEXP_3)) 204 | (terminal _REGEXP_3 "72.3" (alt _REGEXP_4 _REGEXP_5 UCHAR)) 205 | (terminal _REGEXP_4 "72.4" (range "^/\\\n\r")) 206 | (terminal _REGEXP_5 "72.5" (seq "\\" _REGEXP_6)) 207 | (terminal _REGEXP_6 "72.6" (range "nrt\\|.?*+(){}$-[]^/")) 208 | (terminal _REGEXP_2 "72.2" (star _REGEXP_7)) 209 | (terminal _REGEXP_7 "72.7" (range "smix")) 210 | (terminal BLANK_NODE_LABEL "142s" 211 | (seq "_:" _BLANK_NODE_LABEL_1 _BLANK_NODE_LABEL_2)) 212 | (terminal _BLANK_NODE_LABEL_1 "142s.1" (alt PN_CHARS_U _BLANK_NODE_LABEL_3)) 213 | (terminal _BLANK_NODE_LABEL_3 "142s.3" (range "0-9")) 214 | (terminal _BLANK_NODE_LABEL_2 "142s.2" (opt _BLANK_NODE_LABEL_4)) 215 | (terminal _BLANK_NODE_LABEL_4 "142s.4" (seq _BLANK_NODE_LABEL_5 PN_CHARS)) 216 | (terminal _BLANK_NODE_LABEL_5 "142s.5" (star _BLANK_NODE_LABEL_6)) 217 | (terminal _BLANK_NODE_LABEL_6 "142s.6" (alt PN_CHARS ".")) 218 | (terminal LANGTAG "145s" (seq "@" _LANGTAG_1 _LANGTAG_2)) 219 | (terminal _LANGTAG_1 "145s.1" (plus _LANGTAG_3)) 220 | (terminal _LANGTAG_3 "145s.3" (range "a-zA-Z")) 221 | (terminal _LANGTAG_2 "145s.2" (star _LANGTAG_4)) 222 | (terminal _LANGTAG_4 "145s.4" (seq "-" _LANGTAG_5)) 223 | (terminal _LANGTAG_5 "145s.5" (plus _LANGTAG_6)) 224 | (terminal _LANGTAG_6 "145s.6" (range "a-zA-Z0-9")) 225 | (terminal INTEGER "19t" (seq _INTEGER_1 _INTEGER_2)) 226 | (terminal _INTEGER_1 "19t.1" (opt _INTEGER_3)) 227 | (terminal _INTEGER_3 "19t.3" (range "+-")) 228 | (terminal _INTEGER_2 "19t.2" (plus _INTEGER_4)) 229 | (terminal _INTEGER_4 "19t.4" (range "0-9")) 230 | (terminal DECIMAL "20t" (seq _DECIMAL_1 _DECIMAL_2 "." _DECIMAL_3)) 231 | (terminal _DECIMAL_1 "20t.1" (opt _DECIMAL_4)) 232 | (terminal _DECIMAL_4 "20t.4" (range "+-")) 233 | (terminal _DECIMAL_2 "20t.2" (star _DECIMAL_5)) 234 | (terminal _DECIMAL_5 "20t.5" (range "0-9")) 235 | (terminal _DECIMAL_3 "20t.3" (plus _DECIMAL_6)) 236 | (terminal _DECIMAL_6 "20t.6" (range "0-9")) 237 | (terminal DOUBLE "21t" (seq _DOUBLE_1 _DOUBLE_2)) 238 | (terminal _DOUBLE_1 "21t.1" (opt _DOUBLE_3)) 239 | (terminal _DOUBLE_3 "21t.3" (range "+-")) 240 | (terminal _DOUBLE_2 "21t.2" (alt _DOUBLE_4 _DOUBLE_5)) 241 | (terminal _DOUBLE_4 "21t.4" (seq _DOUBLE_6 "." _DOUBLE_7 EXPONENT)) 242 | (terminal _DOUBLE_6 "21t.6" (plus _DOUBLE_8)) 243 | (terminal _DOUBLE_8 "21t.8" (range "0-9")) 244 | (terminal _DOUBLE_7 "21t.7" (star _DOUBLE_9)) 245 | (terminal _DOUBLE_9 "21t.9" (range "0-9")) 246 | (terminal _DOUBLE_5 "21t.5" (seq _DOUBLE_10 _DOUBLE_11 EXPONENT)) 247 | (terminal _DOUBLE_10 "21t.10" (opt ".")) 248 | (terminal _DOUBLE_11 "21t.11" (plus _DOUBLE_12)) 249 | (terminal _DOUBLE_12 "21t.12" (range "0-9")) 250 | (terminal EXPONENT "155s" (seq _EXPONENT_1 _EXPONENT_2 _EXPONENT_3)) 251 | (terminal _EXPONENT_1 "155s.1" (range "eE")) 252 | (terminal _EXPONENT_2 "155s.2" (opt _EXPONENT_4)) 253 | (terminal _EXPONENT_4 "155s.4" (range "+-")) 254 | (terminal _EXPONENT_3 "155s.3" (plus _EXPONENT_5)) 255 | (terminal _EXPONENT_5 "155s.5" (range "0-9")) 256 | (terminal STRING_LITERAL1 "156s" (seq "'" _STRING_LITERAL1_1 "'")) 257 | (terminal _STRING_LITERAL1_1 "156s.1" (star _STRING_LITERAL1_2)) 258 | (terminal _STRING_LITERAL1_2 "156s.2" (alt _STRING_LITERAL1_3 ECHAR UCHAR)) 259 | (terminal _STRING_LITERAL1_3 "156s.3" (range "^#x27#x5C#xA#xD")) 260 | (terminal STRING_LITERAL2 "157s" (seq "\"" _STRING_LITERAL2_1 "\"")) 261 | (terminal _STRING_LITERAL2_1 "157s.1" (star _STRING_LITERAL2_2)) 262 | (terminal _STRING_LITERAL2_2 "157s.2" (alt _STRING_LITERAL2_3 ECHAR UCHAR)) 263 | (terminal _STRING_LITERAL2_3 "157s.3" (range "^#x22#x5C#xA#xD")) 264 | (terminal STRING_LITERAL_LONG1 "158s" (seq "'''" _STRING_LITERAL_LONG1_1)) 265 | (terminal _STRING_LITERAL_LONG1_1 "158s.1" 266 | (seq _STRING_LITERAL_LONG1_2 _STRING_LITERAL_LONG1_3)) 267 | (terminal _STRING_LITERAL_LONG1_2 "158s.2" (opt _STRING_LITERAL_LONG1_4)) 268 | (terminal _STRING_LITERAL_LONG1_4 "158s.4" (alt "'" "''")) 269 | (terminal _STRING_LITERAL_LONG1_3 "158s.3" 270 | (range "^'\\] | ECHAR | UCHAR))* \"'''\"")) 271 | (terminal STRING_LITERAL_LONG2 "159s" (seq "\"\"\"" _STRING_LITERAL_LONG2_1)) 272 | (terminal _STRING_LITERAL_LONG2_1 "159s.1" 273 | (seq _STRING_LITERAL_LONG2_2 _STRING_LITERAL_LONG2_3)) 274 | (terminal _STRING_LITERAL_LONG2_2 "159s.2" (opt _STRING_LITERAL_LONG2_4)) 275 | (terminal _STRING_LITERAL_LONG2_4 "159s.4" (alt "\"" "\"\"")) 276 | (terminal _STRING_LITERAL_LONG2_3 "159s.3" 277 | (range "^\"\\] | ECHAR | UCHAR))* '\"\"\"'")) 278 | (terminal LANG_STRING_LITERAL1 "73" (seq "'" _LANG_STRING_LITERAL1_1 "'" LANGTAG)) 279 | (terminal _LANG_STRING_LITERAL1_1 "73.1" (star _LANG_STRING_LITERAL1_2)) 280 | (terminal _LANG_STRING_LITERAL1_2 "73.2" (alt _LANG_STRING_LITERAL1_3 ECHAR UCHAR)) 281 | (terminal _LANG_STRING_LITERAL1_3 "73.3" (range "^#x27#x5C#xA#xD")) 282 | (terminal LANG_STRING_LITERAL2 "74" (seq "\"" _LANG_STRING_LITERAL2_1 "\"" LANGTAG)) 283 | (terminal _LANG_STRING_LITERAL2_1 "74.1" (star _LANG_STRING_LITERAL2_2)) 284 | (terminal _LANG_STRING_LITERAL2_2 "74.2" (alt _LANG_STRING_LITERAL2_3 ECHAR UCHAR)) 285 | (terminal _LANG_STRING_LITERAL2_3 "74.3" (range "^#x22#x5C#xA#xD")) 286 | (terminal LANG_STRING_LITERAL_LONG1 "75" (seq "'''" _LANG_STRING_LITERAL_LONG1_1)) 287 | (terminal _LANG_STRING_LITERAL_LONG1_1 "75.1" 288 | (seq _LANG_STRING_LITERAL_LONG1_2 _LANG_STRING_LITERAL_LONG1_3)) 289 | (terminal _LANG_STRING_LITERAL_LONG1_2 "75.2" (opt _LANG_STRING_LITERAL_LONG1_4)) 290 | (terminal _LANG_STRING_LITERAL_LONG1_4 "75.4" (alt "'" "''")) 291 | (terminal _LANG_STRING_LITERAL_LONG1_3 "75.3" 292 | (range "^'\\] | ECHAR | UCHAR))* \"'''\" LANGTAG")) 293 | (terminal LANG_STRING_LITERAL_LONG2 "76" 294 | (seq "\"\"\"" _LANG_STRING_LITERAL_LONG2_1)) 295 | (terminal _LANG_STRING_LITERAL_LONG2_1 "76.1" 296 | (seq _LANG_STRING_LITERAL_LONG2_2 _LANG_STRING_LITERAL_LONG2_3)) 297 | (terminal _LANG_STRING_LITERAL_LONG2_2 "76.2" (opt _LANG_STRING_LITERAL_LONG2_4)) 298 | (terminal _LANG_STRING_LITERAL_LONG2_4 "76.4" (alt "\"" "\"\"")) 299 | (terminal _LANG_STRING_LITERAL_LONG2_3 "76.3" 300 | (range "^\"\\] | ECHAR | UCHAR))* '\"\"\"' LANGTAG")) 301 | (terminal UCHAR "26t" (alt _UCHAR_1 _UCHAR_2)) 302 | (terminal _UCHAR_1 "26t.1" (seq "\\u" HEX HEX HEX HEX)) 303 | (terminal _UCHAR_2 "26t.2" (seq "\\U" HEX HEX HEX HEX HEX HEX HEX HEX)) 304 | (terminal ECHAR "160s" (seq "\\" _ECHAR_1)) 305 | (terminal _ECHAR_1 "160s.1" (range "tbnrf\\\"'")) 306 | (terminal PN_CHARS_BASE "164s" 307 | (alt _PN_CHARS_BASE_1 _PN_CHARS_BASE_2 _PN_CHARS_BASE_3 _PN_CHARS_BASE_4 308 | _PN_CHARS_BASE_5 _PN_CHARS_BASE_6 _PN_CHARS_BASE_7 _PN_CHARS_BASE_8 309 | _PN_CHARS_BASE_9 _PN_CHARS_BASE_10 _PN_CHARS_BASE_11 _PN_CHARS_BASE_12 310 | _PN_CHARS_BASE_13 _PN_CHARS_BASE_14 )) 311 | (terminal _PN_CHARS_BASE_1 "164s.1" (range "A-Z")) 312 | (terminal _PN_CHARS_BASE_2 "164s.2" (range "a-z")) 313 | (terminal _PN_CHARS_BASE_3 "164s.3" (range "#x00C0-#x00D6")) 314 | (terminal _PN_CHARS_BASE_4 "164s.4" (range "#x00D8-#x00F6")) 315 | (terminal _PN_CHARS_BASE_5 "164s.5" (range "#x00F8-#x02FF")) 316 | (terminal _PN_CHARS_BASE_6 "164s.6" (range "#x0370-#x037D")) 317 | (terminal _PN_CHARS_BASE_7 "164s.7" (range "#x037F-#x1FFF")) 318 | (terminal _PN_CHARS_BASE_8 "164s.8" (range "#x200C-#x200D")) 319 | (terminal _PN_CHARS_BASE_9 "164s.9" (range "#x2070-#x218F")) 320 | (terminal _PN_CHARS_BASE_10 "164s.10" (range "#x2C00-#x2FEF")) 321 | (terminal _PN_CHARS_BASE_11 "164s.11" (range "#x3001-#xD7FF")) 322 | (terminal _PN_CHARS_BASE_12 "164s.12" (range "#xF900-#xFDCF")) 323 | (terminal _PN_CHARS_BASE_13 "164s.13" (range "#xFDF0-#xFFFD")) 324 | (terminal _PN_CHARS_BASE_14 "164s.14" (range "#x10000-#xEFFFF")) 325 | (terminal PN_CHARS_U "165s" (alt PN_CHARS_BASE "_")) 326 | (terminal PN_CHARS "167s" 327 | (alt PN_CHARS_U "-" _PN_CHARS_1 _PN_CHARS_2 _PN_CHARS_3 _PN_CHARS_4)) 328 | (terminal _PN_CHARS_1 "167s.1" (range "0-9")) 329 | (terminal _PN_CHARS_2 "167s.2" (range "#x00B7")) 330 | (terminal _PN_CHARS_3 "167s.3" (range "#x0300-#x036F")) 331 | (terminal _PN_CHARS_4 "167s.4" (range "#x203F-#x2040")) 332 | (terminal PN_PREFIX "168s" (seq PN_CHARS_BASE _PN_PREFIX_1)) 333 | (terminal _PN_PREFIX_1 "168s.1" (opt _PN_PREFIX_2)) 334 | (terminal _PN_PREFIX_2 "168s.2" (seq _PN_PREFIX_3 PN_CHARS)) 335 | (terminal _PN_PREFIX_3 "168s.3" (star _PN_PREFIX_4)) 336 | (terminal _PN_PREFIX_4 "168s.4" (alt PN_CHARS ".")) 337 | (terminal PN_LOCAL "169s" (seq _PN_LOCAL_1 _PN_LOCAL_2)) 338 | (terminal _PN_LOCAL_1 "169s.1" (alt PN_CHARS_U ":" _PN_LOCAL_3 PLX)) 339 | (terminal _PN_LOCAL_3 "169s.3" (range "0-9")) 340 | (terminal _PN_LOCAL_2 "169s.2" (opt _PN_LOCAL_4)) 341 | (terminal _PN_LOCAL_4 "169s.4" (seq _PN_LOCAL_5 _PN_LOCAL_6)) 342 | (terminal _PN_LOCAL_5 "169s.5" (star _PN_LOCAL_7)) 343 | (terminal _PN_LOCAL_7 "169s.7" (alt PN_CHARS "." ":" PLX)) 344 | (terminal _PN_LOCAL_6 "169s.6" (alt PN_CHARS ":" PLX)) 345 | (terminal PLX "170s" (alt PERCENT PN_LOCAL_ESC)) 346 | (terminal PERCENT "171s" (seq "%" HEX HEX)) 347 | (terminal HEX "172s" (alt _HEX_1 _HEX_2 _HEX_3)) 348 | (terminal _HEX_1 "172s.1" (range "0-9")) 349 | (terminal _HEX_2 "172s.2" (range "A-F")) 350 | (terminal _HEX_3 "172s.3" (range "a-f")) 351 | (terminal PN_LOCAL_ESC "173s" (seq "\\" _PN_LOCAL_ESC_1)) 352 | (terminal _PN_LOCAL_ESC_1 "173s.1" 353 | (alt "_" "~" "." "-" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" "/" "?" "#" 354 | "@" "%" )) 355 | (pass _pass (alt __pass_1 __pass_2 __pass_3)) 356 | (rule __pass_1 (plus __pass_4)) 357 | (terminal __pass_4 (range " \t\r\n")) 358 | (rule __pass_2 (seq "#" __pass_5)) 359 | (rule __pass_5 (star __pass_6)) 360 | (terminal __pass_6 (range "^\r\n")) 361 | (rule __pass_3 (seq "/*" __pass_7 "*/")) 362 | (rule __pass_7 (star __pass_8)) 363 | (rule __pass_8 (alt __pass_9 __pass_10)) 364 | (terminal __pass_9 (range "^*")) 365 | (rule __pass_10 (seq "*" __pass_11)) 366 | (rule __pass_11 (alt __pass_12 "\\/")) 367 | (terminal __pass_12 (range "^/"))) 368 | --------------------------------------------------------------------------------