Skip to content

Commit b699fae

Browse files
committed
Type improvements
1 parent 08a20ca commit b699fae

5 files changed

Lines changed: 203 additions & 59 deletions

File tree

__tests__/presentation-4-parser/traverse.test.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join } from "node:path";
33
import { cwd } from "node:process";
44
import { describe, expect, test } from "vitest";
55
import { Traverse } from "../../src/presentation-4";
6+
import type { Annotation } from "../../src/presentation-4/types";
67

78
describe("presentation-4 traverse", () => {
89
test("dispatches callbacks across mixed resource types", () => {
@@ -88,7 +89,7 @@ describe("presentation-4 traverse", () => {
8889

8990
const annotation = {
9091
id: "https://example.org/anno/1",
91-
type: "Annotation",
92+
type: "Annotation" as const,
9293
motivation: ["painting"],
9394
target: [
9495
{
@@ -104,19 +105,19 @@ describe("presentation-4 traverse", () => {
104105
],
105106
};
106107

107-
const traversed = traverse.traverseAnnotation(annotation, undefined, "$.annotation");
108-
const target = traversed.target;
109-
110-
expect(target.type).toBe("SpecificResource");
111-
expect(target.selector[0].type).toBe("FragmentSelector");
108+
const traversed = traverse.traverseAnnotation(annotation as unknown as Annotation, undefined, "$.annotation");
109+
expect(traversed.target).toMatchObject({
110+
type: "SpecificResource",
111+
selector: [{ type: "FragmentSelector" }],
112+
});
112113
expect(selectorCount).toBe(1);
113114
});
114115

115116
test("accepts List-wrapped annotation body and target values", () => {
116117
const traverse = new Traverse();
117118
const annotation = {
118119
id: "https://example.org/anno/list-wrapper",
119-
type: "Annotation",
120+
type: "Annotation" as const,
120121
motivation: ["painting"],
121122
body: {
122123
type: "List",
@@ -135,26 +136,27 @@ describe("presentation-4 traverse", () => {
135136
};
136137

137138
const traversed = traverse.traverseAnnotation(annotation, undefined, "$.annotation");
138-
expect(Array.isArray(traversed.body)).toBe(false);
139-
expect(Array.isArray(traversed.target)).toBe(false);
140-
expect(traversed.body.type).toBe("List");
141-
expect(traversed.target.type).toBe("List");
142-
expect(traversed.target.items[0].type).toBe("SpecificResource");
143-
expect(traversed.target.items[0].selector[0].type).toBe("FragmentSelector");
139+
expect(traversed).toMatchObject({
140+
body: { type: "List" },
141+
target: {
142+
type: "List",
143+
items: [{ type: "SpecificResource", selector: [{ type: "FragmentSelector" }] }],
144+
},
145+
});
144146
});
145147

146148
test("coerces PointSelector.t to PointSelector.instant by default", () => {
147149
const traverse = new Traverse();
148150
const selector = {
149-
type: "PointSelector",
151+
type: "PointSelector" as const,
150152
x: 1,
151153
y: 2,
152154
t: 3.5,
153155
};
154156

155157
const traversed = traverse.traverseSelector(selector, undefined, "$.selector");
156-
expect(traversed.instant).toBe(3.5);
157-
expect(Object.hasOwn(traversed, "t")).toBe(false);
158+
expect(traversed).toMatchObject({ instant: 3.5 });
159+
expect(traversed).not.toHaveProperty("t");
158160
});
159161

160162
test("can disable PointSelector.t coercion via traverse option", () => {
@@ -165,22 +167,23 @@ describe("presentation-4 traverse", () => {
165167
}
166168
);
167169
const selector = {
168-
type: "PointSelector",
170+
type: "PointSelector" as const,
169171
x: 1,
170172
y: 2,
171173
t: 3.5,
172174
};
173175

174176
const traversed = traverse.traverseSelector(selector, undefined, "$.selector");
175-
expect(traversed.t).toBe(3.5);
176-
expect(Object.hasOwn(traversed, "instant")).toBe(false);
177+
expect(traversed).toMatchObject({ t: 3.5 });
178+
expect(traversed).not.toHaveProperty("instant");
177179
});
178180

179181
test("normalizes paging first/last string references to typed objects", () => {
180182
const traverse = new Traverse();
181183
const annotationCollection = {
182184
id: "https://example.org/annotation-collection/1",
183-
type: "AnnotationCollection",
185+
type: "AnnotationCollection" as const,
186+
label: null,
184187
first: "https://example.org/annotation-collection/1/page/1",
185188
last: "https://example.org/annotation-collection/1/page/2",
186189
items: [],
@@ -203,10 +206,10 @@ describe("presentation-4 traverse", () => {
203206

204207
const collection = {
205208
id: "https://example.org/collection/1",
206-
type: "Collection",
209+
type: "Collection" as const,
210+
label: { en: ["Collection"] },
207211
first: "https://example.org/collection/1/page/1",
208212
last: "https://example.org/collection/1/page/2",
209-
items: [],
210213
};
211214

212215
const traversedCollection = traverse.traverseCollection(collection, undefined, "$.collection");

src/presentation-3/traverse.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,30 @@ export const types = [
3838
"Agent",
3939
];
4040

41-
export type TraversalContext = { parent?: any };
42-
43-
export type Traversal<T> = (jsonLd: T, context: TraversalContext) => Partial<T> | any;
41+
export type Presentation3Resource =
42+
| Collection
43+
| Manifest
44+
| Canvas
45+
| AnnotationCollection
46+
| AnnotationPage
47+
| Annotation
48+
| ContentResource
49+
| ChoiceTarget
50+
| ChoiceBody
51+
| Range
52+
| Service
53+
| ResourceProvider
54+
| SpecificResource
55+
| GeoJSON;
56+
57+
export type TraversalContext = { parent?: unknown };
58+
59+
export type Traversal<T> = (jsonLd: T, context: TraversalContext) => unknown;
60+
61+
export type AllTraversal = <Resource extends Presentation3Resource>(
62+
resource: Resource,
63+
context: TraversalContext
64+
) => unknown;
4465

4566
export type TraversalMap = {
4667
collection?: Array<Traversal<Collection>>;
@@ -62,7 +83,7 @@ export type TraverseOptions = {
6283
allowUndefinedReturn: boolean;
6384
};
6485

65-
export function identifyResource(resource: any, typeHint?: string): string {
86+
export function identifyResource(resource: unknown, typeHint?: string): string {
6687
if (typeof resource === "undefined" || resource === null) {
6788
throw new Error("Null or undefined is not a valid entity.");
6889
}
@@ -76,14 +97,14 @@ export function identifyResource(resource: any, typeHint?: string): string {
7697
throw new Error(`${typeof resource} is not a valid entity`);
7798
}
7899

79-
if (typeof resource!.type === "string") {
100+
if ("type" in resource && typeof resource.type === "string") {
80101
const hasType = types.indexOf(resource.type);
81102
if (hasType !== -1) {
82103
return types[hasType]!;
83104
}
84105
}
85106

86-
if (resource!.profile) {
107+
if ("profile" in resource && resource.profile) {
87108
return "Service";
88109
}
89110

@@ -118,7 +139,7 @@ export class Traverse {
118139
};
119140
}
120141

121-
static all(traversal: (resource: any) => any) {
142+
static all(traversal: AllTraversal) {
122143
return new Traverse({
123144
collection: [traversal],
124145
manifest: [traversal],
@@ -136,7 +157,7 @@ export class Traverse {
136157
});
137158
}
138159

139-
traverseDescriptive<T extends Partial<DescriptiveProperties>>(resource: T): T {
160+
traverseDescriptive<T extends Presentation3Resource & Partial<DescriptiveProperties>>(resource: T): T {
140161
if (resource.thumbnail) {
141162
resource.thumbnail = ensureArray(resource.thumbnail).map((thumbnail) =>
142163
this.traverseType(thumbnail, { parent: resource }, this.traversals.contentResource)
@@ -148,7 +169,7 @@ export class Traverse {
148169
return resource;
149170
}
150171

151-
traverseLinking<T extends Partial<LinkingProperties>>(resource: T): T {
172+
traverseLinking<T extends Presentation3Resource & Partial<LinkingProperties>>(resource: T): T {
152173
if (resource.seeAlso) {
153174
resource.seeAlso = ensureArray(resource.seeAlso).map((content) =>
154175
this.traverseType(content, { parent: resource }, this.traversals.contentResource)
@@ -227,6 +248,7 @@ export class Traverse {
227248
return collection;
228249
}
229250

251+
traverseCollection(collection: Collection, parent?: Presentation3Resource): Collection;
230252
traverseCollection(collection: Collection, parent?: any): Collection {
231253
return this.traverseType<Collection>(
232254
this.traverseDescriptive(
@@ -241,6 +263,7 @@ export class Traverse {
241263
);
242264
}
243265

266+
traverseGeoJson(geoJson: GeoJSON, parent?: Presentation3Resource): GeoJSON;
244267
traverseGeoJson(geoJson: GeoJSON, parent?: any): GeoJSON {
245268
return this.traverseType<GeoJSON>(geoJson, { parent }, this.traversals.geoJson);
246269
}
@@ -276,6 +299,7 @@ export class Traverse {
276299
this.traverseInlineAnnotationPages.bind(this)
277300
);
278301

302+
traverseManifest(manifest: Manifest, parent?: Presentation3Resource): Manifest;
279303
traverseManifest(manifest: Manifest, parent?: any): Manifest {
280304
return this.traverseType<Manifest>(this._traverseManifest(manifest), { parent }, this.traversals.manifest);
281305
}
@@ -309,6 +333,7 @@ export class Traverse {
309333
this.traverseInlineAnnotationPages.bind(this)
310334
);
311335

336+
traverseCanvas(canvas: Canvas, parent?: Presentation3Resource): Canvas;
312337
traverseCanvas(canvas: Canvas, parent?: any): Canvas {
313338
return this.traverseType<Canvas>(this._traverseCanvas(canvas), { parent }, this.traversals.canvas);
314339
}
@@ -328,6 +353,7 @@ export class Traverse {
328353
this.traverseDescriptive.bind(this)
329354
);
330355

356+
traverseAnnotationPage(annotationPageJson: AnnotationPage, parent?: Presentation3Resource): AnnotationPage;
331357
traverseAnnotationPage(annotationPageJson: AnnotationPage, parent?: any): AnnotationPage {
332358
return this.traverseType<AnnotationPage>(
333359
this._traverseAnnotationPage(annotationPageJson),
@@ -363,6 +389,7 @@ export class Traverse {
363389
}
364390

365391
// @todo traverseAnnotationSelector
392+
traverseAnnotation(annotationJson: Annotation, parent?: Presentation3Resource): Annotation;
366393
traverseAnnotation(annotationJson: Annotation, parent?: any): Annotation {
367394
return this.traverseType<Annotation>(
368395
this.traverseLinking(this.traverseAnnotationBody(this.traverseDescriptive(annotationJson as any))),
@@ -384,6 +411,7 @@ export class Traverse {
384411
return contentResourceJson;
385412
}
386413

414+
traverseContentResource(contentResourceJson: ContentResource, parent?: Presentation3Resource): ContentResource;
387415
traverseContentResource(contentResourceJson: ContentResource, parent?: any): ContentResource {
388416
if ((contentResourceJson as any).type === "Choice") {
389417
(contentResourceJson as any).items = (contentResourceJson as any).items.map((choiceItem: ContentResource) => {
@@ -407,6 +435,11 @@ export class Traverse {
407435
);
408436
}
409437

438+
traverseSpecificResource(
439+
specificResource: SpecificResource,
440+
typeHint?: string,
441+
parent?: Presentation3Resource
442+
): SpecificResource;
410443
traverseSpecificResource(specificResource: SpecificResource, typeHint?: string, parent?: any): SpecificResource {
411444
let source = specificResource.source;
412445
if (typeof specificResource.source === "string") {
@@ -420,7 +453,7 @@ export class Traverse {
420453
typeHint === "Canvas" || source.type === "Canvas"
421454
? this.traverseType(source, { parent }, this.traversals.canvas)
422455
: typeHint === "ContentResource"
423-
? this.traverseContentResource(source, { parent })
456+
? this.traverseContentResource(source, parent)
424457
: this.traverseUnknown(source, { parent, typeHint }),
425458
},
426459
{ parent },
@@ -455,10 +488,12 @@ export class Traverse {
455488
this.traverseLinkedCanvases.bind(this)
456489
);
457490

491+
traverseRange(range: Range, parent?: Presentation3Resource): Range;
458492
traverseRange(range: Range, parent?: any): Range {
459493
return this.traverseType<Range>(this._traverseRange(range), { parent }, this.traversals.range);
460494
}
461495

496+
traverseAgent(agent: ResourceProvider, parent?: Presentation3Resource): ResourceProvider;
462497
traverseAgent(agent: ResourceProvider, parent?: any) {
463498
return this.traverseType<ResourceProvider>(
464499
this.traverseDescriptive(this.traverseLinking(agent)),
@@ -473,10 +508,11 @@ export class Traverse {
473508
if (typeof returnValue === "undefined" && !this.options.allowUndefinedReturn) {
474509
return acc;
475510
}
476-
return returnValue;
511+
return returnValue as T;
477512
}, object);
478513
}
479514

515+
traverseService(service: Service, parent?: Presentation3Resource): Service;
480516
traverseService(service: Service, parent?: any): Service {
481517
const _service: any = Object.assign({}, service);
482518
if (_service && _service.service) {
@@ -485,6 +521,10 @@ export class Traverse {
485521
return this.traverseType<Service>(_service, { parent }, this.traversals.service);
486522
}
487523

524+
traverseUnknown(
525+
resource: unknown,
526+
options?: { typeHint?: string; parent?: Presentation3Resource }
527+
): Presentation3Resource;
488528
traverseUnknown(
489529
resource: any,
490530
{ parent, typeHint }: { typeHint?: string; parent?: any } = {}

0 commit comments

Comments
 (0)