shex 0.1.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.
@@ -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