Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Negated Property Set #41

Merged
merged 5 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/curvy-ways-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"clownface-shacl-path": minor
---

Adds `NegatedPropertySet` class to support negated paths in `findNodes`, `toAlgebra` and `toAlgebra`
NOTE: negated paths are not supported in SHACL representation (see https://github.com/w3c/shacl/issues/20)
5 changes: 5 additions & 0 deletions .changeset/pretty-goats-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clownface-shacl-path": minor
---

Allow calling `toSparql` with a path object
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"release": "changeset publish"
},
"dependencies": {
"@rdfjs/term-map": "^2.0.0",
"@rdfjs/term-set": "^2.0.1",
"@tpluscode/rdf-ns-builders": ">=3.0.2",
"@tpluscode/rdf-string": "^1.3.1"
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export {
InversePath,
SequencePath,
ZeroOrOnePath,
NegatedPropertySet,
fromNode,
} from './lib/path.js'
48 changes: 41 additions & 7 deletions src/lib/findNodes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { NamedNode, Term } from '@rdfjs/types'
/* eslint-disable camelcase */
import type { NamedNode, Term, Quad_Predicate, Quad } from '@rdfjs/types'
import type { MultiPointer } from 'clownface'
import TermSet from '@rdfjs/term-set'
import TermMap from '@rdfjs/term-map'
import * as Path from './path.js'
import { ShaclPropertyPath } from './path.js'

interface Context {
pointer: MultiPointer
Expand Down Expand Up @@ -67,6 +68,41 @@ class FindNodesVisitor extends Path.PathVisitor<Term[], Context> {
visitPredicatePath({ term }: Path.PredicatePath, { pointer }: Context): Term[] {
return pointer.out(term).terms
}

visitNegatedPropertySet({ paths }: Path.NegatedPropertySet, { pointer }: Context): Term[] {
const outLinks = [...pointer.dataset.match(pointer.term)]
.reduce(toPredicateMap('object'), new TermMap())

const inLinks = [...pointer.dataset.match(null, null, pointer.term)]
.reduce(toPredicateMap('subject'), new TermMap())
let includeInverse = false

for (const path of paths) {
if (path instanceof Path.PredicatePath) {
outLinks.delete(path.term)
} else {
includeInverse = true
inLinks.delete(path.path.term)
}
}

if (includeInverse) {
return [...new TermSet([...outLinks.values(), ...inLinks.values()].flatMap(v => [...v]))]
}

return [...outLinks.values()].flatMap(v => [...v])
}
}

function toPredicateMap(so: 'subject' | 'object') {
return (map: TermMap<Quad_Predicate, TermSet>, quad: Quad) => {
if (!map.has(quad.predicate)) {
map.set(quad.predicate, new TermSet())
}

map.get(quad.predicate)!.add(quad[so])
return map
}
}

/**
Expand All @@ -75,11 +111,9 @@ class FindNodesVisitor extends Path.PathVisitor<Term[], Context> {
* @param pointer starting node
* @param shPath SHACL Property Path
*/
export function findNodes(pointer: MultiPointer, shPath: MultiPointer | NamedNode | ShaclPropertyPath): MultiPointer {
let path: ShaclPropertyPath
if ('termType' in shPath) {
path = Path.fromNode(pointer.node(shPath))
} else if ('value' in shPath) {
export function findNodes(pointer: MultiPointer, shPath: MultiPointer | NamedNode | Path.ShaclPropertyPath): MultiPointer {
let path: Path.ShaclPropertyPath
if ('termType' in shPath || 'value' in shPath) {
path = Path.fromNode(shPath)
} else {
path = shPath
Expand Down
18 changes: 16 additions & 2 deletions src/lib/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export abstract class PathVisitor<R = void, TArg = unknown> {
if (path instanceof ZeroOrOnePath) {
return this.visitZeroOrOnePath(path, arg)
}
if (path instanceof NegatedPropertySet) {
return this.visitNegatedPropertySet(path, arg)
}

throw new Error('Unexpected path')
}
Expand All @@ -36,6 +39,7 @@ export abstract class PathVisitor<R = void, TArg = unknown> {
abstract visitZeroOrMorePath(path: ZeroOrMorePath, arg?: TArg): R
abstract visitOneOrMorePath(path: OneOrMorePath, arg?: TArg): R
abstract visitZeroOrOnePath(path: ZeroOrOnePath, arg?: TArg): R
abstract visitNegatedPropertySet(path: NegatedPropertySet, arg?: TArg): R
}

export abstract class ShaclPropertyPath {
Expand Down Expand Up @@ -72,8 +76,8 @@ export class AlternativePath extends ShaclPropertyPath {
}
}

export class InversePath extends ShaclPropertyPath {
constructor(public path: ShaclPropertyPath) {
export class InversePath<P extends ShaclPropertyPath = ShaclPropertyPath> extends ShaclPropertyPath {
constructor(public path: P) {
super()
}

Expand Down Expand Up @@ -112,6 +116,16 @@ export class ZeroOrOnePath extends ShaclPropertyPath {
}
}

export class NegatedPropertySet extends ShaclPropertyPath {
constructor(public paths: Array<PredicatePath | InversePath<PredicatePath>>) {
super()
}

accept<T>(visitor: PathVisitor<any, T>, arg: T) {
return visitor.visitNegatedPropertySet(this, arg)
}
}

interface Options {
allowNamedNodeSequencePaths?: boolean
}
Expand Down
31 changes: 28 additions & 3 deletions src/lib/toAlgebra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,39 @@ class ToAlgebra extends Path.PathVisitor<PropertyPath | NamedNode> {
items: [path.path.accept(this)],
}
}

visitNegatedPropertySet({ paths }: Path.NegatedPropertySet): PropertyPath {
return {
type: 'path',
pathType: '!',
items: paths.map((path) => {
if (path instanceof Path.PredicatePath) {
return path.term
}

return {
type: 'path',
pathType: '^',
items: [path.path.term],
}
}),
}
}
}

/**
* Creates a sparqljs object which represents a SHACL path as Property Path
*
* @param path SHACL Property Path
* @param shPath SHACL Property Path
*/
export function toAlgebra(path: MultiPointer | NamedNode): PropertyPath | NamedNode {
export function toAlgebra(shPath: MultiPointer | NamedNode | Path.ShaclPropertyPath): PropertyPath | NamedNode {
let path: Path.ShaclPropertyPath
if ('termType' in shPath || 'value' in shPath) {
path = Path.fromNode(shPath)
} else {
path = shPath
}

const visitor = new ToAlgebra()
return visitor.visit(Path.fromNode(path))
return visitor.visit(path)
}
6 changes: 5 additions & 1 deletion src/lib/toSparql.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NamedNode } from '@rdfjs/types'
import { SparqlTemplateResult, sparql } from '@tpluscode/rdf-string'
import { MultiPointer } from 'clownface'
import { assertWellFormedPath, fromNode, PathVisitor, ShaclPropertyPath } from './path.js'
import { assertWellFormedPath, fromNode, NegatedPropertySet, PathVisitor, ShaclPropertyPath } from './path.js'
import * as Path from './path.js'

class ToSparqlPropertyPath extends PathVisitor<SparqlTemplateResult, { isRoot: boolean }> {
Expand Down Expand Up @@ -45,6 +45,10 @@ class ToSparqlPropertyPath extends PathVisitor<SparqlTemplateResult, { isRoot: b
return sparql`${predicate}`
}

visitNegatedPropertySet(path: NegatedPropertySet): SparqlTemplateResult {
return sparql`!(${path.paths.reduce(this.pathChain('|'), sparql``)})`
}

private pathChain(operator: string) {
return (previous: SparqlTemplateResult, current: Path.ShaclPropertyPath, index: number) => {
if (index === 0) {
Expand Down
Loading
Loading