EdgeAIG's picture
download
raw
25.8 kB
import * as Arr from "../../Array.js";
import * as Equal from "../../Equal.js";
import { format } from "../../Formatter.js";
import { escapeToken } from "../../JsonPointer.js";
import * as Predicate from "../../Predicate.js";
import * as Rec from "../../Record.js";
import * as RegEx from "../../RegExp.js";
import * as SchemaAST from "../../SchemaAST.js";
import * as InternalAnnotations from "./annotations.js";
import * as InternalSchema from "./schema.js";
/** @internal */
export function fromAST(ast) {
const {
references,
representations: schemas
} = fromASTs([ast]);
return {
representation: schemas[0],
references
};
}
/** @internal */
export function fromASTs(asts) {
const references = {};
const referenceMap = new Map();
const uniqueReferences = new Set();
const visiting = new Set();
const schemas = Arr.map(asts, ast => recur(ast));
return {
representations: schemas,
references
};
function gen(prefix) {
let candidate = prefix;
let suffix = 0;
while (uniqueReferences.has(candidate)) {
candidate = `${prefix}${++suffix}`;
}
uniqueReferences.add(candidate);
return candidate;
}
function recur(ast, prefix) {
const found = referenceMap.get(ast);
if (found !== undefined) {
return {
_tag: "Reference",
$ref: found
};
}
const last = SchemaAST.getLastEncoding(ast);
const identifier = InternalAnnotations.resolveIdentifier(ast) ?? prefix;
if (ast !== last) {
return recur(last, identifier);
}
// Has identifier → always create reference
if (identifier !== undefined) {
const reference = gen(identifier);
referenceMap.set(ast, reference);
const out = on(ast);
const found = references[identifier];
// Reuse existing references when duplicate identifiers have the same representation
if (found !== undefined && Equal.equals(out, found)) {
referenceMap.set(ast, identifier);
return {
_tag: "Reference",
$ref: identifier
};
}
references[reference] = out;
return {
_tag: "Reference",
$ref: reference
};
}
// Recursion detected → create reference
if (visiting.has(ast)) {
const reference = gen(`${ast._tag}_`);
referenceMap.set(ast, reference);
return {
_tag: "Reference",
$ref: reference
};
}
// Normal case → inline
visiting.add(ast);
const out = on(ast);
visiting.delete(ast);
// A descendant triggered reference creation (recursion)
const ref = referenceMap.get(ast);
if (ref !== undefined) {
references[ref] = out;
return {
_tag: "Reference",
$ref: ref
};
}
return out;
}
function getEncodedSchema(last) {
const getLink = last.annotations?.toCodecJson ?? last.annotations?.toCodec;
if (Predicate.isFunction(getLink)) {
const tps = last.typeParameters.map(tp => InternalSchema.make(SchemaAST.toEncoded(tp)));
const link = getLink(tps);
return SchemaAST.replaceEncoding(last, [link]);
}
return SchemaAST.null;
}
function on(last) {
const annotations = fromASTAnnotations(last.annotations);
switch (last._tag) {
case "Declaration":
{
// this must be executed before transforming the type parameters
const encodedSchema = recur(getEncodedSchema(last));
return {
_tag: "Declaration",
typeParameters: last.typeParameters.map(ast => recur(ast)),
encodedSchema,
checks: fromASTChecks(last.checks),
...annotations
};
}
case "Null":
case "Undefined":
case "Void":
case "Never":
case "Unknown":
case "Any":
case "Boolean":
case "Symbol":
return {
_tag: last._tag,
...annotations
};
case "String":
{
const contentMediaType = last.annotations?.contentMediaType;
const contentSchema = last.annotations?.contentSchema;
return {
_tag: last._tag,
checks: fromASTChecks(last.checks),
...annotations,
...(typeof contentMediaType === "string" && SchemaAST.isAST(contentSchema) ? {
contentSchema: recur(contentSchema)
} : undefined)
};
}
case "Number":
case "BigInt":
return {
_tag: last._tag,
checks: fromASTChecks(last.checks),
...annotations
};
case "Literal":
return {
_tag: last._tag,
literal: last.literal,
...annotations
};
case "UniqueSymbol":
return {
_tag: last._tag,
symbol: last.symbol,
...annotations
};
case "ObjectKeyword":
return {
_tag: last._tag,
...annotations
};
case "Enum":
return {
_tag: last._tag,
enums: last.enums,
...annotations
};
case "TemplateLiteral":
return {
_tag: last._tag,
parts: last.parts.map(ast => recur(ast)),
...annotations
};
case "Arrays":
return {
_tag: last._tag,
elements: last.elements.map(e => {
const last = SchemaAST.getLastEncoding(e);
return {
isOptional: SchemaAST.isOptional(last),
type: recur(e),
...fromASTAnnotations(last.context?.annotations)
};
}),
rest: last.rest.map(ast => recur(ast)),
checks: fromASTChecks(last.checks),
...annotations
};
case "Objects":
return {
_tag: last._tag,
propertySignatures: last.propertySignatures.map(ps => {
const last = SchemaAST.getLastEncoding(ps.type);
return {
name: ps.name,
type: recur(ps.type),
isOptional: SchemaAST.isOptional(last),
isMutable: SchemaAST.isMutable(last),
...fromASTAnnotations(last.context?.annotations)
};
}),
indexSignatures: last.indexSignatures.map(is => ({
parameter: recur(is.parameter),
type: recur(is.type)
})),
checks: fromASTChecks(last.checks),
...annotations
};
case "Union":
{
const types = InternalSchema.jsonReorder(last.types);
return {
_tag: last._tag,
types: types.map(ast => recur(ast)),
mode: last.mode,
...annotations
};
}
case "Suspend":
{
return {
_tag: "Suspend",
checks: [],
thunk: recur(last.thunk()),
...annotations
};
}
}
}
function fromASTChecks(checks) {
if (!checks) return [];
return checks.map(getCheck).filter(c => c !== undefined);
function getCheck(c) {
switch (c._tag) {
case "Filter":
{
const meta = c.annotations?.meta;
if (meta) {
return {
_tag: "Filter",
meta: meta._tag === "isPropertyNames" ? {
_tag: "isPropertyNames",
propertyNames: recur(meta.propertyNames)
} : meta,
...fromASTAnnotations(c.annotations)
};
}
return undefined;
}
case "FilterGroup":
{
const checks = fromASTChecks(c.checks);
if (Arr.isArrayNonEmpty(checks)) {
return {
_tag: "FilterGroup",
checks,
...fromASTAnnotations(c.annotations)
};
}
}
}
}
}
}
/** @internal */
export const fromASTBlacklist = /*#__PURE__*/new Set([
// `expected` is preserved because is useful to generate descriptions in JSON Schemas
"~structural", "~sentinels", "meta", "arbitrary", "toArbitrary", "toEquivalence", "toFormatter", "toCodec", "toCodecJson", "toCodecIso", SchemaAST.ClassTypeId]);
const standardJsonSchemaAnnotationKeys = /*#__PURE__*/new Set(["title", "description", "default", "examples", "readOnly", "writeOnly", "format", "contentEncoding", "contentMediaType", "contentSchema"]);
function fromASTAnnotations(annotations) {
if (annotations !== undefined) {
const filtered = Rec.filter(annotations, (_, k) => !fromASTBlacklist.has(k));
if (!Rec.isEmptyRecord(filtered)) {
return {
annotations: filtered
};
}
}
return undefined;
}
/** @internal */
export function toJsonSchemaDocument(document, options) {
const {
definitions,
dialect: source,
schemas
} = toJsonSchemaMultiDocument({
representations: [document.representation],
references: document.references
}, options);
const schema = schemas[0];
return {
dialect: source,
schema,
definitions
};
}
/** @internal */
export function toJsonSchemaMultiDocument(multiDocument, options) {
const generateDescriptions = options?.generateDescriptions ?? false;
const additionalProperties = options?.additionalProperties ?? false;
const includeAnnotationKey = options?.includeAnnotationKey;
const definitions = Rec.map(multiDocument.references, d => recur(d));
return {
dialect: "draft-2020-12",
schemas: Arr.map(multiDocument.representations, s => recur(s)),
definitions
};
function recur(s) {
let js = on(s);
if ("annotations" in s) {
const a = collectJsonSchemaAnnotations(s.annotations);
if (a) {
js = {
...js,
...a
};
}
}
if ("checks" in s) {
const checks = collectJsonSchemaChecks(s.checks, js.type);
for (const check of checks) {
js = appendJsonSchema(js, check);
}
}
return js;
}
function on(schema) {
switch (schema._tag) {
case "Any":
case "Unknown":
return {};
case "ObjectKeyword":
return {
anyOf: [{
type: "object"
}, {
type: "array"
}]
};
case "Void":
case "Undefined":
return {
type: "null"
};
case "BigInt":
return {
"type": "string",
"allOf": [{
"pattern": "^-?\\d+$"
}]
};
case "Symbol":
case "UniqueSymbol":
return {
"type": "string",
"allOf": [{
"pattern": "^Symbol\\((.*)\\)$"
}]
};
case "Declaration":
return recur(schema.encodedSchema);
case "Suspend":
return recur(schema.thunk);
case "Reference":
return {
$ref: `#/$defs/${escapeToken(schema.$ref)}`
};
case "Null":
return {
type: "null"
};
case "Never":
return {
not: {}
};
case "String":
{
const out = {
type: "string"
};
if (schema.contentMediaType !== undefined) {
out.contentMediaType = schema.contentMediaType;
}
if (schema.contentSchema !== undefined) {
out.contentSchema = recur(schema.contentSchema);
}
return out;
}
case "Number":
return hasCheck(schema.checks, "isInt") ? {
type: "integer"
} : hasCheck(schema.checks, "isFinite") ? {
type: "number"
} : {
"anyOf": [{
type: "number"
}, {
type: "string",
enum: ["NaN"]
}, {
type: "string",
enum: ["Infinity"]
}, {
type: "string",
enum: ["-Infinity"]
}]
};
case "Boolean":
return {
type: "boolean"
};
case "Literal":
{
const literal = schema.literal;
if (typeof literal === "string") {
return {
type: "string",
enum: [literal]
};
}
if (typeof literal === "number") {
return {
type: "number",
enum: [literal]
};
}
if (typeof literal === "boolean") {
return {
type: "boolean",
enum: [literal]
};
}
// bigint literals are not supported
return {
type: "string",
enum: [String(literal)]
};
}
case "Enum":
{
return recur({
_tag: "Union",
types: schema.enums.map(([title, value]) => ({
_tag: "Literal",
literal: value,
annotations: {
title
}
})),
mode: "anyOf",
annotations: schema.annotations
});
}
case "TemplateLiteral":
{
const pattern = schema.parts.map(getPartPattern).join("");
return {
type: "string",
pattern: `^${pattern}$`
};
}
case "Arrays":
{
// ---------------------------------------------
// handle post rest elements
// ---------------------------------------------
if (schema.rest.length > 1) {
throw new globalThis.Error("Generating a JSON Schema for post-rest elements is not supported");
}
const out = {
type: "array"
};
let minItems = schema.elements.length;
const prefixItems = schema.elements.map(e => {
if (e.isOptional) {
minItems--;
}
const v = recur(e.type);
const a = collectJsonSchemaAnnotations(e.annotations);
return a ? appendJsonSchema(v, a) : v;
});
if (prefixItems.length > 0) {
out.prefixItems = prefixItems;
out.maxItems = schema.elements.length;
if (minItems > 0) {
out.minItems = minItems;
}
} else {
out.items = false;
}
if (schema.rest.length > 0) {
delete out.maxItems;
const rest = recur(schema.rest[0]);
if (Object.keys(rest).length > 0) {
out.items = rest;
} else {
delete out.items;
}
}
return out;
}
case "Objects":
{
if (schema.propertySignatures.length === 0 && schema.indexSignatures.length === 0) {
return {
anyOf: [{
type: "object"
}, {
type: "array"
}]
};
}
const out = {
type: "object"
};
const properties = {};
const required = [];
for (const ps of schema.propertySignatures) {
const name = ps.name;
if (typeof name !== "string") {
throw new globalThis.Error(`Unsupported property signature name: ${format(name)}`);
}
const v = recur(ps.type);
const a = collectJsonSchemaAnnotations(ps.annotations);
properties[name] = a ? appendJsonSchema(v, a) : v;
// Property is required only if it's not explicitly optional AND doesn't contain Undefined
if (!ps.isOptional) {
required.push(name);
}
}
if (Object.keys(properties).length > 0) {
out.properties = properties;
}
if (required.length > 0) {
out.required = required;
}
out.additionalProperties = additionalProperties;
const patternProperties = {};
// Handle index signatures
for (const is of schema.indexSignatures) {
let type = recur(is.type);
// Collapse unannotated Never ({ not: {} }) to false, but keep annotated schemas as objects.
if (Object.keys(type).length === 1 && "not" in type) {
type = false;
}
const patterns = getParameterPatterns(is.parameter);
if (patterns.length > 0) {
for (const pattern of patterns) {
patternProperties[pattern] = type;
}
} else {
out.additionalProperties = type;
}
}
if (Object.keys(patternProperties).length > 0) {
out.patternProperties = patternProperties;
delete out.additionalProperties;
}
if (Predicate.isObject(out.additionalProperties) && Rec.isEmptyRecord(out.additionalProperties)) {
delete out.additionalProperties;
}
return out;
}
case "Union":
{
const types = schema.types.map(recur);
if (types.length === 0) {
// anyOf MUST be a non-empty array
return {
not: {}
};
}
if (types.length > 1) {
const compacted = compactEnums(types);
if (compacted) return compacted;
}
return schema.mode === "anyOf" ? {
anyOf: types
} : {
oneOf: types
};
}
}
}
// Collapses [{type:"string",enum:["a"]},{type:"string",enum:["b"]}] into {type:"string",enum:["a","b"]}.
// Returns undefined if members have different types, extra keys (e.g. title), or empty enums.
function compactEnums(types) {
let sharedType;
const values = [];
for (const t of types) {
const keys = Object.keys(t);
if (keys.length !== 2 || t.type === undefined || !Array.isArray(t.enum) || t.enum.length === 0) {
return undefined;
}
if (sharedType === undefined) {
sharedType = t.type;
} else if (t.type !== sharedType) {
return undefined;
}
for (const v of t.enum) {
values.push(v);
}
}
return {
type: sharedType,
enum: values
};
}
function collectJsonSchemaAnnotations(annotations) {
if (annotations === undefined) return undefined;
const out = {};
if (typeof annotations.title === "string") out.title = annotations.title;
if (typeof annotations.description === "string") out.description = annotations.description;else if (generateDescriptions && typeof annotations.expected === "string") out.description = annotations.expected;
if (annotations.default !== undefined) out.default = annotations.default;
if (Array.isArray(annotations.examples)) out.examples = annotations.examples;
if (typeof annotations.readOnly === "boolean") out.readOnly = annotations.readOnly;
if (typeof annotations.writeOnly === "boolean") out.writeOnly = annotations.writeOnly;
if (typeof annotations.format === "string") out.format = annotations.format;
if (typeof annotations.contentEncoding === "string") out.contentEncoding = annotations.contentEncoding;
if (typeof annotations.contentMediaType === "string") out.contentMediaType = annotations.contentMediaType;
if (includeAnnotationKey) {
for (const [key, value] of Object.entries(annotations)) {
if (value === undefined) continue;
if (standardJsonSchemaAnnotationKeys.has(key)) continue;
if (!includeAnnotationKey(key)) continue;
out[key] = value;
}
}
if (Object.keys(out).length > 0) return out;
}
function collectJsonSchemaChecks(checks, type) {
return checks.map(collectJsonSchemaCheck).filter(c => c !== undefined);
function collectJsonSchemaCheck(check) {
switch (check._tag) {
case "Filter":
return filterToJsonSchema(check, type);
case "FilterGroup":
{
const checks = check.checks.map(collectJsonSchemaCheck).filter(c => c !== undefined);
if (checks.length === 0) return undefined;
let out = {
allOf: checks
};
const a = collectJsonSchemaAnnotations(check.annotations);
if (a) {
out = {
...out,
...a
};
}
return out;
}
}
}
}
function filterToJsonSchema(filter, type) {
const meta = filter.meta;
if (!meta) return undefined;
let out = on(meta);
const a = collectJsonSchemaAnnotations(filter.annotations);
if (a) {
out = {
...out,
...a
};
}
return out;
function on(meta) {
switch (meta._tag) {
case "isMinLength":
return type === "array" ? {
minItems: meta.minLength
} : {
minLength: meta.minLength
};
case "isMaxLength":
return type === "array" ? {
maxItems: meta.maxLength
} : {
maxLength: meta.maxLength
};
case "isLengthBetween":
return type === "array" ? {
allOf: [{
minItems: meta.minimum
}, {
maxItems: meta.maximum
}]
} : {
allOf: [{
minLength: meta.minimum
}, {
maxLength: meta.maximum
}]
};
case "isPattern":
case "isGUID":
case "isULID":
case "isBase64":
case "isBase64Url":
case "isStartsWith":
case "isEndsWith":
case "isIncludes":
case "isUppercased":
case "isLowercased":
case "isCapitalized":
case "isUncapitalized":
case "isTrimmed":
case "isStringFinite":
case "isStringBigInt":
case "isStringSymbol":
return {
pattern: meta.regExp.source
};
case "isUUID":
return {
pattern: meta.regExp.source,
format: "uuid"
};
case "isFinite":
case "isInt":
return undefined;
case "isMultipleOf":
return {
multipleOf: meta.divisor
};
case "isGreaterThanOrEqualTo":
return {
minimum: meta.minimum
};
case "isLessThanOrEqualTo":
return {
maximum: meta.maximum
};
case "isGreaterThan":
return {
exclusiveMinimum: meta.exclusiveMinimum
};
case "isLessThan":
return {
exclusiveMaximum: meta.exclusiveMaximum
};
case "isBetween":
{
return {
[meta.exclusiveMinimum ? "exclusiveMinimum" : "minimum"]: meta.minimum,
[meta.exclusiveMaximum ? "exclusiveMaximum" : "maximum"]: meta.maximum
};
}
case "isUnique":
return {
uniqueItems: true
};
case "isMinProperties":
return {
minProperties: meta.minProperties
};
case "isMaxProperties":
return {
maxProperties: meta.maxProperties
};
case "isPropertiesLengthBetween":
return {
minProperties: meta.minimum,
maxProperties: meta.maximum
};
case "isPropertyNames":
return {
propertyNames: recur(meta.propertyNames)
};
case "isDateValid":
return {
format: "date-time"
};
}
}
}
function getParameterPatterns(parameter) {
switch (parameter._tag) {
default:
throw new globalThis.Error(`Unsupported index signature parameter: ${parameter._tag}`);
case "Reference":
return getParameterPatterns(multiDocument.references[parameter.$ref]);
case "String":
return getPatterns(parameter);
case "TemplateLiteral":
return [`^${parameter.parts.map(getPartPattern).join("")}$`];
case "Union":
return parameter.types.flatMap(getParameterPatterns);
}
}
}
function getPatterns(s) {
return recur(s.checks);
function recur(checks) {
return checks.flatMap(c => {
switch (c._tag) {
case "Filter":
{
if ("regExp" in c.meta) {
return [c.meta.regExp.source];
}
return [];
}
case "FilterGroup":
return recur(c.checks);
}
});
}
}
function hasCheck(checks, tag) {
return checks.some(c => {
switch (c._tag) {
case "Filter":
return c.meta._tag === tag;
case "FilterGroup":
return hasCheck(c.checks, tag);
}
});
}
function appendJsonSchema(a, b) {
if (Object.keys(a).length === 0) return b;
const len = Object.keys(b).length;
if (len === 0) return a;
const members = Array.isArray(b.allOf) && len === 1 ? b.allOf : [b];
if (Array.isArray(a.allOf)) {
return {
...a,
allOf: [...a.allOf, ...members]
};
}
if (typeof a.$ref === "string") {
return {
allOf: [a, ...members]
};
}
return {
...a,
allOf: members
};
}
function getPartPattern(part) {
switch (part._tag) {
case "Literal":
return RegEx.escape(globalThis.String(part.literal));
case "String":
return SchemaAST.STRING_PATTERN;
case "Number":
return SchemaAST.FINITE_PATTERN;
case "TemplateLiteral":
return part.parts.map(getPartPattern).join("");
case "Union":
return part.types.map(getPartPattern).join("|");
default:
throw new globalThis.Error("Unsupported part", {
cause: part
});
}
}
//# sourceMappingURL=representation.js.map

Xet Storage Details

Size:
25.8 kB
·
Xet hash:
4443cf02aa24d803ae21a901052e8c40570be288cd6d21d052bcd6ff5bda9089

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.