Skip to content

Commit

Permalink
feat: select shapes by targets
Browse files Browse the repository at this point in the history
  • Loading branch information
tpluscode committed Nov 14, 2020
1 parent efa4eeb commit c41966b
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-tomatoes-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hydrofoil/shaperone-core": patch
---

Implement complete shape selection based on targets
3 changes: 2 additions & 1 deletion packages/core/models/forms/effects/pushFocusNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PropertyShape } from '@rdfine/shacl'
import type { FocusNode } from '../../../index'
import type { BaseParams } from '../../index'
import type { Store } from '../../../state'
import { matchShapes } from '../../shapes/lib'

export function pushFocusNode(store: Store) {
const dispatch = store.getDispatch()
Expand All @@ -20,7 +21,7 @@ export function pushFocusNode(store: Store) {
focusNode,
editors,
shape: property.node,
shapes: shapes.get(form)?.shapes || [],
shapes: matchShapes(shapes.get(form)?.shapes).to(focusNode),
shouldEnableEditorChoice: formState.shouldEnableEditorChoice,
})
}
Expand Down
3 changes: 2 additions & 1 deletion packages/core/models/forms/effects/shapes/setGraph.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AnyPointer } from 'clownface'
import type { Store } from '../../../../state'
import { SetShapesGraphParams } from '../../../shapes/reducers'
import { matchShapes } from '../../../shapes/lib'

