shacl 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,93 @@
1
+ require 'shacl/format'
2
+ require 'shacl/shapes'
3
+ require 'shacl/refinements'
4
+ require 'rdf/vocab/shacl'
5
+
6
+ ##
7
+ # A SHACL runtime for RDF.rb.
8
+ #
9
+ # @see https://www.w3.org/TR/shacl/
10
+
11
+ module SHACL
12
+ autoload :Algebra, 'shacl/algebra'
13
+ autoload :VERSION, 'shacl/version'
14
+
15
+ ##
16
+ # Transform the given _Shapes Graph_ into a set of executable shapes.
17
+ #
18
+ # A _Shapes Graph_ may contain an `owl:imports` property referencing additional _Shapes Graphs_, which are resolved until no more imports are found.
19
+ #
20
+ # @param (see Shapes#from_graph)
21
+ # @option (see Shapes#from_graph)
22
+ # @return (see Shapes#from_graph)
23
+ # @raise (see Shapes#from_graph)
24
+ def self.get_shapes(shape_graph, **options)
25
+ Shapes.from_graph(shape_graph, **options)
26
+ end
27
+
28
+ ##
29
+ # Parse a given resource into a _Shapes Graph_.
30
+ #
31
+ # @param [String, IO, StringIO, #to_s] input
32
+ # @option (see Shapes#from_graph)
33
+ # @return (see Shapes#from_graph)
34
+ # @raise (see Shapes#from_graph)
35
+ def self.open(input, **options)
36
+ graph = RDF::Graph.load(input, **options)
37
+ self.get_shapes(graph, loaded_graphs: [RDF::URI(input, canonicalize: true)], **options)
38
+ end
39
+
40
+ ##
41
+ # Retrieve shapes from a sh:shapesGraph reference within queryable
42
+ #
43
+ # @param (see Shapes#from_queryable)
44
+ # @option (see Shapes#from_queryable)
45
+ # @return (see Shapes#from_queryable)
46
+ # @raise (see Shapes#from_queryable)
47
+ def self.from_queryable(queryable, **options)
48
+ Shapes.from_queryable(queryable, **options)
49
+ end
50
+
51
+ ##
52
+ # The _Shapes Graph_, is established similar to the _Data Graph_, but may be `nil`. If `nil`, the _Data Graph_ may reference a _Shapes Graph_ thorugh an `sh:shapesGraph` property.
53
+ #
54
+ # Additionally, a _Shapes Graph_ may contain an `owl:imports` property referencing additional _Shapes Graphs_, which are resolved until no more imports are found.
55
+ #
56
+ # Load and validate the given SHACL `expression` string against `queriable`.
57
+ #
58
+ # @param [String, IO, StringIO, #to_s] input
59
+ # @param [RDF::Queryable] queryable
60
+ # @param [Hash{Symbol => Object}] options
61
+ # @options (see Shapes#initialize)
62
+ # @return (see Shapes#execute)
63
+ # @raise (see Shapes#execute)
64
+ def self.execute(input, queryable = nil, **options)
65
+ queryable = queryable || RDF::Graph.new
66
+ shapes = if input
67
+ self.open(input, **options)
68
+ else
69
+ Shapes.from_queryable(queryable)
70
+ end
71
+
72
+ shapes.execute(queryable, **options)
73
+ end
74
+
75
+ class Error < StandardError
76
+ # The status code associated with this error
77
+ attr_reader :code
78
+
79
+ ##
80
+ # Initializes a new patch error instance.
81
+ #
82
+ # @param [String, #to_s] message
83
+ # @param [Hash{Symbol => Object}] options
84
+ # @option options [Integer] :code (422)
85
+ def initialize(message, **options)
86
+ @code = options.fetch(:status_code, 422)
87
+ super(message.to_s)
88
+ end
89
+ end
90
+
91
+ # Shape expectation not satisfied
92
+ class StructureError < Error; end
93
+ end
@@ -0,0 +1,34 @@
1
+ $:.unshift(File.expand_path("../..", __FILE__))
2
+ require 'sxp'
3
+ require_relative "algebra/operator"
4
+
5
+ module SHACL
6
+ # Based on the SPARQL Algebra, operators for executing a patch
7
+ module Algebra
8
+ autoload :And, 'shacl/algebra/and.rb'
9
+ autoload :Datatype, 'shacl/algebra/datatype.rb'
10
+ autoload :Klass, 'shacl/algebra/klass.rb'
11
+ autoload :NodeShape, 'shacl/algebra/node_shape.rb'
12
+ autoload :Not, 'shacl/algebra/not.rb'
13
+ autoload :Or, 'shacl/algebra/or.rb'
14
+ autoload :PropertyShape, 'shacl/algebra/property_shape.rb'
15
+ autoload :QualifiedValueShape, 'shacl/algebra/qualified_value_shape.rb'
16
+ autoload :Shape, 'shacl/algebra/shape.rb'
17
+ autoload :Xone, 'shacl/algebra/xone.rb'
18
+
19
+ def self.from_json(operator, **options)
20
+ raise ArgumentError, "from_json: operator not a Hash: #{operator.inspect}" unless operator.is_a?(Hash)
21
+ type = operator.fetch('type', [])
22
+ type << (operator["path"] ? 'PropertyShape' : 'NodeShape') if type.empty?
23
+ klass = case
24
+ when type.include?('NodeShape') then NodeShape
25
+ when type.include?('PropertyShape') then PropertyShape
26
+ else raise ArgumentError, "from_json: unknown type #{type.inspect}"
27
+ end
28
+
29
+ klass.from_json(operator, **options)
30
+ end
31
+ end
32
+ end
33
+
34
+
@@ -0,0 +1,51 @@
1
+ module SHACL::Algebra
2
+ ##
3
+ class And < Operator
4
+ NAME = :and
5
+
6
+ ##
7
+ # Specifies the condition that each value node conforms to all provided shapes. This is comparable to conjunction and the logical "and" operator.
8
+ #
9
+ # @example
10
+ # ex:SuperShape
11
+ # a sh:NodeShape ;
12
+ # sh:property [
13
+ # sh:path ex:property ;
14
+ # sh:minCount 1 ;
15
+ # ] .
16
+ #
17
+ # ex:ExampleAndShape
18
+ # a sh:NodeShape ;
19
+ # sh:targetNode ex:ValidInstance, ex:InvalidInstance ;
20
+ # sh:and (
21
+ # ex:SuperShape
22
+ # [
23
+ # sh:path ex:property ;
24
+ # sh:maxCount 1 ;
25
+ # ]
26
+ # ) .
27
+ #
28
+ # @param [RDF::Term] node
29
+ # @param [Hash{Symbol => Object}] options
30
+ # @return [Array<SHACL::ValidationResult>]
31
+ def conforms(node, path: nil, depth: 0, **options)
32
+ log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
33
+ operands.each do |op|
34
+ results = op.conforms(node, depth: depth + 1, **options)
35
+ if !results.all?(&:conform?)
36
+ return not_satisfied(focus: node, path: path,
37
+ value: node,
38
+ message: "node does not conform to all shapes",
39
+ resultSeverity: options.fetch(:severity),
40
+ component: RDF::Vocab::SHACL.AndConstraintComponent,
41
+ depth: depth, **options)
42
+ end
43
+ end
44
+ satisfy(focus: node, path: path,
45
+ value: node,
46
+ message: "node conforms to all shapes",
47
+ component: RDF::Vocab::SHACL.AndConstraintComponent,
48
+ depth: depth, **options)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,80 @@
1
+ require_relative "shape"
2
+
3
+ module SHACL::Algebra
4
+ ##
5
+ class NodeShape < SHACL::Algebra::Shape
6
+ NAME = :NodeShape
7
+
8
+ # Validates the specified `node` within `graph`, a list of {ValidationResult}.
9
+ #
10
+ # A node conforms if it is not deactivated and all of its operands conform.
11
+ #
12
+ # @param [RDF::Term] node
13
+ # @param [Hash{Symbol => Object}] options
14
+ # @return [Array<SHACL::ValidationResult>]
15
+ # Returns one or more validation results for each operand.
16
+ def conforms(node, depth: 0, **options)
17
+ return [] if deactivated?
18
+ options = id ? options.merge(shape: id) : options
19
+ options = options.merge(severity: RDF::Vocab::SHACL.Violation)
20
+ log_debug(NAME, depth: depth) {SXP::Generator.string({id: id, node: node}.to_sxp_bin)}
21
+
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
+ # Evaluate against builtins
34
+ builtin_results = @options.map do |k, v|
35
+ self.send("builtin_#{k}".to_sym, v, node, nil, [node], depth: depth + 1, **options) if self.respond_to?("builtin_#{k}".to_sym)
36
+ end.flatten.compact
37
+
38
+ # Handle closed shapes
39
+ # FIXME: this only considers URI paths, not property paths
40
+ closed_results = []
41
+ if @options[:closed]
42
+ shape_paths = operands.select {|o| o.is_a?(PropertyShape)}.map(&:path)
43
+ shape_properties = shape_paths.select {|p| p.is_a?(RDF::URI)}
44
+ shape_properties += Array(@options[:ignoredProperties])
45
+
46
+ closed_results = graph.query(subject: node).map do |statement|
47
+ next if shape_properties.include?(statement.predicate)
48
+ not_satisfied(focus: node,
49
+ value: statement.object,
50
+ path: statement.predicate,
51
+ message: "closed node has extra property",
52
+ resultSeverity: options.fetch(:severity),
53
+ component: RDF::Vocab::SHACL.ClosedConstraintComponent,
54
+ **options)
55
+ end.compact
56
+ end
57
+
58
+ # Evaluate against operands
59
+ op_results = operands.map do |op|
60
+ res = op.conforms(node,
61
+ focus: options.fetch(:focusNode, node),
62
+ depth: depth + 1,
63
+ **options)
64
+ if op.is_a?(NodeShape) && !res.all?(&:conform?)
65
+ # Special case for embedded NodeShape
66
+ not_satisfied(focus: node,
67
+ value: node,
68
+ message: "node does not conform to #{op.id}",
69
+ resultSeverity: options.fetch(:severity),
70
+ component: RDF::Vocab::SHACL.NodeConstraintComponent,
71
+ **options)
72
+ else
73
+ res
74
+ end
75
+ end.flatten.compact
76
+
77
+ builtin_results + closed_results + op_results
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,30 @@
1
+ module SHACL::Algebra
2
+ ##
3
+ class Not < Operator
4
+ NAME = :not
5
+
6
+ ##
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
+ #
9
+ # @param [RDF::Term] node
10
+ # @param [Hash{Symbol => Object}] options
11
+ # @return [Array<SHACL::ValidationResult>]
12
+ def conforms(node, path: nil, depth: 0, **options)
13
+ log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}
14
+ operands.each do |op|
15
+ results = op.conforms(node, depth: depth + 1, **options)
16
+ if results.any?(&:conform?)
17
+ return not_satisfied(focus: node, path: path,
18
+ message: "node does not conform to some shape",
19
+ resultSeverity: options.fetch(:severity),
20
+ component: RDF::Vocab::SHACL.NotConstraintComponent,
21
+ value: node, depth: depth, **options)
22
+ end
23
+ end
24
+ satisfy(focus: node, path: path,
25
+ message: "node conforms to all shapes",
26
+ component: RDF::Vocab::SHACL.NotConstraintComponent,
27
+ value: node, depth: depth, **options)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,329 @@
1
+ require 'sparql/algebra'
2
+ require 'shacl/validation_result'
3
+ require 'json/ld'
4
+
5
+ module SHACL::Algebra
6
+
7
+ ##
8
+ # The SHACL operator.
9
+ #
10
+ # @abstract
11
+ class Operator < SPARQL::Algebra::Operator
12
+ include RDF::Util::Logger
13
+ extend JSON::LD::Utils
14
+
15
+ # All keys associated with shapes which are set in options
16
+ #
17
+ # @return [Array<Symbol>]
18
+ ALL_KEYS = %i(
19
+ id type label name comment description deactivated severity
20
+ order group defaultValue path
21
+ targetNode targetClass targetSubjectsOf targetObjectsOf
22
+ class datatype nodeKind
23
+ minCount maxCount
24
+ minExclusive minInclusive maxExclusive maxInclusive
25
+ minLength maxLength
26
+ pattern flags languageIn uniqueLang
27
+ qualifiedValueShapesDisjoint qualifiedMinCount qualifiedMaxCount
28
+ equals disjoint lessThan lessThanOrEquals
29
+ closed ignoredProperties hasValue in
30
+ ).freeze
31
+
32
+ # Initialization options
33
+ attr_accessor :options
34
+
35
+ # Graph against which shapes are validaed
36
+ attr_accessor :graph
37
+
38
+ ## Class methods
39
+ class << self
40
+ ##
41
+ # Creates an operator instance from a parsed SHACL representation
42
+ # @param [Hash] operator
43
+ # @param [Hash] options ({})
44
+ # @option options [Hash{String => RDF::URI}] :prefixes
45
+ # @return [Operator]
46
+ def from_json(operator, **options)
47
+ operands = []
48
+ node_opts = options.dup
49
+ operator.each do |k, v|
50
+ next if v.nil?
51
+ case k
52
+ # List properties
53
+ when 'and'
54
+ elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
55
+ operands << And.new(*elements, **options.dup)
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'
75
+ operands.push(*as_array(v).map {|vv| PropertyShape.from_json(vv, **options)})
76
+ when 'qualifiedValueShape'
77
+ elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
78
+ operands << QualifiedValueShape.new(*elements, **options.dup)
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'
82
+ node_opts[:targetNode] = as_array(v).map do |vv|
83
+ from_expanded_value(vv, **options)
84
+ end if v
85
+ when 'targetObjectsOf' then node_opts[:targetObjectsOf] = as_array(v).map {|vv| iri(vv, **options)} if v
86
+ when 'targetSubjectsOf' then node_opts[:targetSubjectsOf] = as_array(v).map {|vv| iri(vv, **options)} if v
87
+ when 'type' then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)} if v
88
+ when 'xone'
89
+ elements = as_array(v).map {|vv| SHACL::Algebra.from_json(vv, **options)}
90
+ operands << Xone.new(*elements, **options.dup)
91
+ else
92
+ # Add as a plain option if it is recognized
93
+ node_opts[k.to_sym] = to_rdf(k.to_sym, v, **options) if ALL_KEYS.include?(k.to_sym)
94
+ end
95
+ end
96
+
97
+ new(*operands, **node_opts)
98
+ end
99
+
100
+ # Create URIs
101
+ # @param (see #iri)
102
+ # @option (see #iri)
103
+ # @return (see #iri)
104
+ def iri(value, base: RDF::Vocab::SHACL.to_uri, vocab: true, **options)
105
+ # Context will have been pre-loaded
106
+ @context ||= JSON::LD::Context.parse("http://github.com/ruby-rdf/shacl/")
107
+
108
+ value = value['id'] || value['@id'] if value.is_a?(Hash)
109
+ result = @context.expand_iri(value, base: base, vocab: vocab)
110
+ result = RDF::URI(result) if result.is_a?(String)
111
+ if result.respond_to?(:qname) && result.qname
112
+ result = RDF::URI.new(result.to_s) if result.frozen?
113
+ result.lexical = result.qname.join(':')
114
+ end
115
+ result
116
+ end
117
+
118
+ # Turn a JSON-LD value into its RDF representation
119
+ # @see JSON::LD::ToRDF.item_to_rdf
120
+ # @param [Symbol] term
121
+ # @param [Object] item
122
+ # @return RDF::Term
123
+ def to_rdf(term, item, **options)
124
+ @context ||= JSON::LD::Context.parse("http://github.com/ruby-rdf/shacl/")
125
+
126
+ return item.map {|v| to_rdf(term, v, **options)} if item.is_a?(Array)
127
+
128
+ case
129
+ when item.is_a?(TrueClass) || item.is_a?(FalseClass) || item.is_a?(Numeric)
130
+ return RDF::Literal(item)
131
+ when value?(item)
132
+ value, datatype = item.fetch('@value'), item.fetch('type', nil)
133
+ case value
134
+ when TrueClass, FalseClass, Numeric
135
+ return RDF::Literal(value)
136
+ else
137
+ datatype ||= item.has_key?('@direction') ?
138
+ RDF::URI("https://www.w3.org/ns/i18n##{item.fetch('@language', '').downcase}_#{item['@direction']}") :
139
+ (item.has_key?('@language') ? RDF.langString : RDF::XSD.string)
140
+ end
141
+ datatype = iri(datatype) if datatype
142
+
143
+ # Initialize literal as an RDF literal using value and datatype. If element has the key @language and datatype is xsd:string, then add the value associated with the @language key as the language of the object.
144
+ language = item.fetch('@language', nil) if datatype == RDF.langString
145
+ return RDF::Literal.new(value, datatype: datatype, language: language)
146
+ when node?(item)
147
+ return iri(item, **options)
148
+ when list?(item)
149
+ RDF::List(*item['@list'].map {|v| to_rdf(term, v, **options)})
150
+ when item.is_a?(String)
151
+ RDF::Literal(item)
152
+ else
153
+ raise "Can't transform #{item.inspect} to RDF on property #{term}"
154
+ end
155
+ end
156
+
157
+ # Interpret a JSON-LD expanded value
158
+ # @param [Hash] item
159
+ # @return [RDF::Term]
160
+ def from_expanded_value(item, **options)
161
+ if item['@value']
162
+ value, datatype = item.fetch('@value'), item.fetch('type', nil)
163
+ case value
164
+ when TrueClass, FalseClass
165
+ value = value.to_s
166
+ datatype ||= RDF::XSD.boolean.to_s
167
+ when Numeric
168
+ # Don't serialize as double if there are no fractional bits
169
+ as_double = value.ceil != value || value >= 1e21 || datatype == RDF::XSD.double
170
+ lit = if as_double
171
+ RDF::Literal::Double.new(value, canonicalize: true)
172
+ else
173
+ RDF::Literal.new(value.numerator, canonicalize: true)
174
+ end
175
+
176
+ datatype ||= lit.datatype
177
+ value = lit.to_s.sub("E+", "E")
178
+ else
179
+ datatype ||= item.has_key?('@language') ? RDF.langString : RDF::XSD.string
180
+ end
181
+ datatype = iri(datatype) if datatype
182
+ language = item.fetch('@language', nil) if datatype == RDF.langString
183
+ RDF::Literal.new(value, datatype: datatype, language: language)
184
+ elsif item['id']
185
+ self.iri(item['id'], **options)
186
+ else
187
+ RDF::Node.new
188
+ end
189
+ end
190
+
191
+ ##
192
+ # Parse the "patH" attribute into a SPARQL Property Path and evaluate to find related nodes.
193
+ #
194
+ # @param [Object] path
195
+ # @return [RDF::URI, SPARQL::Algebra::Expression]
196
+ def parse_path(path, **options)
197
+ case path
198
+ when RDF::URI then path
199
+ when String then iri(path)
200
+ when Hash
201
+ # Creates a SPARQL S-Expression resulting in a query which can be used to find corresponding
202
+ {
203
+ alternativePath: :alt,
204
+ inversePath: :reverse,
205
+ oneOrMorePath: :"path+",
206
+ "@list": :seq,
207
+ zeroOrMorePath: :"path*",
208
+ zeroOrOnePath: :"path?",
209
+ }.each do |prop, op_sym|
210
+ if path[prop.to_s]
211
+ value = path[prop.to_s]
212
+ value = value['@list'] if value.is_a?(Hash) && value.key?('@list')
213
+ value = [value] if !value.is_a?(Array)
214
+ value = value.map {|e| parse_path(e, **options)}
215
+ op = SPARQL::Algebra::Operator.for(op_sym)
216
+ if value.length > op.arity
217
+ # Divide into the first operand followed by the operator re-applied to the reamining operands
218
+ value = value.first, apply_op(op, value[1..-1])
219
+ end
220
+ return op.new(*value)
221
+ end
222
+ end
223
+
224
+ if path['id']
225
+ iri(path['id'])
226
+ else
227
+ log_error('PropertyPath', "Can't handle path", **options) {path.to_sxp}
228
+ end
229
+ else
230
+ log_error('PropertyPath', "Can't handle path", **options) {path.to_sxp}
231
+ end
232
+ end
233
+
234
+ # Recursively apply operand to sucessive values until the argument count which is expected is achieved
235
+ def apply_op(op, values)
236
+ if values.length > op.arity
237
+ values = values.first, apply_op(op, values[1..-1])
238
+ end
239
+ op.new(*values)
240
+ end
241
+ protected :apply_op
242
+ end
243
+
244
+ # The ID of this operator
245
+ # @return [RDF::Resource]
246
+ def id; @options[:id]; end
247
+
248
+ # The types associated with this operator
249
+ # @return [Array<RDF::URI>]
250
+ def type; @options[:type]; end
251
+
252
+ # Any label associated with this operator
253
+ # @return [RDF::Literal]
254
+ def label; @options[:label]; end
255
+
256
+ # Is this shape deactivated?
257
+ # @return [Boolean]
258
+ def deactivated?; @options[:deactivated] == RDF::Literal::TRUE; end
259
+
260
+ # Any comment associated with this operator
261
+ # @return [RDF::Literal]
262
+ def comment; @options[:comment]; end
263
+
264
+ # Create URIs
265
+ # @param [RDF::Value, String] value
266
+ # @param [RDF::URI] base Base IRI used for resolving relative values (RDF::Vocab::SHACL.to_uri).
267
+ # @param [Boolean] vocab resolve vocabulary relative to the builtin context.
268
+ # @param [Hash{Symbol => Object}] options
269
+ # @return [RDF::Value]
270
+ def iri(value, base: RDF::Vocab::SHACL.to_uri, vocab: true, **options)
271
+ self.class.iri(value, base: base, vocab: vocab, **options)
272
+ end
273
+
274
+ # Validates the specified `node` within `graph`, a list of {ValidationResult}.
275
+ #
276
+ # A node conforms if it is not deactivated and all of its operands conform.
277
+ #
278
+ # @param [RDF::Term] node
279
+ # @param [Hash{Symbol => Object}] options
280
+ # @return [Array<ValidationResult>]
281
+ def conforms(node, depth: 0, **options)
282
+ raise NotImplemented
283
+ end
284
+
285
+ def to_sxp_bin
286
+ expressions = ALL_KEYS.inject([self.class.const_get(:NAME)]) do |memo, sym|
287
+ @options[sym] ? memo.push([sym, *@options[sym]]) : memo
288
+ end + operands
289
+
290
+ expressions.to_sxp_bin
291
+ end
292
+
293
+ ##
294
+ # Create a result that satisfies the shape.
295
+ #
296
+ # @param [RDF::Term] focus
297
+ # @param [RDF::Resource] shape
298
+ # @param [RDF::URI] component
299
+ # @param [RDF::URI] resultSeverity (nil)
300
+ # @param [Array<RDF::URI>] path (nil)
301
+ # @param [RDF::Term] value (nil)
302
+ # @param [RDF::Term] details (nil)
303
+ # @param [String] message (nil)
304
+ # @return [Array<SHACL::ValidationResult>]
305
+ def satisfy(focus:, shape:, component:, resultSeverity: nil, path: nil, value: nil, details: nil, message: nil, **options)
306
+ log_debug(self.class.const_get(:NAME), "#{'not ' if resultSeverity}satisfied #{value.to_sxp if value}#{': ' + message if message}", **options)
307
+ [SHACL::ValidationResult.new(focus, path, shape, resultSeverity, component,
308
+ details, value, message)]
309
+ end
310
+
311
+ ##
312
+ # Create a result that does not satisfies the shape.
313
+ #
314
+ # @param [RDF::Term] focus
315
+ # @param [RDF::Resource] shape
316
+ # @param [RDF::URI] component
317
+ # @param [RDF::URI] resultSeverity (RDF:::Vocab::SHACL.Violation)
318
+ # @param [Array<RDF::URI>] path (nil)
319
+ # @param [RDF::Term] value (nil)
320
+ # @param [RDF::Term] details (nil)
321
+ # @param [String] message (nil)
322
+ # @return [Array<SHACL::ValidationResult>]
323
+ def not_satisfied(focus:, shape:, component:, resultSeverity: RDF::Vocab::SHACL.Violation, path: nil, value: nil, details: nil, message: nil, **options)
324
+ log_info(self.class.const_get(:NAME), "not satisfied #{value.to_sxp if value}#{': ' + message if message}", **options)
325
+ [SHACL::ValidationResult.new(focus, path, shape, resultSeverity, component,
326
+ details, value, message)]
327
+ end
328
+ end
329
+ end