shex 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,19 +9,35 @@ module ShEx::Algebra
9
9
  super
10
10
  end
11
11
 
12
+ ##
13
+ # Creates an operator instance from a parsed ShExJ representation
14
+ # @param (see Operator#from_shexj)
15
+ # @return [Operator]
16
+ def self.from_shexj(operator, options = {})
17
+ raise ArgumentError unless operator.is_a?(Hash) && operator['type'] == "ShapeRef"
18
+ raise ArgumentError, "missing reference in #{operator.inspect}" unless operator.has_key?('reference')
19
+ super
20
+ end
21
+
12
22
  ##
13
23
  # Satisfies referenced shape.
14
24
  # @param (see Satisfiable#satisfies?)
15
25
  # @return (see Satisfiable#satisfies?)
16
26
  # @raise (see Satisfiable#satisfies?)
17
27
  # @see [https://shexspec.github.io/spec/#shape-expression-semantics]
18
- def satisfies?(focus)
19
- status "ref #{operands.first.to_s}"
20
- matched_shape = referenced_shape.satisfies?(focus)
21
- satisfy satisfied: matched_shape
28
+ def satisfies?(focus, depth: 0)
29
+ status "ref #{operands.first.to_s}", depth: depth
30
+ schema.enter_shape(operands.first, focus) do |shape|
31
+ if shape
32
+ matched_shape = shape.satisfies?(focus, depth: depth + 1)
33
+ satisfy focus: focus, satisfied: matched_shape, depth: depth
34
+ else
35
+ status "Satisfy as #{operands.first} was re-entered for #{focus}", depth: depth
36
+ satisfy focus: focus, satisfied: referenced_shape, depth: depth
37
+ end
38
+ end
22
39
  rescue ShEx::NotSatisfied => e
23
- not_satisfied e.message, unsatisfied: e.expression
24
- raise
40
+ not_satisfied e.message, focus: focus, unsatisfied: e.expression, depth: depth
25
41
  end
26
42
 
27
43
  ##
@@ -29,17 +45,27 @@ module ShEx::Algebra
29
45
  #
30
46
  # @return [Shape]
31
47
  def referenced_shape
32
- schema.shapes[operands.first.to_s]
48
+ @referenced_shape ||= schema.shapes.detect {|s| s.label == operands.first}
33
49
  end
34
50
 
35
51
  ##
36
- # A ShapeRef is valid if it's ancestor schema has any shape with a lable
52
+ # A ShapeRef is valid if it's ancestor schema has any shape with a label
37
53
  # the same as it's reference.
54
+ # A ref cannot reference itself (via whatever path) without going through a TripleConstraint.
55
+ # Even when going through TripleConstraints, there can't be a negative reference.
38
56
  def validate!
39
57
  structure_error("Missing referenced shape: #{operands.first}") if referenced_shape.nil?
40
- # FIXME
41
- #raise ShEx::ParseError, "Self referencing shape: #{operands.first}" if referenced_shape == first_ancestor(Shape)
58
+ raise ShEx::StructureError, "Self referencing shape: #{operands.first}" if referenced_shape == first_ancestor(Satisfiable)
42
59
  super
43
60
  end
61
+
62
+ ##
63
+ # Returns the binary S-Expression (SXP) representation of this operator.
64
+ #
65
+ # @return [Array]
66
+ # @see https://en.wikipedia.org/wiki/S-expression
67
+ def to_sxp_bin
68
+ ([:shapeRef, ([:label, @label] if @label)].compact + operands).to_sxp_bin
69
+ end
44
70
  end
45
71
  end
@@ -8,12 +8,12 @@ module ShEx::Algebra
8
8
  # @param (see Satisfiable#satisfies?)
9
9
  # @return (see Satisfiable#satisfies?)
10
10
  # @raise (see Satisfiable#satisfies?)
11
- def satisfies?(focus)
12
- status ""
13
- matched_op = operands.first.satisfies?(focus)
14
- satisfy satisfied: matched_op
11
+ def satisfies?(focus, depth: 0)
12
+ status "", depth: depth
13
+ matched_op = operands.first.satisfies?(focus, depth: depth + 1)
14
+ satisfy focus: focus, satisfied: matched_op, depth: depth
15
15
  rescue ShEx::NotSatisfied => e
