Release Schema Tools v0.1.1

This commit is contained in:
2026-09-01 14:58:06 +02:00
parent 236b9e62c4
commit 2f60a6d0a7
21 changed files with 264 additions and 65 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 0.1.1 - 2026-09-01
- Bounded literal values, aggregate text, derived XML names, collections, and final output during sample generation.
- Added regression coverage for repeated large referenced defaults and always-invalid boolean schemas.
## 0.1.0 - 2026-09-01
- Added bounded local multi-file workspaces and a no-network reference graph for five schema languages.
+2 -2
View File
@@ -14,7 +14,7 @@ Schema Tools is a standalone local-first application in the [add·ideas Toolbox]
- Conservative comparisons for required properties, types, enums, declarations, operations, parameters, and responses
- Inert rendering, offline PWA support, responsive Toolbox shell integration, and deterministic release archives
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. DTD/entity declarations, remote references, Schematron XPath, extension code, JSON Schema `pattern`, and `patternProperties` are never executed. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. Generated samples are additionally limited to 2 MiB of serialized output, 512 KiB of retained text, and 1,024 characters per derived literal. DTD/entity declarations, remote references, Schematron XPath, extension code, JSON Schema `pattern`, and `patternProperties` are never executed. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
See [Architecture](docs/ARCHITECTURE.md) and [Privacy and security](docs/PRIVACY-SECURITY.md) for the exact capability boundaries.
@@ -30,7 +30,7 @@ npm run test:browser
## Release
`npm run release:artifact` creates deterministic `release/schema-tools-0.1.0.zip` and checksum files.
`npm run release:artifact` creates deterministic `release/schema-tools-0.1.1.zip` and checksum files.
## Licence
+2 -2
View File
@@ -1,7 +1,7 @@
# Corresponding source
The corresponding source for Schema Tools 0.1.0 is available at:
The corresponding source for Schema Tools 0.1.1 is available at:
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.1.0
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.1.1
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
+1 -1
View File
@@ -1,6 +1,6 @@
# Third-party notices
Schema Tools 0.1.0 directly depends on these runtime packages:
Schema Tools 0.1.1 directly depends on these runtime packages:
| Package | Pinned version | Declared licence |
| -------------------------------- | -------------: | ---------------- |
+2 -2
View File
@@ -6,6 +6,6 @@ JSON and YAML values are converted into an acyclic, prototype-safe JSON model wi
Every reference is classified before validation. Only fragments and relative filenames supplied in the current workspace are eligible. Root `$id`/legacy `id` values are deliberately ignored so relative references remain anchored to workspace filenames; nested identifier scopes, named anchors, dynamic/recursive references, and unevaluated keywords are outside the subset and cause a visible refusal. All participating JSON Schema files must use one declared draft. Formats and unknown extension keywords are annotations. Schemas containing `pattern` or `patternProperties` are refused because JavaScript regular-expression execution cannot be reliably time-bounded; they remain inspectable. Decimal arithmetic uses JavaScript numbers, so `multipleOf` applies a small floating-point tolerance.
OpenAPI JSON/YAML receives focused document, operation, response, reference, sample, and comparison logic. It does not run requests and does not claim full OpenAPI conformance. XML uses the browser's inert `DOMParser` only after rejecting DTD and entity declarations. XSD, Relax NG XML syntax, and Schematron are checked for well-formedness and structurally inventoried. Schematron XPath and extensions are retained as text and never executed. XSD and Relax NG sample generation deliberately follows a bounded first branch and is labelled heuristic.
OpenAPI JSON/YAML receives focused document, operation, response, reference, sample, and comparison logic. It does not run requests and does not claim full OpenAPI conformance. XML uses the browser's inert `DOMParser` only after rejecting DTD and entity declarations. XSD, Relax NG XML syntax, and Schematron are checked for well-formedness and structurally inventoried. Schematron XPath and extensions are retained as text and never executed. XSD and Relax NG sample generation deliberately follows a bounded first branch and is labelled heuristic. Across languages, sample generation copies literals rather than retaining source objects, caps individual derived values and names, limits aggregate retained text and nodes, and rejects serialized output above 2 MiB.
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. No worker, storage database, server API, telemetry path, or remote schema resolver exists in v0.1.0.
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. No worker, storage database, server API, telemetry path, or remote schema resolver exists in v0.1.1.
+2 -2
View File
@@ -2,8 +2,8 @@
Schema sources, instances, diagnostics, comparisons, and generated samples stay in page memory. There are no accounts, analytics, telemetry, persistence, remote imports, or runtime third-party assets. Explicit source links are normal navigation only. Clearing or closing the page releases application references but cannot promise forensic erasure from browser or operating-system memory.
Each source is limited to 2 MiB of text; a workspace is limited to 20 documents and 8 MiB. JSON/YAML trees are capped at 25,000 values, 48 levels, and 2,000 entries in one collection. XML is capped at 25,000 elements and 48 levels. References, samples, instances, and diagnostics have separate caps. File byte gates are deliberately conservative before `File.text()` decoding.
Each source is limited to 2 MiB of text; a workspace is limited to 20 documents and 8 MiB. JSON/YAML trees are capped at 25,000 values, 48 levels, and 2,000 entries in one collection. XML is capped at 25,000 elements and 48 levels. References, instances, and diagnostics have separate caps. Sample generation is limited to 2,000 nodes, 20 levels, 512 KiB of retained text, 1,024 characters per copied literal, and 2 MiB of serialized output. File byte gates are deliberately conservative before `File.text()` decoding.
Prototype-sensitive JSON keys, cyclic YAML aliases, NUL input, DTD/entity declarations, absolute/remote/escaping references, and executable schema extensions are rejected. Schematron expressions and imported XML are never executed. JSON Schema formats are annotations, and regex-bearing schema keywords are not executed. The focused validator interprets eligible schemas without dynamic code generation, remote loading, custom code, or `unsafe-eval`; work and diagnostic counts are capped.
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, regular expressions, and full meta-schema validation are outside v0.1.0. XSD, Relax NG, and Schematron instance validation is also outside v0.1.0; OpenAPI inspection is not a full conformance certification.
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, regular expressions, and full meta-schema validation are outside v0.1.1. XSD, Relax NG, and Schematron instance validation is also outside v0.1.1; OpenAPI inspection is not a full conformance certification.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "schema-tools",
"version": "0.1.0",
"version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "schema-tools",
"version": "0.1.0",
"version": "0.1.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"@add-ideas/toolbox-contract": "0.2.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "schema-tools",
"version": "0.1.0",
"version": "0.1.1",
"description": "Inspect, validate, compare, and derive examples from schemas locally in the browser.",
"license": "GPL-3.0-or-later",
"author": "Albrecht Degering",
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 0.1.1 - 2026-09-01
- Bounded literal values, aggregate text, derived XML names, collections, and final output during sample generation.
- Added regression coverage for repeated large referenced defaults and always-invalid boolean schemas.
## 0.1.0 - 2026-09-01
- Added bounded local multi-file workspaces and a no-network reference graph for five schema languages.
+2 -2
View File
@@ -14,7 +14,7 @@ Schema Tools is a standalone local-first application in the [add·ideas Toolbox]
- Conservative comparisons for required properties, types, enums, declarations, operations, parameters, and responses
- Inert rendering, offline PWA support, responsive Toolbox shell integration, and deterministic release archives
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. DTD/entity declarations, remote references, Schematron XPath, extension code, JSON Schema `pattern`, and `patternProperties` are never executed. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
Each document is limited to 2 MiB of text, the workspace to 8 MiB and 20 documents, and parsed trees, references, instances, samples, validation work, and diagnostic output have independent bounds. Generated samples are additionally limited to 2 MiB of serialized output, 512 KiB of retained text, and 1,024 characters per derived literal. DTD/entity declarations, remote references, Schematron XPath, extension code, JSON Schema `pattern`, and `patternProperties` are never executed. JSON Schema validation covers a documented assertion subset rather than claiming full specification conformance. XML languages receive structural inspection—not instance validation—and OpenAPI checks are intentionally focused.
See [Architecture](docs/ARCHITECTURE.md) and [Privacy and security](docs/PRIVACY-SECURITY.md) for the exact capability boundaries.
@@ -30,7 +30,7 @@ npm run test:browser
## Release
`npm run release:artifact` creates deterministic `release/schema-tools-0.1.0.zip` and checksum files.
`npm run release:artifact` creates deterministic `release/schema-tools-0.1.1.zip` and checksum files.
## Licence
+2 -2
View File
@@ -1,7 +1,7 @@
# Corresponding source
The corresponding source for Schema Tools 0.1.0 is available at:
The corresponding source for Schema Tools 0.1.1 is available at:
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.1.0
https://git.add-ideas.de/lotobo/schema-tools/src/tag/v0.1.1
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
+1 -1
View File
@@ -1,6 +1,6 @@
# Third-party notices
Schema Tools 0.1.0 directly depends on these runtime packages:
Schema Tools 0.1.1 directly depends on these runtime packages:
| Package | Pinned version | Declared licence |
| -------------------------------- | -------------: | ---------------- |
+2 -2
View File
@@ -6,6 +6,6 @@ JSON and YAML values are converted into an acyclic, prototype-safe JSON model wi
Every reference is classified before validation. Only fragments and relative filenames supplied in the current workspace are eligible. Root `$id`/legacy `id` values are deliberately ignored so relative references remain anchored to workspace filenames; nested identifier scopes, named anchors, dynamic/recursive references, and unevaluated keywords are outside the subset and cause a visible refusal. All participating JSON Schema files must use one declared draft. Formats and unknown extension keywords are annotations. Schemas containing `pattern` or `patternProperties` are refused because JavaScript regular-expression execution cannot be reliably time-bounded; they remain inspectable. Decimal arithmetic uses JavaScript numbers, so `multipleOf` applies a small floating-point tolerance.
OpenAPI JSON/YAML receives focused document, operation, response, reference, sample, and comparison logic. It does not run requests and does not claim full OpenAPI conformance. XML uses the browser's inert `DOMParser` only after rejecting DTD and entity declarations. XSD, Relax NG XML syntax, and Schematron are checked for well-formedness and structurally inventoried. Schematron XPath and extensions are retained as text and never executed. XSD and Relax NG sample generation deliberately follows a bounded first branch and is labelled heuristic.
OpenAPI JSON/YAML receives focused document, operation, response, reference, sample, and comparison logic. It does not run requests and does not claim full OpenAPI conformance. XML uses the browser's inert `DOMParser` only after rejecting DTD and entity declarations. XSD, Relax NG XML syntax, and Schematron are checked for well-formedness and structurally inventoried. Schematron XPath and extensions are retained as text and never executed. XSD and Relax NG sample generation deliberately follows a bounded first branch and is labelled heuristic. Across languages, sample generation copies literals rather than retaining source objects, caps individual derived values and names, limits aggregate retained text and nodes, and rejects serialized output above 2 MiB.
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. No worker, storage database, server API, telemetry path, or remote schema resolver exists in v0.1.0.
The PWA uses only relative URLs, so the same build works standalone or below a nested portal route. Its service worker caches same-origin files from its own scope. No worker, storage database, server API, telemetry path, or remote schema resolver exists in v0.1.1.
+2 -2
View File
@@ -2,8 +2,8 @@
Schema sources, instances, diagnostics, comparisons, and generated samples stay in page memory. There are no accounts, analytics, telemetry, persistence, remote imports, or runtime third-party assets. Explicit source links are normal navigation only. Clearing or closing the page releases application references but cannot promise forensic erasure from browser or operating-system memory.
Each source is limited to 2 MiB of text; a workspace is limited to 20 documents and 8 MiB. JSON/YAML trees are capped at 25,000 values, 48 levels, and 2,000 entries in one collection. XML is capped at 25,000 elements and 48 levels. References, samples, instances, and diagnostics have separate caps. File byte gates are deliberately conservative before `File.text()` decoding.
Each source is limited to 2 MiB of text; a workspace is limited to 20 documents and 8 MiB. JSON/YAML trees are capped at 25,000 values, 48 levels, and 2,000 entries in one collection. XML is capped at 25,000 elements and 48 levels. References, instances, and diagnostics have separate caps. Sample generation is limited to 2,000 nodes, 20 levels, 512 KiB of retained text, 1,024 characters per copied literal, and 2 MiB of serialized output. File byte gates are deliberately conservative before `File.text()` decoding.
Prototype-sensitive JSON keys, cyclic YAML aliases, NUL input, DTD/entity declarations, absolute/remote/escaping references, and executable schema extensions are rejected. Schematron expressions and imported XML are never executed. JSON Schema formats are annotations, and regex-bearing schema keywords are not executed. The focused validator interprets eligible schemas without dynamic code generation, remote loading, custom code, or `unsafe-eval`; work and diagnostic counts are capped.
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, regular expressions, and full meta-schema validation are outside v0.1.0. XSD, Relax NG, and Schematron instance validation is also outside v0.1.0; OpenAPI inspection is not a full conformance certification.
Sample generation and compatibility results are review aids. A generated document is not guaranteed to satisfy every constraint, and an absence of reported changes does not prove compatibility. JSON validation requires a single declared draft and uses local workspace filenames rather than identifier URIs. Named anchors, nested identifier scopes, dynamic/recursive references, unevaluated keywords, regular expressions, and full meta-schema validation are outside v0.1.1. XSD, Relax NG, and Schematron instance validation is also outside v0.1.1; OpenAPI inspection is not a full conformance certification.
+1 -1
View File
@@ -1,5 +1,5 @@
const CACHE_PREFIX = "schema-tools-shell-";
const CACHE_NAME = CACHE_PREFIX + "0.1.0";
const CACHE_NAME = CACHE_PREFIX + "0.1.1";
const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"];
self.addEventListener("install", (event) => {
event.waitUntil(
+1 -1
View File
@@ -3,7 +3,7 @@
"schemaVersion": 1,
"id": "de.add-ideas.schema-tools",
"name": "Schema Tools",
"version": "0.1.0",
"version": "0.1.1",
"description": "Inspect, validate, compare, and derive schema examples locally.",
"entry": "./",
"icon": "./favicon.svg",
+182 -39
View File
@@ -68,6 +68,9 @@ export const SCHEMA_LIMITS = {
depth: 48,
references: 5_000,
sampleNodes: 2_000,
sampleValueChars: 1_024,
sampleTextChars: 512 * 1_024,
sampleOutputChars: 2 * 1_024 * 1_024,
instanceChars: 1024 * 1024,
instanceNodes: 20_000,
instanceCollection: 2_000,
@@ -934,25 +937,85 @@ function sampleJson(
from: string,
): { value: JsonValue; notices: string[] } {
let nodes = 0;
let textChars = 0;
const active = new Set<string>();
const notices: string[] = [];
const addNotice = (message: string): void => {
if (!notices.includes(message)) notices.push(message);
};
const boundedText = (value: string, label: string): string => {
const remaining = Math.max(0, SCHEMA_LIMITS.sampleTextChars - textChars);
const retained = value.slice(
0,
Math.min(SCHEMA_LIMITS.sampleValueChars, remaining),
);
textChars += retained.length;
if (retained.length < value.length)
addNotice(`${label} was truncated by the sample text safety bound.`);
return retained;
};
const takeNode = (depth: number): boolean => {
nodes += 1;
if (nodes <= SCHEMA_LIMITS.sampleNodes && depth <= 20) return true;
addNotice(
"Sample generation stopped at its 2,000-node / 20-level safety bound.",
);
return false;
};
const copyLiteral = (value: JsonValue, depth: number): JsonValue => {
if (!takeNode(depth)) return null;
if (typeof value === "string")
return boundedText(value, "A literal sample value");
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) {
if (value.length > 100)
addNotice(
"A literal array was truncated to 100 entries in the sample.",
);
return value.slice(0, 100).map((item) => copyLiteral(item, depth + 1));
}
const output: JsonObject = Object.create(null) as JsonObject;
const entries = Object.entries(value);
if (entries.length > 100)
addNotice(
"A literal object was truncated to 100 properties in the sample.",
);
for (const [key, child] of entries.slice(0, 100)) {
if (key.length > 128) {
addNotice(
"A literal object key longer than 128 characters was omitted from the sample.",
);
continue;
}
textChars += key.length;
if (textChars > SCHEMA_LIMITS.sampleTextChars) {
addNotice("The sample text safety bound omitted further values.");
break;
}
output[key] = copyLiteral(child, depth + 1);
}
return output;
};
const build = (
value: JsonValue,
filename: string,
depth: number,
): JsonValue => {
nodes += 1;
if (nodes > SCHEMA_LIMITS.sampleNodes || depth > 20) {
notices.push(
"Sample generation stopped at its 2,000-node / 20-level safety bound.",
if (!takeNode(depth)) return null;
if (typeof value === "boolean") {
addNotice(
`A ${String(value)} boolean subschema has no shape; null was used as a placeholder.`,
);
return null;
}
if (!isObject(value)) return value;
if (!isObject(value))
return typeof value === "string"
? boundedText(value, "A sample string")
: value;
if (typeof value.$ref === "string") {
const resolved = resolveJsonReference(workspace, filename, value.$ref);
if (active.has(resolved.key)) {
notices.push(`Reference cycle stopped at ${resolved.key}.`);
addNotice(`Reference cycle stopped at ${resolved.key.slice(0, 300)}.`);
return null;
}
active.add(resolved.key);
@@ -965,13 +1028,15 @@ function sampleJson(
for (const key of ["examples", "example", "const", "default"] as const) {
const candidate = value[key];
if (key === "examples" && Array.isArray(candidate) && candidate.length)
return candidate[0]!;
if (key !== "examples" && candidate !== undefined) return candidate;
return copyLiteral(candidate[0]!, depth + 1);
if (key !== "examples" && candidate !== undefined)
return copyLiteral(candidate, depth + 1);
}
if (Array.isArray(value.enum) && value.enum.length) return value.enum[0]!;
if (Array.isArray(value.enum) && value.enum.length)
return copyLiteral(value.enum[0]!, depth + 1);
for (const key of ["oneOf", "anyOf"] as const)
if (Array.isArray(value[key]) && value[key]!.length) {
notices.push(`${key} sample uses the first alternative.`);
addNotice(`${key} sample uses the first alternative.`);
return build(value[key]![0]!, filename, depth + 1);
}
if (Array.isArray(value.allOf)) {
@@ -1005,12 +1070,27 @@ function sampleJson(
);
const entries = Object.entries(properties).slice(0, 100);
for (const [key, child] of entries)
if (required.has(key) || entries.length <= 12)
if ((required.has(key) || entries.length <= 12) && key.length <= 128) {
textChars += key.length;
if (textChars > SCHEMA_LIMITS.sampleTextChars) {
addNotice("The sample text safety bound omitted further values.");
break;
}
result[key] = build(child, filename, depth + 1);
} else if (key.length > 128)
addNotice(
"A property name longer than 128 characters was omitted from the sample.",
);
return result;
}
if (type === "array")
return value.items ? [build(value.items, filename, depth + 1)] : [];
if (type === "array") {
const item = Array.isArray(value.prefixItems)
? value.prefixItems[0]
: Array.isArray(value.items)
? value.items[0]
: value.items;
return item === undefined ? [] : [build(item, filename, depth + 1)];
}
if (type === "integer" || type === "number")
return typeof value.minimum === "number" ? value.minimum : 0;
if (type === "boolean") return true;
@@ -1031,7 +1111,9 @@ function sampleJson(
? "x".repeat(Math.min(value.minLength, 100))
: "string";
};
return { value: build(root, from, 0), notices: [...new Set(notices)] };
if (root === false)
throw new TypeError("A false JSON Schema has no valid sample instance.");
return { value: build(root, from, 0), notices };
}
function xmlEscape(value: string): string {
@@ -1044,6 +1126,20 @@ function xmlEscape(value: string): string {
function qNameLocal(value: string): string {
return value.includes(":") ? value.slice(value.lastIndexOf(":") + 1) : value;
}
function sampleXmlName(value: string, fallback: string): string {
const local = qNameLocal(value).slice(0, 128);
return /^[A-Za-z_][A-Za-z0-9_.-]*$/u.test(local) ? local : fallback;
}
function checkedSampleOutput(value: string): string {
if (value.length > SCHEMA_LIMITS.sampleOutputChars)
throw new RangeError(
`Generated sample exceeds the ${Math.round(SCHEMA_LIMITS.sampleOutputChars / 1024 / 1024)} MiB output safety limit.`,
);
return value;
}
function jsonSampleOutput(value: JsonValue): string {
return checkedSampleOutput(`${JSON.stringify(value, null, 2)}\n`);
}
function sampleXsd(document: ParsedSchemaDocument): SampleResult {
const root = document.xml!.documentElement;
@@ -1061,6 +1157,7 @@ function sampleXsd(document: ParsedSchemaDocument): SampleResult {
);
const active = new Set<Element>();
let nodes = 0;
let adjustedName = false;
const valueForType = (type: string) => {
const local = qNameLocal(type);
if (["boolean"].includes(local)) return "true";
@@ -1096,9 +1193,11 @@ function sampleXsd(document: ParsedSchemaDocument): SampleResult {
if (active.has(element)) return "";
active.add(element);
try {
const name =
const rawName =
element.getAttribute("name") ||
qNameLocal(element.getAttribute("ref") || "element");
const name = sampleXmlName(rawName, "element");
if (name !== qNameLocal(rawName)) adjustedName = true;
const typeName = element.getAttribute("type") || "";
const type = typeName
? namedTypes.get(qNameLocal(typeName))
@@ -1108,10 +1207,18 @@ function sampleXsd(document: ParsedSchemaDocument): SampleResult {
const attributes = type
? localElements(type, "attribute")
.slice(0, 20)
.map(
(attribute) =>
` ${xmlEscape(attribute.getAttribute("name") || qNameLocal(attribute.getAttribute("ref") || "attribute"))}="${xmlEscape(valueForType(attribute.getAttribute("type") || "string"))}"`,
)
.map((attribute) => {
const rawAttributeName =
attribute.getAttribute("name") ||
qNameLocal(attribute.getAttribute("ref") || "attribute");
const attributeName = sampleXmlName(
rawAttributeName,
"attribute",
);
if (attributeName !== qNameLocal(rawAttributeName))
adjustedName = true;
return ` ${attributeName}="${xmlEscape(valueForType(attribute.getAttribute("type") || "string"))}"`;
})
.join("")
: "";
const children = type
@@ -1129,19 +1236,32 @@ function sampleXsd(document: ParsedSchemaDocument): SampleResult {
const enumeration = type
? localElements(type, "enumeration")[0]?.getAttribute("value")
: undefined;
body = xmlEscape(enumeration ?? valueForType(typeName));
body = xmlEscape(
(enumeration ?? valueForType(typeName)).slice(
0,
SCHEMA_LIMITS.sampleValueChars,
),
);
}
return `<${xmlEscape(name)}${attributes}>${body}</${xmlEscape(name)}>`;
} finally {
active.delete(element);
}
};
const notices = [
"Heuristic XSD sample: occurrence constraints, substitution groups, wildcards, facets, and namespaces may require manual correction.",
];
const output = checkedSampleOutput(
`<?xml version="1.0" encoding="UTF-8"?>\n${render(first, 0)}\n`,
);
if (adjustedName)
notices.push(
"An overlong or invalid derived XML name was replaced in the sample.",
);
return {
output: `<?xml version="1.0" encoding="UTF-8"?>\n${render(first, 0)}\n`,
output,
mediaType: "application/xml",
notices: [
"Heuristic XSD sample: occurrence constraints, substitution groups, wildcards, facets, and namespaces may require manual correction.",
],
notices,
};
}
@@ -1157,21 +1277,28 @@ function sampleRelaxNg(document: ParsedSchemaDocument): SampleResult {
if (!start) throw new TypeError("Relax NG grammar has no start pattern.");
let nodes = 0;
const active = new Set<string>();
let adjustedName = false;
const renderPatterns = (container: Element, depth: number): string => {
if (nodes > SCHEMA_LIMITS.sampleNodes || depth > 20) return "";
for (const pattern of directChildren(container)) {
if (pattern.localName === "element") {
nodes += 1;
const name =
const rawName =
pattern.getAttribute("name") ||
directChildren(pattern, "name")[0]?.textContent?.trim() ||
"element";
const name = sampleXmlName(rawName, "element");
if (name !== qNameLocal(rawName)) adjustedName = true;
const attributes = directChildren(pattern, "attribute")
.slice(0, 20)
.map(
(attribute) =>
` ${xmlEscape(attribute.getAttribute("name") || "attribute")}="string"`,
)
.map((attribute) => {
const rawAttributeName =
attribute.getAttribute("name") || "attribute";
const attributeName = sampleXmlName(rawAttributeName, "attribute");
if (attributeName !== qNameLocal(rawAttributeName))
adjustedName = true;
return ` ${attributeName}="string"`;
})
.join("");
const body =
renderPatterns(pattern, depth + 1) ||
@@ -1204,7 +1331,12 @@ function sampleRelaxNg(document: ParsedSchemaDocument): SampleResult {
return value;
}
if (pattern.localName === "value")
return xmlEscape(pattern.textContent ?? "value");
return xmlEscape(
(pattern.textContent ?? "value").slice(
0,
SCHEMA_LIMITS.sampleValueChars,
),
);
if (pattern.localName === "data" || pattern.localName === "text")
return "string";
}
@@ -1215,12 +1347,19 @@ function sampleRelaxNg(document: ParsedSchemaDocument): SampleResult {
throw new TypeError(
"Could not derive a concrete element from the Relax NG start pattern.",
);
const notices = [
"Heuristic Relax NG sample uses the first choice and one repetition; datatype parameters and external grammars are not evaluated.",
];
if (adjustedName)
notices.push(
"An overlong or invalid derived XML name was replaced in the sample.",
);
return {
output: `<?xml version="1.0" encoding="UTF-8"?>\n${output}\n`,
output: checkedSampleOutput(
`<?xml version="1.0" encoding="UTF-8"?>\n${output}\n`,
),
mediaType: "application/xml",
notices: [
"Heuristic Relax NG sample uses the first choice and one repetition; datatype parameters and external grammars are not evaluated.",
],
notices,
};
}
@@ -1236,7 +1375,9 @@ function openApiSample(
const first = schemas ? Object.entries(schemas)[0] : undefined;
if (first) {
const result = sampleJson(workspace, first[1], document.name);
result.notices.unshift(`Generated from components.schemas.${first[0]}.`);
result.notices.unshift(
`Generated from components.schemas.${first[0].slice(0, 128)}.`,
);
return result;
}
const paths = isObject(root.paths) ? root.paths : {};
@@ -1272,7 +1413,7 @@ export function generateSample(
if (document.language === "json-schema") {
const result = sampleJson(workspace, document.json!, document.name);
return {
output: JSON.stringify(result.value, null, 2) + "\n",
output: jsonSampleOutput(result.value),
mediaType: "application/json",
notices: result.notices,
};
@@ -1280,7 +1421,7 @@ export function generateSample(
if (document.language === "openapi") {
const result = openApiSample(workspace, document);
return {
output: JSON.stringify(result.value, null, 2) + "\n",
output: jsonSampleOutput(result.value),
mediaType: "application/json",
notices: result.notices,
};
@@ -1288,7 +1429,9 @@ export function generateSample(
if (document.language === "xsd") return sampleXsd(document);
if (document.language === "relax-ng") return sampleRelaxNg(document);
return {
output: `<?xml version="1.0" encoding="UTF-8"?>\n<root/>\n`,
output: checkedSampleOutput(
`<?xml version="1.0" encoding="UTF-8"?>\n<root/>\n`,
),
mediaType: "application/xml",
notices: [
"Schematron does not define an instance vocabulary. This neutral skeleton is only a starting point; assertions are not executed.",
@@ -2154,7 +2297,7 @@ export function validateJsonInstance(
name,
"error",
"/",
"Instance validation is available only for JSON Schema documents in v0.1.0.",
"Instance validation is available only for JSON Schema documents in this release.",
),
],
};
+1 -1
View File
@@ -3,7 +3,7 @@
"schemaVersion": 1,
"id": "de.add-ideas.schema-tools",
"name": "Schema Tools",
"version": "0.1.0",
"version": "0.1.1",
"description": "Inspect, validate, compare, and derive schema examples locally.",
"entry": "./",
"icon": "./favicon.svg",
+1 -1
View File
@@ -1 +1 @@
export const APP_VERSION = "0.1.0";
export const APP_VERSION = "0.1.1";
+1 -1
View File
@@ -70,7 +70,7 @@ test("serves release identity and hardened headers", async ({ request }) => {
const manifest = await request.get("/deep/nested/schema/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.schema-tools",
version: "0.1.0",
version: "0.1.1",
entry: "./",
privacy: { processing: "local", telemetry: false },
});
+46
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
SCHEMA_LIMITS,
compareSchemas,
generateSample,
inspectWorkspace,
@@ -60,6 +61,51 @@ describe("schema workspace", () => {
expect(invalid.diagnostics[0]?.message).toContain("required");
});
it("bounds repeated large literal samples reached through local references", () => {
const properties = Object.fromEntries(
Array.from({ length: 100 }, (_, index) => [
`value${index}`,
{ $ref: "literal.json#/$defs/large" },
]),
);
const workspace = inspectWorkspace([
{
name: "root.json",
source: JSON.stringify({
type: "object",
required: Object.keys(properties),
properties,
}),
},
{
name: "literal.json",
source: JSON.stringify({
$defs: { large: { default: "x".repeat(100_000) } },
}),
},
]);
const sample = generateSample(workspace);
const values = Object.values(JSON.parse(sample.output) as object);
expect(sample.output.length).toBeLessThanOrEqual(
SCHEMA_LIMITS.sampleOutputChars,
);
expect(values).toHaveLength(100);
expect(
values.every(
(value) => String(value).length <= SCHEMA_LIMITS.sampleValueChars,
),
).toBe(true);
expect(sample.notices.join("\n")).toMatch(/truncated.*safety bound/iu);
});
it("refuses to claim a sample for the always-invalid false schema", () => {
const workspace = inspectWorkspace([
{ name: "impossible.json", source: "false" },
]);
expect(() => generateSample(workspace)).toThrow(/no valid sample/iu);
});
it("blocks remote and missing references without attempting resolution", () => {
const workspace = inspectWorkspace([
{