shex 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,9 +6,22 @@ module ShEx::Algebra
6
6
 
7
7
  def initialize(*args, **options)
8
8
  case
9
- when args.length <= 1
10
- raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 1..)"
9
+ when args.length < 2
10
+ raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 2..)"
11
11
  end
12
+
13
+ # All arguments must be Satisfiable
14
+ raise ArgumentError, "All operands must be Shape operands" unless args.all? {|o| o.is_a?(Satisfiable)}
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')
12
25
  super
13
26
  end
14
27
 
@@ -17,27 +30,31 @@ module ShEx::Algebra
17
30
  # @param (see Satisfiable#satisfies?)
18
31
  # @return (see Satisfiable#satisfies?)
19
32
  # @raise (see Satisfiable#satisfies?)
20
- def satisfies?(focus)
21
- status ""
33
+ def satisfies?(focus, depth: 0)
34
+ status "", depth: depth
22
35
  expressions = operands.select {|o| o.is_a?(Satisfiable)}
23
36
  unsatisfied = []
24
37
  expressions.any? do |op|
25
38
  begin
26
- matched_op = op.satisfies?(focus)
27
- return satisfy satisfied: matched_op, unsatisfied: unsatisfied
39
+ matched_op = op.satisfies?(focus, depth: depth + 1)
40
+ return satisfy focus: focus, satisfied: matched_op, depth: depth
28
41
  rescue ShEx::NotSatisfied => e
29
- status "unsatisfied #{focus}"
42
+ status "unsatisfied #{focus}", depth: depth
30
43
  op = op.dup
31
44
  op.satisfied = e.expression.satisfied
32
45
  op.unsatisfied = e.expression.unsatisfied
33
46
  unsatisfied << op
34
- status("unsatisfied: #{e.message}")
47
+ status "unsatisfied: #{e.message}", depth: depth
35
48
  false
36
49
  end
37
50
  end
38
51
 
39
52
  not_satisfied "Expected some expression to be satisfied",
40
- unsatisfied: unsatisfied
53
+ focus: focus, unsatisfied: unsatisfied, depth: depth
54
+ end
55
+
56
+ def json_type
57
+ "ShapeOr"
41
58
  end
42
59
  end
43
60
  end
@@ -6,20 +6,16 @@ module ShEx::Algebra
6
6
  ##
7
7
  # Satisfies method
8
8
  # @param [RDF::Resource] focus
9
- # @return [TripleExpression] with `matched` and `satisfied` accessors for matched triples and sub-expressions
9
+ # @param [Integer] depth for logging
10
+ # @param [Hash{Symbol => Object}] options
11
+ # Other, operand-specific options
12
+ # @return [Operator] with `matched` and `satisfied` accessors for matched triples and sub-expressions
10
13
  # @raise [ShEx::NotMatched] with `expression` accessor to access `matched` and `unmatched` statements along with `satisfied` and `unsatisfied` operations.
11
14
  # @see [https://shexspec.github.io/spec/#shape-expression-semantics]
12
- def satisfies?(focus)
15
+ def satisfies?(focus, depth: 0, **options)
13
16
  raise NotImplementedError, "#satisfies? Not implemented in #{self.class}"
14
17
  end
15
18
 
16
- ##
17
- # Included TripleExpressions
18
- # @return [Array<TripleExpressions>]
19
- def triple_expressions
20
- operands.select {|o| o.is_a?(Satisfiable)}.map(&:triple_expressions).flatten.uniq
21
- end
22
-
23
19
  # This operator includes Satisfiable
24
20
  def satisfiable?; true; end
25
21
  end
@@ -11,55 +11,95 @@ module ShEx::Algebra
11
11
  # @return [Hash{RDF::Resource => RDF::Resource}]
12
12
  attr_reader :map
13
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)
29
+ super
30
+ each_descendant do |op|
31
+ # Set schema everywhere
32
+ op.schema = self
33
+ end
34
+ end
35
+
14
36
  ##
15
37
  # Match on schema. Finds appropriate shape for node, and matches that shape.
