Skip to content

Commit

Permalink
Merge pull request #236 from hypermedia-app/dash-uri-starts
Browse files Browse the repository at this point in the history
feat: pattern for new URIs
  • Loading branch information
tpluscode committed Sep 5, 2022
2 parents 68d98aa + 5563b66 commit 995a7f6
Show file tree
Hide file tree
Showing 13 changed files with 495 additions and 168 deletions.
6 changes: 6 additions & 0 deletions .changeset/wicked-squids-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@hydrofoil/shaperone-core": patch
---

When adding a new value to a property which was `sh:nodeKind sh:IRI`, an empty IRI `<>` was always created, even if it clashed without existing nodes. Now random IRI references will be generated.
Also, if a property is annotated with `sh1:iriPrefix`, it will be used as base for the created URIs
4 changes: 2 additions & 2 deletions demos/lit-html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"clownface": "^1",
"lit": "^2.0.0",
"multiselect-combo-box": "^2.4.2",
"nanoid": "^3.1.31",
"nanoid": "^4",
"rdf-ext": "^1.3.0"
},
"devDependencies": {
Expand All @@ -54,7 +54,7 @@
"@babel/plugin-proposal-optional-chaining": "^7.10.1",
"@babel/preset-typescript": "^7.10.1",
"@types/clownface": "^1",
"@types/nanoid": "^2.1.0",
"@types/nanoid": "^3",
"@types/rdf-ext": "^1.3.8",
"@types/rdf-js": "^4",
"@types/rdfjs__fetch-lite": "^2.0.2",
Expand Down
28 changes: 27 additions & 1 deletion dist/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ This page describes how various SHACL constructs are handled and, where applicab

[ucr]: https://www.w3.org/TR/shacl-ucr/

## Default value and new graph nodes

When adding a new object to a property, one of the following can happen:

1. If the property has `sh:defaultValue`, that value will be used as new object of that property
2. If the form has `sh:nodeKind` which is `sh:BlankNode`, `sh:BlankNodeOrIRI` or `sh:BlankNodeOrLiteral`, a new blank node will be used as new object of that property
3. If the form has `sh:nodeKind sh:IRI`, a new random, relative URI will be used as new object of that property
4. If the form has `sh:nodeKind` which is `sh:IRI`, `sh:LiteralOrIRI` or `sh:BlankNodeOrIRI` and a non-empty value of `sh1:iriPrefix`, a new, random URI will be used as new object of that property, such as that the `sh1:iriPrefix` is used to prefix
5. Otherwise, do not add a new node to graph.

> [!HINT]
> The value of `sh1:iriPrefix` does not have to be a URI itself but also a relative reference
> [!TIP]
> The shape below will mint new URIs similar to `/resource/{uuid}` but also allow literals in validation
> ```turtle
> PREFIX sh1: <https://hypermedia.app/shaperone#>
> PREFIX sh: <http://www.w3.org/ns/shacl#>
> PREFIX ex: <http://example.com/>
>
> [
> a sh:PropertyShape ;
> sh:path ex:property ;
> sh:nodeKind sh:LiteralOrIRI ;
> sh1:iriPrefix "/resource/" ;
> ]
## [Logical Constraint Components](https://www.w3.org/TR/shacl/#core-components-logical)
The predicates `sh:or`, `sh:or`, `sh:and` and `sh:xone` represents logical operators which specify additional conditions which apply to sets of shapes and properties.
Expand Down Expand Up @@ -50,7 +77,6 @@ ex:PersonShape
[node-logical]: ${playground}/#shapes=%40prefix+sh%3A+%3Chttp%3A%2F%2Fwww.w3.org%2Fns%2Fshacl%23%3E+.%0A%40prefix+schema%3A+%3Chttp%3A%2F%2Fschema.org%2F%3E+.%0A%40prefix+rdfs%3A+%3Chttp%3A%2F%2Fwww.w3.org%2F2000%2F01%2Frdf-schema%23%3E+.%0A%40prefix+ex%3A+%3Chttp%3A%2F%2Fexample.com%2F%3E+.%0A%0Aex%3APersonShape%0A++a+sh%3AShape+%3B%0A++sh%3AtargetClass+schema%3APerson+%3B%0A++rdfs%3Alabel+%22Person%22+%3B%0A++sh%3Aproperty+ex%3ALastNameProperty+%3B%0A++sh%3Aor+%28+%0A++++ex%3AFirstNameProperty+%0A++++ex%3AGivenNameProperty%0A++%29%0A.%0A%0Aex%3ALastNameProperty+%0A++a+sh%3APropertyShape+%3B%0A++sh%3Apath+schema%3AfamilyName+%3B%0A++sh%3Aorder+20+%3B%0A++sh%3AmaxCount+1+%3B%0A.%0A%0Aex%3AFirstNameProperty%0A++sh%3Apath+schema%3AfirstName+%3B++++%0A++sh%3Aorder+10+%3B%0A++sh%3AmaxCount+1+%3B%0A.%0A%0Aex%3AGivenNameProperty%0A++sh%3Apath+schema%3AgivenName+%3B%0A++sh%3Aorder+10+%3B%0A++sh%3AmaxCount+1+%3B%0A.&resource=%7B%0A++%22%40context%22%3A+%7B%0A++++%22rdf%22%3A+%22http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23%22%2C%0A++++%22rdfs%22%3A+%22http%3A%2F%2Fwww.w3.org%2F2000%2F01%2Frdf-schema%23%22%2C%0A++++%22xsd%22%3A+%22http%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema%23%22%2C%0A++++%22schema%22%3A+%22http%3A%2F%2Fschema.org%2F%22%0A++%7D%2C%0A++%22%40id%22%3A+%22http%3A%2F%2Fexample.com%2FJohn_Doe%22%2C%0A++%22%40type%22%3A+%22schema%3APerson%22%2C%0A++%22schema%3AfamilyName%22%3A+%22Doe%22%2C%0A++%22schema%3AfirstName%22%3A+%22John%22%0A%7D&selectedResource=http%3A%2F%2Fexample.com%2FJohn_Doe
## Hidden properties
Shaperone will mark properties annotated with `dash:hidden`. The default Web Components renderer will remove them from the rendered form.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"@tpluscode/eslint-config": "^0.3.2",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"@web/dev-server-esbuild": "^0.2.16",
"@web/dev-server-rollup": "^0.3.5",
"@web/dev-server-esbuild": "^0.3.2",
"@web/dev-server-rollup": "^0.3.19",
"@web/test-runner": "^0.13.15",
"babel-plugin-add-import-extension": "^1.6.0",
"c8": "^7.1.2",
Expand Down
64 changes: 0 additions & 64 deletions packages/core-tests/models/resources/lib/defaultValue.test.ts

This file was deleted.

173 changes: 173 additions & 0 deletions packages/core-tests/models/resources/lib/objectValue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { describe, it } from 'mocha'
import { expect } from 'chai'
import cf from 'clownface'
import $rdf from 'rdf-ext'
import { literal } from '@rdf-esm/data-model'
import { xsd, rdf, foaf, dash, sh } from '@tpluscode/rdf-ns-builders'
import { NodeKind, NodeKindEnum } from '@rdfine/shacl'
import { defaultValue } from '@hydrofoil/shaperone-core/models/resources/lib/objectValue.js'
import { propertyShape } from '@shaperone/testing/util.js'
import { Term } from 'rdf-js'
import sh1 from '@hydrofoil/shaperone-core/ns.js'

describe('core/models/resources/lib/defaultValue', () => {
it('returns default value from property', () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
defaultValue: literal('foo', xsd.anySimpleType),
})

// when
const pointer = defaultValue(property, graph.blankNode())

// then
expect(pointer?.term).to.deep.eq(literal('foo', xsd.anySimpleType))
})

it('returns null when there is no nodeKind', () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
})

// when
const pointer = defaultValue(property, graph.blankNode())

// then
expect(pointer).to.be.null
})

it('returns null when there is nodeKind is sh:IRIOrLiteral ad there is no sh1:iriPrefix', () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind: sh.IRIOrLiteral,
})

// when
const pointer = defaultValue(property, graph.blankNode())

// then
expect(pointer).to.be.null
})

it('creates a random IRI when sh:nodeKind sh:IRI', () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind: sh.IRI,
})

// when
const first = defaultValue(property, graph.blankNode())
const second = defaultValue(property, graph.blankNode())

// then
expect(first?.term).not.to.deep.eq(second)
})

it('uses base from sh1:iriPrefix when sh:nodeKind sh:IRI', () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind: sh.IRI,
[sh1.iriPrefix.value]: 'http://example.com/foo/',
})

// when
const term = defaultValue(property, graph.blankNode())?.term

// then
expect(term?.termType).to.eq('NamedNode')
expect(term?.value).to.match(/^http:\/\/example.com\/foo\/.+$/)
})

it('creates a URI node when node kind is sh:BlankNodeOrIRI and property has sh1:iriPrefix', () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind: sh.BlankNodeOrIRI,
[sh1.iriPrefix.value]: 'http://example.com/foo/',
})

// when
const term = defaultValue(property, graph.blankNode())?.term

// then
expect(term?.termType).to.eq('NamedNode')
expect(term?.value).to.match(/^http:\/\/example.com\/foo\/.+$/)
})

it('creates a URI node when node kind is sh:IRIOrLiteral and property has sh1:iriPrefix', () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind: sh.IRIOrLiteral,
[sh1.iriPrefix.value]: 'http://example.com/foo/',
})

// when
const term = defaultValue(property, graph.blankNode())?.term

// then
expect(term?.termType).to.eq('NamedNode')
expect(term?.value).to.match(/^http:\/\/example.com\/foo\/.+$/)
})

const resourceNodeKinds: [NodeKind, Term['termType']][] = [
[NodeKindEnum.BlankNode, 'BlankNode'],
[NodeKindEnum.BlankNodeOrIRI, 'BlankNode'],
[NodeKindEnum.BlankNodeOrLiteral, 'BlankNode'],
[NodeKindEnum.IRI, 'NamedNode'],
]

resourceNodeKinds.forEach(([nodeKind, termType]) => {
it(`creates a node of type ${termType} when nodeKind is ${nodeKind.value}`, () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind,
})

// when
const pointer = defaultValue(property, graph.blankNode())

// then
expect(pointer?.term?.termType).to.eq(termType)
})

it(`adds sh:class as rdf:type to node kind ${nodeKind.value}`, () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind,
class: foaf.Agent,
[sh1.iriPrefix.value]: 'http://example.com/foo/',
})

// when
const pointer = defaultValue(property, graph.blankNode())

// then
expect(pointer?.out(rdf.type).term).to.deep.eq(foaf.Agent)
})

it(`does not add rdf:type when node kind is ${nodeKind.value} but editor is ${dash.InstancesSelectEditor.value}`, () => {
// given
const graph = cf({ dataset: $rdf.dataset() })
const property = propertyShape(graph.blankNode(), {
nodeKind,
class: foaf.Agent,
[dash.editor.value]: dash.InstancesSelectEditor,
[sh1.iriPrefix.value]: 'http://example.com/foo/',
})

// when
const pointer = defaultValue(property, graph.blankNode())

// then
expect(pointer?.out(rdf.type).term).to.be.undefined
})
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Store } from '../../../../state'
import { notify } from '../../lib/notify.js'
import { Params } from '../../../forms/reducers/addFormField'
import { defaultValue } from '../../lib/defaultValue.js'
import { defaultValue } from '../../lib/objectValue.js'

export default function (store: Store) {
const dispatch = store.getDispatch()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-continue */
import { GraphPointer } from 'clownface'
import { rdf } from '@tpluscode/rdf-ns-builders'
import { defaultValue } from '../../lib/defaultValue.js'
import { defaultValue } from '../../lib/objectValue.js'
import { notify } from '../../lib/notify.js'
import type { Store } from '../../../../state'
import { Params } from '../../../forms/reducers/replaceFocusNodes'
Expand Down
27 changes: 0 additions & 27 deletions packages/core/models/resources/lib/defaultValue.ts

This file was deleted.

Loading

0 comments on commit 995a7f6

Please sign in to comment.