Release API Tools 0.1.0

This commit is contained in:
2026-09-01 12:39:23 +02:00
commit fbcd0e56d6
63 changed files with 10155 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
import type {
ApiDocument,
JsonObject,
JsonValue,
LocalWorkspace,
} from "./types";
import { normalizeFilename } from "./parse";
function parentDirectory(filename: string): string {
const index = filename.lastIndexOf("/");
return index < 0 ? "" : filename.slice(0, index + 1);
}
function normalizeRelative(base: string, relative: string): string {
const segments = `${parentDirectory(base)}${relative}`.split("/");
const output: string[] = [];
for (const segment of segments) {
if (!segment || segment === ".") continue;
if (segment === "..") {
if (!output.length)
throw new TypeError("Reference escapes the local workspace");
output.pop();
} else output.push(segment);
}
return normalizeFilename(output.join("/"));
}
function pointerValue(value: JsonValue, fragment: string): JsonValue {
if (!fragment || fragment === "#") return value;
if (!fragment.startsWith("#/"))
throw new SyntaxError(
`Only JSON Pointer fragments are supported: ${fragment}`,
);
let current: JsonValue = value;
for (const raw of fragment.slice(2).split("/")) {
const token = decodeURIComponent(raw)
.replace(/~1/gu, "/")
.replace(/~0/gu, "~");
if (Array.isArray(current)) {
if (!/^(?:0|[1-9]\d*)$/u.test(token) || Number(token) >= current.length)
throw new ReferenceError(`Array reference token not found: ${token}`);
current = current[Number(token)]!;
} else if (
current &&
typeof current === "object" &&
Object.hasOwn(current, token)
)
current = current[token]!;
else throw new ReferenceError(`Reference token not found: ${token}`);
}
return current;
}
export function createWorkspace(
entry: ApiDocument,
supporting: readonly ApiDocument[] = [],
): LocalWorkspace {
const documents = new Map<string, ApiDocument>();
for (const document of [entry, ...supporting]) {
if (documents.has(document.filename))
throw new TypeError(`Duplicate local filename: ${document.filename}`);
documents.set(document.filename, document);
}
return { entry: entry.filename, documents };
}
export function resolveLocalReference(
workspace: LocalWorkspace,
reference: string,
from = workspace.entry,
): { document: ApiDocument; value: JsonValue; key: string } {
if (
/^[a-z][a-z0-9+.-]*:/iu.test(reference) ||
reference.startsWith("//") ||
reference.startsWith("/")
)
throw new TypeError(
`Remote or absolute references are disabled: ${reference}`,
);
const hash = reference.indexOf("#");
const filePart = hash < 0 ? reference : reference.slice(0, hash);
const fragment = hash < 0 ? "" : reference.slice(hash);
const filename = filePart ? normalizeRelative(from, filePart) : from;
const document = workspace.documents.get(filename);
if (!document)
throw new ReferenceError(
`Local reference file was not supplied: ${filename}`,
);
return {
document,
value: pointerValue(document.value, fragment),
key: `${filename}${fragment}`,
};
}
function asObject(value: JsonValue | undefined): JsonObject | undefined {
return value && typeof value === "object" && !Array.isArray(value)
? value
: undefined;
}
export interface SampleResult {
value: JsonValue;
notices: string[];
}
export function generateSchemaSample(
workspace: LocalWorkspace,
schema: JsonValue,
from = workspace.entry,
): SampleResult {
const notices: string[] = [];
const active = new Set<string>();
let nodes = 0;
const build = (
value: JsonValue,
filename: string,
depth: number,
): JsonValue => {
nodes += 1;
if (nodes > 2_000 || depth > 20) {
notices.push(
"Sample generation reached its bounded depth or node limit.",
);
return null;
}
const object = asObject(value);
if (!object) return value;
if (typeof object.$ref === "string") {
const resolved = resolveLocalReference(workspace, object.$ref, filename);
if (active.has(resolved.key)) {
notices.push(`Reference cycle stopped at ${resolved.key}.`);
return { $cycle: resolved.key };
}
active.add(resolved.key);
try {
return build(resolved.value, resolved.document.filename, depth + 1);
} finally {
active.delete(resolved.key);
}
}
for (const key of ["example", "default"] as const)
if (object[key] !== undefined) return object[key]!;
if (Array.isArray(object.enum) && object.enum.length)
return object.enum[0]!;
if (Array.isArray(object.allOf)) {
const merged: JsonObject = Object.create(null) as JsonObject;
for (const child of object.allOf) {
const sample = build(child, filename, depth + 1);
if (asObject(sample)) Object.assign(merged, sample);
else notices.push("A non-object allOf sample could not be merged.");
}
return merged;
}
for (const key of ["oneOf", "anyOf"] as const)
if (Array.isArray(object[key]) && object[key]!.length) {
notices.push(`${key} uses the first alternative.`);
return build(object[key]![0]!, filename, depth + 1);
}
const type =
typeof object.type === "string"
? object.type
: object.properties
? "object"
: object.items
? "array"
: undefined;
if (type === "object") {
const output: JsonObject = Object.create(null) as JsonObject;
const properties = asObject(object.properties) ?? {};
for (const [key, child] of Object.entries(properties).slice(0, 100))
output[key] = build(child, filename, depth + 1);
return output;
}
if (type === "array")
return object.items ? [build(object.items, filename, depth + 1)] : [];
if (type === "integer" || type === "number")
return typeof object.minimum === "number" ? object.minimum : 0;
if (type === "boolean") return true;
if (type === "null") return null;
const format = typeof object.format === "string" ? object.format : "";
return (
(
{
date: "2026-09-01",
"date-time": "2026-09-01T12:00:00Z",
email: "user@example.test",
uuid: "00000000-0000-4000-8000-000000000000",
uri: "https://example.test/resource",
hostname: "example.test",
ipv4: "192.0.2.1",
ipv6: "2001:db8::1",
binary: "<binary>",
} as Record<string, string>
)[format] ??
(typeof object.pattern === "string"
? `<string matching ${object.pattern}>`
: "string")
);
};
return { value: build(schema, from, 0), notices: [...new Set(notices)] };
}