-
Notifications
You must be signed in to change notification settings - Fork 1
/
typed_graph.ts
272 lines (243 loc) · 7.83 KB
/
typed_graph.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/* eslint @typescript-eslint/no-explicit-any: 0 */
import type { Stream } from '@rdfjs/types/stream'
import { isLeft } from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/function'
import { DocmapsFactory } from './types'
import * as TE from 'fp-ts/lib/TaskEither'
import * as E from 'fp-ts/lib/Either'
import * as t from 'io-ts'
import SerializerJsonld from '@rdfjs/serializer-jsonld-ext'
import * as util from 'util'
const DM_JSONLD_CONTEXT = 'https://w3id.org/docmaps/context.jsonld'
export const TypedNodeShape = t.type({
type: t.union([t.string, t.array(t.string)]),
})
export type TypedNodeShapeT = t.TypeOf<typeof TypedNodeShape>
// TODO: make this generic/injectable
export type TypesFactory = typeof DocmapsFactory
export type TypesFactoryKeys = keyof TypesFactory
/** JSON-LD Framing document for docmaps
*
* This is a constant of fixed type. It seems
* to work well for serialization, may not be the
* only thing that does .
*
* TODO: can this be made shorter , to not repeat?
*
* @since 0.14.0
*/
export const DocmapNormalizedFrame = {
type: 'docmap',
'first-step': { '@embed': '@never' },
'pwo:hasStep': {
'@embed': '@always',
'@requireAll': false,
'next-step': { '@embed': '@never', '@omitDefault': true },
'previous-step': { '@embed': '@never', '@omitDefault': true },
'@id': {},
},
}
/** Any key-value map of flat values.
*
* This type is used to specify constraints on a JSON-LD
* frame for purposes of selecting specific objects
* in a small triplestore.
*
* @since 0.14.0
*/
export type FrameConstraint =
| {
// the recommended selector.
id: string
}
| { [key: string]: any }
/** Union type representing discoverable Typed codecs.
*
* It is unclear if the `type`-based frame is
* actually needed, because we expect the triples
* to be filtered upstream. However we will
* continue to support it until clarity emerges
* that it is redundant.
*
* @since 0.14.0
* @deprecated 0.14.0
*/
export type FrameTyper = {
// 'id'?: string,
type: TypesFactoryKeys
}
/** Union type representing allowed Frames.
*
* It is unclear if the `type`-based frame is
* actually needed, because we expect the triples
* to be filtered upstream. However we will
* continue to support it until clarity emerges
* that it is redundant.
*
* @since 0.11.0
*/
export type FrameSelection = FrameTyper | typeof DocmapNormalizedFrame | FrameConstraint
/** A type-aware structure for extracting objects from quads.
*
* This structure is capable of detecting based on the @type key
* which of the allowed docmap codecs should be used for validation;
* this is done with `pickStream`.
* However, because it may create any one of those types, there is
* still the need to cast the result or do type matching on it. It
* is therefore unclear if this provides much value over having
* to know the resulting type in advance.
*
* @since 0.11.0
*/
export class TypedGraph {
factory: TypesFactory
constructor(factory: TypesFactory = DocmapsFactory) {
this.factory = factory
}
/** Thin error-throwing wrapper around `.decode`.
*
* This method exists for compatibility.
*
* @since 0.11.0
*/
parseJsonldWithCodec<C extends t.Mixed>(c: C, jsonld: any): t.TypeOf<C> {
const typedResult = c.decode(jsonld)
if (isLeft(typedResult)) {
throw new Error(`decoding failed: ${util.inspect(typedResult.left, { depth: 5 })}`)
}
return typedResult.right
}
/** chooses a codec.
*
* Returns errors or a Codec, based on the `type` key of
* the input object.
*
* @since 0.11.0
*/
codecFor(jsonld: any): t.Mixed {
// console.log(util.inspect(jsonld, {depth: null, colors: true}))
// allow multiple types, whichever is first we should use
let typesArr: string[]
if (Array.isArray(jsonld['type'])) {
typesArr = jsonld['type']
} else {
// wrap it in array
typesArr = [jsonld['type']]
}
const errors: Error[] = []
for (const tIdStr of typesArr) {
const tId = tIdStr as TypesFactoryKeys
if (!tId) {
errors.push(
new Error(
`unable to type a jsonld object without type field: ${JSON.stringify(
jsonld,
null,
' ',
)}`,
),
)
continue
}
const t = this.factory[tId]
if (!t) {
errors.push(
new Error(
`unable to type jsonld object: type \`${tId}\` is foreign to this type factory`,
),
)
continue
}
return t
}
// did not find a valid parsing
throw errors
}
// converts a Stream<Quad> (eventemitter of quad) into a single Object jsonld or error.
private oneJsonldFrom(s: Stream, frame: FrameSelection): TE.TaskEither<Error, object> {
const context = {
'@context': {
'@import': DM_JSONLD_CONTEXT,
},
...frame,
} as any // TODO: this cast is needed because DefinitelyTyped definition is out of date for this library.
const serializer = new SerializerJsonld({
context: context,
frame: true,
skipContext: true,
})
const output = serializer.import(s)
return TE.tryCatch(
() =>
new Promise((res, rej) => {
let done = false
output
// TODO: in what cases does this behave differently from computing on `end` messages?
// currentlly it is for sure only handling hte first data object
.on('data', (jsonld) => {
try {
done = true
res(jsonld)
} catch (errors) {
// TODO better error message handling
done = true
rej(new Error('no type annotations were parseable for object', { cause: errors }))
}
})
output.on('error', (err) => {
done = true
rej(new Error('error received from upstream', { cause: err }))
})
output.on('end', () => {
if (!done) {
rej(new Error('jsonld streaming exited unexpectedly'))
}
})
output.on('close', () => {
if (!done) {
rej(new Error('jsonld streaming exited unexpectedly'))
}
})
}),
(reason) => new Error('unable to pick jsonld from stream', { cause: reason }),
)
}
/** Consumes a Stream, and may produce a typed object.
*
* @param frame: FrameSelection a JSON-LD Frame for serialization. Probably should be the Docmap Frame.
* @param codec: Codec any io-ts codec, such as the ones provided in types.ts. This method is type-safe
* guaranteed to return an object of the type corresponding to the chosen Codec (or error).
* @param s: Stream a stream of Quads or anything else accepted by `jsonld-serializer-ext`.
*
* @since 0.11.0
*/
pickStreamWithCodec<C extends t.Mixed>(
frame: FrameSelection,
codec: C,
s: Stream,
): TE.TaskEither<Error, t.TypeOf<C>> {
const te_ld = this.oneJsonldFrom(s, frame)
const decodeWithError = E.mapLeft(
(errors) => new Error('failed to decode a docmap', { cause: errors }),
)
return pipe(
te_ld,
TE.chainEitherK((ld) => pipe(ld, codec.decode, decodeWithError)),
)
}
/** Consumes a Stream, and may produce a typed object.
*
* @param s: Stream a stream of Quads or anything else accepted by `jsonld-serializer-ext`.
* @param frame: FrameSelection a JSON-LD Frame for serialization. Probably should be the Docmap Frame.
*
* @since 0.11.0
*/
pickStream(s: Stream, frame: FrameSelection): TE.TaskEither<Error, TypedNodeShapeT> {
return pipe(
TE.Do,
TE.bind('jsonld', () => this.oneJsonldFrom(s, frame)),
TE.bind('codec', ({ jsonld }) => TE.of(this.codecFor(jsonld))),
TE.map(({ codec, jsonld }) => this.parseJsonldWithCodec(codec, jsonld)),
)
}
}