shex 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +119 -2
- data/VERSION +1 -1
- data/etc/doap.ttl +2 -2
- data/lib/shex.rb +21 -2
- data/lib/shex/algebra.rb +41 -3
- data/lib/shex/algebra/and.rb +27 -6
- data/lib/shex/algebra/annotation.rb +19 -0
- data/lib/shex/algebra/each_of.rb +32 -19
- data/lib/shex/algebra/external.rb +9 -6
- data/lib/shex/algebra/inclusion.rb +29 -18
- data/lib/shex/algebra/node_constraint.rb +45 -36
- data/lib/shex/algebra/not.rb +19 -4
- data/lib/shex/algebra/one_of.rb +26 -16
- data/lib/shex/algebra/operator.rb +350 -34
- data/lib/shex/algebra/or.rb +26 -9
- data/lib/shex/algebra/satisfiable.rb +5 -9
- data/lib/shex/algebra/schema.rb +87 -75
- data/lib/shex/algebra/semact.rb +69 -19
- data/lib/shex/algebra/shape.rb +28 -19
- data/lib/shex/algebra/shape_ref.rb +36 -10
- data/lib/shex/algebra/start.rb +5 -5
- data/lib/shex/algebra/stem.rb +18 -3
- data/lib/shex/algebra/stem_range.rb +24 -5
- data/lib/shex/algebra/triple_constraint.rb +26 -13
- data/lib/shex/algebra/triple_expression.rb +3 -2
- data/lib/shex/algebra/value.rb +5 -5
- data/lib/shex/extensions/extension.rb +160 -0
- data/lib/shex/extensions/test.rb +26 -0
- data/lib/shex/parser.rb +12 -25
- data/lib/shex/shex_context.rb +85 -0
- data/lib/shex/version.rb +19 -0
- metadata +35 -11
- data/lib/shex/algebra/base.rb +0 -6
- data/lib/shex/algebra/prefix.rb +0 -6
- data/lib/shex/algebra/unary_shape.rb +0 -6
@@ -6,19 +6,22 @@ module ShEx::Algebra
|
|
6
6
|
|
7
7
|
#
|
8
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?(focus)
|
9
|
+
def satisfies?(focus, depth: 0)
|
10
10
|
extern_shape = nil
|
11
11
|
|
12
12
|
# Find the label for this external
|
13
|
-
label
|
14
|
-
not_satisfied("Can't find label for this extern") unless label
|
13
|
+
not_satisfied("Can't find label for this extern", depth: depth) unless label
|
15
14
|
|
16
15
|
schema.external_schemas.each do |schema|
|
17
|
-
extern_shape ||= schema.shapes
|
16
|
+
extern_shape ||= schema.shapes.detect {|s| s.label == label}
|
18
17
|
end
|
19
18
|
|
20
|
-
not_satisfied("External not configured for this shape") unless extern_shape
|
21
|
-
extern_shape.satisfies?(focus)
|
19
|
+
not_satisfied("External not configured for this shape", depth: depth) unless extern_shape
|
20
|
+
extern_shape.satisfies?(focus, depth: depth + 1)
|
21
|
+
end
|
22
|
+
|
23
|
+
def json_type
|
24
|
+
"ShapeExternal"
|
22
25
|
end
|
23
26
|
end
|
24
27
|
end
|
@@ -4,6 +4,16 @@ module ShEx::Algebra
|
|
4
4
|
include TripleExpression
|
5
5
|
NAME = :inclusion
|
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'] == "Inclusion"
|
13
|
+
raise ArgumentError, "missing include in #{operator.inspect}" unless operator.has_key?('include')
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
7
17
|
def initialize(arg, **options)
|
8
18
|
raise ArgumentError, "Shape inclusion must be an IRI or BNode: #{arg}" unless arg.is_a?(RDF::Resource)
|
9
19
|
super
|
@@ -12,17 +22,17 @@ module ShEx::Algebra
|
|
12
22
|
##
|
13
23
|
# In this case, we accept an array of statements, and match based on cardinality.
|
14
24
|
#
|
15
|
-
# @param
|
16
|
-
# @return
|
17
|
-
# @raise
|
18
|
-
def matches(
|
25
|
+
# @param (see TripleExpression#matches)
|
26
|
+
# @return (see TripleExpression#matches)
|
27
|
+
# @raise (see TripleExpression#matches)
|
28
|
+
def matches(arcs_in, arcs_out, depth: 0)
|
19
29
|
status "referenced_shape: #{operands.first}"
|
20
|
-
expression = referenced_shape.
|
30
|
+
expression = referenced_shape.expression
|
21
31
|
max = maximum
|
22
|
-
matched_expression = expression.matches(
|
23
|
-
satisfy matched: matched_expression.matched
|
32
|
+
matched_expression = expression.matches(arcs_in, arcs_out, depth: depth + 1)
|
33
|
+
satisfy matched: matched_expression.matched, depth: depth
|
24
34
|
rescue ShEx::NotMatched => e
|
25
|
-
not_matched e.message, unsatisfied: e.expression
|
35
|
+
not_matched e.message, unsatisfied: e.expression, depth: depth
|
26
36
|
end
|
27
37
|
|
28
38
|
##
|
@@ -30,7 +40,7 @@ module ShEx::Algebra
|
|
30
40
|
#
|
31
41
|
# @return [Operand]
|
32
42
|
def referenced_shape
|
33
|
-
schema.shapes
|
43
|
+
@referenced_shape ||= schema.shapes.detect {|s| s.label == operands.first}
|
34
44
|
end
|
35
45
|
|
36
46
|
##
|
@@ -41,16 +51,17 @@ module ShEx::Algebra
|
|
41
51
|
def validate!
|
42
52
|
structure_error("Missing included shape: #{operands.first}") if referenced_shape.nil?
|
43
53
|
structure_error("Self included shape: #{operands.first}") if referenced_shape == first_ancestor(Shape)
|
44
|
-
|
45
|
-
triple_expressions = referenced_shape.triple_expressions
|
46
|
-
case triple_expressions.length
|
47
|
-
when 0
|
48
|
-
structure_error("Includes shape with no triple expressions")
|
49
|
-
when 1
|
50
|
-
else
|
51
|
-
structure_error("Includes shape with multiple triple expressions")
|
52
|
-
end
|
54
|
+
structure_error("Referenced shape must be a Shape: #{operands.first}") unless referenced_shape.is_a?(Shape)
|
53
55
|
super
|
54
56
|
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Returns the binary S-Expression (SXP) representation of this operator.
|
60
|
+
#
|
61
|
+
# @return [Array]
|
62
|
+
# @see https://en.wikipedia.org/wiki/S-expression
|
63
|
+
def to_sxp_bin
|
64
|
+
([:inclusion, ([:label, @label] if @label)].compact + operands).to_sxp_bin
|
65
|
+
end
|
55
66
|
end
|
56
67
|
end
|
@@ -4,19 +4,28 @@ module ShEx::Algebra
|
|
4
4
|
include Satisfiable
|
5
5
|
NAME = :nodeConstraint
|
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'] == 'NodeConstraint'
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
7
16
|
#
|
8
17
|
# S is a NodeConstraint and satisfies2(focus, 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
18
|
# @param (see Satisfiable#satisfies?)
|
10
19
|
# @return (see Satisfiable#satisfies?)
|
11
20
|
# @raise (see Satisfiable#satisfies?)
|
12
|
-
def satisfies?(focus)
|
13
|
-
status ""
|
14
|
-
satisfies_node_kind?(focus) &&
|
15
|
-
satisfies_datatype?(focus) &&
|
16
|
-
satisfies_string_facet?(focus) &&
|
17
|
-
satisfies_numeric_facet?(focus) &&
|
18
|
-
satisfies_values?(focus) &&
|
19
|
-
satisfy
|
21
|
+
def satisfies?(focus, depth: 0)
|
22
|
+
status "", depth: depth
|
23
|
+
satisfies_node_kind?(focus, depth: depth + 1) &&
|
24
|
+
satisfies_datatype?(focus, depth: depth + 1) &&
|
25
|
+
satisfies_string_facet?(focus, depth: depth + 1) &&
|
26
|
+
satisfies_numeric_facet?(focus, depth: depth + 1) &&
|
27
|
+
satisfies_values?(focus, depth: depth + 1) &&
|
28
|
+
satisfy(depth: depth)
|
20
29
|
end
|
21
30
|
|
22
31
|
private
|
@@ -25,8 +34,8 @@ module ShEx::Algebra
|
|
25
34
|
# Satisfies Node Kind Constraint
|
26
35
|
# @return [Boolean] `true` if satisfied, `false` if it does not apply
|
27
36
|
# @raise [ShEx::NotSatisfied] if not satisfied
|
28
|
-
def satisfies_node_kind?(value)
|
29
|
-
kind = case operands.
|
37
|
+
def satisfies_node_kind?(value, depth: 0)
|
38
|
+
kind = case operands.detect {|o| o.is_a?(Symbol)}
|
30
39
|
when :iri then RDF::URI
|
31
40
|
when :bnode then RDF::Node
|
32
41
|
when :literal then RDF::Literal
|
@@ -34,9 +43,9 @@ module ShEx::Algebra
|
|
34
43
|
else return true
|
35
44
|
end
|
36
45
|
|
37
|
-
not_satisfied "Node was #{value.inspect} expected kind #{kind}" unless
|
46
|
+
not_satisfied "Node was #{value.inspect} expected kind #{kind}", depth: depth unless
|
38
47
|
value.is_a?(kind)
|
39
|
-
status "right kind: #{value}: #{kind}"
|
48
|
+
status "right kind: #{value}: #{kind}", depth: depth
|
40
49
|
true
|
41
50
|
end
|
42
51
|
|
@@ -44,13 +53,13 @@ module ShEx::Algebra
|
|
44
53
|
# Datatype Constraint
|
45
54
|
# @return [Boolean] `true` if satisfied, `false` if it does not apply
|
46
55
|
# @raise [ShEx::NotSatisfied] if not satisfied
|
47
|
-
def satisfies_datatype?(value)
|
48
|
-
dt =
|
56
|
+
def satisfies_datatype?(value, depth: 0)
|
57
|
+
dt = op_fetch(:datatype)
|
49
58
|
return true unless dt
|
50
59
|
|
51
|
-
not_satisfied "Node was #{value.inspect}, expected datatype #{dt}" unless
|
60
|
+
not_satisfied "Node was #{value.inspect}, expected datatype #{dt}", depth: depth unless
|
52
61
|
value.is_a?(RDF::Literal) && value.datatype == RDF::URI(dt)
|
53
|
-
status "right datatype: #{value}: #{dt}"
|
62
|
+
status "right datatype: #{value}: #{dt}", depth: depth
|
54
63
|
true
|
55
64
|
end
|
56
65
|
|
@@ -59,7 +68,7 @@ module ShEx::Algebra
|
|
59
68
|
# Checks all length/minlength/maxlength/pattern facets against the string representation of the value.
|
60
69
|
# @return [Boolean] `true` if satisfied, `false` if it does not apply
|
61
70
|
# @raise [ShEx::NotSatisfied] if not satisfied
|
62
|
-
def satisfies_string_facet?(value)
|
71
|
+
def satisfies_string_facet?(value, depth: 0)
|
63
72
|
length = op_fetch(:length)
|
64
73
|
minlength = op_fetch(:minlength)
|
65
74
|
maxlength = op_fetch(:maxlength)
|
@@ -71,15 +80,15 @@ module ShEx::Algebra
|
|
71
80
|
when RDF::Node then value.id
|
72
81
|
else value.to_s
|
73
82
|
end
|
74
|
-
not_satisfied "Node #{v_s.inspect} length not #{length}" if
|
83
|
+
not_satisfied "Node #{v_s.inspect} length not #{length}", depth: depth if
|
75
84
|
length && v_s.length != length.to_i
|
76
|
-
not_satisfied"Node #{v_s.inspect} length < #{minlength}" if
|
85
|
+
not_satisfied"Node #{v_s.inspect} length < #{minlength}", depth: depth if
|
77
86
|
minlength && v_s.length < minlength.to_i
|
78
|
-
not_satisfied "Node #{v_s.inspect} length > #{maxlength}" if
|
87
|
+
not_satisfied "Node #{v_s.inspect} length > #{maxlength}", depth: depth if
|
79
88
|
maxlength && v_s.length > maxlength.to_i
|
80
|
-
not_satisfied "Node #{v_s.inspect} does not match #{pattern}" if
|
89
|
+
not_satisfied "Node #{v_s.inspect} does not match #{pattern}", depth: depth if
|
81
90
|
pattern && !Regexp.new(pattern).match(v_s)
|
82
|
-
status "right string facet: #{value}"
|
91
|
+
status "right string facet: #{value}", depth: depth
|
83
92
|
true
|
84
93
|
end
|
85
94
|
|
@@ -88,7 +97,7 @@ module ShEx::Algebra
|
|
88
97
|
# Checks all numeric facets against the value.
|
89
98
|
# @return [Boolean] `true` if satisfied, `false` if it does not apply
|
90
99
|
# @raise [ShEx::NotSatisfied] if not satisfied
|
91
|
-
def satisfies_numeric_facet?(value)
|
100
|
+
def satisfies_numeric_facet?(value, depth: 0)
|
92
101
|
mininclusive = op_fetch(:mininclusive)
|
93
102
|
minexclusive = op_fetch(:minexclusive)
|
94
103
|
maxinclusive = op_fetch(:maxinclusive)
|
@@ -98,32 +107,32 @@ module ShEx::Algebra
|
|
98
107
|
|
99
108
|
return true if (mininclusive || minexclusive || maxinclusive || maxexclusive || totaldigits || fractiondigits).nil?
|
100
109
|
|
101
|
-
not_satisfied "Node #{value.inspect} not numeric" unless
|
110
|
+
not_satisfied "Node #{value.inspect} not numeric", depth: depth unless
|
102
111
|
value.is_a?(RDF::Literal::Numeric)
|
103
112
|
|
104
|
-
not_satisfied "Node #{value.inspect} not decimal" if
|
113
|
+
not_satisfied "Node #{value.inspect} not decimal", depth: depth if
|
105
114
|
(totaldigits || fractiondigits) && (!value.is_a?(RDF::Literal::Decimal) || value.invalid?)
|
106
115
|
|
107
116
|
numeric_value = value.object
|
108
117
|
case
|
109
|
-
when !mininclusive.nil? && numeric_value < mininclusive.object then not_satisfied("Node #{value.inspect} < #{mininclusive.object}")
|
110
|
-
when !minexclusive.nil? && numeric_value <= minexclusive.object then not_satisfied("Node #{value.inspect} not <= #{minexclusive.object}")
|
111
|
-
when !maxinclusive.nil? && numeric_value > maxinclusive.object then not_satisfied("Node #{value.inspect} > #{maxinclusive.object}")
|
112
|
-
when !maxexclusive.nil? && numeric_value >= maxexclusive.object then not_satisfied("Node #{value.inspect} >= #{maxexclusive.object}")
|
118
|
+
when !mininclusive.nil? && numeric_value < mininclusive.object then not_satisfied("Node #{value.inspect} < #{mininclusive.object}", depth: depth)
|
119
|
+
when !minexclusive.nil? && numeric_value <= minexclusive.object then not_satisfied("Node #{value.inspect} not <= #{minexclusive.object}", depth: depth)
|
120
|
+
when !maxinclusive.nil? && numeric_value > maxinclusive.object then not_satisfied("Node #{value.inspect} > #{maxinclusive.object}", depth: depth)
|
121
|
+
when !maxexclusive.nil? && numeric_value >= maxexclusive.object then not_satisfied("Node #{value.inspect} >= #{maxexclusive.object}", depth: depth)
|
113
122
|
when !totaldigits.nil?
|
114
123
|
md = value.canonicalize.to_s.match(/([1-9]\d*|0)?(?:\.(\d+)(?!0))?/)
|
115
124
|
digits = md ? (md[1].to_s + md[2].to_s) : ""
|
116
125
|
if digits.length > totaldigits.to_i
|
117
|
-
not_satisfied "Node #{value.inspect} total digits != #{totaldigits}"
|
126
|
+
not_satisfied "Node #{value.inspect} total digits != #{totaldigits}", depth: depth
|
118
127
|
end
|
119
128
|
when !fractiondigits.nil?
|
120
129
|
md = value.canonicalize.to_s.match(/\.(\d+)(?!0)?/)
|
121
130
|
num = md ? md[1].to_s : ""
|
122
131
|
if num.length > fractiondigits.to_i
|
123
|
-
not_satisfied "Node #{value.inspect} fractional digits != #{fractiondigits}"
|
132
|
+
not_satisfied "Node #{value.inspect} fractional digits != #{fractiondigits}", depth: depth
|
124
133
|
end
|
125
134
|
end
|
126
|
-
status "right numeric facet: #{value}"
|
135
|
+
status "right numeric facet: #{value}", depth: depth
|
127
136
|
true
|
128
137
|
end
|
129
138
|
|
@@ -132,12 +141,12 @@ module ShEx::Algebra
|
|
132
141
|
# Checks all numeric facets against the value.
|
133
142
|
# @return [Boolean] `true` if satisfied, `false` if it does not apply
|
134
143
|
# @raise [ShEx::NotSatisfied] if not satisfied
|
135
|
-
def satisfies_values?(value)
|
144
|
+
def satisfies_values?(value, depth: 0)
|
136
145
|
values = operands.select {|op| op.is_a?(Value)}
|
137
146
|
return true if values.empty?
|
138
|
-
matched_value = values.detect {|v| v.match?(value)}
|
139
|
-
not_satisfied "
|
140
|
-
status "right value: #{value}"
|
147
|
+
matched_value = values.detect {|v| v.match?(value, depth: depth + 1)}
|
148
|
+
not_satisfied "Value #{value.to_sxp} not expected, wanted #{values.to_sxp}", depth: depth unless matched_value
|
149
|
+
status "right value: #{value}", depth: depth
|
141
150
|
true
|
142
151
|
end
|
143
152
|
|
data/lib/shex/algebra/not.rb
CHANGED
@@ -4,20 +4,35 @@ module ShEx::Algebra
|
|
4
4
|
include Satisfiable
|
5
5
|
NAME = :not
|
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'] == 'ShapeNot'
|
13
|
+
raise ArgumentError, "missing shapeExpr in #{operator.inspect}" unless operator.has_key?('shapeExpr')
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
7
17
|
#
|
8
18
|
# S is a ShapeNot and for the shape expression se2 at shapeExpr, notSatisfies(n, se2, G, m).
|
9
19
|
# @param (see Satisfiable#satisfies?)
|
10
20
|
# @return (see Satisfiable#satisfies?)
|
11
21
|
# @raise (see Satisfiable#satisfies?)
|
12
22
|
# @see [https://shexspec.github.io/spec/#shape-expression-semantics]
|
13
|
-
def satisfies?(focus)
|
23
|
+
def satisfies?(focus, depth: 0)
|
14
24
|
status ""
|
15
25
|
satisfied_op = begin
|
16
|
-
operands.
|
26
|
+
operands.last.satisfies?(focus, depth: depth + 1)
|
17
27
|
rescue ShEx::NotSatisfied => e
|
18
|
-
return satisfy satisfied: e.expression.unsatisfied
|
28
|
+
return satisfy focus: focus, satisfied: e.expression.unsatisfied, depth: depth
|
19
29
|
end
|
20
|
-
not_satisfied "Expression should not have matched",
|
30
|
+
not_satisfied "Expression should not have matched",
|
31
|
+
focus: focus, unsatisfied: satisfied_op, depth: depth
|
32
|
+
end
|
33
|
+
|
34
|
+
def json_type
|
35
|
+
"ShapeNot"
|
21
36
|
end
|
22
37
|
end
|
23
38
|
end
|
data/lib/shex/algebra/one_of.rb
CHANGED
@@ -4,12 +4,23 @@ module ShEx::Algebra
|
|
4
4
|
include TripleExpression
|
5
5
|
NAME = :oneOf
|
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'] == 'OneOf'
|
13
|
+
raise ArgumentError, "missing expressions in #{operator.inspect}" unless operator.has_key?('expressions')
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
7
17
|
##
|
8
18
|
# `expr` is a OneOf and there is some shape expression `se2` in shapeExprs such that a `matches(T, se2, m)`...
|
9
19
|
#
|
10
|
-
# @param
|
11
|
-
# @return
|
12
|
-
|
20
|
+
# @param (see TripleExpression#matches)
|
21
|
+
# @return (see TripleExpression#matches)
|
22
|
+
# @raise (see TripleExpression#matches)
|
23
|
+
def matches(arcs_in, arcs_out, depth: 0)
|
13
24
|
results, satisfied, unsatisfied = [], [], []
|
14
25
|
num_iters, max = 0, maximum
|
15
26
|
|
@@ -18,22 +29,21 @@ module ShEx::Algebra
|
|
18
29
|
while num_iters < max
|
19
30
|
matched_something = operands.select {|o| o.is_a?(TripleExpression)}.any? do |op|
|
20
31
|
begin
|
21
|
-
matched_op = op.matches(
|
32
|
+
matched_op = op.matches(arcs_in, arcs_out, depth: depth + 1)
|
22
33
|
satisfied << matched_op
|
23
34
|
results += matched_op.matched
|
24
|
-
|
25
|
-
|
35
|
+
arcs_in -= matched_op.matched
|
36
|
+
arcs_out -= matched_op.matched
|
37
|
+
status "matched #{matched_op.matched.to_sxp}", depth: depth
|
26
38
|
rescue ShEx::NotMatched => e
|
27
|
-
status "not matched: #{e.message}"
|
28
|
-
|
29
|
-
op.unmatched = statements - results
|
30
|
-
unsatisfied << op
|
39
|
+
status "not matched: #{e.message}", depth: depth
|
40
|
+
unsatisfied << e.expression
|
31
41
|
false
|
32
42
|
end
|
33
43
|
end
|
34
44
|
break unless matched_something
|
35
45
|
num_iters += 1
|
36
|
-
status "matched #{results.length} statements after #{num_iters} iterations"
|
46
|
+
status "matched #{results.length} statements after #{num_iters} iterations", depth: depth
|
37
47
|
end
|
38
48
|
|
39
49
|
# Max violations handled in Shape
|
@@ -42,15 +52,15 @@ module ShEx::Algebra
|
|
42
52
|
end
|
43
53
|
|
44
54
|
# Last, evaluate semantic acts
|
45
|
-
semantic_actions.
|
46
|
-
op.satisfies?(results)
|
55
|
+
semantic_actions.each do |op|
|
56
|
+
op.satisfies?(matched: results, depth: depth + 1)
|
47
57
|
end unless results.empty?
|
48
58
|
|
49
|
-
satisfy matched: results, satisfied: satisfied,
|
59
|
+
satisfy matched: results, satisfied: satisfied, depth: depth
|
50
60
|
rescue ShEx::NotMatched, ShEx::NotSatisfied => e
|
51
61
|
not_matched e.message,
|
52
|
-
matched: results, unmatched: (
|
53
|
-
satisfied: satisfied, unsatisfied: unsatisfied
|
62
|
+
matched: results, unmatched: ((arcs_in + arcs_out).uniq - results),
|
63
|
+
satisfied: satisfied, unsatisfied: unsatisfied, depth: depth
|
54
64
|
end
|
55
65
|
end
|
56
66
|
end
|
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'sparql/algebra'
|
2
|
+
require 'json/ld/preloaded'
|
3
|
+
require 'shex/shex_context'
|
2
4
|
|
3
5
|
module ShEx::Algebra
|
4
6
|
|
@@ -30,6 +32,8 @@ module ShEx::Algebra
|
|
30
32
|
# any additional options
|
31
33
|
# @option options [Boolean] :memoize (false)
|
32
34
|
# whether to memoize results for particular operands
|
35
|
+
# @option options [RDF::Resource] :label
|
36
|
+
# Identifier of the operator
|
33
37
|
# @raise [TypeError] if any operand is invalid
|
34
38
|
def initialize(*operands)
|
35
39
|
@options = operands.last.is_a?(Hash) ? operands.pop.dup : {}
|
@@ -51,15 +55,7 @@ module ShEx::Algebra
|
|
51
55
|
end
|
52
56
|
end
|
53
57
|
|
54
|
-
|
55
|
-
options[:depth] = 0
|
56
|
-
each_descendant(1) do |depth, operand|
|
57
|
-
if operand.respond_to?(:options)
|
58
|
-
operand.options[:logger] = options[:logger]
|
59
|
-
operand.options[:depth] = depth
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
58
|
+
@label = options[:label]
|
63
59
|
end
|
64
60
|
|
65
61
|
##
|
@@ -85,6 +81,16 @@ module ShEx::Algebra
|
|
85
81
|
# Does this operator a SemAct?
|
86
82
|
def semact?; false; end
|
87
83
|
|
84
|
+
##
|
85
|
+
# On a result instance, the focus of the expression
|
86
|
+
def focus
|
87
|
+
Array(operands.detect {|op| op.is_a?(Array) && op[0] == :focus} || [:focus])[1]
|
88
|
+
end
|
89
|
+
def focus=(node)
|
90
|
+
operands.delete_if {|op| op.is_a?(Array) && op[0] == :focus}
|
91
|
+
operands << [:focus, node]
|
92
|
+
end
|
93
|
+
|
88
94
|
##
|
89
95
|
# On a result instance, the statements that matched this expression.
|
90
96
|
# @return [Array<Statement>]
|
@@ -129,13 +135,26 @@ module ShEx::Algebra
|
|
129
135
|
operands << ops.unshift(:unsatisfied) unless (ops || []).empty?
|
130
136
|
end
|
131
137
|
|
138
|
+
##
|
139
|
+
# On a result instance, the failure message. (failure only).
|
140
|
+
# @return [String]
|
141
|
+
def message
|
142
|
+
(operands.detect {|op| op.is_a?(Array) && op[0] == :message} || [:message])[1..-1]
|
143
|
+
end
|
144
|
+
def message=(str)
|
145
|
+
operands.delete_if {|op| op.is_a?(Array) && op[0] == :message}
|
146
|
+
operands << [:message, str]
|
147
|
+
end
|
148
|
+
|
132
149
|
##
|
133
150
|
# Duplication this operand, and add `matched`, `unmatched`, `satisfied`, and `unsatisfied` operands for accessing downstream.
|
134
151
|
#
|
135
152
|
# @return [Operand]
|
136
|
-
def satisfy(matched: nil, unmatched: nil, satisfied: nil, unsatisfied: nil)
|
137
|
-
log_debug(self.class.const_get(:NAME), "satisfied",
|
153
|
+
def satisfy(focus: nil, matched: nil, unmatched: nil, satisfied: nil, unsatisfied: nil, message: nil, **opts)
|
154
|
+
log_debug(self.class.const_get(:NAME), "satisfied", **opts) unless message
|
138
155
|
expression = self.dup
|
156
|
+
expression.message = message if message
|
157
|
+
expression.focus = focus if focus
|
139
158
|
expression.matched = Array(matched) if matched
|
140
159
|
expression.unmatched = Array(unmatched) if unmatched
|
141
160
|
expression.satisfied = Array(satisfied) if satisfied
|
@@ -145,25 +164,17 @@ module ShEx::Algebra
|
|
145
164
|
|
146
165
|
##
|
147
166
|
# Exception handling
|
148
|
-
def not_matched(message,
|
149
|
-
expression = opts.fetch(:expression, self).satisfy(
|
150
|
-
matched: matched,
|
151
|
-
unmatched: unmatched,
|
152
|
-
satisfied: satisfied,
|
153
|
-
unsatisfied: unsatisfied)
|
167
|
+
def not_matched(message, **opts, &block)
|
168
|
+
expression = opts.fetch(:expression, self).satisfy(message: message, **opts)
|
154
169
|
exception = opts.fetch(:exception, ShEx::NotMatched)
|
155
|
-
status(message) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
|
170
|
+
status(message, **opts) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
|
156
171
|
raise exception.new(message, expression: expression)
|
157
172
|
end
|
158
173
|
|
159
|
-
def not_satisfied(message,
|
160
|
-
expression = opts.fetch(:expression, self).satisfy(
|
161
|
-
matched: matched,
|
162
|
-
unmatched: unmatched,
|
163
|
-
satisfied: satisfied,
|
164
|
-
unsatisfied: unsatisfied)
|
174
|
+
def not_satisfied(message, **opts)
|
175
|
+
expression = opts.fetch(:expression, self).satisfy(message: message, **opts)
|
165
176
|
exception = opts.fetch(:exception, ShEx::NotSatisfied)
|
166
|
-
status(message) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
|
177
|
+
status(message, **opts) {(block_given? ? block.call : "") + "expression: #{expression.to_sxp}"}
|
167
178
|
raise exception.new(message, expression: expression)
|
168
179
|
end
|
169
180
|
|
@@ -173,8 +184,8 @@ module ShEx::Algebra
|
|
173
184
|
log_error(message, depth: options.fetch(:depth, 0), exception: exception) {"expression: #{expression.to_sxp}"}
|
174
185
|
end
|
175
186
|
|
176
|
-
def status(message, &block)
|
177
|
-
log_debug(self.class.const_get(:NAME)
|
187
|
+
def status(message, **opts, &block)
|
188
|
+
log_debug(self.class.const_get(:NAME).to_s + (@label ? "(#{@label})" : ""), message, **opts, &block)
|
178
189
|
true
|
179
190
|
end
|
180
191
|
|
@@ -184,6 +195,16 @@ module ShEx::Algebra
|
|
184
195
|
# @return [Array]
|
185
196
|
attr_reader :operands
|
186
197
|
|
198
|
+
##
|
199
|
+
# The label (or subject) of this operand
|
200
|
+
# @return [RDF::Resource]
|
201
|
+
attr_accessor :label
|
202
|
+
|
203
|
+
##
|
204
|
+
# Logging support (reader is in RDF::Util::Logger)
|
205
|
+
# @return [Logger]
|
206
|
+
attr_writer :logger
|
207
|
+
|
187
208
|
##
|
188
209
|
# Returns the operand at the given `index`.
|
189
210
|
#
|
@@ -200,8 +221,9 @@ module ShEx::Algebra
|
|
200
221
|
# @return [Array]
|
201
222
|
# @see https://en.wikipedia.org/wiki/S-expression
|
202
223
|
def to_sxp_bin
|
203
|
-
|
204
|
-
[
|
224
|
+
[self.class.const_get(:NAME)] +
|
225
|
+
(label ? [[:label, label]] : []) +
|
226
|
+
(operands || []).map(&:to_sxp_bin)
|
205
227
|
end
|
206
228
|
|
207
229
|
##
|
@@ -219,6 +241,286 @@ module ShEx::Algebra
|
|
219
241
|
to_sxp_bin.to_sxp
|
220
242
|
end
|
221
243
|
|
244
|
+
##
|
245
|
+
# Creates an operator instance from a parsed ShExJ representation
|
246
|
+
# @param [Hash] operator
|
247
|
+
# @param [Hash] options ({})
|
248
|
+
# @option options [RDF::URI] :base
|
249
|
+
# @option options [Hash{String => RDF::URI}] :prefixes
|
250
|
+
# @return [Operator]
|
251
|
+
def self.from_shexj(operator, options = {})
|
252
|
+
options[:context] ||= JSON::LD::Context.parse(ShEx::CONTEXT)
|
253
|
+
operands = []
|
254
|
+
label = nil
|
255
|
+
|
256
|
+
operator.each do |k, v|
|
257
|
+
case k
|
258
|
+
when /length|pattern|clusive/ then operands << [k.to_sym, v]
|
259
|
+
when 'label' then label = iri(v, options)
|
260
|
+
when 'min', 'max', 'inverse', 'closed' then operands << [k.to_sym, v]
|
261
|
+
when 'nodeKind' then operands << v.to_sym
|
262
|
+
when 'object' then operands << value(v, options)
|
263
|
+
when 'start' then operands << Start.new(ShEx::Algebra.from_shexj(v, options))
|
264
|
+
when '@context' then
|
265
|
+
options[:context] = JSON::LD::Context.parse(v)
|
266
|
+
options[:base_uri] = options[:context].base
|
267
|
+
when 'shapes'
|
268
|
+
operands << case v
|
269
|
+
when Array
|
270
|
+
[:shapes] + v.map {|vv| ShEx::Algebra.from_shexj(vv, options)}
|
271
|
+
else
|
272
|
+
raise "Expected value of shapes #{v.inspect}"
|
273
|
+
end
|
274
|
+
when 'reference', 'include', 'stem', 'name'
|
275
|
+
# Value may be :wildcard for stem
|
276
|
+
operands << (v.is_a?(Symbol) ? v : iri(v, options))
|
277
|
+
when 'predicate' then operands << iri(v, options)
|
278
|
+
when 'extra', 'datatype'
|
279
|
+
v = [v] unless v.is_a?(Array)
|
280
|
+
operands << (v.map {|op| iri(op, options)}).unshift(k.to_sym)
|
281
|
+
when 'exclusions'
|
282
|
+
v = [v] unless v.is_a?(Array)
|
283
|
+
operands << v.map do |op|
|
284
|
+
op.is_a?(Hash) ?
|
285
|
+
ShEx::Algebra.from_shexj(op, options) :
|
286
|
+
value(op, options)
|
287
|
+
end.unshift(:exclusions)
|
288
|
+
when 'min', 'max', 'inverse', 'closed', 'valueExpr', 'semActs',
|
289
|
+
'shapeExpr', 'shapeExprs', 'startActs', 'expression',
|
290
|
+
'expressions', 'annotations'
|
291
|
+
v = [v] unless v.is_a?(Array)
|
292
|
+
operands += v.map {|op| ShEx::Algebra.from_shexj(op, options)}
|
293
|
+
when 'code'
|
294
|
+
operands << v
|
295
|
+
when 'values'
|
296
|
+
v = [v] unless v.is_a?(Array)
|
297
|
+
operands += v.map do |op|
|
298
|
+
Value.new(value(op, options))
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
new(*operands, label: label)
|
304
|
+
end
|
305
|
+
|
306
|
+
def json_type
|
307
|
+
self.class.name.split('::').last
|
308
|
+
end
|
309
|
+
|
310
|
+
def to_json(options = nil)
|
311
|
+
self.to_h.to_json(options)
|
312
|
+
end
|
313
|
+
|
314
|
+
##
|
315
|
+
# Create a hash version of the operator, suitable for turning into JSON.
|
316
|
+
# @return [Hash]
|
317
|
+
def to_h
|
318
|
+
obj = json_type == 'Schema' ?
|
319
|
+
{'@context' => ShEx::CONTEXT, 'type' => json_type} :
|
320
|
+
{'type' => json_type}
|
321
|
+
obj['label'] = label.to_s if label
|
322
|
+
operands.each do |op|
|
323
|
+
case op
|
324
|
+
when Array
|
325
|
+
# First element should be a symbol
|
326
|
+
case sym = op.first
|
327
|
+
when :datatype,
|
328
|
+
:pattern then obj[op.first.to_s] = op.last.to_s
|
329
|
+
when :exclusions then obj['exclusions'] = Array(op[1..-1]).map {|v| serialize_value(v)}
|
330
|
+
when :extra then (obj['extra'] ||= []).concat Array(op[1..-1]).map(&:to_s)
|
331
|
+
# FIXME Shapes should be an array, not a hash
|
332
|
+
when :shapes then obj['shapes'] = Array(op[1..-1]).map {|v| v.to_h}
|
333
|
+
when :minlength,
|
334
|
+
:maxlength,
|
335
|
+
:length,
|
336
|
+
:mininclusive,
|
337
|
+
:maxinclusive,
|
338
|
+
:minexclusive,
|
339
|
+
:maxexclusive,
|
340
|
+
:totaldigits,
|
341
|
+
:fractiondigits then obj[op.first.to_s] = op.last.object
|
342
|
+
when :min, :max then obj[op.first.to_s] = op.last
|
343
|
+
when :base, :prefix
|
344
|
+
# Ignore base and prefix
|
345
|
+
when Symbol then obj[sym.to_s] = Array(op[1..-1]).map(&:to_h)
|
346
|
+
else
|
347
|
+
raise "Expected array to start with a symbol for #{self}"
|
348
|
+
end
|
349
|
+
when :wildcard then obj['stem'] = {'type' => 'Wildcard'}
|
350
|
+
when Annotation then (obj['annotations'] ||= []) << op.to_h
|
351
|
+
when SemAct then (obj[is_a?(Schema) ? 'startActs' : 'semActs'] ||= []) << op.to_h
|
352
|
+
when Start then obj['start'] = op.operands.first.to_h
|
353
|
+
when RDF::Value
|
354
|
+
case self
|
355
|
+
when TripleConstraint then obj['predicate'] = op.to_s
|
356
|
+
when Stem, StemRange then obj['stem'] = op.to_s
|
357
|
+
when Inclusion then obj['include'] = op.to_s
|
358
|
+
when ShapeRef then obj['reference'] = op.to_s
|
359
|
+
when SemAct then obj[op.is_a?(RDF::URI) ? 'name' : 'code'] = op.to_s
|
360
|
+
else
|
361
|
+
raise "How to serialize Value #{op.inspect} to json for #{self}"
|
362
|
+
end
|
363
|
+
when Symbol
|
364
|
+
case self
|
365
|
+
when NodeConstraint then obj['nodeKind'] = op.to_s
|
366
|
+
when Shape then obj['closed'] = true
|
367
|
+
when TripleConstraint then obj['inverse'] = true
|
368
|
+
else
|
369
|
+
raise "How to serialize Symbol #{op.inspect} to json for #{self}"
|
370
|
+
end
|
371
|
+
when TripleConstraint, EachOf, OneOf, Inclusion
|
372
|
+
case self
|
373
|
+
when EachOf, OneOf
|
374
|
+
(obj['expressions'] ||= []) << op.to_h
|
375
|
+
else
|
376
|
+
obj['expression'] = op.to_h
|
377
|
+
end
|
378
|
+
when NodeConstraint
|
379
|
+
case self
|
380
|
+
when And, Or
|
381
|
+
(obj['shapeExprs'] ||= []) << op.to_h
|
382
|
+
else
|
383
|
+
obj['valueExpr'] = op.to_h
|
384
|
+
end
|
385
|
+
when And, Or, Shape, Not, ShapeRef
|
386
|
+
case self
|
387
|
+
when And, Or
|
388
|
+
(obj['shapeExprs'] ||= []) << op.to_h
|
389
|
+
when TripleConstraint
|
390
|
+
obj['valueExpr'] = op.to_h
|
391
|
+
else
|
392
|
+
obj['shapeExpr'] = op.to_h
|
393
|
+
end
|
394
|
+
when Value
|
395
|
+
obj['values'] ||= []
|
396
|
+
Array(op).map {|o| o.operands}.flatten.each do |oo|
|
397
|
+
obj['values'] << serialize_value(oo)
|
398
|
+
end
|
399
|
+
else
|
400
|
+
raise "How to serialize #{op.inspect} to json for #{self}"
|
401
|
+
end
|
402
|
+
end
|
403
|
+
obj
|
404
|
+
end
|
405
|
+
|
406
|
+
##
|
407
|
+
# Returns the Base URI defined for the parser,
|
408
|
+
# as specified or when parsing a BASE prologue element.
|
409
|
+
#
|
410
|
+
# @example
|
411
|
+
# base #=> RDF::URI('http://example.com/')
|
412
|
+
#
|
413
|
+
# @return [HRDF::URI]
|
414
|
+
def base_uri
|
415
|
+
@options[:base_uri]
|
416
|
+
end
|
417
|
+
|
418
|
+
# Create URIs
|
419
|
+
# @param [RDF::Value, String] value
|
420
|
+
# @param [Hash{Symbol => Object}] options
|
421
|
+
# @option options [RDF::URI] :base_uri
|
422
|
+
# @option options [Hash{String => RDF::URI}] :prefixes
|
423
|
+
# @option options [JSON::LD::Context] :context
|
424
|
+
# @return [RDF::Value]
|
425
|
+
def iri(value, options = @options)
|
426
|
+
self.class.iri(value, options)
|
427
|
+
end
|
428
|
+
|
429
|
+
# Create URIs
|
430
|
+
# @param (see #iri)
|
431
|
+
# @option (see #iri)
|
432
|
+
# @return (see #iri)
|
433
|
+
def self.iri(value, options)
|
434
|
+
# If we have a base URI, use that when constructing a new URI
|
435
|
+
base_uri = options[:base_uri]
|
436
|
+
|
437
|
+
case value
|
438
|
+
when Hash
|
439
|
+
# A JSON-LD node reference
|
440
|
+
v = options[:context].expand_value(value)
|
441
|
+
raise "Expected #{value.inspect} to be a JSON-LD Node Reference" unless JSON::LD::Utils.node_reference?(v)
|
442
|
+
self.iri(v['@id'], options)
|
443
|
+
when RDF::URI
|
444
|
+
if base_uri && value.relative?
|
445
|
+
base_uri.join(value)
|
446
|
+
else
|
447
|
+
value
|
448
|
+
end
|
449
|
+
when RDF::Value then value
|
450
|
+
when /^_:/ then
|
451
|
+
id = value[2..-1].to_s
|
452
|
+
RDF::Node.intern(id)
|
453
|
+
when /^(\w+):(\S+)$/
|
454
|
+
prefixes = options.fetch(:prefixes, {})
|
455
|
+
if prefixes.has_key?($1)
|
456
|
+
prefixes[$1].join($2)
|
457
|
+
elsif RDF.type == value
|
458
|
+
a = RDF.type.dup; a.lexical = 'a'; a
|
459
|
+
elsif options[:context]
|
460
|
+
options[:context].expand_iri(value, vocab: true)
|
461
|
+
else
|
462
|
+
RDF::URI(value)
|
463
|
+
end
|
464
|
+
else
|
465
|
+
if options[:context]
|
466
|
+
options[:context].expand_iri(value, document: true)
|
467
|
+
elsif base_uri
|
468
|
+
base_uri.join(value)
|
469
|
+
elsif base_uri
|
470
|
+
base_uri.join(value)
|
471
|
+
else
|
472
|
+
RDF::URI(value)
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
# Create Values, with "clever" matching to see if it might be a value, IRI or BNode.
|
478
|
+
# @param [RDF::Value, String] value
|
479
|
+
# @param [Hash{Symbol => Object}] options
|
480
|
+
# @option options [RDF::URI] :base_uri
|
481
|
+
# @option options [Hash{String => RDF::URI}] :prefixes
|
482
|
+
# @return [RDF::Value]
|
483
|
+
def value(value, options = @options)
|
484
|
+
self.class.value(value, options)
|
485
|
+
end
|
486
|
+
|
487
|
+
# Create Values, with "clever" matching to see if it might be a value, IRI or BNode.
|
488
|
+
# @param (see #value)
|
489
|
+
# @option (see #value)
|
490
|
+
# @return (see #value)
|
491
|
+
def self.value(value, options)
|
492
|
+
# If we have a base URI, use that when constructing a new URI
|
493
|
+
case value
|
494
|
+
when Hash
|
495
|
+
# Either a value object or a node reference
|
496
|
+
if value['uri']
|
497
|
+
iri(value['uri'], options)
|
498
|
+
elsif value['value']
|
499
|
+
RDF::Literal(value['value'], datatype: value['type'], language: value['language'])
|
500
|
+
else
|
501
|
+
ShEx::Algebra.from_shexj(value, options)
|
502
|
+
end
|
503
|
+
else iri(value, options)
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
##
|
508
|
+
# Serialize a value, either as JSON, or as modififed N-Triples
|
509
|
+
#
|
510
|
+
# @param [RDF::Value, Operator] value
|
511
|
+
# @return [String]
|
512
|
+
def serialize_value(value)
|
513
|
+
case value
|
514
|
+
when RDF::Literal
|
515
|
+
{'value' => value.to_s}.
|
516
|
+
merge(value.has_datatype? ? {'type' => value.datatype.to_s} : {}).
|
517
|
+
merge(value.has_language? ? {'language' => value.language.to_s} : {})
|
518
|
+
when RDF::Resource
|
519
|
+
value.to_s
|
520
|
+
else value.to_h
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
222
524
|
##
|
223
525
|
# Returns a developer-friendly representation of this operator.
|
224
526
|
#
|
@@ -243,6 +545,12 @@ module ShEx::Algebra
|
|
243
545
|
# @return [Enumerator]
|
244
546
|
def each_descendant(depth = 0, &block)
|
245
547
|
if block_given?
|
548
|
+
|
549
|
+
case block.arity
|
550
|
+
when 1 then block.call(self)
|
551
|
+
else block.call(depth, self)
|
552
|
+
end
|
553
|
+
|
246
554
|
operands.each do |operand|
|
247
555
|
case operand
|
248
556
|
when Array
|
@@ -252,11 +560,6 @@ module ShEx::Algebra
|
|
252
560
|
else
|
253
561
|
operand.each_descendant(depth + 1, &block) if operand.respond_to?(:each_descendant)
|
254
562
|
end
|
255
|
-
|
256
|
-
case block.arity
|
257
|
-
when 1 then block.call(operand)
|
258
|
-
else block.call(depth, operand)
|
259
|
-
end
|
260
563
|
end
|
261
564
|
end
|
262
565
|
enum_for(:each_descendant)
|
@@ -296,6 +599,19 @@ module ShEx::Algebra
|
|
296
599
|
self
|
297
600
|
end
|
298
601
|
|
602
|
+
protected
|
603
|
+
def dup
|
604
|
+
operands = @operands.map {|o| o.dup rescue o}
|
605
|
+
self.class.new(*operands, label: @label)
|
606
|
+
end
|
607
|
+
|
608
|
+
##
|
609
|
+
# Implement `to_hash` only if accessed; otherwise, it becomes an _Implicit Accessor_ which will cause problems with splat arguments, which causes the last to be turned into a hash for extracting keyword aruments.
|
610
|
+
def method_missing(method, *args)
|
611
|
+
return to_h(*args) if method == :hash
|
612
|
+
super
|
613
|
+
end
|
614
|
+
|
299
615
|
##
|
300
616
|
# A unary operator.
|
301
617
|
#
|