shex 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ module ShEx::Algebra
2
+ ##
3
+ class External < Operator
4
+ include Satisfiable
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?(n)
10
+ extern_shape = nil
11
+
12
+ # Find the label for this external
13
+ label = schema.shapes.key(self)
14
+ not_satisfied("Can't find label for this extern") unless label
15
+
16
+ schema.external_schemas.each do |schema|
17
+ extern_shape ||= schema.shapes[label]
18
+ end
19
+
20
+ not_satisfied("External not configured for this shape") unless extern_shape
21
+ extern_shape.satisfies?(n)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ module ShEx::Algebra
2
+ ##
3
+ class Inclusion < Operator
4
+ include TripleExpression
5
+ NAME = :inclusion
6
+
7
+ def initialize(arg, **options)
8
+ raise ArgumentError, "Shape inclusion must be an IRI or BNode: #{arg}" unless arg.is_a?(RDF::Resource)
9
+ super
10
+ end
11
+
12
+ ##
13
+ # In this case, we accept an array of statements, and match based on cardinality.
14
+ #
15
+ # @param [Array<RDF::Statement>] t
16
+ # @return [Array<RDF::Statement>]
17
+ # @raise NotMatched, ShEx::NotSatisfied
18
+ def matches(t)
19
+ status "referenced_shape: #{operands.first}"
20
+ expression = referenced_shape.triple_expressions.first
21
+ max = maximum
22
+ results = expression.matches(t)
23
+
24
+ # Max violations handled in Shape
25
+ not_matched "Minimum Cardinality Violation: #{results.length} < #{minimum}" if
26
+ results.length < minimum
27
+
28
+ results
29
+ end
30
+
31
+ ##
32
+ # Returns the referenced shape
33
+ #
34
+ # @return [Operand]
35
+ def referenced_shape
36
+ schema.shapes[operands.first.to_s]
37
+ end
38
+
39
+ ##
40
+ # A Inclusion is valid if it's ancestor schema has any shape with a lable
41
+ # the same as it's reference.
42
+ #
43
+ # An Inclusion object's include property must appear in the schema's shapes map and the corresponding triple expression must be a Shape with a tripleExpr. The function dereference(include) returns the shape's tripleExpr.
44
+ def validate!
45
+ structure_error("Missing included shape: #{operands.first}") if referenced_shape.nil?
46
+ structure_error("Self included shape: #{operands.first}") if referenced_shape == first_ancestor(Shape)
47
+
48
+ triple_expressions = referenced_shape.triple_expressions
49
+ case triple_expressions.length
50
+ when 0
51
+ structure_error("Includes shape with no triple expressions")
52
+ when 1
53
+ else
54
+ structure_error("Includes shape with multiple triple expressions")
55
+ end
56
+ super
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,148 @@
1
+ module ShEx::Algebra
2
+ ##
3
+ class NodeConstraint < Operator
4
+ include Satisfiable
5
+ NAME = :nodeConstraint
6
+
7
+ #
8
+ # S is a NodeConstraint and satisfies2(n, se) as described below in Node Constraints. Note that testing if a node satisfies a node constraint does not require a graph or shapeMap.
9
+ # @param [RDF::Resource] n
10
+ # @return [Boolean] `true` if satisfied, `false` if it does not apply
11
+ # @raise [ShEx::NotSatisfied] if not satisfied
12
+ def satisfies?(n)
13
+ status ""
14
+ satisfies_node_kind?(n) &&
15
+ satisfies_datatype?(n) &&
16
+ satisfies_string_facet?(n) &&
17
+ satisfies_numeric_facet?(n) &&
18
+ satisfies_values?(n)
19
+ end
20
+
21
+ private
22
+
23
+ ##
24
+ # Satisfies Node Kind Constraint
25
+ # @return [Boolean] `true` if satisfied, `false` if it does not apply
26
+ # @raise [ShEx::NotSatisfied] if not satisfied
27
+ def satisfies_node_kind?(value)
28
+ kind = case operands.first
29
+ when :iri then RDF::URI
30
+ when :bnode then RDF::Node
31
+ when :literal then RDF::Literal
32
+ when :nonliteral then RDF::Resource
33
+ else return true
34
+ end
35
+
36
+ not_satisfied "Node was #{value.inspect} expected kind #{kind}" unless
37
+ value.is_a?(kind)
38
+ status "right kind: #{value}: #{kind}"
39
+ true
40
+ end
41
+
42
+ ##
43
+ # Datatype Constraint
44
+ # @return [Boolean] `true` if satisfied, `false` if it does not apply
45
+ # @raise [ShEx::NotSatisfied] if not satisfied
46
+ def satisfies_datatype?(value)
47
+ dt = operands[1] if operands.first == :datatype
48
+ return true unless dt
49
+
50
+ not_satisfied "Node was #{value.inspect}, expected datatype #{dt}" unless
51
+ value.is_a?(RDF::Literal) && value.datatype == RDF::URI(dt)
52
+ status "right datatype: #{value}: #{dt}"
53
+ true
54
+ end
55
+
56
+ ##
57
+ # String Facet Constraint
58
+ # Checks all length/minlength/maxlength/pattern facets against the string representation of the value.
59
+ # @return [Boolean] `true` if satisfied, `false` if it does not apply
60
+ # @raise [ShEx::NotSatisfied] if not satisfied
61
+ def satisfies_string_facet?(value)
62
+ length = op_fetch(:length)
63
+ minlength = op_fetch(:minlength)
64
+ maxlength = op_fetch(:maxlength)
65
+ pattern = op_fetch(:pattern)
66
+
67
+ return true if (length || minlength || maxlength || pattern).nil?
68
+
69
+ v_s = case value
70
+ when RDF::Node then value.id
71
+ else value.to_s
72
+ end
73
+ not_satisfied "Node #{v_s.inspect} length not #{length}" if
74
+ length && v_s.length != length.to_i
75
+ not_satisfied"Node #{v_s.inspect} length < #{minlength}" if
76
+ minlength && v_s.length < minlength.to_i
77
+ not_satisfied "Node #{v_s.inspect} length > #{maxlength}" if
78
+ maxlength && v_s.length > maxlength.to_i
79
+ not_satisfied "Node #{v_s.inspect} does not match #{pattern}" if
80
+ pattern && !Regexp.new(pattern).match(v_s)
81
+ status "right string facet: #{value}"
82
+ true
83
+ end
84
+
85
+ ##
86
+ # Numeric Facet Constraint
87
+ # Checks all numeric facets against the value.
88
+ # @return [Boolean] `true` if satisfied, `false` if it does not apply
89
+ # @raise [ShEx::NotSatisfied] if not satisfied
90
+ def satisfies_numeric_facet?(value)
91
+ mininclusive = op_fetch(:mininclusive)
92
+ minexclusive = op_fetch(:minexclusive)
93
+ maxinclusive = op_fetch(:maxinclusive)
94
+ maxexclusive = op_fetch(:maxexclusive)
95
+ totaldigits = op_fetch(:totaldigits)
96
+ fractiondigits = op_fetch(:fractiondigits)
97
+
98
+ return true if (mininclusive || minexclusive || maxinclusive || maxexclusive || totaldigits || fractiondigits).nil?
99
+
100
+ not_satisfied "Node #{value.inspect} not numeric" unless
101
+ value.is_a?(RDF::Literal::Numeric)
102
+
103
+ not_satisfied "Node #{value.inspect} not decimal" if
104
+ (totaldigits || fractiondigits) && (!value.is_a?(RDF::Literal::Decimal) || value.invalid?)
105
+
106
+ numeric_value = value.object
107
+ case
108
+ when !mininclusive.nil? && numeric_value < mininclusive.object then not_satisfied("Node #{value.inspect} < #{mininclusive.object}")
109
+ when !minexclusive.nil? && numeric_value <= minexclusive.object then not_satisfied("Node #{value.inspect} not <= #{minexclusive.object}")
110
+ when !maxinclusive.nil? && numeric_value > maxinclusive.object then not_satisfied("Node #{value.inspect} > #{maxinclusive.object}")
111
+ when !maxexclusive.nil? && numeric_value >= maxexclusive.object then not_satisfied("Node #{value.inspect} >= #{maxexclusive.object}")
112
+ when !totaldigits.nil?
113
+ md = value.canonicalize.to_s.match(/([1-9]\d*|0)?(?:\.(\d+)(?!0))?/)
114
+ digits = md ? (md[1].to_s + md[2].to_s) : ""
115
+ if digits.length > totaldigits.to_i
116
+ not_satisfied "Node #{value.inspect} total digits != #{totaldigits}"
117
+ end
118
+ when !fractiondigits.nil?
119
+ md = value.canonicalize.to_s.match(/\.(\d+)(?!0)?/)
120
+ num = md ? md[1].to_s : ""
121
+ if num.length > fractiondigits.to_i
122
+ not_satisfied "Node #{value.inspect} fractional digits != #{fractiondigits}"
123
+ end
124
+ end
125
+ status "right numeric facet: #{value}"
126
+ true
127
+ end
128
+
129
+ ##
130
+ # Value Constraint
131
+ # Checks all numeric facets against the value.
132
+ # @return [Boolean] `true` if satisfied, `false` if it does not apply
133
+ # @raise [ShEx::NotSatisfied] if not satisfied
134
+ def satisfies_values?(value)
135
+ values = operands.select {|op| op.is_a?(Value)}
136
+ return true if values.empty?
137
+ matched_value = values.detect {|v| v.match?(value)}
138
+ not_satisfied "Node #{value.inspect} not expected" unless matched_value
139
+ status "right value: #{value}"
140
+ true
141
+ end
142
+
143
+ # Returns the value of a particular facet
144
+ def op_fetch(which)
145
+ (operands.detect {|op| op.is_a?(Array) && op[0] == which} || [])[1]
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,19 @@
1
+ module ShEx::Algebra
2
+ ##
3
+ class Not < Operator::Unary
4
+ include Satisfiable
5
+ NAME = :not
6
+
7
+ #
8
+ # S is a ShapeNot and for the shape expression se2 at shapeExpr, notSatisfies(n, se2, G, m).
9
+ # @param [RDF::Resource] n
10
+ # @return [Boolean] `true` when satisfied, meaning that the operand was not satisfied
11
+ # @raise [ShEx::NotSatisfied] if not satisfied, meaning that the operand was satisfied
12
+ def satisfies?(n)
13
+ status ""
14
+ operands.last.not_satisfies?(n)
15
+ status "not satisfied"
16
+ true
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ module ShEx::Algebra
2
+ ##
3
+ class OneOf < Operator
4
+ include TripleExpression
5
+ NAME = :oneOf
6
+
7
+ ##
8
+ # `expr` is a OneOf and there is some shape expression `se2` in shapeExprs such that a `matches(T, se2, m)`...
9
+ #
10
+ # @param [Array<RDF::Statement>] t
11
+ # @return [Array<RDF::Statement]
12
+ def matches(t)
13
+ results = []
14
+ statements = t.dup
15
+ num_iters = 0
16
+ max = maximum
17
+
18
+ # 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.
19
+ status ""
20
+ while num_iters < max
21
+ matched_something = operands.select {|o| o.is_a?(TripleExpression)}.any? do |op|
22
+ begin
23
+ matched = op.matches(statements)
24
+ results += matched
25
+ statements -= matched
26
+ status "matched #{t.first.to_sxp}"
27
+ rescue NotMatched => e
28
+ log_recover("oneOf: ignore error: #{e.message}", depth: options.fetch(:depth, 0))
29
+ false
30
+ end
31
+ end
32
+ break unless matched_something
33
+ num_iters += 1
34
+ status "matched #{results.length} statements after #{num_iters} iterations"
35
+ end
36
+
37
+ # Max violations handled in Shape
38
+ not_matched "Minimum Cardinality Violation: #{num_iters} < #{minimum}" if
39
+ num_iters < minimum
40
+
41
+ # Last, evaluate semantic acts
42
+ semantic_actions.all? do |op|
43
+ op.satisfies?(results)
44
+ end unless results.empty?
45
+
46
+ status "one of satisfied"
47
+ results
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,274 @@
1
+ require 'sparql/algebra'
2
+ require 'sparql/extensions'
3
+
4
+ module ShEx::Algebra
5
+
6
+ ##
7
+ # The ShEx operator.
8
+ #
9
+ # @abstract
10
+ class Operator
11
+ extend SPARQL::Algebra::Expression
12
+ include RDF::Util::Logger
13
+
14
+ # Location of schema including this operator
15
+ attr_accessor :schema
16
+
17
+ # Initialization options
18
+ attr_accessor :options
19
+
20
+ ARITY = -1 # variable arity
21
+
22
+ ##
23
+ # Initializes a new operator instance.
24
+ #
25
+ # @overload initialize(*operands)
26
+ # @param [Array<RDF::Term>] operands
27
+ #
28
+ # @overload initialize(*operands, options)
29
+ # @param [Array<RDF::Term>] operands
30
+ # @param [Hash{Symbol => Object}] options
31
+ # any additional options
32
+ # @option options [Boolean] :memoize (false)
33
+ # whether to memoize results for particular operands
34
+ # @raise [TypeError] if any operand is invalid
35
+ def initialize(*operands)
36
+ @options = operands.last.is_a?(Hash) ? operands.pop.dup : {}
37
+ @operands = operands.map! do |operand|
38
+ case operand
39
+ when Array
40
+ operand.each do |op|
41
+ op.parent = self if op.respond_to?(:parent=)
42
+ end
43
+ operand
44
+ when Operator, RDF::Term, RDF::Query, RDF::Query::Pattern, Array, Symbol
45
+ operand.parent = self if operand.respond_to?(:parent=)
46
+ operand
47
+ when TrueClass, FalseClass, Numeric, String, DateTime, Date, Time
48
+ RDF::Literal(operand)
49
+ when NilClass
50
+ raise ArgumentError, "Found nil operand for #{self.class.name}"
51
+ else raise TypeError, "invalid ShEx::Algebra::Operator operand: #{operand.inspect}"
52
+ end
53
+ end
54
+
55
+ if options[:logger]
56
+ options[:depth] = 0
57
+ each_descendant(1) do |depth, operand|
58
+ if operand.respond_to?(:options)
59
+ operand.options[:logger] = options[:logger]
60
+ operand.options[:depth] = depth
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ ##
67
+ # Is this shape closed?
68
+ # @return [Boolean]
69
+ def closed?
70
+ operands.include?(:closed)
71
+ end
72
+
73
+ ##
74
+ # Semantic Actions
75
+ # @return [Array<SemAct>]
76
+ def semantic_actions
77
+ operands.select {|o| o.is_a?(SemAct)}
78
+ end
79
+
80
+ # Does this operator include Satisfiable?
81
+ def satisfiable?; false; end
82
+
83
+ # Does this operator include TripleExpression?
84
+ def triple_expression?; false; end
85
+
86
+ # Does this operator a SemAct?
87
+ def semact?; false; end
88
+
89
+ ##
90
+ # Exception handling
91
+ def not_matched(message, **opts)
92
+ expression = opts.fetch(:expression, self)
93
+ exception = opts.fetch(:exception, NotMatched)
94
+ log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"}
95
+ end
96
+
97
+ def not_satisfied(message, **opts)
98
+ expression = opts.fetch(:expression, self)
99
+ exception = opts.fetch(:exception, ShEx::NotSatisfied)
100
+ log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"}
101
+ end
102
+
103
+ def structure_error(message, **opts)
104
+ expression = opts.fetch(:expression, self)
105
+ exception = opts.fetch(:exception, ShEx::StructureError)
106
+ log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"}
107
+ end
108
+
109
+ def status(message, &block)
110
+ log_info(self.class.const_get(:NAME), message, depth: options.fetch(:depth, 0), &block)
111
+ true
112
+ end
113
+
114
+ ##
115
+ # The operands to this operator.
116
+ #
117
+ # @return [Array]
118
+ attr_reader :operands
119
+
120
+ ##
121
+ # Returns the operand at the given `index`.
122
+ #
123
+ # @param [Integer] index
124
+ # an operand index in the range `(0...(operands.count))`
125
+ # @return [RDF::Term]
126
+ def operand(index = 0)
127
+ operands[index]
128
+ end
129
+
130
+ ##
131
+ # Returns the SPARQL S-Expression (SSE) representation of this operator.
132
+ #
133
+ # @return [Array]
134
+ # @see http://openjena.org/wiki/SSE
135
+ def to_sxp_bin
136
+ operator = [self.class.const_get(:NAME)].flatten.first
137
+ [operator, *(operands || []).map(&:to_sxp_bin)]
138
+ end
139
+
140
+ ##
141
+ # Returns an S-Expression (SXP) representation of this operator
142
+ #
143
+ # @return [String]
144
+ def to_sxp
145
+ begin
146
+ require 'sxp' # @see http://rubygems.org/gems/sxp
147
+ rescue LoadError
148
+ abort "SPARQL::Algebra::Operator#to_sxp requires the SXP gem (hint: `gem install sxp')."
149
+ end
150
+ require 'sparql/algebra/sxp_extensions'
151
+
152
+ to_sxp_bin.to_sxp
153
+ end
154
+
155
+ ##
156
+ # Returns a developer-friendly representation of this operator.
157
+ #
158
+ # @return [String]
159
+ def inspect
160
+ sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, operands.to_sse.gsub(/\s+/m, ' '))
161
+ end
162
+
163
+ ##
164
+ # @param [Statement] other
165
+ # @return [Boolean]
166
+ def eql?(other)
167
+ other.class == self.class && other.operands == self.operands
168
+ end
169
+ alias_method :==, :eql?
170
+
171
+ ##
172
+ # Enumerate via depth-first recursive descent over operands, yielding each operator
173
+ # @param [Integer] depth incrementeded for each depth of operator, and provided to block if Arity is 2
174
+ # @yield operator
175
+ # @yieldparam [Object] operator
176
+ # @return [Enumerator]
177
+ def each_descendant(depth = 0, &block)
178
+ if block_given?
179
+ operands.each do |operand|
180
+ case operand
181
+ when Array
182
+ operand.each do |op|
183
+ op.each_descendant(depth + 1, &block) if op.respond_to?(:each_descendant)
184
+ end
185
+ else
186
+ operand.each_descendant(depth + 1, &block) if operand.respond_to?(:each_descendant)
187
+ end
188
+
189
+ case block.arity
190
+ when 1 then block.call(operand)
191
+ else block.call(depth, operand)
192
+ end
193
+ end
194
+ end
195
+ enum_for(:each_descendant)
196
+ end
197
+ alias_method :descendants, :each_descendant
198
+ alias_method :each, :each_descendant
199
+
200
+ ##
201
+ # Parent expression, if any
202
+ #
203
+ # @return [Operator]
204
+ def parent; @options[:parent]; end
205
+
206
+ ##
207
+ # Parent operator, if any
208
+ #
209
+ # @return [Operator]
210
+ def parent=(operator)
211
+ @options[:parent]= operator
212
+ end
213
+
214
+ ##
215
+ # First ancestor operator of type `klass`
216
+ #
217
+ # @param [Class] klass
218
+ # @return [Operator]
219
+ def first_ancestor(klass)
220
+ parent.is_a?(klass) ? parent : parent.first_ancestor(klass) if parent
221
+ end
222
+
223
+ ##
224
+ # Validate all operands, operator specific classes should override for operator-specific validation
225
+ # @return [SPARQL::Algebra::Expression] `self`
226
+ # @raise [ShEx::StructureError] if the value is invalid
227
+ def validate!
228
+ operands.each {|op| op.validate! if op.respond_to?(:validate!)}
229
+ self
230
+ end
231
+
232
+ ##
233
+ # A unary operator.
234
+ #
235
+ # Operators of this kind take one operand.
236
+ #
237
+ # @abstract
238
+ class Unary < Operator
239
+ ARITY = 1
240
+
241
+ ##
242
+ # @param [RDF::Term] arg1
243
+ # the first operand
244
+ # @param [Hash{Symbol => Object}] options
245
+ # any additional options (see {Operator#initialize})
246
+ def initialize(arg1, options = {})
247
+ raise ArgumentError, "wrong number of arguments (given 2, expected 1)" unless options.is_a?(Hash)
248
+ super
249
+ end
250
+ end # Unary
251
+
252
+ ##
253
+ # A binary operator.
254
+ #
255
+ # Operators of this kind take two operands.
256
+ #
257
+ # @abstract
258
+ class Binary < Operator
259
+ ARITY = 2
260
+
261
+ ##
262
+ # @param [RDF::Term] arg1
263
+ # the first operand
264
+ # @param [RDF::Term] arg2
265
+ # the second operand
266
+ # @param [Hash{Symbol => Object}] options
267
+ # any additional options (see {Operator#initialize})
268
+ def initialize(arg1, arg2, options = {})
269
+ raise ArgumentError, "wrong number of arguments (given 3, expected 2)" unless options.is_a?(Hash)
270
+ super
271
+ end
272
+ end # Binary
273
+ end
274
+ end