Skip to content

Commit

Permalink
Finish 0.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Sep 1, 2023
2 parents a616712 + ce69f96 commit a82df96
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 66 deletions.
16 changes: 5 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,12 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby:
- 2.6
- 2.7
- "3.0"
- 3.1
- ruby-head
- jruby
ruby: ['3.0', 3.1, 3.2, ruby-head, jruby]
steps:
- name: Clone repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Clone Test Suite repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: w3c/data-shapes
path: spec/w3c-data-shapes
Expand All @@ -44,7 +38,7 @@ jobs:
- name: Run tests
run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v1.1.2
if: "matrix.ruby == '3.0'"
uses: coverallsapp/github-action@v2
if: "matrix.ruby == '3.2'"
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/generate-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
name: Update gh-pages with docs
steps:
- name: Clone repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
Expand Down
4 changes: 3 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ gem 'sparql', git: 'https://github.com/ruby-rdf/sparql.git',
gem 'sxp', git: 'https://github.com/dryruby/sxp.rb.git', branch: 'develop'

group :development, :test do
gem 'ebnf', git: 'https://github.com/dryruby/ebnf.git', branch: 'develop'
gem 'rdf-isomorphic', git: 'https://github.com/ruby-rdf/rdf-isomorphic.git', branch: 'develop'
gem 'rdf-aggregate-repo', git: 'https://github.com/ruby-rdf/rdf-aggregate-repo.git', branch: 'develop'
gem 'rdf-ordered-repo', git: 'https://github.com/ruby-rdf/rdf-ordered-repo.git', branch: 'develop'
gem 'rdf-reasoner', git: 'https://github.com/ruby-rdf/rdf-reasoner.git', branch: 'develop'
gem 'rdf-spec', git: 'https://github.com/ruby-rdf/rdf-spec.git', branch: 'develop'
gem 'rdf-turtle', git: 'https://github.com/ruby-rdf/rdf-turtle.git', branch: 'develop'
gem 'rdf-vocab', git: 'https://github.com/ruby-rdf/rdf-vocab.git', branch: 'develop'
gem 'rdf-xsd', git: 'https://github.com/ruby-rdf/rdf-xsd.git', branch: 'develop'
gem 'sparql-client', git: 'https://github.com/ruby-rdf/sparql-client.git', branch: 'develop'

gem 'rake'
gem 'simplecov', '~> 0.21', platforms: :mri
gem 'simplecov', '~> 0.22', platforms: :mri
gem 'simplecov-lcov', '~> 0.8', platforms: :mri
end

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is a pure-Ruby library for working with the [Shape Constraint Language][SHACL Spec] to validate the shape of [RDF][] graphs.

