Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions __tests__/presentation-4-parser/traverse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join } from "node:path";
import { cwd } from "node:process";
import { describe, expect, test } from "vitest";
import { Traverse } from "../../src/presentation-4";
import type { Annotation } from "../../src/presentation-4/types";

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

const annotation = {
id: "https://example.org/anno/1",
type: "Annotation",
type: "Annotation" as const,
motivation: ["painting"],
target: [
{
Expand All @@ -104,19 +105,19 @@ describe("presentation-4 traverse", () => {
],
};

const traversed = traverse.traverseAnnotation(annotation, undefined, "$.annotation");
const target = traversed.target;

expect(target.type).toBe("SpecificResource");
expect(target.selector[0].type).toBe("FragmentSelector");
const traversed = traverse.traverseAnnotation(annotation as unknown as Annotation, undefined, "$.annotation");
expect(traversed.target).toMatchObject({
type: "SpecificResource",
selector: [{ type: "FragmentSelector" }],
});
expect(selectorCount).toBe(1);
});

test("accepts List-wrapped annotation body and target values", () => {
const traverse = new Traverse();
const annotation = {
id: "https://example.org/anno/list-wrapper",
type: "Annotation",
type: "Annotation" as const,
motivation: ["painting"],
body: {
type: "List",
Expand All @@ -135,26 +136,27 @@ describe("presentation-4 traverse", () => {
};

const traversed = traverse.traverseAnnotation(annotation, undefined, "$.annotation");
expect(Array.isArray(traversed.body)).toBe(false);
expect(Array.isArray(traversed.target)).toBe(false);
expect(traversed.body.type).toBe("List");
expect(traversed.target.type).toBe("List");
expect(traversed.target.items[0].type).toBe("SpecificResource");
expect(traversed.target.items[0].selector[0].type).toBe("FragmentSelector");
expect(traversed).toMatchObject({
body: { type: "List" },
target: {
type: "List",
items: [{ type: "SpecificResource", selector: [{ type: "FragmentSelector" }] }],
},
});
});

test("coerces PointSelector.t to PointSelector.instant by default", () => {
const traverse = new Traverse();
const selector = {
type: "PointSelector",
type: "PointSelector" as const,
x: 1,
y: 2,
t: 3.5,
};

const traversed = traverse.traverseSelector(selector, undefined, "$.selector");
expect(traversed.instant).toBe(3.5);
expect(Object.hasOwn(traversed, "t")).toBe(false);
expect(traversed).toMatchObject({ instant: 3.5 });
expect(traversed).not.toHaveProperty("t");
});

test("can disable PointSelector.t coercion via traverse option", () => {
Expand All @@ -165,22 +167,23 @@ describe("presentation-4 traverse", () => {
}
);
const selector = {
type: "PointSelector",
type: "PointSelector" as const,
x: 1,
y: 2,
t: 3.5,
};

const traversed = traverse.traverseSelector(selector, undefined, "$.selector");
expect(traversed.t).toBe(3.5);
expect(Object.hasOwn(traversed, "instant")).toBe(false);
expect(traversed).toMatchObject({ t: 3.5 });
expect(traversed).not.toHaveProperty("instant");
});

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

const collection = {
id: "https://example.org/collection/1",
type: "Collection",
type: "Collection" as const,
label: { en: ["Collection"] },
first: "https://example.org/collection/1/page/1",
last: "https://example.org/collection/1/page/2",
items: [],
};

const traversedCollection = traverse.traverseCollection(collection, undefined, "$.collection");
Expand Down
62 changes: 51 additions & 11 deletions src/presentation-3/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,30 @@ export const types = [
"Agent",
];

export type TraversalContext = { parent?: any };

export type Traversal<T> = (jsonLd: T, context: TraversalContext) => Partial<T> | any;
export type Presentation3Resource =
| Collection
| Manifest
| Canvas
| AnnotationCollection
| AnnotationPage
| Annotation
| ContentResource
| ChoiceTarget
| ChoiceBody
| Range
| Service
| ResourceProvider
| SpecificResource
| GeoJSON;

export type TraversalContext = { parent?: unknown };

export type Traversal<T> = (jsonLd: T, context: TraversalContext) => unknown;

export type AllTraversal = <Resource extends Presentation3Resource>(
resource: Resource,
context: TraversalContext
) => unknown;

export type TraversalMap = {
collection?: Array<Traversal<Collection>>;
Expand All @@ -62,7 +83,7 @@ export type TraverseOptions = {
allowUndefinedReturn: boolean;
};

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

if (typeof resource!.type === "string") {
if ("type" in resource && typeof resource.type === "string") {
const hasType = types.indexOf(resource.type);
if (hasType !== -1) {
return types[hasType]!;
}
}

if (resource!.profile) {
if ("profile" in resource && resource.profile) {
return "Service";
}

Expand Down Expand Up @@ -118,7 +139,7 @@ export class Traverse {
};
}

static all(traversal: (resource: any) => any) {
static all(traversal: AllTraversal) {
return new Traverse({
collection: [traversal],
manifest: [traversal],
Expand All @@ -136,7 +157,7 @@ export class Traverse {
});
}

traverseDescriptive<T extends Partial<DescriptiveProperties>>(resource: T): T {
traverseDescriptive<T extends Presentation3Resource & Partial<DescriptiveProperties>>(resource: T): T {
if (resource.thumbnail) {
resource.thumbnail = ensureArray(resource.thumbnail).map((thumbnail) =>
this.traverseType(thumbnail, { parent: resource }, this.traversals.contentResource)
Expand All @@ -148,7 +169,7 @@ export class Traverse {
return resource;
}

traverseLinking<T extends Partial<LinkingProperties>>(resource: T): T {
traverseLinking<T extends Presentation3Resource & Partial<LinkingProperties>>(resource: T): T {
if (resource.seeAlso) {
resource.seeAlso = ensureArray(resource.seeAlso).map((content) =>
this.traverseType(content, { parent: resource }, this.traversals.contentResource)
Expand Down Expand Up @@ -227,6 +248,7 @@ export class Traverse {
return collection;
}

traverseCollection(collection: Collection, parent?: Presentation3Resource): Collection;
traverseCollection(collection: Collection, parent?: any): Collection {
return this.traverseType<Collection>(
this.traverseDescriptive(
Expand All @@ -241,6 +263,7 @@ export class Traverse {
);
}

traverseGeoJson(geoJson: GeoJSON, parent?: Presentation3Resource): GeoJSON;
traverseGeoJson(geoJson: GeoJSON, parent?: any): GeoJSON {
return this.traverseType<GeoJSON>(geoJson, { parent }, this.traversals.geoJson);
}
Expand Down Expand Up @@ -276,6 +299,7 @@ export class Traverse {
this.traverseInlineAnnotationPages.bind(this)
);

traverseManifest(manifest: Manifest, parent?: Presentation3Resource): Manifest;
traverseManifest(manifest: Manifest, parent?: any): Manifest {
return this.traverseType<Manifest>(this._traverseManifest(manifest), { parent }, this.traversals.manifest);
}
Expand Down Expand Up @@ -309,6 +333,7 @@ export class Traverse {
this.traverseInlineAnnotationPages.bind(this)
);

traverseCanvas(canvas: Canvas, parent?: Presentation3Resource): Canvas;
traverseCanvas(canvas: Canvas, parent?: any): Canvas {
return this.traverseType<Canvas>(this._traverseCanvas(canvas), { parent }, this.traversals.canvas);
}
Expand All @@ -328,6 +353,7 @@ export class Traverse {
this.traverseDescriptive.bind(this)
);

traverseAnnotationPage(annotationPageJson: AnnotationPage, parent?: Presentation3Resource): AnnotationPage;
traverseAnnotationPage(annotationPageJson: AnnotationPage, parent?: any): AnnotationPage {
return this.traverseType<AnnotationPage>(
this._traverseAnnotationPage(annotationPageJson),
Expand Down Expand Up @@ -363,6 +389,7 @@ export class Traverse {
}

// @todo traverseAnnotationSelector
traverseAnnotation(annotationJson: Annotation, parent?: Presentation3Resource): Annotation;
traverseAnnotation(annotationJson: Annotation, parent?: any): Annotation {
return this.traverseType<Annotation>(
this.traverseLinking(this.traverseAnnotationBody(this.traverseDescriptive(annotationJson as any))),
Expand All @@ -384,6 +411,7 @@ export class Traverse {
return contentResourceJson;
}

traverseContentResource(contentResourceJson: ContentResource, parent?: Presentation3Resource): ContentResource;
traverseContentResource(contentResourceJson: ContentResource, parent?: any): ContentResource {
if ((contentResourceJson as any).type === "Choice") {
(contentResourceJson as any).items = (contentResourceJson as any).items.map((choiceItem: ContentResource) => {
Expand All @@ -407,6 +435,11 @@ export class Traverse {
);
}

traverseSpecificResource(
specificResource: SpecificResource,
typeHint?: string,
parent?: Presentation3Resource
): SpecificResource;
traverseSpecificResource(specificResource: SpecificResource, typeHint?: string, parent?: any): SpecificResource {
let source = specificResource.source;
if (typeof specificResource.source === "string") {
Expand All @@ -420,7 +453,7 @@ export class Traverse {
typeHint === "Canvas" || source.type === "Canvas"
? this.traverseType(source, { parent }, this.traversals.canvas)
: typeHint === "ContentResource"
? this.traverseContentResource(source, { parent })
? this.traverseContentResource(source, parent)
: this.traverseUnknown(source, { parent, typeHint }),
},
{ parent },
Expand Down Expand Up @@ -455,10 +488,12 @@ export class Traverse {
this.traverseLinkedCanvases.bind(this)
);

traverseRange(range: Range, parent?: Presentation3Resource): Range;
traverseRange(range: Range, parent?: any): Range {
return this.traverseType<Range>(this._traverseRange(range), { parent }, this.traversals.range);
}

traverseAgent(agent: ResourceProvider, parent?: Presentation3Resource): ResourceProvider;
traverseAgent(agent: ResourceProvider, parent?: any) {
return this.traverseType<ResourceProvider>(
this.traverseDescriptive(this.traverseLinking(agent)),
Expand All @@ -473,10 +508,11 @@ export class Traverse {
if (typeof returnValue === "undefined" && !this.options.allowUndefinedReturn) {
return acc;
}
return returnValue;
return returnValue as T;
}, object);
}

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

traverseUnknown(
resource: unknown,
options?: { typeHint?: string; parent?: Presentation3Resource }
): Presentation3Resource;
traverseUnknown(
resource: any,
{ parent, typeHint }: { typeHint?: string; parent?: any } = {}
Expand Down
Loading
Loading