16
38
  #
17
- # @param [RDF::Resource] focus
39
+ # @param [RDF::Term] focus
18
40
  # @param [RDF::Queryable] graph
19
41
  # @param [Hash{RDF::Resource => RDF::Resource}] map
20
42
  # @param [Array<Schema, String>] shapeExterns ([])
21
43
  # One or more schemas, or paths to ShEx schema resources used for finding external shapes.
22
44
  # @return [Operand] Returns operand graph annotated with satisfied and unsatisfied operations.
45
+ # @param [Hash{Symbol => Object}] options
46
+ # @option options [String] :base_uri
23
47
  # @raise [ShEx::NotSatisfied] along with operand graph described for return
24
- def execute(focus, graph, map, shapeExterns: [], **options)
25
- @graph = graph
48
+ def execute(focus, graph, map, shapeExterns: [], depth: 0, **options)
49
+ @graph, @shapes_entered = graph, {}
26
50
  @external_schemas = shapeExterns
27
- focus = iri(focus)
51
+ focus = value(focus)
52
+
53
+ logger = options[:logger] || @options[:logger]
54
+ each_descendant do |op|
55
+ # Set logging everywhere
56
+ op.logger = logger
57
+ end
58
+
59
+ # Initialize Extensions
60
+ @extensions = {}
61
+ each_descendant do |op|
62
+ next unless op.is_a?(SemAct)
63
+ name = op.operands.first.to_s
64
+ if ext_class = ShEx::Extension.find(name)
65
+ @extensions[name] ||= ext_class.new(schema: self, depth: depth, **options)
66
+ end
67
+ end
68
+
69
+ # If `n` is a Blank Node, we won't find it through normal matching, find an equivalent node in the graph having the same label
70
+ graph_focus = graph.enum_term.detect {|t| t.node? && t.id == focus.id} if focus.is_a?(RDF::Node)
71
+ graph_focus ||= focus
72
+
28
73
  # Make sure they're URIs
29
- @map = (map || {}).inject({}) {|memo, (k,v)| memo.merge(iri(k).to_s => iri(v).to_s)}
74
+ @map = (map || {}).inject({}) {|memo, (k,v)| memo.merge(value(k) => iri(v))}
30
75
 
31
76
  # First, evaluate semantic acts
32
77
  semantic_actions.all? do |op|
33
- op.satisfies?([])
78
+ op.satisfies?([], depth: depth + 1)
34
79
  end
35
80
 
36
81
  # Keep a new Schema, specifically for recording actions
37
82
  satisfied_schema = Schema.new
38
83
  # Next run any start expression
39
84
  if start
40
- status("start") {"expression: #{start.to_sxp}"}
41
- satisfied_schema.operands << start.satisfies?(focus)
85
+ satisfied_schema.operands << start.satisfies?(focus, depth: depth + 1)
42
86
  end
43
87
 
44
88
  # Add shape result(s)
45
89
  satisfied_shapes = {}
46
90
  satisfied_schema.operands << [:shapes, satisfied_shapes] unless shapes.empty?
47
91
 
48
- label = @map[focus.to_s]
49
- if label && !label.empty?
50
- shape = shapes[label]
51
- structure_error("No shape found for #{label}") unless shape
52
-
53
- # If `n` is a Blank Node, we won't find it through normal matching, find an equivalent node in the graph having the same label
54
- if focus.is_a?(RDF::Node)
55
- n = graph.enum_term.detect {|t| t.id == focus.id}
56
- focus = n if n
92
+ # Match against all shapes associated with the labels for focus
93
+ Array(@map[focus]).each do |label|
94
+ enter_shape(label, focus) do |shape|
95
+ satisfied_shapes[label] = shape.satisfies?(graph_focus, depth: depth + 1)
57
96
  end
58
-
59
- satisfied_shapes[label] = shape.satisfies?(focus)
60
97
  end
61
- status "schema satisfied"
98
+ status "schema satisfied", depth: depth
62
99
  satisfied_schema
