shacl 0.2.1 → 0.4.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 +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
|