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