shacl 0.2.1 → 0.4.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 +11 -6
- data/VERSION +1 -1
- data/lib/rdf/vocab/shacl.rb +1096 -1096
- data/lib/shacl/algebra/and.rb +3 -2
- data/lib/shacl/algebra/constraint_component.rb +67 -0
- data/lib/shacl/algebra/node_shape.rb +10 -16
- data/lib/shacl/algebra/not.rb +3 -2
- data/lib/shacl/algebra/operator.rb +312 -44
- data/lib/shacl/algebra/or.rb +3 -2
- data/lib/shacl/algebra/pattern.rb +43 -0
- data/lib/shacl/algebra/property_shape.rb +11 -16
- data/lib/shacl/algebra/{qualified_value_shape.rb → qualified_value.rb} +14 -8
- data/lib/shacl/algebra/shape.rb +12 -41
- data/lib/shacl/algebra/sparql_constraint.rb +160 -0
- data/lib/shacl/algebra/xone.rb +3 -2
- data/lib/shacl/algebra.rb +30 -13
- data/lib/shacl/context.rb +5 -0
- data/lib/shacl/shapes.rb +99 -14
- data/lib/shacl/validation_result.rb +22 -19
- data/lib/shacl.rb +10 -5
- metadata +36 -19
data/lib/shacl/algebra/and.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class AndConstraintComponent < ConstraintComponent
|
4
4
|
NAME = :and
|
5
5
|
|
6
6
|
##
|
@@ -25,7 +25,8 @@ module SHACL::Algebra
|
|
25
25
|
# ]
|
26
26
|
# ) .
|
27
27
|
#
|
28
|
-
# @param [RDF::Term] node
|
28
|
+
# @param [RDF::Term] node focus node
|
29
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
29
30
|
# @param [Hash{Symbol => Object}] options
|
30
31
|
# @return [Array<SHACL::ValidationResult>]
|
31
32
|
def conforms(node, path: nil, depth: 0, **options)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative "operator"
|
2
|
+
|
3
|
+
module SHACL::Algebra
|
4
|
+
##
|
5
|
+
# Constraint Components define basic constraint behaivor through _mandatory_ and _optional_ parameters. Constraints are accessed through their parameters.
|
6
|
+
#
|
7
|
+
class ConstraintComponent < Operator
|
8
|
+
|
9
|
+
# Class Methods
|
10
|
+
class << self
|
11
|
+
##
|
12
|
+
# Creates an operator instance from a parsed SHACL representation.
|
13
|
+
#
|
14
|
+
# Special case for SPARQL ConstraintComponents.
|
15
|
+
#
|
16
|
+
# @param [Hash] operator
|
17
|
+
# @param [Hash] options ({})
|
18
|
+
# @option options [Hash{String => RDF::URI}] :prefixes
|
19
|
+
# @return [Operator]
|
20
|
+
def from_json(operator, **options)
|
21
|
+
operands = []
|
22
|
+
|
23
|
+
# Component is known by its subject IRI
|
24
|
+
id = operator.fetch('id')
|
25
|
+
|
26
|
+
# Component class (for instantiation) is based on the _local name_ of the component IRI
|
27
|
+
class_name = ncname(id)
|
28
|
+
|
29
|
+
parameters = operator.fetch('parameter', []).inject({}) do |memo, param|
|
30
|
+
# Symbolize keys
|
31
|
+
param = param.inject({}) {|memo, (k,v)| memo.merge(k.to_sym => v)}
|
32
|
+
|
33
|
+
plc = ncname(param[:path])
|
34
|
+
|
35
|
+
# Add class and local name
|
36
|
+
param = param.merge(class: class_name, local_name: plc)
|
37
|
+
memo.merge(param[:path] => param)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Add parameters to operator lookup
|
41
|
+
add_component(class_name, parameters)
|
42
|
+
|
43
|
+
# Add parameter identifiers to operands
|
44
|
+
operands << [:parameters, parameters.keys]
|
45
|
+
|
46
|
+
# FIXME: labelTemplate
|
47
|
+
|
48
|
+
validator = %w(validator nodeValidator propertyValidator).inject(nil) do |memo, p|
|
49
|
+
memo || (SPARQLConstraintComponent.from_json(operator[p]) if operator.key?(p))
|
50
|
+
end
|
51
|
+
raise SHACL::Error, "Constraint Component has no validator" unless validator
|
52
|
+
|
53
|
+
operands << [:validator, validator]
|
54
|
+
|
55
|
+
new(*operands, **options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Extract the NCName tail of an IRI as a symbol.
|
59
|
+
#
|
60
|
+
# @param [RDF::URI] uri
|
61
|
+
# @return [Symbol]
|
62
|
+
def ncname(uri)
|
63
|
+
uri.to_s.match(/(\w+)$/).to_s.to_sym
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -9,30 +9,22 @@ module SHACL::Algebra
|
|
9
9
|
#
|
10
10
|
# A node conforms if it is not deactivated and all of its operands conform.
|
11
11
|
#
|
12
|
-
# @param [RDF::Term] node
|
12
|
+
# @param [RDF::Term] node focus node
|
13
13
|
# @param [Hash{Symbol => Object}] options
|
14
14
|
# @return [Array<SHACL::ValidationResult>]
|
15
15
|
# Returns one or more validation results for each operand.
|
16
16
|
def conforms(node, depth: 0, **options)
|
17
17
|
return [] if deactivated?
|
18
18
|
options = id ? options.merge(shape: id) : options
|
19
|
-
options = options
|
19
|
+
options[:severity] = @options[:severity] if @options[:severity]
|
20
|
+
options[:severity] ||= RDF::Vocab::SHACL.Violation
|
20
21
|
log_debug(NAME, depth: depth) {SXP::Generator.string({id: id, node: node}.to_sxp_bin)}
|
21
22
|
|
22
|
-
# Add some instance options to the argument
|
23
|
-
options = %i{
|
24
|
-
flags
|
25
|
-
qualifiedMinCount
|
26
|
-
qualifiedMaxCount
|
27
|
-
qualifiedValueShapesDisjoint
|
28
|
-
severity
|
29
|
-
}.inject(options) do |memo, sym|
|
30
|
-
@options[sym] ? memo.merge(sym => @options[sym]) : memo
|
31
|
-
end
|
32
|
-
|
33
23
|
# Evaluate against builtins
|
34
24
|
builtin_results = @options.map do |k, v|
|
35
|
-
self.send("builtin_#{k}".to_sym, v, node, nil, [node],
|
25
|
+
self.send("builtin_#{k}".to_sym, v, node, nil, [node],
|
26
|
+
depth: depth + 1,
|
27
|
+
**options) if self.respond_to?("builtin_#{k}".to_sym)
|
36
28
|
end.flatten.compact
|
37
29
|
|
38
30
|
# Handle closed shapes
|
@@ -49,10 +41,12 @@ module SHACL::Algebra
|
|
49
41
|
value: statement.object,
|
50
42
|
path: statement.predicate,
|
51
43
|
message: "closed node has extra property",
|
52
|
-
resultSeverity: options
|
44
|
+
resultSeverity: options[:severity],
|
53
45
|
component: RDF::Vocab::SHACL.ClosedConstraintComponent,
|
54
46
|
**options)
|
55
47
|
end.compact
|
48
|
+
elsif @options[:ignoredProperties]
|
49
|
+
raise SHACL::Error, "shape has ignoredProperties without being closed"
|
56
50
|
end
|
57
51
|
|
58
52
|
# Evaluate against operands
|
@@ -66,7 +60,7 @@ module SHACL::Algebra
|
|
66
60
|
not_satisfied(focus: node,
|
67
61
|
value: node,
|
68
62
|
message: "node does not conform to #{op.id}",
|
69
|
-
resultSeverity: options
|
63
|
+
resultSeverity: options[:severity],
|
70
64
|
component: RDF::Vocab::SHACL.NodeConstraintComponent,
|
71
65
|
**options)
|
72
66
|
else
|
data/lib/shacl/algebra/not.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class NotConstraintComponent < ConstraintComponent
|
4
4
|
NAME = :not
|
5
5
|
|
6
6
|
##
|
7
7
|
# Specifies the condition that each value node cannot conform to a given shape. This is comparable to negation and the logical "not" operator.
|
8
8
|
#
|
9
|
-
# @param [RDF::Term] node
|
9
|
+
# @param [RDF::Term] node focus node
|
10
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
10
11
|
# @param [Hash{Symbol => Object}] options
|
11
12
|
# @return [Array<SHACL::ValidationResult>]
|
12
13
|
def conforms(node, path: nil, depth: 0, **options)
|
@@ -15,28 +15,204 @@ module SHACL::Algebra
|
|
15
15
|
# All keys associated with shapes which are set in options
|
16
16
|
#
|
17
17
|
# @return [Array<Symbol>]
|
18
|
-
|
18
|
+
BUILTIN_KEYS = %i(
|
19
19
|
id type label name comment description deactivated severity
|
20
|
+
message
|
20
21
|
order group defaultValue path
|
21
22
|
targetNode targetClass targetSubjectsOf targetObjectsOf
|
22
23
|
class datatype nodeKind
|
23
24
|
minCount maxCount
|
24
25
|
minExclusive minInclusive maxExclusive maxInclusive
|
25
26
|
minLength maxLength
|
26
|
-
|
27
|
-
qualifiedValueShapesDisjoint qualifiedMinCount qualifiedMaxCount
|
27
|
+
languageIn uniqueLang
|
28
28
|
equals disjoint lessThan lessThanOrEquals
|
29
29
|
closed ignoredProperties hasValue in
|
30
|
+
declare namespace prefix
|
30
31
|
).freeze
|
31
32
|
|
32
33
|
# Initialization options
|
33
34
|
attr_accessor :options
|
34
35
|
|
35
|
-
# Graph against which shapes are
|
36
|
+
# Graph against which shapes are validated.
|
37
|
+
# @return [RDF::Queryable]
|
36
38
|
attr_accessor :graph
|
37
39
|
|
40
|
+
# Graph from which original shapes were loaded.
|
41
|
+
# @return [RDF::Graph]
|
42
|
+
attr_accessor :shapes_graph
|
43
|
+
|
44
|
+
# Parameters to components.
|
45
|
+
PARAMETERS = {
|
46
|
+
and: {class: :AndConstraintComponent},
|
47
|
+
class: {
|
48
|
+
class: :ClassConstraintComponent,
|
49
|
+
nodeKind: :IRI,
|
50
|
+
},
|
51
|
+
closed: {
|
52
|
+
class: :ClosedConstraintComponent,
|
53
|
+
datatype: RDF::XSD.boolean,
|
54
|
+
},
|
55
|
+
datatype: {
|
56
|
+
class: :DatatypeConstraintComponent,
|
57
|
+
nodeKind: :IRI,
|
58
|
+
maxCount: 1,
|
59
|
+
},
|
60
|
+
disjoint: {
|
61
|
+
class: :DisjointConstraintComponent,
|
62
|
+
nodeKind: :IRI,
|
63
|
+
},
|
64
|
+
equals: {
|
65
|
+
class: :EqualsConstraintComponent,
|
66
|
+
nodeKind: :IRI,
|
67
|
+
},
|
68
|
+
expression: {class: :ExpressionConstraintComponent},
|
69
|
+
flags: {
|
70
|
+
class: :PatternConstraintComponent,
|
71
|
+
datatype: RDF::XSD.string,
|
72
|
+
optional: true
|
73
|
+
},
|
74
|
+
hasValue: {
|
75
|
+
class: :HasValueConstraintComponent,
|
76
|
+
nodeKind: :IRIOrLiteral,
|
77
|
+
},
|
78
|
+
ignoredProperties: {
|
79
|
+
class: :ClosedConstraintComponent,
|
80
|
+
nodeKind: :IRI, # Added
|
81
|
+
optional: true,
|
82
|
+
},
|
83
|
+
in: {
|
84
|
+
class: :InConstraintComponent,
|
85
|
+
nodeKind: :IRIOrLiteral,
|
86
|
+
#maxCount: 1, # List internalized
|
87
|
+
},
|
88
|
+
languageIn: {
|
89
|
+
class: :LanguageInConstraintComponent,
|
90
|
+
datatype: RDF::XSD.string, # Added
|
91
|
+
#maxCount: 1, # List internalized
|
92
|
+
},
|
93
|
+
lessThan: {
|
94
|
+
class: :LessThanConstraintComponent,
|
95
|
+
nodeKind: :IRI,
|
96
|
+
},
|
97
|
+
lessThanOrEquals: {
|
98
|
+
class: :LessThanOrEqualsConstraintComponent,
|
99
|
+
nodeKind: :IRI,
|
100
|
+
},
|
101
|
+
maxCount: {
|
102
|
+
class: :MaxCountConstraintComponent,
|
103
|
+
datatype: RDF::XSD.integer,
|
104
|
+
maxCount: 1,
|
105
|
+
},
|
106
|
+
maxExclusive: {
|
107
|
+
class: :MaxExclusiveConstraintComponent,
|
108
|
+
maxCount: 1,
|
109
|
+
nodeKind: :Literal,
|
110
|
+
},
|
111
|
+
maxInclusive: {
|
112
|
+
class: :MaxInclusiveConstraintComponent,
|
113
|
+
maxCount: 1,
|
114
|
+
nodeKind: :Literal,
|
115
|
+
},
|
116
|
+
maxLength: {
|
117
|
+
class: :MaxLengthConstraintComponent,
|
118
|
+
datatype: RDF::XSD.integer,
|
119
|
+
maxCount: 1,
|
120
|
+
},
|
121
|
+
minCount: {
|
122
|
+
class: :MinCountConstraintComponent,
|
123
|
+
datatype: RDF::XSD.integer,
|
124
|
+
maxCount: 1,
|
125
|
+
},
|
126
|
+
minExclusive: {
|
127
|
+
class: :MinExclusiveConstraintComponent,
|
128
|
+
maxCount: 1,
|
129
|
+
nodeKind: :Literal,
|
130
|
+
},
|
131
|
+
minInclusive: {
|
132
|
+
class: :MinInclusiveConstraintComponent,
|
133
|
+
maxCount: 1,
|
134
|
+
nodeKind: :Literal,
|
135
|
+
},
|
136
|
+
minLength: {
|
137
|
+
class: :MinLengthConstraintComponent,
|
138
|
+
datatype: RDF::XSD.integer,
|
139
|
+
maxCount: 1,
|
140
|
+
},
|
141
|
+
node: {class: :NodeConstraintComponent},
|
142
|
+
nodeKind: {
|
143
|
+
class: :NodeKindConstraintComponent,
|
144
|
+
in: %i(BlankNode IRI Literal BlankNodeOrIRI BlankNodeOrLiteral IRIOrLiteral),
|
145
|
+
maxCount: 1,
|
146
|
+
},
|
147
|
+
not: {class: :NotConstraintComponent},
|
148
|
+
or: {class: :OrConstraintComponent},
|
149
|
+
pattern: {
|
150
|
+
class: :PatternConstraintComponent,
|
151
|
+
datatype: RDF::XSD.string,
|
152
|
+
},
|
153
|
+
property: {class: :PropertyConstraintComponent},
|
154
|
+
qualifiedMaxCount: {
|
155
|
+
class: :QualifiedValueConstraintComponent,
|
156
|
+
datatype: RDF::XSD.integer,
|
157
|
+
},
|
158
|
+
qualifiedValueShape: {
|
159
|
+
class: :QualifiedValueConstraintComponent,
|
160
|
+
},
|
161
|
+
qualifiedValueShapesDisjoint: {
|
162
|
+
class: :QualifiedValueConstraintComponent,
|
163
|
+
datatype: RDF::XSD.boolean,
|
164
|
+
optional: true,
|
165
|
+
},
|
166
|
+
qualifiedMinCount: {
|
167
|
+
class: :QualifiedValueConstraintComponent,
|
168
|
+
datatype: RDF::XSD.integer
|
169
|
+
},
|
170
|
+
sparql: {class: :SPARQLConstraintComponent},
|
171
|
+
uniqueLang: {
|
172
|
+
class: :UniqueLangConstraintComponent,
|
173
|
+
datatype: RDF::XSD.boolean,
|
174
|
+
maxCount: 1,
|
175
|
+
},
|
176
|
+
xone: {class: :XoneConstraintComponent},
|
177
|
+
}
|
178
|
+
|
38
179
|
## Class methods
|
39
180
|
class << self
|
181
|
+
# Add parameters and class def from a SPARQL-based Constraint Component
|
182
|
+
#
|
183
|
+
# @param [RDF::URI] cls The URI of the constraint component.
|
184
|
+
# @param [Hash{Symbol => Hash}] parameters Definitions of mandatory and optional parameters for this component.
|
185
|
+
def add_component(cls, parameters)
|
186
|
+
# Remember added paraemters.
|
187
|
+
# FIXME: should merge parameters
|
188
|
+
@added_parameters = (@added_parameters || {}).merge(parameters)
|
189
|
+
# Rebuild
|
190
|
+
@params = @component_params = nil
|
191
|
+
end
|
192
|
+
|
193
|
+
# Defined parameters for components, which may be supplemented by SPARQL-based Constraint Components. A parameter may be mapped to more than one component class.
|
194
|
+
#
|
195
|
+
# @return [Hash{Symbol => Hash}] Returns each parameter referencing the component classes it is used in, and the property validators for values of that parameter.
|
196
|
+
def params
|
197
|
+
@params ||= PARAMETERS.merge(@added_parameters || {})
|
198
|
+
end
|
199
|
+
|
200
|
+
# Constraint Component classes indexed to their mandatory and optional parameters, which may be supplemented by SPARQL-based Constraint Components.
|
201
|
+
#
|
202
|
+
# @return [Hash{Symbol => Hash}]
|
203
|
+
# Returns a hash relating each component URI to its optional and mandatory parameters.
|
204
|
+
def component_params
|
205
|
+
@component_params ||= params.inject({}) do |memo, (param, properties)|
|
206
|
+
memo.merge(Array(properties[:class]).inject(memo) do |mem, cls|
|
207
|
+
entry = mem.fetch(cls, {})
|
208
|
+
param_type = properties[:optional] ? :optional : :mandatory
|
209
|
+
entry[param_type] ||= []
|
210
|
+
entry[param_type] << param
|
211
|
+
mem.merge(cls => entry)
|
212
|
+
end)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
40
216
|
##
|
41
217
|
# Creates an operator instance from a parsed SHACL representation
|
42
218
|
# @param [Hash] operator
|
@@ -45,52 +221,143 @@ module SHACL::Algebra
|
|
45
221
|
# @return [Operator]
|
46
222
|
def from_json(operator, **options)
|
47
223
|
operands = []
|
224
|
+
|
225
|
+
# Node options used to instantiate the relevant class instance.
|
48
226
|
node_opts = options.dup
|
227
|
+
|
228
|
+
# Node Options and operands on shape or node, which are not Constraint Component Parameters
|
49
229
|
operator.each do |k, v|
|
50
|
-
|
230
|
+
k = k.to_sym
|
231
|
+
next if v.nil? || params.include?(k)
|
51
232
|
case k
|
52
233
|
# List properties
|
53
|
-
when
|
54
|
-
|
55
|
-
|
56
|
-
when 'class' then node_opts[:class] = as_array(v).map {|vv| iri(vv, **options)} if v
|
57
|
-
when 'datatype' then node_opts[:datatype] = iri(v, **options)
|
58
|
-
when 'disjoint' then node_opts[:disjoint] = as_array(v).map {|vv| iri(vv, **options)} if v
|
59
|
-
when 'equals' then node_opts[:equals] = iri(v, **options)
|
60
|
-
when 'id' then node_opts[:id] = iri(v, vocab: false, **options)
|
61
|
-
when 'ignoredProperties' then node_opts[:ignoredProperties] = as_array(v).map {|vv| iri(vv, **options)} if v
|
62
|
-
when 'lessThan' then node_opts[:lessThan] = iri(v, **options)
|
63
|
-
when 'lessThanOrEquals' then node_opts[:lessThanOrEquals] = iri(v, **options)
|
64
|
-
when 'node'
|
65
|
-
operands.push(*as_array(v).map {|vv| NodeShape.from_json(vv, **options)})
|
66
|
-
when 'nodeKind' then node_opts[:nodeKind] = iri(v, **options)
|
67
|
-
when 'not'
|
68
|
-
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
69
|
-
operands << Not.new(*elements, **options.dup)
|
70
|
-
when 'or'
|
71
|
-
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
72
|
-
operands << Or.new(*elements, **options.dup)
|
73
|
-
when 'path' then node_opts[:path] = parse_path(v, **options)
|
74
|
-
when 'property'
|
234
|
+
when :id then node_opts[:id] = iri(v, vocab: false, **options)
|
235
|
+
when :path then node_opts[:path] = parse_path(v, **options)
|
236
|
+
when :property
|
75
237
|
operands.push(*as_array(v).map {|vv| PropertyShape.from_json(vv, **options)})
|
76
|
-
when
|
77
|
-
|
78
|
-
|
79
|
-
when 'severity' then node_opts[:severity] = iri(v, **options)
|
80
|
-
when 'targetClass' then node_opts[:targetClass] = as_array(v).map {|vv| iri(vv, **options)} if v
|
81
|
-
when 'targetNode'
|
238
|
+
when :severity then node_opts[:severity] = iri(v, **options)
|
239
|
+
when :targetClass then node_opts[:targetClass] = as_array(v).map {|vv| iri(vv, **options)}
|
240
|
+
when :targetNode
|
82
241
|
node_opts[:targetNode] = as_array(v).map do |vv|
|
83
242
|
from_expanded_value(vv, **options)
|
84
|
-
end
|
85
|
-
when
|
86
|
-
when
|
87
|
-
when
|
88
|
-
when 'xone'
|
89
|
-
elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
90
|
-
operands << Xone.new(*elements, **options.dup)
|
243
|
+
end
|
244
|
+
when :targetObjectsOf then node_opts[:targetObjectsOf] = as_array(v).map {|vv| iri(vv, **options)}
|
245
|
+
when :targetSubjectsOf then node_opts[:targetSubjectsOf] = as_array(v).map {|vv| iri(vv, **options)}
|
246
|
+
when :type then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)}
|
91
247
|
else
|
92
|
-
|
93
|
-
|
248
|
+
if BUILTIN_KEYS.include?(k)
|
249
|
+
# Add as a plain option otherwise
|
250
|
+
node_opts[k] = to_rdf(k, v, **options)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Node Options and operands on shape or node, which are Constraint Component Parameters.
|
256
|
+
# For constraints with a defined Ruby class, the primary parameter is the NAME from the constraint class. Other parameters are added as named operands to the component operator.
|
257
|
+
used_components = {}
|
258
|
+
operator.each do |k, v|
|
259
|
+
k = k.to_sym
|
260
|
+
next if v.nil? || !params.include?(k)
|
261
|
+
param_props = params[k]
|
262
|
+
param_classes = Array(param_props[:class])
|
263
|
+
|
264
|
+
# Keep track of components which have been used.
|
265
|
+
param_classes.each {|cls| used_components[cls] ||= {}}
|
266
|
+
|
267
|
+
# Check parameter constraints
|
268
|
+
v = as_array(v)
|
269
|
+
if param_props[:maxCount] && v.length > param_props[:maxCount]
|
270
|
+
raise SHACL::Error, "Property #{k} on #{self.const_get(:NAME)} has too many values: #{v.inspect}"
|
271
|
+
end
|
272
|
+
|
273
|
+
# If an optional parameter exists without corresponding mandatory parameters on a given shape, raise a SHACL::Error.
|
274
|
+
#
|
275
|
+
# Records any instances of components which are created to re-attach non-primary parameters after all operators are processed.
|
276
|
+
instances = case k
|
277
|
+
# List properties
|
278
|
+
when :node
|
279
|
+
as_array(v).map {|vv| NodeShape.from_json(vv, **options)}
|
280
|
+
when :property
|
281
|
+
as_array(v).map {|vv| PropertyShape.from_json(vv, **options)}
|
282
|
+
when :sparql
|
283
|
+
as_array(v).map {|vv| SPARQLConstraintComponent.from_json(vv, **options)}
|
284
|
+
else
|
285
|
+
# Process parameter values based on nodeKind, in, and datatype.
|
286
|
+
elements = if param_props[:nodeKind]
|
287
|
+
case param_props[:nodeKind]
|
288
|
+
when :IRI
|
289
|
+
v.map {|vv| iri(vv, **options)}
|
290
|
+
when :Literal
|
291
|
+
v.map do |vv|
|
292
|
+
vv.is_a?(Hash) ?
|
293
|
+
from_expanded_value(vv, **options) :
|
294
|
+
RDF::Literal(vv)
|
295
|
+
end
|
296
|
+
when :IRIOrLiteral
|
297
|
+
to_rdf(k, v, **options)
|
298
|
+
end
|
299
|
+
elsif param_props[:in]
|
300
|
+
v.map do |vv|
|
301
|
+
iri(vv, **options) if param_props[:in].include?(vv.to_sym)
|
302
|
+
end
|
303
|
+
elsif param_props[:datatype]
|
304
|
+
v.map {|vv| RDF::Literal(vv, datatype: param_props[:datatype])}
|
305
|
+
else
|
306
|
+
v.map {|vv| SHACL::Algebra.from_json(vv, **options)}
|
307
|
+
end
|
308
|
+
|
309
|
+
# Builtins are added as options to the operator, otherwise, they are class instances of constraint components added as operators.
|
310
|
+
if BUILTIN_KEYS.include?(k)
|
311
|
+
node_opts[k] = elements
|
312
|
+
[] # No instances created
|
313
|
+
else
|
314
|
+
klass = SHACL::Algebra.const_get(Array(param_props[:class]).first)
|
315
|
+
|
316
|
+
name = klass.const_get(:NAME)
|
317
|
+
# If the key `k` is the same as the NAME of the class, create the instance with the defined element values.
|
318
|
+
if name == k
|
319
|
+
param_classes.each do |cls|
|
320
|
+
# Add `k` as a mandatory parameter fulfilled
|
321
|
+
(used_components[cls][:mandatory_parameters] ||= []) << k
|
322
|
+
end
|
323
|
+
|
324
|
+
# Instantiate the compoent
|
325
|
+
elements.map {|e| klass.new(*e, **options.dup)}
|
326
|
+
else
|
327
|
+
# Add non-primary parameters for subsequent insertion
|
328
|
+
param_classes.each do |cls|
|
329
|
+
# Add `k` as a mandatory parameter fulfilled if it is so defined
|
330
|
+
(used_components[cls][:mandatory_parameters] ||= []) << k unless
|
331
|
+
params[k][:optional]
|
332
|
+
|
333
|
+
# Add parameter as S-Expression operand
|
334
|
+
(used_components[cls][:parameters] ||= []) << elements.unshift(k)
|
335
|
+
end
|
336
|
+
[] # No instances created
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# Record the instances created by class and its operands
|
342
|
+
param_classes.each do |cls|
|
343
|
+
used_components[cls][:instances] = instances
|
344
|
+
end
|
345
|
+
|
346
|
+
# FIXME: Only add instances when all mandatory parameters are present.
|
347
|
+
operands.push(*instances)
|
348
|
+
end
|
349
|
+
|
350
|
+
# Append any parameters to the used components
|
351
|
+
used_components.each do |cls, props|
|
352
|
+
instances = props[:instances]
|
353
|
+
next unless instances # BUILTINs
|
354
|
+
|
355
|
+
parameters = props.fetch(:parameters, [])
|
356
|
+
instances.each do |op|
|
357
|
+
parameters.each do |param|
|
358
|
+
# Note the potential that the parameter gets added twice, if there are multiple classes for both the primary and secondary paramters.
|
359
|
+
op.operands << param
|
360
|
+
end
|
94
361
|
end
|
95
362
|
end
|
96
363
|
|
@@ -189,7 +456,7 @@ module SHACL::Algebra
|
|
189
456
|
end
|
190
457
|
|
191
458
|
##
|
192
|
-
# Parse the "
|
459
|
+
# Parse the "path" attribute into a SPARQL Property Path and evaluate to find related nodes.
|
193
460
|
#
|
194
461
|
# @param [Object] path
|
195
462
|
# @return [RDF::URI, SPARQL::Algebra::Expression]
|
@@ -282,8 +549,9 @@ module SHACL::Algebra
|
|
282
549
|
raise NotImplemented
|
283
550
|
end
|
284
551
|
|
552
|
+
# Create structure for serializing this component/shape, beginning with its cononical name.
|
285
553
|
def to_sxp_bin
|
286
|
-
expressions =
|
554
|
+
expressions = BUILTIN_KEYS.inject([self.class.const_get(:NAME)]) do |memo, sym|
|
287
555
|
@options[sym] ? memo.push([sym, *@options[sym]]) : memo
|
288
556
|
end + operands
|
289
557
|
|
data/lib/shacl/algebra/or.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module SHACL::Algebra
|
2
2
|
##
|
3
|
-
class
|
3
|
+
class OrConstraintComponent < ConstraintComponent
|
4
4
|
NAME = :or
|
5
5
|
|
6
6
|
##
|
@@ -21,7 +21,8 @@ module SHACL::Algebra
|
|
21
21
|
# ]
|
22
22
|
# ) .
|
23
23
|
#
|
24
|
-
# @param [RDF::Term] node
|
24
|
+
# @param [RDF::Term] node focus node
|
25
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
25
26
|
# @param [Hash{Symbol => Object}] options
|
26
27
|
# @return [Array<SHACL::ValidationResult>]
|
27
28
|
def conforms(node, path: nil, depth: 0, **options)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module SHACL::Algebra
|
2
|
+
##
|
3
|
+
class PatternConstraintComponent < ConstraintComponent
|
4
|
+
NAME = :pattern
|
5
|
+
|
6
|
+
##
|
7
|
+
# Specifies a regular expression that each value node matches to satisfy the condition.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# ex:PatternExampleShape
|
11
|
+
# a sh:NodeShape ;
|
12
|
+
# sh:targetNode ex:Bob, ex:Alice, ex:Carol ;
|
13
|
+
# sh:property [
|
14
|
+
# sh:path ex:bCode ;
|
15
|
+
# sh:pattern "^B" ; # starts with 'B'
|
16
|
+
# sh:flags "i" ; # Ignore case
|
17
|
+
# ] .
|
18
|
+
#
|
19
|
+
# @param [RDF::Term] node focus node
|
20
|
+
# @param [RDF::URI, SPARQL::Algebra::Expression] path (nil) the property path from the focus node to the value nodes.
|
21
|
+
# @param [Hash{Symbol => Object}] options
|
22
|
+
# @return [Array<SHACL::ValidationResult>]
|
23
|
+
def conforms(node, path: nil, depth: 0, **options)
|
24
|
+
log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
|
25
|
+
pattern = Array(operands.first).first
|
26
|
+
flags = operands.last.last if operands.last.is_a?(Array) && operands.last.first == :flags
|
27
|
+
flags = flags.to_s
|
28
|
+
regex_opts = 0
|
29
|
+
regex_opts |= Regexp::MULTILINE if flags.include?(?m)
|
30
|
+
regex_opts |= Regexp::IGNORECASE if flags.include?(?i)
|
31
|
+
regex_opts |= Regexp::EXTENDED if flags.include?(?x)
|
32
|
+
pat = Regexp.new(pattern, regex_opts)
|
33
|
+
|
34
|
+
compares = !node.node? && pat.match?(node.to_s)
|
35
|
+
satisfy(focus: node, path: path,
|
36
|
+
value: node,
|
37
|
+
message: "is#{' not' unless compares} a match #{pat.inspect}",
|
38
|
+
resultSeverity: (options.fetch(:severity) unless compares),
|
39
|
+
component: RDF::Vocab::SHACL.PatternConstraintComponent,
|
40
|
+
**options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|