export default function setGraph(store: Store) {
const dispatch = store.getDispatch()
Expand All @@ -25,7 +26,7 @@ export default function setGraph(store: Store) {
form,
focusNode,
editors,
shapes: shapes.get(form)?.shapes || [],
shapes: matchShapes(shapes.get(form)?.shapes).to(focusNode),
shouldEnableEditorChoice: formState.shouldEnableEditorChoice,
})
})
Expand Down
2 changes: 1 addition & 1 deletion packages/core/models/forms/lib/stateBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export function initialiseFocusNode(params: InitializeParams, previous: FocusNod
const { properties, groups } = initialisePropertyShapes(shape, params, previous)

return {
shape: shapes[0],
shape,
shapes,
focusNode,
groups,
Expand Down
14 changes: 10 additions & 4 deletions packages/core/models/forms/reducers/replaceFocusNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@ import produce from 'immer'
import { BaseParams, formStateReducer } from '../../index'
import { initialiseFocusNode } from '../lib/stateBuilder'
import type { FormState } from '../index'
import { matchShapes } from '../../shapes/lib'

type StackAction = {appendToStack?: true} | {replaceStack?: true}

export type Params = Parameters<typeof initialiseFocusNode>[0] & StackAction & BaseParams

export const createFocusNodeState = formStateReducer((state: FormState, { focusNode, ...rest }: Params) => produce(state, (draft) => {
draft.focusNodes[focusNode.value] = initialiseFocusNode({
focusNode,
...rest,
}, state.focusNodes[focusNode.value])
let { shapes } = rest

if ('appendToStack' in rest && rest.appendToStack) {
draft.focusStack.push(focusNode)
shapes = matchShapes(shapes).to(focusNode)
}

if ('replaceStack' in rest && rest.replaceStack) {
draft.focusStack = [focusNode]
shapes = matchShapes(shapes).to(focusNode)
}

draft.focusNodes[focusNode.value] = initialiseFocusNode({
focusNode,
...rest,
shapes,
}, state.focusNodes[focusNode.value])
}))
49 changes: 41 additions & 8 deletions packages/core/models/shapes/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
import { Shape } from '@rdfine/shacl'
import { rdf } from '@tpluscode/rdf-ns-builders'
import { rdf, sh, rdfs } from '@tpluscode/rdf-ns-builders'
import TermMap from '@rdf-esm/term-map'
import { FocusNode } from '../../../index'

export function matchFor(focusNode: FocusNode) {
return (shape: Shape) => {
const { targetNode, targetClass } = shape
const scores = new TermMap([
[sh.targetNode, 20],
[sh.targetClass, 10],
[sh.targetObjectsOf, 5],
[sh.targetSubjectsOf, 5],
])

function toScoring(focusNode: FocusNode) {
return (matched: [Shape, number][], shape: Shape): [Shape, number][] => {
let score = 0
const { targetNode, targetClass, targetObjectsOf, targetSubjectsOf } = shape
if (targetNode.some(targetNode => targetNode.equals(focusNode.term))) {
return true
score = scores.get(sh.targetNode) || 0
}

const classIds = targetClass.map(c => c.id)
if (focusNode.has(rdf.type, classIds).values.some(Boolean)) {
return true
if (shape.types.has(rdfs.Class)) {
classIds.push(shape.id)
}
if (focusNode.has(rdf.type, classIds).terms.length) {
score = Math.max(score, scores.get(sh.targetClass) || 0)
}

if (targetSubjectsOf && focusNode.out(targetSubjectsOf.id).terms.length) {
score = Math.max(score, scores.get(sh.targetSubjectsOf) || 0)
}

if (targetObjectsOf && focusNode.in(targetObjectsOf.id).terms.length) {
score = Math.max(score, scores.get(sh.targetObjectsOf) || 0)
}

return [
...matched,
[shape, score],
]
}
}

return false
export function matchShapes(shapes: Shape[] = []): { to: (focusNode: FocusNode) => Shape[] } {
return {
to(focusNode: FocusNode) {
return shapes.reduce(toScoring(focusNode), [])
.filter(match => match[1] > 0)
.sort((left, right) => right[1] - left[1])
.map(([shape]) => shape)
},
}
}
98 changes: 98 additions & 0 deletions packages/core/test/models/shapes/lib/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, it } from 'mocha'
import clownface from 'clownface'
import $rdf from 'rdf-ext'
import { rdf, schema, rdfs } from '@tpluscode/rdf-ns-builders'
import { expect } from 'chai'
import { nodeShape } from '../../../util'
import { matchShapes } from '../../../../models/shapes/lib'

describe('models/shapes/lib', () => {
describe('matchShapes', () => {
const focusNode = clownface({ dataset: $rdf.dataset() })
.namedNode('john')
.addOut(rdf.type, schema.Person)
.addOut(schema.alumniOf, $rdf.namedNode('UCLA'))
.addIn(schema.parent, $rdf.namedNode('jane'))

const johnShape = nodeShape({ targetNode: [$rdf.namedNode('john')] })
const personClassShape = nodeShape({ targetClass: [schema.Person] })
const alumnusShape = nodeShape({ targetSubjectsOf: schema.alumniOf })
const parentShape = nodeShape({ targetObjectsOf: schema.parent })
const implicitPersonTargetShape = nodeShape(schema.Person, { types: [rdfs.Class] })

it('prefers sh:targetNode over all other shape targets', () => {
// given
const shapes = [
personClassShape,
alumnusShape,
johnShape,
parentShape,
]

// when
const matched = matchShapes(shapes).to(focusNode)

// then
expect(matched).to.have.length(4)
expect(matched[0].id).to.deep.eq(johnShape.id)
})

it('prefers sh:targetClass over property target shapes', () => {
// given
const shapes = [
alumnusShape,
personClassShape,
parentShape,
]

// when
const matched = matchShapes(shapes).to(focusNode)

// then
expect(matched).to.have.length(3)
expect(matched[0].id).to.deep.eq(personClassShape.id)
})

it('prefers implicit rdf:Class shape over property shapes', () => {
// given
const shapes = [
alumnusShape,
implicitPersonTargetShape,
parentShape,
]

// when
const matched = matchShapes(shapes).to(focusNode)

// then
expect(matched).to.have.length(3)
expect(matched[0].id).to.deep.eq(implicitPersonTargetShape.id)
})

it('matches shape by subject usage in graph', () => {
// given
const shapes = [
alumnusShape,
]

// when
const matched = matchShapes(shapes).to(focusNode)

// then
expect(matched[0].id).to.deep.eq(alumnusShape.id)
})

it('matches shape by object usage in graph', () => {
// given
const shapes = [
parentShape,
]

// when
const matched = matchShapes(shapes).to(focusNode)

// then
expect(matched[0].id).to.deep.eq(parentShape.id)
})
})
})
16 changes: 14 additions & 2 deletions packages/core/test/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PropertyShape } from '@rdfine/shacl'
import { PropertyShapeMixin } from '@rdfine/shacl'
import type { NodeShape, PropertyShape } from '@rdfine/shacl'
import { NodeShapeMixin, PropertyShapeMixin } from '@rdfine/shacl'
import RdfResource, { Initializer, ResourceIdentifier } from '@tpluscode/rdfine/RdfResource'
import clownface, { GraphPointer } from 'clownface'
import * as $rdf from '@rdf-esm/dataset'
Expand Down Expand Up @@ -29,3 +29,15 @@ export function propertyShape(shape?: GraphPointer<ResourceIdentifier> | Initial
initializer,
})
}

function isTerm(term: any): term is ResourceIdentifier {
return 'termType' in term
}

export function nodeShape(idOrInit: ResourceIdentifier | Initializer<NodeShape>, shape?: Initializer<NodeShape>): NodeShape {
const graph = clownface({ dataset: $rdf.dataset() })
if (isTerm(idOrInit)) {
return new NodeShapeMixin.Class(graph.node(idOrInit), shape)
}
return new NodeShapeMixin.Class(graph.blankNode(), idOrInit)
}

0 comments on commit c41966b

Please sign in to comment.