shex 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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