Skip to content

Commit 034c3e1

Browse files
committed
Deduplicate warnings in validator
1 parent 9070a1d commit 034c3e1

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,29 @@ describe("presentation-4 validator", () => {
4848
expect(report.issues.some((issue) => issue.code === "canvas-height-required")).toBe(true);
4949
});
5050

51+
test("prefers specific property requirement issues over generic class requirement issues", () => {
52+
const invalid = {
53+
"@context": "http://iiif.io/api/presentation/4/context.json",
54+
id: "https://example.org/manifest/missing-canvas-height",
55+
type: "Manifest",
56+
label: { en: ["missing canvas height"] },
57+
items: [
58+
{
59+
id: "https://example.org/canvas/1",
60+
type: "Canvas",
61+
width: 1000,
62+
items: [],
63+
},
64+
],
65+
};
66+
67+
const report = validatePresentation4(invalid);
68+
const heightIssues = report.issues.filter((issue) => issue.path === "$.items[0].height");
69+
70+
expect(heightIssues.some((issue) => issue.code === "canvas-height-required")).toBe(true);
71+
expect(heightIssues.some((issue) => issue.code === "class-requirement-must")).toBe(false);
72+
});
73+
5174
test("rejects array-form annotation body/target in favor of object/List", () => {
5275
const invalid = {
5376
"@context": "http://iiif.io/api/presentation/4/context.json",
@@ -795,4 +818,46 @@ describe("presentation-4 validator", () => {
795818
expect(report.issues.some((item) => item.code === "class-requirement-property-not-listed")).toBe(true);
796819
expect(report.issues.some((item) => item.code === "annotation-body-value-forbidden")).toBe(true);
797820
});
821+
822+
test("deduplicates exact duplicate issues emitted by multiple validation passes", () => {
823+
const invalid = {
824+
"@context": "http://iiif.io/api/presentation/4/context.json",
825+
id: "https://example.org/manifest/annotation-body-value-dedupe",
826+
type: "Manifest",
827+
label: { en: ["annotation body value dedupe"] },
828+
items: [
829+
{
830+
id: "https://example.org/canvas/1",
831+
type: "Canvas",
832+
width: 1000,
833+
height: 1000,
834+
items: [
835+
{
836+
id: "https://example.org/canvas/1/page/1",
837+
type: "AnnotationPage",
838+
items: [
839+
{
840+
id: "https://example.org/canvas/1/page/1/anno/1",
841+
type: "Annotation",
842+
motivation: ["commenting"],
843+
target: {
844+
id: "https://example.org/canvas/1",
845+
type: "Canvas",
846+
},
847+
bodyValue: "invalid body value",
848+
},
849+
],
850+
},
851+
],
852+
},
853+
],
854+
};
855+
856+
const report = validatePresentation4(invalid, { mode: "tolerant" });
857+
const bodyValueIssues = report.issues.filter((issue) => issue.code === "annotation-body-value-forbidden");
858+
const firstBodyValueIssue = bodyValueIssues.at(0);
859+
860+
expect(bodyValueIssues).toHaveLength(1);
861+
expect(firstBodyValueIssue?.path).toBe("$.items[0].items[0].items[0].bodyValue");
862+
});
798863
});

src/presentation-4/validator.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,43 @@ function issue(
7474
});
7575
}
7676

77+
function dedupeValidationIssues(issues: ValidationIssue[]): ValidationIssue[] {
78+
const uniqueIssues: ValidationIssue[] = [];
79+
const exactKeys = new Set<string>();
80+
81+
for (const current of issues) {
82+
const key = [
83+
current.code,
84+
current.severity,
85+
current.path,
86+
current.resourceId || "",
87+
current.resourceType || "",
88+
current.specRef || "",
89+
current.message,
90+
].join("|");
91+
if (exactKeys.has(key)) {
92+
continue;
93+
}
94+
exactKeys.add(key);
95+
uniqueIssues.push(current);
96+
}
97+
98+
const hasSpecificIssueByPath = new Map<string, boolean>();
99+
for (const current of uniqueIssues) {
100+
const isClassRequirement = current.code === "class-requirement-must" || current.code === "class-requirement-should";
101+
if (!isClassRequirement) {
102+
hasSpecificIssueByPath.set(`${current.severity}|${current.path}`, true);
103+
}
104+
}
105+
106+
return uniqueIssues.filter((current) => {
107+
if (current.code !== "class-requirement-must" && current.code !== "class-requirement-should") {
108+
return true;
109+
}
110+
return !hasSpecificIssueByPath.get(`${current.severity}|${current.path}`);
111+
});
112+
}
113+
77114
function isPositiveInteger(value: any): boolean {
78115
return typeof value === "number" && Number.isInteger(value) && value > 0;
79116
}
@@ -1127,7 +1164,7 @@ export function validatePresentation4(input: unknown, options: ValidateOptions =
11271164
issues.push(...runPostNormalizationValidation(normalized));
11281165
}
11291166

1130-
const report = createValidationReport(issues, {
1167+
const report = createValidationReport(dedupeValidationIssues(issues), {
11311168
classRequirements: {
11321169
nodesChecked: classRequirementResult.stats.nodesChecked,
11331170
mustChecks: classRequirementResult.stats.mustChecks,

0 commit comments

Comments
 (0)