100
+ ensure
101
+ # Close Semantic Action extensions
102
+ @extensions.values.each {|ext| ext.close(schema: self, depth: depth, **options)}
63
103
  end
64
104
 
65
105
  ##
@@ -81,17 +121,37 @@ module ShEx::Algebra
81
121
 
82
122
  ##
83
123
  # Shapes as a hash
84
- # @return [Hash{RDF::Resource => Operator}]
124
+ # @return [Array<Operator>]
85
125
  def shapes
86
126
  @shapes ||= begin
87
- shapes = operands.detect {|op| op.is_a?(Array) && op.first == :shapes}
88
- shapes = shapes ? shapes.last : {}
89
- shapes.inject({}) do |memo, (label, operand)|
90
- memo.merge(label.to_s => operand)
91
- end
127
+ shapes = Array(operands.detect {|op| op.is_a?(Array) && op.first == :shapes})
128
+ Array(shapes[1..-1])
92
129
  end
93
130
  end
94
131
 
132
+ ##
133
+ # 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.
134
+ # @param [RDF::Resource] label
135
+ # @param [RDF::Resource] node
136
+ # @yield :shape
137
+ # @yieldparam [Satisfiable] shape, or `nil` if shape already entered
138
+ # @return [Satisfiable]
139
+ def enter_shape(label, node, &block)
140
+ shape = shapes.detect {|s| s.label == label}
141
+ structure_error("No shape found for #{label}") unless shape
142
+ @shapes_entered[label] ||= {}
143
+ if @shapes_entered[label][node]
144
+ block.call(false)
145
+ else
146
+ @shapes_entered[label][node] = self
147
+ begin
148
+ block.call(shape)
149
+ ensure
150
+ @shapes_entered[label].delete(node)
151
+ end
152
+ end
153
+ end
154
+
95
155
  ##
96
156
  # Externally loaded schemas, lazily evaluated
97
157
  # @return [Array<Schema>]
@@ -108,54 +168,6 @@ module ShEx::Algebra
108
168
  end
109
169
  end
110
170
 
111
- ##
112
- # Enumerate via depth-first recursive descent over operands, yielding each operator
113
- # @yield operator
114
- # @yieldparam [Object] operator
115
- # @return [Enumerator]
116
- def each_descendant(depth = 0, &block)
117
- if block_given?
118
- super(depth + 1, &block)
119
- shapes.values.each do |op|
120
- op.each_descendant(depth + 1, &block) if op.respond_to?(:each_descendant)
121
-
122
- case block.arity
123
- when 1 then block.call(op)
124
- else block.call(depth, op)
125
- end
126
- end
127
- end
128
- enum_for(:each_descendant)
129
- end
130
-
131
- ##
132
- # Returns the Base URI defined for the parser,
133
- # as specified or when parsing a BASE prologue element.
134
- #
135
- # @example
136
- # base #=> RDF::URI('http://example.com/')
137
- #
138
- # @return [HRDF::URI]
139
- def base_uri
140
- RDF::URI(@options[:base_uri]) if @options[:base_uri]
141
- end
142
-
143
- # Create URIs
144
- def iri(value)
145
- # If we have a base URI, use that when constructing a new URI
146
- case value
147
- when RDF::Value then value
148
- when /^_:/ then RDF::Node(value[2..-1].to_s)
149
- else
150
- value = RDF::URI(value)
151
- if base_uri && value.relative?
152
- base_uri.join(value)
153
- else
154
- value
155
- end
156
- end
157
- end
158
-
159
171
  ##
160
172
  # Start action, if any
161
173
  def start
@@ -167,7 +179,7 @@ module ShEx::Algebra
167
179
  # @return [SPARQL::Algebra::Expression] `self`
168
180
  # @raise [ArgumentError] if the value is invalid
169
181
  def validate!
170
- shapes.values.each {|op| op.validate! if op.respond_to?(:validate!)}
182
+ shapes.each {|op| op.validate! if op.respond_to?(:validate!)}
171
183
  super
