Skip to content

Commit

Permalink
feat: deep sh:node
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Jan 9, 2023
1 parent a2c57ce commit d2d5996
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 88 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-wolves-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/shape-to-query": minor
---

Support for deep `sh:node` which get combined with parent property shapes' paths
6 changes: 6 additions & 0 deletions .changeset/sweet-spoons-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hydrofoil/shape-to-query": minor
---

Changed the signature of `shapeToPatterns`. Now it returns an object with functions to create full `CONSTRUCT` and `WHERE` clauses
and implements the `Iterable<SparqlTemplateResult>` interface
8 changes: 4 additions & 4 deletions packages/labs/describeStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DescribeStrategyFactory } from '@hydrofoil/labyrinth/describeStrategy'
import { CONSTRUCT } from '@tpluscode/sparql-builder'
import { CONSTRUCT, sparql } from '@tpluscode/sparql-builder'
import clownface, { GraphPointer } from 'clownface'
import { shapeToPatterns } from '@hydrofoil/shape-to-query'
import { VALUES } from '@tpluscode/sparql-builder/expressions'
Expand Down Expand Up @@ -29,18 +29,18 @@ export const constructByNodeShape: DescribeStrategyFactory<[Options]> =
throw new Error(`Did not find ${shapePath.value} on any of the resource's classes`)
}

const patterns = [typeShape, ...shapes].map((shape, index) => shapeToPatterns(shape, {
const patterns = [typeShape, ...shapes].map((shape, index) => [...shapeToPatterns(shape, {
subjectVariable: 'resource',
objectVariablePrefix: `${index}`,
}))
})])

return (...terms) => {
const resources = terms.map(term => ({ resource: term }))

return CONSTRUCT`${patterns}`
.WHERE`
${VALUES(...resources)}
${patterns.reduce(toUnion)}
${patterns.reduce(toUnion, sparql``)}
`.execute(client.query)
}
}
2 changes: 1 addition & 1 deletion packages/labs/lib/sparql.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sparql, SparqlTemplateResult } from '@tpluscode/sparql-builder'

