-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathvalidate.ts
More file actions
90 lines (79 loc) · 3.33 KB
/
Copy pathvalidate.ts
File metadata and controls
90 lines (79 loc) · 3.33 KB
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
import path from 'node:path';
import { DeckRenderError } from '../errors/index.js';
import type { RenderArtifact } from '../types.js';
import type { EngineOutput } from './engine.js';
import { isHttpUrl } from './artifacts.js';
/** Validate the runtime contract at the boundary where third-party engines enter the SDK. */
export function validateEngineOutput(engineName: string, value: unknown): EngineOutput {
if (!value || typeof value !== 'object') {
throw invalidOutput(engineName, 'expected an object.');
}
const output = value as Record<string, unknown>;
if (!Array.isArray(output.artifacts) || output.artifacts.length === 0) {
throw invalidOutput(engineName, 'artifacts must be a non-empty array.');
}
if (!isPositiveSafeInteger(output.totalPages)) {
throw invalidOutput(engineName, 'totalPages must be a positive integer.');
}
const artifacts = output.artifacts.map((artifact, index) => validateArtifact(engineName, artifact, index));
const highestPage = Math.max(...artifacts.map((artifact) => artifact.page));
if (output.totalPages < highestPage) {
throw invalidOutput(
engineName,
`totalPages (${output.totalPages}) cannot be less than artifact page ${highestPage}.`
);
}
return { artifacts, totalPages: output.totalPages };
}
function validateArtifact(engineName: string, value: unknown, index: number): RenderArtifact {
const label = `artifact ${index + 1}`;
if (!value || typeof value !== 'object') {
throw invalidOutput(engineName, `${label} must be an object.`);
}
const artifact = value as Record<string, unknown>;
if (!isPositiveSafeInteger(artifact.page)) {
throw invalidOutput(engineName, `${label}.page must be a positive integer.`);
}
if (typeof artifact.source !== 'string' || artifact.source.trim().length === 0) {
throw invalidOutput(engineName, `${label}.source must be a non-empty string.`);
}
if (!isHttpUrl(artifact.source) && !path.isAbsolute(artifact.source)) {
throw invalidOutput(engineName, `${label}.source must be an http(s) URL or an absolute path.`);
}
if (typeof artifact.ext !== 'string' || !/^\.[a-z0-9]+$/i.test(artifact.ext)) {
throw invalidOutput(engineName, `${label}.ext must be an extension such as .png.`);
}
validateOptionalNumber(engineName, label, artifact, 'bytes', false);
validateOptionalNumber(engineName, label, artifact, 'width', true);
validateOptionalNumber(engineName, label, artifact, 'height', true);
return value as RenderArtifact;
}
function validateOptionalNumber(
engineName: string,
label: string,
artifact: Record<string, unknown>,
key: 'bytes' | 'width' | 'height',
positive: boolean
): void {
const value = artifact[key];
if (value === undefined) {
return;
}
const valid =
typeof value === 'number' &&
Number.isFinite(value) &&
Number.isSafeInteger(value) &&
(positive ? value > 0 : value >= 0);
if (!valid) {
throw invalidOutput(
engineName,
`${label}.${key} must be a ${positive ? 'positive' : 'non-negative'} integer when present.`
);
}
}
function isPositiveSafeInteger(value: unknown): value is number {
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
}
function invalidOutput(engineName: string, detail: string): DeckRenderError {
return DeckRenderError.render(`Engine "${engineName}" returned invalid output: ${detail}`);
}