Skip to content

Commit 7633c77

Browse files
Merge pull request #62 from commitd/stuarthendren/issue60
feat(rdf): adds label decorators and rdf procesing options
2 parents b0a3ade + e626514 commit 7633c77

File tree

9 files changed

+339
-21
lines changed

9 files changed

+339
-21
lines changed

packages/graph-rdf/src/RdfGraph.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ it('Create from ttl string', () => {
3030
})
3131

3232
it('Create from ttl string using prefixes', () => {
33-
const contentModel = buildGraph(sample, { usePrefix: true })
33+
const contentModel = buildGraph(sample, { usePrefixId: true })
3434
expect(Object.keys(contentModel.nodes)).toHaveLength(16)
3535
expect(Object.keys(contentModel.edges)).toHaveLength(13)
3636

@@ -51,7 +51,7 @@ it('Create from ttl string using prefixes', () => {
5151

5252
it('Can process literals to rdf literal string', () => {
5353
const contentModel = buildGraph(sample, {
54-
usePrefix: true,
54+
usePrefixId: true,
5555
literals: LiteralOption.AS_STRING,
5656
})
5757
const node = contentModel.getNode('txn:123') as ModelNode
@@ -65,7 +65,7 @@ it('Can process literals to rdf literal string', () => {
6565

6666
it('Can process literals to value only', () => {
6767
const contentModel = buildGraph(sample, {
68-
usePrefix: true,
68+
usePrefixId: true,
6969
literals: LiteralOption.VALUE_ONLY,
7070
})
7171
const node = contentModel.getNode('txn:123') as ModelNode
@@ -77,7 +77,7 @@ it('Can process literals to value only', () => {
7777

7878
it('Can parse simple example by adding missing prefixes', () => {
7979
const contentModel = buildGraph(small, {
80-
usePrefix: true,
80+
usePrefixId: true,
8181
additionalPrefixes: {
8282
owl: 'http://www.w3.org/2002/07/owl#',
8383
xsd: 'http://www.w3.org/2001/XMLSchema#',
@@ -143,7 +143,7 @@ it('Decorates by default', () => {
143143

144144
it('Decorates by default', () => {
145145
const contentModel = buildGraph(decorated, {
146-
usePrefix: true,
146+
usePrefixId: true,
147147
decorate: false,
148148
literals: LiteralOption.VALUE_ONLY,
149149
})

packages/graph-rdf/src/RdfGraph.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ContentModel, ModelEdge, ModelNode } from '@committed/graph'
1+
import { ContentModel, ModelEdge, ModelItem, ModelNode } from '@committed/graph'
22
import {
33
DataFactory,
44
Literal,
@@ -69,26 +69,39 @@ export enum LiteralOption {
6969
}
7070

7171
export enum RdfFormat {
72-
/** Suports a permissive superset of Turtle, TriG, N-Triple and N-Quad */
72+
/** Supports a permissive superset of Turtle, TriG, N-Triple and N-Quad */
7373
Turtle = 'turtle',
7474
/** Supports N3 format */
7575
N3 = 'N3',
7676
}
7777

7878
export interface RdfOptions {
79-
usePrefix: boolean
79+
/** use the prefix in the node ids */
80+
usePrefixId: boolean
81+
/** Define how literals are converted to be used in the graph */
8082
literals: LiteralOption
83+
/** Declare the serialization format of the RDF */
8184
format: RdfFormat
85+
/** Transfer declarations from the http://ont.committed.io/graph/decorator namespace to decoration properties of the graph */
8286
decorate: boolean
87+
/** Declare the attribute to use as the label */
8388
label?: string
89+
/** Declare the property to use as the node type */
8490
type?: string
91+
/** Declare the base IRI of the graph */
8592
baseIRI?: string
93+
/** Declare a blank node prefix to be used */
8694
blankNodePrefix?: string
95+
/** Add additional prefixes to the used */
8796
additionalPrefixes?: Record<string, string>
97+
/** Node processor to apply to nodes after conversion */
98+
nodeProcessor?: (node: ModelNode) => ModelNode
99+
/** Edge processor to apply to edges after conversion */
100+
edgeProcessor?: (edge: ModelEdge) => ModelEdge
88101
}
89102

90103
export const DEFAULT_RDF_OPTIONS: RdfOptions = {
91-
usePrefix: false,
104+
usePrefixId: false,
92105
literals: LiteralOption.AS_OBJECT,
93106
format: RdfFormat.Turtle,
94107
label: rdfsLabel.value,
@@ -97,7 +110,10 @@ export const DEFAULT_RDF_OPTIONS: RdfOptions = {
97110
}
98111

99112
interface GraphBuilderOptions
100-
extends Pick<RdfOptions, 'usePrefix' | 'literals' | 'decorate'> {
113+
extends Pick<
114+
RdfOptions,
115+
'usePrefixId' | 'literals' | 'decorate' | 'nodeProcessor' | 'edgeProcessor'
116+
> {
101117
prefixes: Record<string, string>
102118
label?: NamedNode
103119
type?: NamedNode
@@ -120,9 +136,29 @@ class GraphBuilder {
120136
this.triples.forEach((t) => this.addTriple(t))
121137
this.attributes.forEach((t) => this.addAttribute(t))
122138

139+
this.processNodes()
140+
this.processEdges()
123141
return ContentModel.fromRaw({ nodes: this.nodes, edges: this.edges })
124142
}
125143

144+
private processNodes() {
145+
const nodeProcessor = this.options.nodeProcessor
146+
if (nodeProcessor !== undefined) {
147+
Object.keys(this.nodes).forEach((key) => {
148+
this.nodes[key] = nodeProcessor(this.nodes[key])
149+
})
150+
}
151+
}
152+
153+
private processEdges() {
154+
const edgeProcessor = this.options.edgeProcessor
155+
if (edgeProcessor !== undefined) {
156+
Object.keys(this.edges).forEach((key) => {
157+
this.edges[key] = edgeProcessor(this.edges[key])
158+
})
159+
}
160+
}
161+
126162
private addTriple(t: Triple): void {
127163
if (t.object.termType === 'Literal') {
128164
this.attributes.push(t)
@@ -213,7 +249,7 @@ class GraphBuilder {
213249
}
214250

215251
toPrefixedId(id: string): string {
216-
if (this.options.usePrefix) {
252+
if (this.options.usePrefixId) {
217253
const match = Object.keys(this.options.prefixes).find((prefix) =>
218254
id.startsWith(prefix)
219255
)
@@ -300,7 +336,7 @@ export function buildGraph(
300336
)
301337

302338
const builderOptions = {
303-
label: label !== undefined ? DataFactory.namedNode(label) : undefined,
339+
label: typeof label === 'string' ? DataFactory.namedNode(label) : undefined,
304340
type: type !== undefined ? DataFactory.namedNode(type) : undefined,
305341
prefixes,
306342
...rest,

packages/graph-rdf/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * as Rdf from './RdfGraph'
2+
export * as RdfUtil from './utils'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './labels'
2+
export * from './processors'
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { labelWithPrefix, labelWithFragment } from './labels'
2+
3+
it('Can label by prefixed id', () => {
4+
const decorator = labelWithPrefix({
5+
'https://example.org/data/': 'data',
6+
'https://example.org/demo#': 'demo',
7+
})
8+
9+
expect(
10+
decorator({ id: 'https://example.org/data/test', attributes: {} }).label
11+
).toBe('data:test')
12+
expect(
13+
decorator({ id: 'https://example.org/demo#TEST', attributes: {} }).label
14+
).toBe('demo:TEST')
15+
expect(
16+
decorator({ id: 'https://example.org/other', attributes: {} }).label
17+
).toBe('https://example.org/other')
18+
})
19+
20+
it('Can label by id fragment', () => {
21+
const decorator = labelWithFragment()
22+
23+
expect(
24+
decorator({ id: 'https://example.org/data/test', attributes: {} }).label
25+
).toBe('test')
26+
expect(
27+
decorator({ id: 'https://example.org/demo#TEST', attributes: {} }).label
28+
).toBe('TEST')
29+
expect(
30+
decorator({ id: 'https://example.org/other', attributes: {} }).label
31+
).toBe('other')
32+
})
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ModelItem } from '@committed/graph'
2+
3+
const labelNodeBy =
4+
<T extends ModelItem>(mapping: (item: T) => string) =>
5+
(item: T) => ({ label: mapping(item) })
6+
7+
const idTo = (mapping: (id: string) => string) => (item: ModelItem) =>
8+
mapping(item.id)
9+
10+
export const prefixedId =
11+
(prefixes: Record<string, string>) =>
12+
(id: string): string => {
13+
const match = Object.keys(prefixes).find((prefix) => id.startsWith(prefix))
14+
if (match !== undefined) {
15+
const prefix = prefixes[match]
16+
return `${prefix}:${id.substr(match.length)}`
17+
}
18+
return id
19+
}
20+
21+
export const fragmentId = (id: string): string => {
22+
try {
23+
const url = new URL(id)
24+
const hash = url.hash
25+
if (hash) {
26+
return hash.substr(1)
27+
}
28+
const path = url.pathname
29+
return path.substring(path.lastIndexOf('/') + 1)
30+
} catch (e) {
31+
return id
32+
}
33+
}
34+
35+
/**
36+
*
37+
* Creates a decorator to map ids to labels with RDF style prefixes using the provided prefixes.
38+
*
39+
* @param prefixes map of prefix to full URI
40+
* @returns item decorator
41+
*/
42+
export const labelWithPrefix = <T extends ModelItem>(
43+
prefixes: Record<string, string>
44+
): ((item: T) => { label: string }) =>
45+
labelNodeBy<T>(idTo(prefixedId(prefixes)))
46+
47+
/**
48+
*
49+
* Creates a decorator to map ids to labels using the fragment of the URL or last path part
50+
*
51+
* @returns item decorator
52+
*/
53+
export const labelWithFragment = <T extends ModelItem>(): ((item: T) => {
54+
label: string
55+
}) => labelNodeBy<T>(idTo(fragmentId))
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { ModelEdge, ModelNode } from '@committed/graph'
2+
import { cleanProcessor } from './processors'
3+
4+
it('Can label by fragment id', () => {
5+
expect(
6+
cleanProcessor<ModelNode>({
7+
id: 'https://example.org/data/test',
8+
attributes: {},
9+
}).label
10+
).toBe('test')
11+
expect(
12+
cleanProcessor<ModelNode>({
13+
id: 'https://example.org/demo#TEST',
14+
attributes: {},
15+
}).label
16+
).toBe('TEST')
17+
expect(cleanProcessor<ModelNode>({ id: 'test', attributes: {} }).label).toBe(
18+
'test'
19+
)
20+
expect(
21+
cleanProcessor<ModelNode>({ id: 'test', label: 'label', attributes: {} })
22+
.label
23+
).toBe('label')
24+
})
25+
26+
it('Can clean attribute labels', () => {
27+
expect(
28+
cleanProcessor<ModelNode>({
29+
id: 'https://example.org/data/test',
30+
attributes: { 'https://example.org/data/test': 'test' },
31+
}).attributes
32+
).toStrictEqual({ test: 'test' })
33+
expect(
34+
cleanProcessor<ModelNode>({
35+
id: 'https://example.org/demo#TEST',
36+
attributes: {
37+
'https://example.org/demo#TEST': 'TEST',
38+
},
39+
}).attributes
40+
).toStrictEqual({ TEST: 'TEST' })
41+
expect(
42+
cleanProcessor<ModelNode>({ id: 'test', attributes: { test: 'test' } })
43+
.attributes
44+
).toStrictEqual({ test: 'test' })
45+
})
46+
47+
it('Can process type', () => {
48+
expect(
49+
cleanProcessor<ModelNode>({
50+
id: 'https://example.org/demo#TEST',
51+
attributes: { type: 'https://example.org/demo#TEST' },
52+
}).attributes
53+
).toStrictEqual({ type: 'TEST' })
54+
})
55+
56+
it('Can rewite label if equal id or matches edge predicate format', () => {
57+
expect(
58+
cleanProcessor<ModelNode>({
59+
id: 'https://example.org/data/test',
60+
label: 'https://example.org/data/test',
61+
attributes: {},
62+
}).label
63+
).toBe('test')
64+
expect(
65+
cleanProcessor<ModelEdge>({
66+
id: 'https://example.org/demo#source|https://example.org/demo#TEST|https://example.org/demo#target',
67+
source: 'https://example.org/demo#source',
68+
target: 'https://example.org/demo#target',
69+
label: 'https://example.org/demo#TEST',
70+
attributes: {},
71+
}).label
72+
).toBe('TEST')
73+
expect(cleanProcessor<ModelNode>({ id: 'test', attributes: {} }).label).toBe(
74+
'test'
75+
)
76+
expect(
77+
cleanProcessor<ModelNode>({ id: 'test', label: 'label', attributes: {} })
78+
.label
79+
).toBe('label')
80+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ModelAttributeSet, ModelEdge, ModelNode } from '@committed/graph'
2+
import { fragmentId } from './labels'
3+
4+
/**
5+
*
6+
* @param item Opinionated function to process rdf nodes and edges for a cleaner presentation in the graph
7+
* @returns
8+
*/
9+
export const cleanProcessor = <T extends ModelNode | ModelEdge>(item: T): T => {
10+
if (typeof item.label === 'string' && item.id.includes(`|${item.label}|`)) {
11+
item.label = fragmentId(item.label)
12+
}
13+
14+
if (item.label === undefined || item.label === item.id) {
15+
item.label = fragmentId(item.id)
16+
}
17+
18+
if (typeof item.attributes.type === 'string') {
19+
item.attributes.type = fragmentId(item.attributes.type)
20+
}
21+
22+
const attributes: ModelAttributeSet = {}
23+
Object.keys(item.attributes).forEach((key) => {
24+
attributes[fragmentId(key)] = item.attributes[key]
25+
})
26+
item.attributes = attributes
27+
28+
return item
29+
}

0 commit comments

Comments
 (0)