shacl 0.1.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.
@@ -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