16
- not_satisfied e.message, unsatisfied: e.expression
16
+ not_satisfied e.message, focus: focus, unsatisfied: e.expression, depth: depth
17
17
  raise
18
18
  end
19
19
  end
@@ -3,18 +3,33 @@ module ShEx::Algebra
3
3
  class Stem < Operator::Unary
4
4
  NAME = :stem
5
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'] == "Stem"
12
+ raise ArgumentError, "missing stem in #{operator.inspect}" unless operator.has_key?('stem')
13
+ super
14
+ end
15
+
6
16
  ##
7
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:
8
18
  #
9
19
  # * vsv is a Stem with stem st and nodeIn(n, st).
10
- def match?(value)
20
+ def match?(value, depth: 0)
11
21
  if value.start_with?(operands.first)
12
- status "matched #{value}"
22
+ status "matched #{value}", depth: depth
13
23
  true
14
24
  else
15
- status "not matched #{value}"
25
+ status "not matched #{value}", depth: depth
16
26
  false
17
27
  end
18
28
  end
29
+
30
+ def json_type
31
+ # FIXME: This is funky, due to oddities in normative shexj
32
+ parent.is_a?(Value) ? 'StemRange' : 'Stem'
33
+ end
19
34
  end
20
35
  end
@@ -3,12 +3,31 @@ module ShEx::Algebra
3
3
  class StemRange < Operator::Binary
4
4
  NAME = :stemRange
5
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'] == 'StemRange'
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
+ Stem.from_shexj(operator.merge('type' => 'Stem'), options)
22
+ end
23
+ end
24
+
6
25
  ##
7
26
  # 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
27
  #
9
28
  # * 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).
10
29
  # * vsv is a Wildcard with exclusions excls and there is no x in excls such that nodeIn(n, excl).
11
- def match?(value)
30
+ def match?(value, depth: 0)
12
31
  initial_match = case operands.first
13
32
  when :wildcard then true
14
33
  when RDF::Value then value.start_with?(operands.first)
@@ -16,22 +35,22 @@ module ShEx::Algebra
16
35
  end
17
36
 
18
37
  unless initial_match
19
- status "#{value} does not match #{operands.first}"
38
+ status "#{value} does not match #{operands.first}", depth: depth
20
39
  return false
21
40
  end
22
41
 
23
42
  if exclusions.any? do |exclusion|
24
43
  case exclusion
25
44
  when RDF::Value then value == exclusion
26
- when Stem then exclusion.match?(value)
45
+ when Stem then exclusion.match?(value, depth: depth + 1)
27
46
  else false
28
47
  end
29
48
  end
30
- status "#{value} excluded"
49
+ status "#{value} excluded", depth: depth
31
50
  return false
32
51
  end
33
52
 
34
- status "matched #{value}"
53
+ status "matched #{value}", depth: depth
35
54
  true
36
55
  end
37
56
 
@@ -4,25 +4,36 @@ module ShEx::Algebra
4
4
  include TripleExpression
5
5
  NAME = :tripleConstraint
6
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
+
7
17
  ##
8
18
  # In this case, we accept an array of statements, and match based on cardinality.
9
19
  #
10
20
  # @param (see TripleExpression#matches)
11
21
  # @return (see TripleExpression#matches)
12
22
  # @raise (see TripleExpression#matches)
13
- def matches(statements)
14
- status "predicate #{predicate}"
15
- results, satisfied, unsatisfied = [], [], []
23
+ def matches(arcs_in, arcs_out, depth: 0)
24
+ status "predicate #{predicate}", depth: depth
25
+ results, unmatched, satisfied, unsatisfied = [], [], [], []
16
26
  num_iters, max = 0, maximum
17
27
 
28
+ statements = inverse? ? arcs_in : arcs_out
18
29
  statements.select {|st| st.predicate == predicate}.each do |statement|
19
30
  break if num_iters == max # matched enough
20
31
 
21
32
  value = inverse? ? statement.subject : statement.object
22
33
 
23
34
  begin
24
- shape && (matched_shape = shape.satisfies?(value))
25
- status "matched #{statement.to_sxp}"
35
+ shape && (matched_shape = shape.satisfies?(value, depth: depth + 1))
36
+ status "matched #{statement.to_sxp}", depth: depth
26
37
  if matched_shape