172
184
  end
173
185
  end
@@ -3,35 +3,85 @@ module ShEx::Algebra
3
3
  class SemAct < Operator
4
4
  NAME = :semact
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'] == "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
+ # @param [String] code
22
+ # @param [Array<RDF::Statement>] arcs_in available statements to be matched having `focus` as an object
23
+ # @param [Array<RDF::Statement>] arcs_out available statements to be matched having `focus` as a subject
24
+ # @param [Integer] depth for logging
25
+ # @param [Hash{Symbol => Object}] options
26
+ # Other, operand-specific options
27
+ # @return [Boolean] Returning `false` results in {ShEx::NotSatisfied} exception
28
+ def enter(**options)
29
+ if implementation = schema.extensions[operands.first.to_s]
30
+ implementation.enter(code: operands[0], expression: parent, **options)
31
+ end
32
+ end
33
+
6
34
  #
7
35
  # The evaluation semActsSatisfied on a list of SemActs returns success or failure. The evaluation of an individual SemAct is implementation-dependent.
8
- # @param [Array<RDF::Statement>] statements
36
+ #
37
+ # 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.
38
+ #
39
+ # @param [Object] focus (ignored)
40
+ # @param [Array<RDF::Statement>] matched matched statements
41
+ # @param [Array<RDF::Statement>] unmatched unmatched statements
9
42
  # @return [Boolean] `true` if satisfied, `false` if it does not apply
10
43
  # @raise [ShEx::NotSatisfied] if not satisfied