export function toUnion(previous: SparqlTemplateResult, current: SparqlTemplateResult, index: number): SparqlTemplateResult {
export function toUnion(previous: SparqlTemplateResult, current: SparqlTemplateResult[], index: number): SparqlTemplateResult {
if (index === 0) {
return sparql`{
${current}
Expand Down
4 changes: 2 additions & 2 deletions packages/labs/test/describeStrategy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('@hydrofoil/creta-labs/describeStrategy', () => {
expect(client.query.construct.firstCall.firstArg).to.be.a.query(sparql`
CONSTRUCT {
?resource ${rdf.type} ?resource_0_0 .
?resource a ${foaf.Person} .
?resource ${rdf.type} ${foaf.Person} .
?resource ${foaf.name} ?resource_1_0 .
} WHERE {
VALUES ?resource { ${ex.foobar} }
Expand All @@ -54,7 +54,7 @@ describe('@hydrofoil/creta-labs/describeStrategy', () => {
}
UNION
{
?resource a ${foaf.Person} .
?resource ${rdf.type} ${foaf.Person} .
?resource ${foaf.name} ?resource_1_0 .
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/labyrinth/lib/query/memberAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function createPatternValue(variable: string) {
if (isNodeShape(ptr)) {
const subjectVariable = `${variable}${index}`

return [$rdf.variable(subjectVariable), shapeToPatterns(ptr, { subjectVariable })]
return [$rdf.variable(subjectVariable), sparql`${shapeToPatterns(ptr, { subjectVariable })}`]
}

return [null]
Expand Down
4 changes: 2 additions & 2 deletions packages/labyrinth/test/lib/query/memberAssertion.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parse } from '@labyrinth/testing/nodeFactory'
import { hydra, sh } from '@tpluscode/rdf-ns-builders'
import { hydra, rdf, sh } from '@tpluscode/rdf-ns-builders'
import { expect } from 'chai'
import $rdf from 'rdf-ext'
import { ex } from '@labyrinth/testing/namespace'
Expand Down Expand Up @@ -54,7 +54,7 @@ describe('@hydrofoil/labyrinth/lib/query/memberAssertion', () => {
// then
await expect(patterns).to.be.a.query(sparql`SELECT * {
?member ${ex.foo} ?ma_o0 .
?ma_o0 a ${ex.Bar} .
?ma_o0 ${rdf.type} ${ex.Bar} .
}`)
})
})
128 changes: 95 additions & 33 deletions packages/shape-to-query/lib/shapeToPatterns.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,87 @@
import { NamedNode } from 'rdf-js'
import { NamedNode, Quad } from 'rdf-js'
import type { GraphPointer } from 'clownface'
import { sparql, SparqlTemplateResult } from '@tpluscode/rdf-string'
import { sh, xsd } from '@tpluscode/rdf-ns-builders'
import { rdf, sh, xsd } from '@tpluscode/rdf-ns-builders'
import $rdf from '@rdfjs/data-model'
import { IN } from '@tpluscode/sparql-builder/expressions'
import TermSet from '@rdfjs/term-set'
import { isNamedNode } from 'is-graph-pointer'
import { create, PrefixedVariable } from './variableFactory'

export interface Options {
subjectVariable?: string
focusNode?: NamedNode
objectVariablePrefix?: string
strict?: boolean
patternsOnly?: true
}

type PropertyShapeOptions = Pick<Options, 'objectVariablePrefix' | 'strict'>
type PropertyShapeOptions = Pick<Options, 'objectVariablePrefix'>

const TRUE = $rdf.literal('true', xsd.boolean)

export function shapeToPatterns(shape: GraphPointer, options: Options): SparqlTemplateResult {
interface ShapePatterns extends Iterable<SparqlTemplateResult> {
whereClause(): SparqlTemplateResult
constructClause(): SparqlTemplateResult
}

export function shapeToPatterns(shape: GraphPointer, options: Options): ShapePatterns {
const focusNode = create({
focusNode: options.focusNode,
prefix: options.subjectVariable || 'resource',
})

return sparql`${targetClass(shape, focusNode, options.patternsOnly)}
${propertyShapes(shape, focusNode, options)}`
const { targetClassPattern, targetClassFilter } = targetClass(shape, focusNode)
const resourcePatterns = [...deepPropertyShapePatterns({
shape,
focusNode,
options,
})]

const flatPatterns = () => [targetClassPattern, ...resourcePatterns
.flat()
.reduce((set, quad) => set.add(quad), new TermSet()),
].map(quad => sparql`${quad}`)

return {
[Symbol.iterator]() {
return flatPatterns()[Symbol.iterator]()
},
constructClause() {
return sparql`
${[...flatPatterns()]}
`
},
whereClause() {
return sparql`
${targetClassPattern}
${targetClassFilter}
${toUnion(resourcePatterns)}
`
},
}
}

function targetClass(shape: GraphPointer, focusNode: PrefixedVariable, patternsOnly?: boolean) {
const targetClass = shape.out(sh.targetClass).terms
if (!targetClass.length) {
return ''
function targetClass(shape: GraphPointer, focusNode: PrefixedVariable) {
const targetClass = shape.out(sh.targetClass)
if (!targetClass.terms.length) {
return {}
}

if (targetClass.length === 1) {
return sparql`${focusNode()} a ${targetClass} .`
if (isNamedNode(targetClass)) {
return {
targetClassPattern: $rdf.quad(focusNode(), rdf.type, targetClass.term),
}
}

const typeVar = focusNode.extend('targetClass')
if (patternsOnly) {
return sparql`${focusNode()} a ${typeVar()} .`
}

return sparql`
${focusNode()} a ${typeVar()} .
FILTER ( ${typeVar()} ${IN(...targetClass)} )
`
return {
targetClassPattern: $rdf.quad(focusNode(), rdf.type, typeVar()),
targetClassFilter: sparql`FILTER ( ${typeVar()} ${IN(...targetClass.terms)} )`,
}
}

function propertyShapes(shape: GraphPointer, focusNode: PrefixedVariable, options: PropertyShapeOptions) {
const propertyPatterns = shape.out(sh.property)
.filter(propShape => !propShape.has(sh.deactivated, TRUE).term)
.map((propShape, index) => {
const variable = options.objectVariablePrefix
? focusNode.extend(options.objectVariablePrefix).extend(index)
: focusNode.extend(index)

return sparql`${focusNode()} ${propShape.out(sh.path).term} ${variable()} .`
})

if (propertyPatterns.length > 1 && options.strict === false) {
function toUnion(propertyPatterns: Quad[][]) {
if (propertyPatterns.length > 1) {
return propertyPatterns.reduce((union, next, index) => {
if (index === 0) {
return sparql`{ ${next} }`
Expand All @@ -74,3 +95,44 @@ function propertyShapes(shape: GraphPointer, focusNode: PrefixedVariable, option

return propertyPatterns
}

interface PropertyShapePatterns {
shape: GraphPointer
focusNode: PrefixedVariable
options: PropertyShapeOptions
parentPatterns?: Quad[]
}

function * deepPropertyShapePatterns({ shape, focusNode, options, parentPatterns = [] }: PropertyShapePatterns): Generator<Quad[]> {
const activeProperties = shape.out(sh.property)
.filter(propShape => !propShape.has(sh.deactivated, TRUE).term)
.toArray()

for (const [index, propShape] of activeProperties.entries()) {
const variable = options.objectVariablePrefix
? focusNode.extend(options.objectVariablePrefix).extend(index)
: focusNode.extend(index)

const path = propShape.out(sh.path)
if (!isNamedNode(path)) {
continue
}

const selfPatterns = [...parentPatterns, $rdf.quad(focusNode(), path.term, variable())]

yield selfPatterns

const shNodes = propShape.out(sh.node).toArray()
for (const shNode of shNodes) {
const deepPatterns = deepPropertyShapePatterns({
shape: shNode,
focusNode: variable,
options,
parentPatterns: selfPatterns,
})
for (const deepPattern of deepPatterns) {
yield deepPattern
}
}
}
}
6 changes: 4 additions & 2 deletions packages/shape-to-query/lib/shapeToQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { CONSTRUCT } from '@tpluscode/sparql-builder'
import { shapeToPatterns, Options } from './shapeToPatterns'

export function construct(shape: GraphPointer, options: Omit<Options, 'strict'> = {}) {
const patterns = shapeToPatterns(shape, options)

return CONSTRUCT`
${shapeToPatterns(shape, { ...options, strict: true, patternsOnly: true })}
${patterns.constructClause()}
`.WHERE`
${shapeToPatterns(shape, { ...options, strict: false })}
${patterns.whereClause()}
`
}
4 changes: 3 additions & 1 deletion packages/shape-to-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
},
"dependencies": {
"@rdfjs/data-model": "^1",
"@rdfjs/term-set": "^1",
"@tpluscode/rdf-ns-builders": "^2.0.1",
"@tpluscode/rdf-string": "^0.2.26",
"@tpluscode/sparql-builder": "^0.3"
"@tpluscode/sparql-builder": "^0.3",
"is-graph-pointer": "^1.2.2"
},
"devDependencies": {
"@labyrinth/testing": "^0.0.18",
Expand Down
Loading

0 comments on commit d2d5996

Please sign in to comment.