[![Gem Version](https://badge.fury.io/rb/shacl.png)](https://badge.fury.io/rb/shacl)
[![Gem Version](https://badge.fury.io/rb/shacl.svg)](https://badge.fury.io/rb/shacl)
[![Build Status](https://github.com/ruby-rdf/shacl/workflows/CI/badge.svg?branch=develop)](https://github.com/ruby-rdf/shacl/actions?query=workflow%3ACI)
[![Coverage Status](https://coveralls.io/repos/github/ruby-rdf/shacl/badge.svg?branch=develop)](https://coveralls.io/github/ruby-rdf/shacl?branch=develop)
[![Gitter chat](https://badges.gitter.im/ruby-rdf/rdf.png)](https://gitter.im/ruby-rdf/rdf)
Expand Down Expand Up @@ -87,10 +87,10 @@ This implementation is certainly not performant. Some things that can be be cons

## Dependencies

* [Ruby](https://ruby-lang.org/) (>= 2.6)
* [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.2)
* [Ruby](https://ruby-lang.org/) (>= 3.0)
* [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.3)
* [SPARQL](https://rubygems.org/gems/sparql) (~> 3.2)
* [json-ld](https://rubygems.org/gems/json-ld) (~> 3.2)
* [json-ld](https://rubygems.org/gems/json-ld) (~> 3.3)
* [sxp](https://rubygems.org/gems/sxp) (~> 1.2)

## Installation
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.0
0.4.0
11 changes: 10 additions & 1 deletion lib/shacl/algebra.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@ def self.from_json(operator, **options)
return operator['@list'].map {|e| from_json(e, **options)} if operator.key?('@list')

type = operator.fetch('type', [])
type << (operator["path"] ? 'PropertyShape' : 'NodeShape') if type.empty?
if type.empty?
type << if operator["path"]
'PropertyShape'
elsif operator['nodeValidator'] || operator['propertyValidator'] || operator['validator']
'ConstraintComponent'
else
'NodeShape'
end
end
klass = case
when type.include?('NodeShape') then NodeShape
when type.include?('PropertyShape') then PropertyShape
when type.include?('ConstraintComponent') then ConstraintComponent
else raise SHACL::Error, "from_json: unknown type #{type.inspect}"
end

Expand Down
58 changes: 58 additions & 0 deletions lib/shacl/algebra/constraint_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,63 @@ module SHACL::Algebra
# Constraint Components define basic constraint behaivor through _mandatory_ and _optional_ parameters. Constraints are accessed through their parameters.
#
class ConstraintComponent < Operator

# Class Methods
class << self
##
# Creates an operator instance from a parsed SHACL representation.
#
# Special case for SPARQL ConstraintComponents.
#
# @param [Hash] operator
# @param [Hash] options ({})
# @option options [Hash{String => RDF::URI}] :prefixes
# @return [Operator]
def from_json(operator, **options)
operands = []

# Component is known by its subject IRI
id = operator.fetch('id')

# Component class (for instantiation) is based on the _local name_ of the component IRI
class_name = ncname(id)

parameters = operator.fetch('parameter', []).inject({}) do |memo, param|
# Symbolize keys
param = param.inject({}) {|memo, (k,v)| memo.merge(k.to_sym => v)}

plc = ncname(param[:path])

# Add class and local name
param = param.merge(class: class_name, local_name: plc)
memo.merge(param[:path] => param)
end

# Add parameters to operator lookup
add_component(class_name, parameters)

# Add parameter identifiers to operands
operands << [:parameters, parameters.keys]

# FIXME: labelTemplate

validator = %w(validator nodeValidator propertyValidator).inject(nil) do |memo, p|
memo || (SPARQLConstraintComponent.from_json(operator[p]) if operator.key?(p))
end
raise SHACL::Error, "Constraint Component has no validator" unless validator

operands << [:validator, validator]

new(*operands, **options)
end

# Extract the NCName tail of an IRI as a symbol.
#
# @param [RDF::URI] uri
# @return [Symbol]
def ncname(uri)
uri.to_s.match(/(\w+)$/).to_s.to_sym
end
end
end
end
78 changes: 57 additions & 21 deletions lib/shacl/algebra/operator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class datatype nodeKind
# Graph from which original shapes were loaded.
# @return [RDF::Graph]
attr_accessor :shapes_graph

# Parameters to components.
PARAMETERS = {
and: {class: :AndConstraintComponent},
Expand Down Expand Up @@ -151,19 +152,19 @@ class datatype nodeKind
},
property: {class: :PropertyConstraintComponent},
qualifiedMaxCount: {
class: :QualifiedMaxCountConstraintComponent,
class: :QualifiedValueConstraintComponent,
datatype: RDF::XSD.integer,
},
qualifiedValueShape: {
class: %i(QualifiedMaxCountConstraintComponent QualifiedMinCountConstraintComponent),
class: :QualifiedValueConstraintComponent,
},
qualifiedValueShapesDisjoint: {
class: %i(QualifiedMaxCountConstraintComponent QualifiedMinCountConstraintComponent),
class: :QualifiedValueConstraintComponent,
datatype: RDF::XSD.boolean,
optional: true,
},
qualifiedMinCount: {
class: :QualifiedMinCountConstraintComponent,
class: :QualifiedValueConstraintComponent,
datatype: RDF::XSD.integer
},
sparql: {class: :SPARQLConstraintComponent},
Expand All @@ -175,21 +176,43 @@ class datatype nodeKind
xone: {class: :XoneConstraintComponent},
}

# Constraint Component classes indexed to their mandatory and optional parameters.
#
# @note for builtins, corresponding Ruby classes may not exist.
COMPONENT_PARAMS = PARAMETERS.inject({}) do |memo, (param, properties)|
memo.merge(Array(properties[:class]).inject(memo) do |mem, cls|
entry = mem.fetch(cls, {})
param_type = properties[:optional] ? :optional : :mandatory
entry[param_type] ||= []
entry[param_type] << param
mem.merge(cls => entry)
end)
end

## Class methods
class << self
# Add parameters and class def from a SPARQL-based Constraint Component
#
# @param [RDF::URI] cls The URI of the constraint component.
# @param [Hash{Symbol => Hash}] parameters Definitions of mandatory and optional parameters for this component.
def add_component(cls, parameters)
# Remember added paraemters.
# FIXME: should merge parameters
@added_parameters = (@added_parameters || {}).merge(parameters)
# Rebuild
@params = @component_params = nil
end

# Defined parameters for components, which may be supplemented by SPARQL-based Constraint Components. A parameter may be mapped to more than one component class.
#
# @return [Hash{Symbol => Hash}] Returns each parameter referencing the component classes it is used in, and the property validators for values of that parameter.
def params
@params ||= PARAMETERS.merge(@added_parameters || {})
end

# Constraint Component classes indexed to their mandatory and optional parameters, which may be supplemented by SPARQL-based Constraint Components.
#
# @return [Hash{Symbol => Hash}]
# Returns a hash relating each component URI to its optional and mandatory parameters.
def component_params
@component_params ||= params.inject({}) do |memo, (param, properties)|
memo.merge(Array(properties[:class]).inject(memo) do |mem, cls|
entry = mem.fetch(cls, {})
param_type = properties[:optional] ? :optional : :mandatory
entry[param_type] ||= []
entry[param_type] << param
mem.merge(cls => entry)
end)
end
end

##
# Creates an operator instance from a parsed SHACL representation
# @param [Hash] operator
Expand All @@ -205,7 +228,7 @@ def from_json(operator, **options)
# Node Options and operands on shape or node, which are not Constraint Component Parameters
operator.each do |k, v|
k = k.to_sym
next if v.nil? || PARAMETERS.include?(k)
next if v.nil? || params.include?(k)
case k
# List properties
when :id then node_opts[:id] = iri(v, vocab: false, **options)
Expand Down Expand Up @@ -234,8 +257,8 @@ def from_json(operator, **options)
used_components = {}
operator.each do |k, v|
k = k.to_sym
next if v.nil? || !PARAMETERS.include?(k)
param_props = PARAMETERS[k]
next if v.nil? || !params.include?(k)
param_props = params[k]
param_classes = Array(param_props[:class])

# Keep track of components which have been used.
Expand Down Expand Up @@ -293,21 +316,34 @@ def from_json(operator, **options)
name = klass.const_get(:NAME)
# If the key `k` is the same as the NAME of the class, create the instance with the defined element values.
if name == k
param_classes.each do |cls|
# Add `k` as a mandatory parameter fulfilled
(used_components[cls][:mandatory_parameters] ||= []) << k
end

# Instantiate the compoent
elements.map {|e| klass.new(*e, **options.dup)}
else
# Add non-primary parameters for subsequent insertion
param_classes.each do |cls|
# Add `k` as a mandatory parameter fulfilled if it is so defined
(used_components[cls][:mandatory_parameters] ||= []) << k unless
params[k][:optional]

# Add parameter as S-Expression operand
(used_components[cls][:parameters] ||= []) << elements.unshift(k)
end
[] # No instances created
end
end
end

# Record the instances created by class and add as operands
# Record the instances created by class and its operands
param_classes.each do |cls|
used_components[cls][:instances] = instances
end

# FIXME: Only add instances when all mandatory parameters are present.
operands.push(*instances)
end

Expand Down
18 changes: 5 additions & 13 deletions lib/shacl/algebra/qualified_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ def conforms(node, path:, value_nodes:, depth: 0, **options)
params, ops = operands.partition {|o| o.is_a?(Array) && o.first.is_a?(Symbol)}
params = params.inject({}) {|memo, a| memo.merge(a.first => a.last)}

max_count = params[:qualifiedMinCount].to_i
min_count = params[:qualifiedMinCount].to_i
max_count = params[:qualifiedMinCount]
min_count = params[:qualifiedMinCount]
# FIXME: figure this out
disjoint = !!params[:qualifiedValueShapesDisjoint]

Expand All @@ -29,33 +29,25 @@ def conforms(node, path:, value_nodes:, depth: 0, **options)

count = results.select(&:conform?).length
log_debug(NAME, depth: depth) {"#{count}/#{results} conforming shapes"}
if count < min_count
if min_count && count < min_count.to_i
not_satisfied(focus: node, path: path,
message: "only #{count} conforming values, requires at least #{min_count}",
resultSeverity: options.fetch(:severity),
component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
depth: depth, **options)
elsif count > max_count
elsif max_count && count > max_count.to_i
not_satisfied(focus: node, path: path,
message: "#{count} conforming values, requires at most #{max_count}",
resultSeverity: options.fetch(:severity),
component: RDF::Vocab::SHACL.QualifiedMaxCountConstraintComponent,
depth: depth, **options)
else
satisfy(focus: node, path: path,
message: "#{min_count} <= #{count} <= #{max_count} values conform",
message: "#{min_count.to_i} <= #{count} <= #{max_count || 'inf'} values conform",
component: RDF::Vocab::SHACL.QualifiedMinCountConstraintComponent,
depth: depth, **options)
end
end
end
end

# Version on QualifiedConstraintComponent with required `qualifiedMaxCount` parameter
class QualifiedMaxCountConstraintComponent < QualifiedValueConstraintComponent
end

# Version on QualifiedConstraintComponent with required `qualifiedMinCount` parameter
class QualifiedMinCountConstraintComponent < QualifiedValueConstraintComponent
end
end
Loading

0 comments on commit a82df96

Please sign in to comment.