Class: SHACL::Algebra::SPARQLConstraintComponent

Inherits:
ConstraintComponent show all
Defined in:
lib/shacl/algebra/sparql_constraint.rb

Constant Summary collapse

NAME =
:sparql
UNSUPPORTED_SPARQL_OPERATORS =

SPARQL Operators prohibited from being used in expression.

[
  SPARQL::Algebra::Operator::Minus,
  SPARQL::Algebra::Operator::Service,
  SPARQL::Algebra::Operator::Table,
]
PRE_BOUND =

Potentially pre-bound variables.

%i(currentShape shapesGraph PATH this value)
BUILTIN_KEYS =

All keys associated with shapes which are set in options

Returns:

  • (Array<Symbol>)
%i(
  type label name comment description deactivated severity
  message path
  ask select
  declare namespace prefix prefixes select ask
).freeze

Constants inherited from Operator

Operator::PARAMETERS

Instance Attribute Summary

Attributes inherited from Operator

#graph, #options, #shapes_graph

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ConstraintComponent

ncname

Methods inherited from Operator

add_component, apply_op, #comment, component_params, #deactivated?, from_expanded_value, #id, iri, #iri, #label, #not_satisfied, params, parse_path, #satisfy, to_rdf, #to_sxp_bin, #type

Class Method Details

.from_json(operator, **options) ⇒ Operator

Creates an operator instance from a parsed SHACL representation.

Special case for SPARQLComponenet due to general recursion.

Parameters:

  • operator (Hash)
  • options (Hash)

    ({})

Options Hash (**options):

  • :prefixes (Hash{String => RDF::URI})

Returns:



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/shacl/algebra/sparql_constraint.rb', line 92

def from_json(operator, **options)
  prefixes, query = [], ""
  operands = []
  node_opts = options.dup
  operator.each do |k, v|
    next if v.nil?
    case k
    # List properties
    when 'path'               then node_opts[:path] = parse_path(v, **options)
    when 'prefixes'
      prefixes = extract_prefixes(v)
    when 'severity'           then node_opts[:severity] = iri(v, **options)
    when 'type'               then node_opts[:type] = as_array(v).map {|vv| iri(vv, **options)} if v
    else
      node_opts[k.to_sym] = to_rdf(k.to_sym, v, **options) if BUILTIN_KEYS.include?(k.to_sym)
    end
  end

  query_string = prefixes.join("\n") + node_opts[:select] || node_opts[:ask]
  query = SPARQL.parse(query_string)

  options[:logger].info("#{NAME} SXP: #{query.to_sxp}") if options[:logger]

  # Queries have restrictions
  operators = query.descendants.to_a.unshift(query)

  if node_opts[:ask] && !operators.any? {|op| op.is_a?(SPARQL::Algebra::Operator::Ask)}
    raise SHACL::Error, "Ask query must have ask operator"
  elsif node_opts[:select] && !operators.any? {|op| op.is_a?(SPARQL::Algebra::Operator::Project)}
    raise SHACL::Error, "Select query must have project operator"
  end

  uh_oh = (operators.map(&:class) & UNSUPPORTED_SPARQL_OPERATORS).map {|c| c.const_get(:NAME)}

  unless uh_oh.empty?
    raise SHACL::Error, "Query must not include operators #{uh_oh.to_sxp}: #{query_string}"
  end

  # Additionally, queries must not bind to special variables
  operators.select {|op| op.is_a?(SPARQL::Algebra::Operator::Extend)}.each do |extend|
    if extend.operands.first.any? {|v, e| PRE_BOUND.include?(v.to_sym)}
      raise SHACL::Error, "Query must not bind pre-bound variables: #{query_string}"
    end
  end

  operands << query
  new(*operands, **node_opts)
end

Instance Method Details

#conforms(node, path: nil, depth: 0, **options) ⇒ Array<SHACL::ValidationResult>

Validates the specified property within graph, a list of ValidationResult.

A property conforms the nodes found by evaluating it’s path all conform.

Last operand is the parsed query. Bound variables are added as a table entry joined to the query.

Parameters:

  • node (RDF::Term)

    focus node

  • path (RDF::URI, SPARQL::Algebra::Expression) (defaults to: nil)

    the property path from the focus node to the value nodes.

  • options (Hash{Symbol => Object})

Returns:



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/shacl/algebra/sparql_constraint.rb', line 31

def conforms(node, path: nil, depth: 0, **options)
  return [] if deactivated?
  options = {severity: RDF::Vocab::SHACL.Violation}.merge(options)
  log_debug(NAME, depth: depth) {SXP::Generator.string({node: node}.to_sxp_bin)}

  # Aggregate repo containing both data-graph (as the default) and shapes-graph, named by it's IRI
  aggregate = RDF::AggregateRepo.new(graph.data, shapes_graph.data) do |ag|
    ag.default false
    ag.named shapes_graph.graph_name if shapes_graph.graph_name
  end

  aggregate.default_graph
  bindings = RDF::Query::Solution.new({
    currentShape: options[:shape],
    shapesGraph: shapes_graph.graph_name,
    PATH: path,
    this: node,
  }.compact)
  solutions = operands.last.execute(aggregate,
    bindings: bindings,
    depth: depth + 1,
    logger: (@logger || @options[:logger]),
    **options)
  if solutions.empty?
    satisfy(focus: node, path: path,
      message: @options.fetch(:message, "node conforms to SPARQL component"),
      component: RDF::Vocab::SHACL.SPARQLConstraintComponent,
      depth: depth, **options)
  else
    solutions.map do |solution|
      not_satisfied(focus: node, path: (path || solution[:path]),
        value: (solution[:value] || node),
        message: @options.fetch(:message, "node does not coform to SPARQL component"),
        resultSeverity: options.fetch(:severity),
        component: RDF::Vocab::SHACL.SPARQLConstraintComponent,
        depth: depth, **options)
    end
  end
end