Skip to content

Commit

Permalink
Merge pull request #41 from hypermedia-app/negated-set
Browse files Browse the repository at this point in the history
Negated Property Set
  • Loading branch information
tpluscode authored Oct 24, 2024
2 parents 4ebdf67 + 2781660 commit 20dab71
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 14 deletions.
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

0 comments on commit 20dab71

Please sign in to comment.