11
- def satisfies?(statements)
12
- # FIXME: should have a registry
13
- case operands.first.to_s
14
- when "http://shex.io/extensions/Test/"
15
- str = if md = /^ *(fail|print) *\( *(?:(\"(?:[^\\"]|\\")*\")|([spo])) *\) *$/.match(operands[1].to_s)
16
- md[2] || case md[3]
17
- when 's' then statements.first.subject
18
- when 'p' then statements.first.predicate
19
- when 'o' then statements.first.object
20
- else statements.first.to_sxp
21
- end.to_s
22
- else
23
- statements.empty? ? 'no statement' : statements.first.to_sxp
44
+ def satisfies?(focus, matched: [], unmatched: [], depth: 0)
45
+ if implementation = schema.extensions[operands.first.to_s]
46
+ if matched.empty?
47
+ implementation.visit(code: operands[1],
48
+ expression: parent,
49
+ depth: depth) ||
50
+ not_satisfied("SemAct failed", unmatched: unmatched)
24
51
  end
25
- $stdout.puts str
26
- status str
27
- not_satisfied "fail" if md && md[1] == 'fail'
28
- true
52
+ matched.all? do |statement|
53
+ implementation.visit(code: operands[1],
54
+ matched: statement,
55
+ expression: parent,
56
+ depth: depth)
57
+ end || not_satisfied("SemAct failed", matched: matched, unmatched: unmatched)
29
58
  else
30
- status("unknown SemAct name #{operands.first}") {"expression: #{self.to_sxp}"}
59
+ status("unknown SemAct name #{operands.first}", depth: depth) {"expression: #{self.to_sxp}"}
31
60
  false
32
61
  end
33
62
  end
34
63
 
64
+ ##
65
+ # Called on exit from containing {ShEx::TripleExpression}
66
+ #
67
+ # @param [String] code
68
+ # @param [Array<RDF::Statement>] matched statements matched by this expression
69
+ # @param [Array<RDF::Statement>] unmatched statements considered, but not matched by this expression
70
+ # @param [ShEx::Algebra::TripleExpression] expression containing this semantic act
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
+
35
85
  # Does This operator is SemAct
36
86
  def semact?; true; end
37
87
  end
@@ -19,13 +19,21 @@ module ShEx::Algebra
19
19
  # @return [Array<RDF::Statement>]
20
20
  attr_accessor :unmatchables
21
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
+
22
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)`.
23
32
  # @param (see Satisfiable#satisfies?)
24
33
  # @return (see Satisfiable#satisfies?)
25
34
  # @raise (see Satisfiable#satisfies?)
26
- def satisfies?(focus)
27
- expression = operands.detect {|op| op.is_a?(TripleExpression)}
28
-
35
+ def satisfies?(focus, depth: 0)
36
+ expression = self.expression
29
37
  # neigh(G, n) is the neighbourhood of the node n in the graph G.
30
38
  #
31
39
  # neigh(G, n) = arcsOut(G, n) ∪ arcsIn(G, n)
@@ -34,8 +42,8 @@ module ShEx::Algebra
34
42
  neigh = (arcs_in + arcs_out).uniq
35
43
 
36
44
  # `matched` is the subset of statements which match `expression`.
37
- status("arcsIn: #{arcs_in.count}, arcsOut: #{arcs_out.count}")
38
- matched_expression = expression.matches(neigh) if expression
45
+ status("arcsIn: #{arcs_in.count}, arcsOut: #{arcs_out.count}", depth: depth)
46
+ matched_expression = expression.matches(arcs_in, arcs_out, depth: depth + 1) if expression
39
47
  matched = Array(matched_expression && matched_expression.matched)
40
48
 
41
49
  # `remainder` is the set of unmatched statements
@@ -55,7 +63,7 @@ module ShEx::Algebra
55
63
  unmatched = matchables.select do |statement|
56
64
  expression.triple_constraints.any? do |expr|
57
65
  begin
58
- statement.predicate == expr.predicate && expr.matches([statement])
66
+ statement.predicate == expr.predicate && expr.matches([], [statement], depth: depth + 1)
59
67
  rescue ShEx::NotMatched
60
68
  false # Expected not to match
61
69
  end
@@ -65,7 +73,8 @@ module ShEx::Algebra
65
73
  not_satisfied "Statements remain matching TripleConstraints",
66
74
  matched: matched,
67
75
  unmatched: unmatched,
68
- satisfied: expression
76
+ satisfied: expression,
77
+ depth: depth
69
78
  end
70
79
 
71
80
  # There is no triple in matchables whose predicate does not appear in extra.
@@ -74,30 +83,30 @@ module ShEx::Algebra
74
83
  not_satisfied "Statements remains with predicate #{unmatched.map(&:predicate).compact.join(',')} not in extra",
75
84
  matched: matched,
76
85
  unmatched: unmatched,
77
- satisfied: expression
86
+ satisfied: expression,
87
+ depth: depth
78
88
  end
79
89
 
80
90
  # closed is false or unmatchables is empty.
81
- not_satisfied "Unmatchables remain on a closed shape" unless !closed? || unmatchables.empty?
91
+ not_satisfied "Unmatchables remain on a closed shape", depth: depth unless !closed? || unmatchables.empty?
82
92
 
83
93
  # Presumably, to be satisfied, there must be some triples in matches
84
-
85
- semantic_actions.all? do |op|
86
- # FIXME: what triples to run against satisfies?
87
- op.satisfies?(matched)
94
+ semantic_actions.each do |op|
95
+ op.satisfies?(matched, matched: matched, depth: depth + 1)
88
96
  end unless matched.empty?
89
97
 
90
98
  # FIXME: also record matchables, outs and others?
91
- satisfy matched: matched
99
+ satisfy focus: focus, matched: matched, depth: depth
92
100
  rescue ShEx::NotMatched => e
93
- not_satisfied e.message, unsatisfied: e.expression
101
+ not_satisfied e.message, focus: focus, unsatisfied: e.expression, depth: depth
94
102
  end
95
103
 
104
+
96
105
  ##
97
- # Included TripleExpressions
98
- # @return [Array<TripleExpressions>]
99
- def triple_expressions
100
- operands.select {|op| op.is_a?(TripleExpression)}
106
+ # The optional TripleExpression for this Shape.
107
+ # @return [TripleExpression]
108
+ def expression
109
+ operands.detect {|op| op.is_a?(TripleExpression)}
101
110
  end
102
111
 
103
112
  private