27
38
  matched_shape.matched = [statement]
28
39
  statement = statement.dup.extend(ReferencedStatement)
@@ -32,9 +43,11 @@ module ShEx::Algebra
32
43
  results << statement
33
44
  num_iters += 1
34
45
  rescue ShEx::NotSatisfied => e
35
- status "not satisfied: #{e.message}"
46
+ status "not satisfied: #{e.message}", depth: depth
47
+ unsatisfied << e.expression
36
48
  statement = statement.dup.extend(ReferencedStatement)
37
- unmatched << statement.referenced = shape
49
+ statement.referenced = shape
50
+ unmatched << statement
38
51
  end
39
52
  end
40
53
 
@@ -44,22 +57,22 @@ module ShEx::Algebra
44
57
  end
45
58
 
46
59
  # Last, evaluate semantic acts
47
- semantic_actions.all? do |op|
48
- op.satisfies?(results)
60
+ semantic_actions.each do |op|
61
+ op.satisfies?(results, matched: results, depth: depth + 1)
49
62
  end unless results.empty?
50
63
 
51
- satisfy matched: results, satisfied: satisfied, unsatisfied: unsatisfied
64
+ satisfy matched: results, unmatched: unmatched,
65
+ satisfied: satisfied, unsatisfied: unsatisfied, depth: depth
52
66
  rescue ShEx::NotMatched, ShEx::NotSatisfied => e
53
67
  not_matched e.message,
54
- matched: results, unmatched: (statements - results),
55
- satisfied: satisfied, unsatisfied: unsatisfied
68
+ matched: results, unmatched: unmatched,
69
+ satisfied: satisfied, unsatisfied: unsatisfied, depth: depth
56
70
  end
57
71
 
58
72
  def predicate
59
73
  operands.detect {|o| o.is_a?(RDF::URI)}
60
74
  end
61
75
 
62
-
63
76
  ##
64
77
  # Included TripleConstraints
65
78
  # @return [Array<TripleConstraints>]
@@ -8,10 +8,11 @@ module ShEx::Algebra
8
8
  #
9
9
  # Behavior should be overridden in subclasses, which end by calling this through `super`.
10
10
  #
11
- # @param [Array<RDF::Statement>] statements
11
+ # @param [Array<RDF::Statement>] arcs_in
12
+ # @param [Array<RDF::Statement>] arcs_out
12
13
  # @return [TripleExpression] with `matched` accessor for matched triples
13
14
  # @raise [ShEx::NotMatched] with `expression` accessor to access `matched` and `unmatched` statements along with `satisfied` and `unsatisfied` operations.
14
- def matches(statements)
15
+ def matches(arcs_in, arcs_out, depth: 0)
15
16
  raise NotImplementedError, "#matches Not implemented in #{self.class}"
16
17
  end
17
18
 
@@ -10,17 +10,17 @@ module ShEx::Algebra
10
10
  # * vsv is a Stem with stem st and nodeIn(n, st).
11
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
12
  # * vsv is a Wildcard with exclusions excls and there is no x in excls such that nodeIn(n, excl).
13
- def match?(value)
14
- status ""
13
+ def match?(value, depth: 0)
14
+ status "", depth: depth
15
15
  if case expr = operands.first
16
16
  when RDF::Value then value.eql?(expr)
17
- when Stem, StemRange then expr.match?(value)
17
+ when Stem, StemRange then expr.match?(value, depth: depth + 1)
18
18
  else false
19
19
  end
20
- status "matched #{value}"
20
+ status "matched #{value}", depth: depth
21
21
  true
22
22
  else
23
- status "not matched #{value}"
23
+ status "not matched #{value}", depth: depth
24
24
  false
25
25
  end
26
26
  end
@@ -0,0 +1,160 @@
1
+ $:.unshift(File.expand_path("../..", __FILE__))
2
+ require 'sparql/algebra'
3
+
4
+ ##
5
+ # Abstract class of ShEx [Extension](https://shexspec.github.io/spec/#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 https://shexspec.github.io/spec/#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<RDF::Statement>] arcs_in available statements to be matched
112
+ # @param [Array<RDF::Statement>] 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<RDF::Statement>] matched statements matched by this expression
141
+ # @param [Array<RDF::Statement>] 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