| 1 | import * as Arr from "../../Array.ts" |
| 2 | import { format, formatPropertyKey } from "../../Formatter.ts" |
| 3 | import type * as Schema from "../../Schema.ts" |
| 4 | import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" |
| 5 | import { errorWithPath } from "../errors.ts" |
| 6 | import * as InternalRecord from "../record.ts" |
| 7 | import * as InternalAnnotations from "./annotations.ts" |
| 8 | |
| 9 | type Path = ReadonlyArray<string | number> |
| 10 | type CheckRepresentationAnnotation = SchemaRepresentation.CheckRepresentationAnnotation< |
| 11 | SchemaRepresentation.Representation |
| 12 | > |
| 13 | |
| 14 | /** @internal */ |
| 15 | export function makeCode(runtime: string, Type: string): SchemaRepresentation.Code { |
| 16 | return { runtime, Type } |
| 17 | } |
| 18 | |
| 19 | function renderEmittableAnnotation(input: unknown): string | undefined { |
| 20 | if ( |
| 21 | input === null || |
| 22 | typeof input === "string" || |
| 23 | typeof input === "boolean" || |
| 24 | typeof input === "number" || |
| 25 | typeof input === "bigint" |
| 26 | ) return format(input) |
| 27 | if (typeof input === "symbol") { |
| 28 | const key = globalThis.Symbol.keyFor(input) |
| 29 | return key === undefined ? undefined : `Symbol.for(${format(key)})` |
| 30 | } |
| 31 | if (typeof input !== "object") return undefined |
| 32 | if (Array.isArray(input)) { |
| 33 | const values: Array<string> = [] |
| 34 | for (const value of input) { |
| 35 | const rendered = renderEmittableAnnotation(value) |
| 36 | if (rendered === undefined) return undefined |
| 37 | values.push(rendered) |
| 38 | } |
| 39 | return `[${values.join(", ")}]` |
| 40 | } |
| 41 | |
| 42 | const entries: Array<string> = [] |
| 43 | for (const [key, value] of Object.entries(input)) { |
| 44 | const rendered = renderEmittableAnnotation(value) |
| 45 | if (rendered === undefined) return undefined |
| 46 | entries.push(`${formatPropertyKey(key)}: ${rendered}`) |
| 47 | } |
| 48 | return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }` |
| 49 | } |
| 50 | |
| 51 | function renderAnnotations( |
| 52 | annotations: Schema.Annotations.Annotations | undefined |
| 53 | ): string | undefined { |
| 54 | if (annotations === undefined) return undefined |
| 55 | const entries: Array<string> = [] |
| 56 | for (const [key, value] of Object.entries(annotations)) { |
| 57 | if (InternalAnnotations.annotationExcludedKeys.has(key)) continue |
| 58 | const rendered = renderEmittableAnnotation(value) |
| 59 | if (rendered !== undefined) entries.push(`${formatPropertyKey(key)}: ${rendered}`) |
| 60 | } |
| 61 | return entries.length === 0 ? undefined : `{ ${entries.join(", ")} }` |
| 62 | } |
| 63 | |
| 64 | /** @internal */ |
| 65 | export function sanitizeJavaScriptIdentifier(input: string): string { |
| 66 | if (input.length === 0) return "_" |
| 67 | const out = input.replace(/[^A-Za-z0-9_$]/gu, "_") |
| 68 | const first = out[0] |
| 69 | return first >= "a" && first <= "z" |
| 70 | ? first.toUpperCase() + out.slice(1) |
| 71 | : first >= "0" && first <= "9" |
| 72 | ? `_${out}` |
| 73 | : out |
| 74 | } |
| 75 | |
| 76 | function isSimpleLiveLiteral( |
| 77 | representation: SchemaRepresentation.Representation |
| 78 | ): representation is SchemaRepresentation.Literal { |
| 79 | return representation._tag === "Literal" && representation.checks.length === 0 && |
| 80 | representation.annotations === undefined |
| 81 | } |
| 82 | |
| 83 | /** @internal */ |
| 84 | export interface TopologicalSort { |
| 85 | readonly nonRecursives: ReadonlyArray<{ |
| 86 | readonly $ref: string |
| 87 | readonly representation: SchemaRepresentation.Representation |
| 88 | }> |
| 89 | readonly recursives: Readonly<Record<string, SchemaRepresentation.Representation>> |
| 90 | } |
| 91 | |
| 92 | /** @internal */ |
| 93 | export function topologicalSort( |
| 94 | references: SchemaRepresentation.References |
| 95 | ): TopologicalSort { |
| 96 | const identifiers = Object.keys(references) |
| 97 | const identifierSet = new Set(identifiers) |
| 98 | |
| 99 | function collectRefs(root: SchemaRepresentation.Representation): ReadonlySet<string> { |
| 100 | const refs = new Set<string>() |
| 101 | const visited = new WeakSet<object>() |
| 102 | const stack: Array<SchemaRepresentation.Representation> = [root] |
| 103 | |
| 104 | function pushRepresentationSchemas(representation: CheckRepresentationAnnotation | undefined): void { |
| 105 | if (representation?.schemas !== undefined) stack.push(...representation.schemas) |
| 106 | } |
| 107 | |
| 108 | function pushChecks( |
| 109 | checks: ReadonlyArray<SchemaRepresentation.Check> |
| 110 | ): void { |
| 111 | for (const check of checks) { |
| 112 | pushRepresentationSchemas(check.representation) |
| 113 | if (check._tag === "FilterGroup") pushChecks(check.checks) |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | while (stack.length > 0) { |
| 118 | const representation = stack.pop()! |
| 119 | if (visited.has(representation)) continue |
| 120 | visited.add(representation) |
| 121 | if (representation._tag === "Reference") { |
| 122 | if (identifierSet.has(representation.$ref)) refs.add(representation.$ref) |
| 123 | continue |
| 124 | } |
| 125 | |
| 126 | pushChecks(representation.checks) |
| 127 | switch (representation._tag) { |
| 128 | case "Declaration": |
| 129 | pushRepresentationSchemas(representation.representation) |
| 130 | stack.push(...representation.typeParameters) |
| 131 | break |
| 132 | case "Suspend": |
| 133 | stack.push(representation.thunk) |
| 134 | break |
| 135 | case "TemplateLiteral": |
| 136 | stack.push(...representation.parts) |
| 137 | break |
| 138 | case "Arrays": |
| 139 | for (const element of representation.elements) stack.push(element.type) |
| 140 | stack.push(...representation.rest) |
| 141 | break |
| 142 | case "Objects": |
| 143 | for (const property of representation.propertySignatures) stack.push(property.type) |
| 144 | for (const signature of representation.indexSignatures) { |
| 145 | stack.push(signature.parameter, signature.type) |
| 146 | } |
| 147 | break |
| 148 | case "Union": |
| 149 | stack.push(...representation.types) |
| 150 | break |
| 151 | } |
| 152 | } |
| 153 | return refs |
| 154 | } |
| 155 | |
| 156 | const dependencies = new Map<string, ReadonlySet<string>>( |
| 157 | identifiers.map((identifier) => [identifier, collectRefs(references[identifier])]) |
| 158 | ) |
| 159 | const recursive = new Set<string>() |
| 160 | const state = new Map<string, 0 | 1 | 2>() |
| 161 | const stack: Array<string> = [] |
| 162 | |
| 163 | function visit(identifier: string): void { |
| 164 | const current = state.get(identifier) ?? 0 |
| 165 | if (current === 1) { |
| 166 | const start = stack.indexOf(identifier) |
| 167 | for (let index = start; index < stack.length; index++) recursive.add(stack[index]) |
| 168 | return |
| 169 | } |
| 170 | if (current === 2) return |
| 171 | state.set(identifier, 1) |
| 172 | stack.push(identifier) |
| 173 | for (const dependency of dependencies.get(identifier)!) visit(dependency) |
| 174 | stack.pop() |
| 175 | state.set(identifier, 2) |
| 176 | } |
| 177 | |
| 178 | for (const identifier of identifiers) visit(identifier) |
| 179 | |
| 180 | const inDegree = new Map<string, number>() |
| 181 | const dependents = new Map<string, Set<string>>() |
| 182 | for (const identifier of identifiers) { |
| 183 | if (!recursive.has(identifier)) { |
| 184 | inDegree.set(identifier, 0) |
| 185 | dependents.set(identifier, new Set()) |
| 186 | } |
| 187 | } |
| 188 | for (const [identifier, internalDependencies] of dependencies) { |
| 189 | if (recursive.has(identifier)) continue |
| 190 | for (const dependency of internalDependencies) { |
| 191 | if (recursive.has(dependency)) continue |
| 192 | inDegree.set(identifier, inDegree.get(identifier)! + 1) |
| 193 | dependents.get(dependency)!.add(identifier) |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | const queue: Array<string> = [] |
| 198 | for (const [identifier, degree] of inDegree) { |
| 199 | if (degree === 0) queue.push(identifier) |
| 200 | } |
| 201 | const nonRecursives: Array<{ |
| 202 | readonly $ref: string |
| 203 | readonly representation: SchemaRepresentation.Representation |
| 204 | }> = [] |
| 205 | for (let index = 0; index < queue.length; index++) { |
| 206 | const $ref = queue[index] |
| 207 | nonRecursives.push({ $ref, representation: references[$ref] }) |
| 208 | for (const dependent of dependents.get($ref)!) { |
| 209 | const degree = inDegree.get(dependent)! - 1 |
| 210 | inDegree.set(dependent, degree) |
| 211 | if (degree === 0) queue.push(dependent) |
| 212 | } |
| 213 | } |
| 214 | const recursives: Record<string, SchemaRepresentation.Representation> = {} |
| 215 | for (const identifier of recursive) InternalRecord.assignProperty(recursives, identifier, references[identifier]) |
| 216 | return { nonRecursives, recursives } |
| 217 | } |
| 218 | |
| 219 | /** @internal */ |
| 220 | export function toCodeDocument( |
| 221 | document: SchemaRepresentation.MultiDocument |
| 222 | ): SchemaRepresentation.CodeDocument { |
| 223 | const artifacts: Array<SchemaRepresentation.Artifact> = [] |
| 224 | const sorted = topologicalSort(document.references) |
| 225 | const sanitizedReferences = new Map<string, string>() |
| 226 | const uniqueIdentifiers = new Set<string>() |
| 227 | let compilingRecursiveDefinition = false |
| 228 | let explicitSuspendDepth = 0 |
| 229 | |
| 230 | for (const { $ref } of sorted.nonRecursives) ensureUniqueIdentifier($ref) |
| 231 | for (const $ref of Object.keys(sorted.recursives)) ensureUniqueIdentifier($ref) |
| 232 | |
| 233 | const nonRecursives = sorted.nonRecursives.map(({ $ref, representation }) => ({ |
| 234 | $ref: ensureUniqueIdentifier($ref), |
| 235 | code: recur(representation, ["references", $ref]) |
| 236 | })) |
| 237 | const recursives: Record<string, SchemaRepresentation.Code> = {} |
| 238 | for (const [$ref, representation] of Object.entries(sorted.recursives)) { |
| 239 | compilingRecursiveDefinition = true |
| 240 | InternalRecord.assignProperty(recursives, ensureUniqueIdentifier($ref), recur(representation, ["references", $ref])) |
| 241 | compilingRecursiveDefinition = false |
| 242 | } |
| 243 | const codes = document.representations.map((representation, index) => |
| 244 | recur(representation, ["representations", index]) |
| 245 | ) |
| 246 | |
| 247 | return { |
| 248 | codes, |
| 249 | references: { nonRecursives, recursives }, |
| 250 | artifacts |
| 251 | } |
| 252 | |
| 253 | function ensureUniqueIdentifier(original: string): string { |
| 254 | const existing = sanitizedReferences.get(original) |
| 255 | if (existing !== undefined) return existing |
| 256 | const candidate = freshIdentifier(original) |
| 257 | sanitizedReferences.set(original, candidate) |
| 258 | return candidate |
| 259 | } |
| 260 | |
| 261 | function freshIdentifier(seed: string): string { |
| 262 | const sanitized = sanitizeJavaScriptIdentifier(seed) |
| 263 | let candidate = sanitized |
| 264 | let suffix = 0 |
| 265 | while (uniqueIdentifiers.has(candidate)) candidate = `${sanitized}${++suffix}` |
| 266 | uniqueIdentifiers.add(candidate) |
| 267 | return candidate |
| 268 | } |
| 269 | |
| 270 | function addImport(importDeclaration: string): void { |
| 271 | if (!artifacts.some((artifact) => artifact._tag === "Import" && artifact.importDeclaration === importDeclaration)) { |
| 272 | artifacts.push({ _tag: "Import", importDeclaration }) |
| 273 | } |
| 274 | } |
| 275 | |
| 276 | function addSymbol(symbol: symbol): string { |
| 277 | const identifier = freshIdentifier("_symbol") |
| 278 | const key = globalThis.Symbol.keyFor(symbol) |
| 279 | const description = symbol.description |
| 280 | artifacts.push({ |
| 281 | _tag: "Symbol", |
| 282 | identifier, |
| 283 | code: makeCode( |
| 284 | key === undefined |
| 285 | ? `Symbol(${description === undefined ? "" : format(description)})` |
| 286 | : `Symbol.for(${format(key)})`, |
| 287 | `typeof ${identifier}` |
| 288 | ) |
| 289 | }) |
| 290 | return identifier |
| 291 | } |
| 292 | |
| 293 | function annotationSchemas( |
| 294 | representation: CheckRepresentationAnnotation | undefined, |
| 295 | path: Path |
| 296 | ): ReadonlyArray<SchemaRepresentation.Code> { |
| 297 | return representation?.schemas?.map((schema, index) => recur(schema, [...path, "schemas", index])) ?? [] |
| 298 | } |
| 299 | |
| 300 | function runtimeAnnotate( |
| 301 | annotations: Schema.Annotations.Annotations | undefined, |
| 302 | method: "annotate" | "annotateKey" = "annotate" |
| 303 | ): string { |
| 304 | const rendered = renderAnnotations(annotations) |
| 305 | return rendered === undefined ? "" : `.${method}(${rendered})` |
| 306 | } |
| 307 | |
| 308 | function unionOptionsRuntime(options: NonNullable<SchemaRepresentation.Union["options"]>): string { |
| 309 | if (!Object.hasOwn(options, "mode")) return "{}" |
| 310 | return `{ mode: ${options.mode === undefined ? "undefined" : format(options.mode)} }` |
| 311 | } |
| 312 | |
| 313 | function compileCheck( |
| 314 | check: SchemaRepresentation.Check, |
| 315 | path: Path |
| 316 | ): string { |
| 317 | const callback = check.annotations?.toCode |
| 318 | let runtime: string |
| 319 | if (callback !== undefined) { |
| 320 | const schemas = annotationSchemas(check.representation, [...path, "representation"]) |
| 321 | const output = (callback as SchemaRepresentation.Generation.Check)({ schemas }) |
| 322 | for (const importDeclaration of output.importDeclarations ?? []) addImport(importDeclaration) |
| 323 | runtime = output.runtime |
| 324 | } else if (check._tag === "Filter") { |
| 325 | throw errorWithPath("Missing toCode callback", [...path, "annotations", "toCode"]) |
| 326 | } else { |
| 327 | runtime = `Schema.makeFilterGroup([${ |
| 328 | check.checks.map((child, index) => compileCheck(child, [...path, "checks", index])).join(", ") |
| 329 | }])` |
| 330 | } |
| 331 | runtime += runtimeAnnotate(check.annotations) |
| 332 | if (check._tag === "Filter" && check.aborted) runtime += ".abort()" |
| 333 | return runtime |
| 334 | } |
| 335 | |
| 336 | function applyNode( |
| 337 | base: SchemaRepresentation.Code, |
| 338 | representation: Exclude<SchemaRepresentation.Representation, SchemaRepresentation.Reference>, |
| 339 | path: Path |
| 340 | ): SchemaRepresentation.Code { |
| 341 | let runtime = base.runtime + runtimeAnnotate(representation.annotations) |
| 342 | for (let index = 0; index < representation.checks.length; index++) { |
| 343 | const check = representation.checks[index] |
| 344 | runtime += `.check(${compileCheck(check, [...path, "checks", index])})` |
| 345 | } |
| 346 | return makeCode(runtime, base.Type) |
| 347 | } |
| 348 | |
| 349 | function recur( |
| 350 | representation: SchemaRepresentation.Representation, |
| 351 | path: Path |
| 352 | ): SchemaRepresentation.Code { |
| 353 | if (representation._tag === "Reference") { |
| 354 | if (!Object.hasOwn(document.references, representation.$ref)) { |
| 355 | throw errorWithPath(`Invalid reference ${representation.$ref}`, [...path, "$ref"]) |
| 356 | } |
| 357 | const identifier = ensureUniqueIdentifier(representation.$ref) |
| 358 | if ( |
| 359 | compilingRecursiveDefinition && explicitSuspendDepth === 0 && |
| 360 | Object.hasOwn(sorted.recursives, representation.$ref) |
| 361 | ) { |
| 362 | return makeCode(`Schema.suspend((): Schema.Codec<${identifier}> => ${identifier})`, identifier) |
| 363 | } |
| 364 | return makeCode(identifier, identifier) |
| 365 | } |
| 366 | return applyNode(on(representation, path), representation, path) |
| 367 | } |
| 368 | |
| 369 | function on( |
| 370 | representation: Exclude<SchemaRepresentation.Representation, SchemaRepresentation.Reference>, |
| 371 | path: Path |
| 372 | ): SchemaRepresentation.Code { |
| 373 | switch (representation._tag) { |
| 374 | case "Declaration": { |
| 375 | const callback = representation.annotations?.toCode |
| 376 | if (callback === undefined) { |
| 377 | throw errorWithPath("Missing toCode callback", [...path, "annotations", "toCode"]) |
| 378 | } |
| 379 | const typeParameters = representation.typeParameters.map((typeParameter, index) => |
| 380 | recur(typeParameter, [...path, "typeParameters", index]) |
| 381 | ) |
| 382 | const output = (callback as SchemaRepresentation.Generation.Declaration)({ typeParameters }) |
| 383 | for (const importDeclaration of output.importDeclarations ?? []) addImport(importDeclaration) |
| 384 | return makeCode(output.runtime, output.Type) |
| 385 | } |
| 386 | case "Suspend": { |
| 387 | explicitSuspendDepth++ |
| 388 | const thunk = recur(representation.thunk, [...path, "thunk"]) |
| 389 | explicitSuspendDepth-- |
| 390 | return makeCode(`Schema.suspend((): Schema.Codec<${thunk.Type}> => ${thunk.runtime})`, thunk.Type) |
| 391 | } |
| 392 | case "Null": |
| 393 | return makeCode("Schema.Null", "null") |
| 394 | case "Undefined": |
| 395 | return makeCode("Schema.Undefined", "undefined") |
| 396 | case "Void": |
| 397 | return makeCode("Schema.Void", "void") |
| 398 | case "Never": |
| 399 | return makeCode("Schema.Never", "never") |
| 400 | case "Unknown": |
| 401 | return makeCode("Schema.Unknown", "unknown") |
| 402 | case "Any": |
| 403 | return makeCode("Schema.Any", "any") |
| 404 | case "String": |
| 405 | return makeCode("Schema.String", "string") |
| 406 | case "Number": |
| 407 | return makeCode("Schema.Number", "number") |
| 408 | case "Boolean": |
| 409 | return makeCode("Schema.Boolean", "boolean") |
| 410 | case "BigInt": |
| 411 | return makeCode("Schema.BigInt", "bigint") |
| 412 | case "Symbol": |
| 413 | return makeCode("Schema.Symbol", "symbol") |
| 414 | case "Literal": { |
| 415 | const literal = format(representation.literal) |
| 416 | return makeCode(`Schema.Literal(${literal})`, literal) |
| 417 | } |
| 418 | case "UniqueSymbol": { |
| 419 | const identifier = addSymbol(representation.symbol) |
| 420 | return makeCode(`Schema.UniqueSymbol(${identifier})`, `typeof ${identifier}`) |
| 421 | } |
| 422 | case "ObjectKeyword": |
| 423 | return makeCode("Schema.ObjectKeyword", "object") |
| 424 | case "Enum": { |
| 425 | const identifier = freshIdentifier("_Enum") |
| 426 | artifacts.push({ |
| 427 | _tag: "Enum", |
| 428 | identifier, |
| 429 | code: makeCode( |
| 430 | `enum ${identifier} { ${ |
| 431 | representation.enums.map(([name, value]) => `${format(name)} = ${format(value)}`).join( |
| 432 | ", " |
| 433 | ) |
| 434 | } }`, |
| 435 | `typeof ${identifier}` |
| 436 | ) |
| 437 | }) |
| 438 | return makeCode(`Schema.Enum(${identifier})`, identifier) |
| 439 | } |
| 440 | case "TemplateLiteral": { |
| 441 | const parts = representation.parts.map((part, index) => recur(part, [...path, "parts", index])) |
| 442 | const Type = `\`${parts.map((part) => `\${${part.Type}}`).join("")}\`` |
| 443 | return makeCode(`Schema.TemplateLiteral([${parts.map((part) => part.runtime).join(", ")}])`, Type) |
| 444 | } |
| 445 | case "Arrays": { |
| 446 | const elements = representation.elements.map((element, index) => { |
| 447 | const type = recur(element.type, [...path, "elements", index, "type"]) |
| 448 | return makeCode( |
| 449 | `${element.isOptional ? "Schema.optionalKey(" : ""}${type.runtime}${element.isOptional ? ")" : ""}${ |
| 450 | runtimeAnnotate(element.annotations, "annotateKey") |
| 451 | }`, |
| 452 | element.isOptional ? `(${type.Type})?` : type.Type |
| 453 | ) |
| 454 | }) |
| 455 | const rest = representation.rest.map((item, index) => recur(item, [...path, "rest", index])) |
| 456 | let code: SchemaRepresentation.Code |
| 457 | if (Arr.isArrayNonEmpty(rest)) { |
| 458 | const item = rest[0] |
| 459 | if (elements.length === 0 && rest.length === 1) { |
| 460 | code = makeCode(`Schema.Array(${item.runtime})`, `ReadonlyArray<${item.Type}>`) |
| 461 | } else { |
| 462 | const post = rest.slice(1) |
| 463 | code = makeCode( |
| 464 | `Schema.TupleWithRest(Schema.Tuple([${elements.map((element) => element.runtime).join(", ")}]), [${ |
| 465 | rest.map((item) => item.runtime).join(", ") |
| 466 | }])`, |
| 467 | `readonly [${elements.map((element) => element.Type).join(", ")}, ...Array<${item.Type}>${ |
| 468 | post.length > 0 ? `, ${post.map((item) => item.Type).join(", ")}` : "" |
| 469 | }]` |
| 470 | ) |
| 471 | } |
| 472 | } else { |
| 473 | code = makeCode( |
| 474 | `Schema.Tuple([${elements.map((element) => element.runtime).join(", ")}])`, |
| 475 | `readonly [${elements.map((element) => element.Type).join(", ")}]` |
| 476 | ) |
| 477 | } |
| 478 | return code |
| 479 | } |
| 480 | case "Objects": { |
| 481 | const properties = representation.propertySignatures.map((property, index) => { |
| 482 | const isSymbol = typeof property.name === "symbol" |
| 483 | const name = isSymbol |
| 484 | ? `[${addSymbol(property.name)}]` |
| 485 | : formatPropertyKey(property.name) |
| 486 | const type = recur(property.type, [...path, "propertySignatures", index, "type"]) |
| 487 | let runtime = type.runtime |
| 488 | if (property.isMutable) runtime = `Schema.mutableKey(${runtime})` |
| 489 | if (property.isOptional) runtime = `Schema.optionalKey(${runtime})` |
| 490 | runtime += runtimeAnnotate(property.annotations, "annotateKey") |
| 491 | const runtimeName = property.name === "__proto__" ? `[${name}]` : name |
| 492 | const typeName = `${property.isMutable ? "" : "readonly "}${name}${property.isOptional ? "?" : ""}` |
| 493 | return { |
| 494 | code: makeCode(`${runtimeName}: ${runtime}`, `${typeName}: ${type.Type}`) |
| 495 | } |
| 496 | }) |
| 497 | const indexSignatures = representation.indexSignatures.map((signature, index) => ({ |
| 498 | parameter: recur(signature.parameter, [...path, "indexSignatures", index, "parameter"]), |
| 499 | type: recur(signature.type, [...path, "indexSignatures", index, "type"]) |
| 500 | })) |
| 501 | const propertyRuntimes = properties.map((property) => property.code.runtime).join(", ") |
| 502 | const propertyTypes = properties.map((property) => property.code.Type).join(", ") |
| 503 | let code: SchemaRepresentation.Code |
| 504 | if (indexSignatures.length === 0) { |
| 505 | code = makeCode( |
| 506 | `Schema.Struct({ ${propertyRuntimes} })`, |
| 507 | `{ ${propertyTypes} }` |
| 508 | ) |
| 509 | } else if (properties.length === 0 && indexSignatures.length === 1) { |
| 510 | const signature = indexSignatures[0] |
| 511 | code = makeCode( |
| 512 | `Schema.Record(${signature.parameter.runtime}, ${signature.type.runtime})`, |
| 513 | `{ readonly [x: ${signature.parameter.Type}]: ${signature.type.Type} }` |
| 514 | ) |
| 515 | } else { |
| 516 | const indexRuntimes = indexSignatures.map((signature) => |
| 517 | `Schema.Record(${signature.parameter.runtime}, ${signature.type.runtime})` |
| 518 | ).join(", ") |
| 519 | const indexTypes = indexSignatures.map((signature) => |
| 520 | `readonly [x: ${signature.parameter.Type}]: ${signature.type.Type}` |
| 521 | ) |
| 522 | code = properties.length === 0 |
| 523 | ? makeCode( |
| 524 | `Schema.StructWithRest(Schema.Struct({ ${propertyRuntimes} }), [${indexRuntimes}])`, |
| 525 | `{ ${indexTypes.join(", ")} }` |
| 526 | ) |
| 527 | : makeCode( |
| 528 | `Schema.StructWithRest(Schema.Struct({ ${propertyRuntimes} }), [${indexRuntimes}])`, |
| 529 | [`{ ${propertyTypes} }`, ...indexTypes.map((indexType) => `{ ${indexType} }`)].join(" & ") |
| 530 | ) |
| 531 | } |
| 532 | return code |
| 533 | } |
| 534 | case "Union": { |
| 535 | if (representation.options === undefined && representation.types.length === 0) { |
| 536 | return makeCode("Schema.Never", "never") |
| 537 | } |
| 538 | if (representation.options === undefined && representation.types.every(isSimpleLiveLiteral)) { |
| 539 | const literals = representation.types.map((literal) => format(literal.literal)) |
| 540 | return literals.length === 1 |
| 541 | ? makeCode(`Schema.Literal(${literals[0]})`, literals[0]) |
| 542 | : makeCode(`Schema.Literals([${literals.join(", ")}])`, literals.join(" | ")) |
| 543 | } |
| 544 | const types = representation.types.map((type, index) => recur(type, [...path, "types", index])) |
| 545 | const options = representation.options === undefined |
| 546 | ? "" |
| 547 | : `, ${unionOptionsRuntime(representation.options)}` |
| 548 | return makeCode( |
| 549 | `Schema.Union([${types.map((type) => type.runtime).join(", ")}]${options})`, |
| 550 | types.length === 0 ? "never" : types.map((type) => type.Type).join(" | ") |
| 551 | ) |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | } |