Merge branch 'codex/canonical-tool-output' into codex/code-mode-typed-results
This commit is contained in:
@@ -1111,7 +1111,7 @@ export class ToolRegistry extends Service {
|
||||
if (deferredContexts === undefined) throw new Error('tool registry scheduler invariant violated: unprepared execution')
|
||||
const resultWithDeferredContexts: ToolExecutionResult = deferredContexts.length === 0
|
||||
? normalized
|
||||
: this.markCanonical({
|
||||
: this.markCanonical(exec, {
|
||||
...normalized,
|
||||
additionalContexts: [
|
||||
...deferredContexts,
|
||||
@@ -1262,7 +1262,7 @@ export class ToolRegistry extends Service {
|
||||
const decisionContexts = decision.additionalContexts ?? []
|
||||
if (decision.kind === 'block') {
|
||||
const message = failureMessageFromContent(decision.feedback)
|
||||
return this.markCanonical({
|
||||
return this.markCanonical(exec, {
|
||||
content: decision.feedback,
|
||||
isError: true,
|
||||
error: { message },
|
||||
@@ -1283,24 +1283,24 @@ export class ToolRegistry extends Service {
|
||||
const tool = this.get(exec.name, exec.agent)
|
||||
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
||||
const replaced = this.createSuccessResult(exec, tool, decision.value)
|
||||
return this.markCanonical({
|
||||
return this.markCanonical(exec, {
|
||||
...replaced,
|
||||
...additionalContexts.length > 0 ? { additionalContexts } : {},
|
||||
})
|
||||
}
|
||||
return this.markCanonical({
|
||||
return this.markCanonical(exec, {
|
||||
...result,
|
||||
...decision.content !== undefined ? { content: decision.content } : {},
|
||||
...additionalContexts.length > 0 ? { additionalContexts } : {},
|
||||
})
|
||||
}
|
||||
|
||||
/** Results created by the registry already own a validated, frozen canonical value. */
|
||||
private readonly canonicalResults = new WeakSet<object>()
|
||||
/** Registry-normalized results and the exact dispatch that validated each value. */
|
||||
private readonly canonicalResults = new WeakMap<object, ToolExecutionToken>()
|
||||
|
||||
/** Mark a registry-normalized result without freezing presentation fields prematurely. */
|
||||
private markCanonical<T extends ToolExecutionResult>(result: T): T {
|
||||
this.canonicalResults.add(result)
|
||||
/** Mark one registry-normalized result as canonical only for its owning dispatch. */
|
||||
private markCanonical<T extends ToolExecutionResult>(exec: ToolExecution, result: T): T {
|
||||
this.canonicalResults.set(result, exec.token)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1327,7 +1327,7 @@ export class ToolRegistry extends Service {
|
||||
}
|
||||
meta = snapshotProjection(tool.name, 'presentationMeta', projected)
|
||||
}
|
||||
return this.markCanonical(this.materializeFinalResult({
|
||||
return this.markCanonical(exec, this.materializeFinalResult({
|
||||
isError: false,
|
||||
value,
|
||||
content,
|
||||
@@ -1337,9 +1337,9 @@ export class ToolRegistry extends Service {
|
||||
|
||||
/** Normalize an around-dispatch wrapper's authored result through the owning output contract. */
|
||||
private normalizeDispatchResult(exec: ToolExecution, result: ToolExecutionResult): ToolExecutionResult {
|
||||
if (this.canonicalResults.has(result)) return result
|
||||
if (this.canonicalResults.get(result) === exec.token) return result
|
||||
if (result.isError) {
|
||||
return this.markCanonical({
|
||||
return this.markCanonical(exec, {
|
||||
isError: true,
|
||||
error: result.error,
|
||||
content: result.content,
|
||||
@@ -1350,7 +1350,7 @@ export class ToolRegistry extends Service {
|
||||
const tool = this.get(exec.name, exec.agent)
|
||||
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
||||
const normalized = this.createSuccessResult(exec, tool, result.value)
|
||||
return this.markCanonical({
|
||||
return this.markCanonical(exec, {
|
||||
...normalized,
|
||||
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
|
||||
})
|
||||
|
||||
@@ -86,6 +86,26 @@ const CONSTRAINT_KEYWORDS = new Set([
|
||||
const ANNOTATION_KEYWORDS = new Set(['description', 'title', 'default', 'examples'])
|
||||
const SCHEMA_TYPES: readonly JsonSchemaType[] = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']
|
||||
|
||||
/* jscpd:ignore-start -- this realm boundary mirrors the session-owned lossless-JSON intrinsic test */
|
||||
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
|
||||
function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): boolean {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor')
|
||||
const constructor: unknown = descriptor?.value
|
||||
if (typeof constructor !== 'function') return false
|
||||
try {
|
||||
return constructor.name === name
|
||||
&& constructor.prototype === prototype
|
||||
&& Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
|
||||
function isIntrinsicObjectPrototype(value: object): boolean {
|
||||
return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for a realm-agnostic plain JSON record without accepting arrays or
|
||||
* exotic objects.
|
||||
@@ -94,8 +114,61 @@ const SCHEMA_TYPES: readonly JsonSchemaType[] = ['object', 'array', 'string', 'n
|
||||
*/
|
||||
export function isPlainJsonRecord(value: unknown): value is Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
||||
const proto: unknown = Object.getPrototypeOf(value)
|
||||
return proto === null || Object.getPrototypeOf(proto) === null
|
||||
try {
|
||||
const prototype: unknown = Object.getPrototypeOf(value)
|
||||
return prototype === null
|
||||
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether an array uses one realm's intrinsic `Array.prototype`. */
|
||||
function hasPlainArrayPrototype(value: unknown[]): boolean {
|
||||
const prototype: unknown = Object.getPrototypeOf(value)
|
||||
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
|
||||
const objectPrototype: unknown = Object.getPrototypeOf(prototype)
|
||||
return typeof objectPrototype === 'object'
|
||||
&& objectPrototype !== null
|
||||
&& isIntrinsicObjectPrototype(objectPrototype)
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
/** Return whether a record contains only own enumerable string keys. */
|
||||
function hasOnlyEnumerableStringKeys(value: object): boolean {
|
||||
try {
|
||||
return Reflect.ownKeys(value)
|
||||
.every(key => typeof key === 'string' && Object.prototype.propertyIsEnumerable.call(value, key))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for an ordinary schema record whose keys survive JSON projection.
|
||||
* @param value - candidate record from any JavaScript realm.
|
||||
* @returns Whether the record has an intrinsic prototype and only own enumerable string keys.
|
||||
*/
|
||||
export function isJsonSchemaRecord(value: unknown): value is Record<string, unknown> {
|
||||
return isPlainJsonRecord(value) && hasOnlyEnumerableStringKeys(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for a dense ordinary array with no JSON-invisible decorations.
|
||||
* @param value - candidate array from any JavaScript realm.
|
||||
* @returns Whether the array is intrinsic, dense, and undecorated.
|
||||
*/
|
||||
export function isPlainJsonArray(value: unknown): value is unknown[] {
|
||||
if (!Array.isArray(value)) return false
|
||||
try {
|
||||
if (!hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1) return false
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
if (!Object.hasOwn(value, index)) return false
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Lossless finite JSON number, excluding negative zero. */
|
||||
@@ -133,12 +206,13 @@ function checkObjectSchemaTail(
|
||||
properties: unknown,
|
||||
violations: string[],
|
||||
): void {
|
||||
const required = node.required
|
||||
if (Object.hasOwn(node, 'required')) {
|
||||
if (!Array.isArray(required) || required.some(entry => typeof entry !== 'string')) {
|
||||
const hasRequired = Object.hasOwn(node, 'required')
|
||||
const required = hasRequired ? node.required : undefined
|
||||
if (hasRequired) {
|
||||
if (!isPlainJsonArray(required) || required.some(entry => typeof entry !== 'string')) {
|
||||
violations.push(`${path}.required must be an array of strings`)
|
||||
} else {
|
||||
const declared = isPlainJsonRecord(properties) ? properties : {}
|
||||
const declared = isJsonSchemaRecord(properties) ? properties : {}
|
||||
for (const key of required as string[]) {
|
||||
if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`)
|
||||
}
|
||||
@@ -169,7 +243,7 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
|
||||
}
|
||||
|
||||
const { node, path } = task
|
||||
if (!isPlainJsonRecord(node)) {
|
||||
if (!isJsonSchemaRecord(node)) {
|
||||
violations.push(`${path} must be a schema object`)
|
||||
continue
|
||||
}
|
||||
@@ -192,10 +266,10 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
|
||||
}
|
||||
violations.push(`${path}.${key} is not a supported keyword (subset: type/oneOf/properties/required/additionalProperties/items/enum/const + annotations)`)
|
||||
}
|
||||
if (node.description !== undefined && typeof node.description !== 'string') {
|
||||
if (Object.hasOwn(node, 'description') && typeof node.description !== 'string') {
|
||||
violations.push(`${path}.description must be a string`)
|
||||
}
|
||||
if (node.title !== undefined && typeof node.title !== 'string') {
|
||||
if (Object.hasOwn(node, 'title') && typeof node.title !== 'string') {
|
||||
violations.push(`${path}.title must be a string`)
|
||||
}
|
||||
|
||||
@@ -215,7 +289,7 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
|
||||
if (hasOneOf) {
|
||||
const oneOf = node.oneOf
|
||||
tasks.push({ kind: 'one-of-tail', node, path })
|
||||
if (!Array.isArray(oneOf) || oneOf.length < 2) {
|
||||
if (!isPlainJsonArray(oneOf) || oneOf.length < 2) {
|
||||
violations.push(`${path}.oneOf must be an array of at least two schemas`)
|
||||
} else {
|
||||
for (let index = oneOf.length - 1; index >= 0; index--) {
|
||||
@@ -249,10 +323,10 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
|
||||
|
||||
switch (schemaType) {
|
||||
case 'object': {
|
||||
const properties = node.properties
|
||||
const properties = Object.hasOwn(node, 'properties') ? node.properties : undefined
|
||||
tasks.push({ kind: 'object-tail', node, path, properties })
|
||||
if (Object.hasOwn(node, 'properties')) {
|
||||
if (!isPlainJsonRecord(properties)) {
|
||||
if (!isJsonSchemaRecord(properties)) {
|
||||
violations.push(`${path}.properties must be an object of schemas`)
|
||||
} else {
|
||||
const entries = Object.entries(properties)
|
||||
@@ -275,18 +349,21 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
|
||||
case 'integer':
|
||||
case 'boolean':
|
||||
case 'null': {
|
||||
const allowed = node.enum
|
||||
const enumValid = Array.isArray(allowed)
|
||||
const hasEnum = Object.hasOwn(node, 'enum')
|
||||
const allowed = hasEnum ? node.enum : undefined
|
||||
const enumValid = isPlainJsonArray(allowed)
|
||||
&& allowed.length > 0
|
||||
&& allowed.every(entry => scalarMatches(schemaType, entry))
|
||||
if (Object.hasOwn(node, 'enum') && !enumValid) {
|
||||
if (hasEnum && !enumValid) {
|
||||
violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`)
|
||||
}
|
||||
const constValid = scalarMatches(schemaType, node.const)
|
||||
if (Object.hasOwn(node, 'const')) {
|
||||
const hasConst = Object.hasOwn(node, 'const')
|
||||
const declaredConst = hasConst ? node.const : undefined
|
||||
const constValid = scalarMatches(schemaType, declaredConst)
|
||||
if (hasConst) {
|
||||
if (!constValid) {
|
||||
violations.push(`${path}.const must be a ${schemaType} value`)
|
||||
} else if (enumValid && !allowed.includes(node.const as JsonSchemaScalar)) {
|
||||
} else if (enumValid && !allowed.includes(declaredConst)) {
|
||||
violations.push(`${path}.const must be one of ${path}.enum when both are declared`)
|
||||
}
|
||||
}
|
||||
@@ -320,7 +397,8 @@ export function assertSupportedJsonSchema(schema: unknown): asserts schema is Js
|
||||
export function assertObjectJsonSchema(schema: unknown): asserts schema is ObjectJsonSchema {
|
||||
const violations: string[] = []
|
||||
checkSchemaNode(schema, 'schema', violations, new Set())
|
||||
if (violations.length === 0 && (schema as JsonSchemaNode).type !== 'object') {
|
||||
if (violations.length === 0
|
||||
&& (!isJsonSchemaRecord(schema) || !Object.hasOwn(schema, 'type') || schema.type !== 'object')) {
|
||||
violations.push('schema.type must be "object" (structured output is object-rooted)')
|
||||
}
|
||||
if (violations.length > 0) throw new JsonSchemaError(violations)
|
||||
@@ -395,8 +473,9 @@ function valueFrame(node: JsonSchemaNode, value: unknown, path: string): ValueFr
|
||||
|
||||
/** Validate one scalar node after its primitive type check. */
|
||||
function checkScalarValue(node: JsonSchemaNode, value: unknown, path: string): string[] {
|
||||
if (node.enum !== undefined && !node.enum.includes(value as JsonSchemaScalar)) {
|
||||
return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(node.enum)}`]
|
||||
const allowed = Object.hasOwn(node, 'enum') ? node.enum : undefined
|
||||
if (allowed !== undefined && !allowed.includes(value as JsonSchemaScalar)) {
|
||||
return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(allowed)}`]
|
||||
}
|
||||
if (Object.hasOwn(node, 'const') && value !== node.const) {
|
||||
return [`"${diagnosticPath(path)}" must be ${JSON.stringify(node.const)}`]
|
||||
@@ -455,9 +534,9 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
|
||||
continue
|
||||
}
|
||||
|
||||
const nodeType = frame.node.type
|
||||
const nodeType = Object.hasOwn(frame.node, 'type') ? frame.node.type : undefined
|
||||
frame.catches = !(nodeType !== undefined && !(SCHEMA_TYPES as readonly unknown[]).includes(nodeType))
|
||||
const oneOf = frame.node.oneOf
|
||||
const oneOf = Object.hasOwn(frame.node, 'oneOf') ? frame.node.oneOf : undefined
|
||||
if (oneOf !== undefined) {
|
||||
frame.kind = 'oneOf'
|
||||
frame.children = Array.from(oneOf, branch => ({ node: branch, value: frame.value, path: frame.path }))
|
||||
@@ -477,9 +556,10 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
|
||||
finish([`"${diagnosticPath(frame.path)}" must be an object`])
|
||||
break
|
||||
}
|
||||
const properties = frame.node.properties ?? {}
|
||||
const properties = Object.hasOwn(frame.node, 'properties') ? frame.node.properties ?? {} : {}
|
||||
const violations: string[] = []
|
||||
for (const key of frame.node.required ?? []) {
|
||||
const required = Object.hasOwn(frame.node, 'required') ? frame.node.required ?? [] : []
|
||||
for (const key of required) {
|
||||
if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined) {
|
||||
violations.push(`missing required property "${propertyPath(frame.path, key)}"`)
|
||||
}
|
||||
@@ -490,7 +570,7 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
|
||||
children.push({ node: child, value: frame.value[key], path: propertyPath(frame.path, key) })
|
||||
}
|
||||
const tailViolations: string[] = []
|
||||
if (frame.node.additionalProperties === false) {
|
||||
if (Object.hasOwn(frame.node, 'additionalProperties') && frame.node.additionalProperties === false) {
|
||||
for (const key of Object.keys(frame.value)) {
|
||||
if (!Object.hasOwn(properties, key)) {
|
||||
tailViolations.push(`"${propertyPath(frame.path, key)}" is not a declared property (additionalProperties: false)`)
|
||||
@@ -510,7 +590,7 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
|
||||
finish([`"${diagnosticPath(frame.path)}" must be an array`])
|
||||
break
|
||||
}
|
||||
const items = frame.node.items
|
||||
const items = Object.hasOwn(frame.node, 'items') ? frame.node.items : undefined
|
||||
const children = items === undefined
|
||||
? []
|
||||
: frame.value.flatMap((entry, index): ValueChild[] => [{ node: items, value: entry, path: `${frame.path}[${index}]` }])
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import type { ToolDefinition, ToolRunContext, ToolResult } from './index.ts'
|
||||
import { assertSupportedJsonSchema, isPlainJsonRecord, JsonSchemaError, validateJsonSchemaValue } from './json-schema.ts'
|
||||
import { assertSupportedJsonSchema, isJsonSchemaRecord, isPlainJsonArray, JsonSchemaError, validateJsonSchemaValue } from './json-schema.ts'
|
||||
import type { JsonSchemaNode, JsonSchemaScalar, ObjectJsonSchema } from './json-schema.ts'
|
||||
import type { ToolCallView, ToolResultView } from './presentation.ts'
|
||||
|
||||
@@ -100,7 +100,10 @@ export type ParameterPropertySpec = ValueSchemaSpec & { required?: true }
|
||||
* Tool parameter schema. The map itself is an implicit open object root;
|
||||
* requiredness remains a per-property `required: true` annotation.
|
||||
*/
|
||||
export type ParameterSchemaSpec = Record<string, ParameterPropertySpec>
|
||||
export type ParameterSchemaSpec = {
|
||||
[key: string]: ParameterPropertySpec
|
||||
[key: symbol]: never
|
||||
}
|
||||
|
||||
/** Raw JSON Schema projection of the implicit parameter object. */
|
||||
export interface ParameterJsonSchema extends ObjectJsonSchema {
|
||||
@@ -110,30 +113,29 @@ export interface ParameterJsonSchema extends ObjectJsonSchema {
|
||||
/** Flatten an intersection into one object type for readable hovers. */
|
||||
type Simplify<T> = { [K in keyof T]: T[K] } & {}
|
||||
|
||||
/** Keys of a property map marked `required: true`. */
|
||||
type RequiredKeys<S extends ParameterSchemaSpec> = {
|
||||
[K in keyof S]: S[K] extends { required: true } ? K : never
|
||||
}[keyof S]
|
||||
/** String keys of one property map; runtime compilation rejects symbol keys. */
|
||||
type StringKeyOf<S> = Extract<keyof S, string>
|
||||
|
||||
/** Advance the bounded inference walk through one nested schema node. */
|
||||
type NextDepth<D extends readonly unknown[]> = readonly [...D, unknown]
|
||||
/** Keys of a property map marked `required: true`. */
|
||||
type RequiredKeys<S> = {
|
||||
[K in StringKeyOf<S>]: S[K] extends { required: true } ? K : never
|
||||
}[StringKeyOf<S>]
|
||||
|
||||
/** Infer the declared value of one parameter property without key optionality. */
|
||||
type InferProperty<P extends ParameterPropertySpec, D extends readonly unknown[]> =
|
||||
P extends ValueSchemaSpec ? InferValue<P, D> : never
|
||||
type InferProperty<P, Depth extends unknown[]> = InferValueAt<P, Depth>
|
||||
|
||||
/** Infer an implicit property map into required and optional object keys. */
|
||||
type InferProperties<S extends ParameterSchemaSpec, D extends readonly unknown[]> = Simplify<
|
||||
& { [K in RequiredKeys<S>]: InferProperty<S[K], D> }
|
||||
& { [K in Exclude<keyof S, RequiredKeys<S>>]?: InferProperty<S[K], D> }
|
||||
type InferProperties<S, Depth extends unknown[]> = Simplify<
|
||||
& { [K in RequiredKeys<S>]: InferProperty<S[K], Depth> }
|
||||
& { [K in Exclude<StringKeyOf<S>, RequiredKeys<S>>]?: InferProperty<S[K], Depth> }
|
||||
>
|
||||
|
||||
/** Infer an explicit object node, including its declared openness. */
|
||||
type InferObject<S extends ObjectValueSchemaSpec, D extends readonly unknown[]> =
|
||||
S extends { properties: infer P extends ParameterSchemaSpec }
|
||||
type InferObject<S extends { additionalProperties: boolean }, Depth extends unknown[]> =
|
||||
S extends { properties: infer P }
|
||||
? S['additionalProperties'] extends true
|
||||
? InferProperties<P, D> & Record<string, JsonValue>
|
||||
: InferProperties<P, D>
|
||||
? InferProperties<P, Depth> & Record<string, JsonValue>
|
||||
: InferProperties<P, Depth>
|
||||
: S['additionalProperties'] extends true
|
||||
? Record<string, JsonValue>
|
||||
: Record<string, never>
|
||||
@@ -144,25 +146,33 @@ type InferScalar<S, Fallback> =
|
||||
S extends { enum: readonly (infer E)[] } ? E :
|
||||
Fallback
|
||||
|
||||
/** Add one schema-container level to bounded compile-time inference. */
|
||||
type NextInferenceDepth<Depth extends unknown[]> = [unknown, ...Depth]
|
||||
|
||||
/** Infer one node without recursively checking it against the full author union. */
|
||||
type InferValueAt<S, Depth extends unknown[]> =
|
||||
Depth['length'] extends 16 ? JsonValue :
|
||||
S extends { type: 'string' } ? InferScalar<S, string> :
|
||||
S extends { type: 'number' | 'integer' } ? InferScalar<S, number> :
|
||||
S extends { type: 'boolean' } ? InferScalar<S, boolean> :
|
||||
S extends { type: 'null' } ? null :
|
||||
S extends { type: 'array' }
|
||||
? S extends { items: infer I } ? InferValueAt<I, NextInferenceDepth<Depth>>[] : JsonValue[]
|
||||
: S extends { type: 'object'; additionalProperties: boolean }
|
||||
? InferObject<S, NextInferenceDepth<Depth>>
|
||||
: S extends { type: 'json' } ? JsonValue :
|
||||
S extends { oneOf: readonly unknown[] }
|
||||
? InferValueAt<S['oneOf'][number], NextInferenceDepth<Depth>>
|
||||
: never
|
||||
|
||||
/**
|
||||
* Infer the TypeScript value accepted by an author-facing value schema.
|
||||
* Output schemas may therefore infer object, array, scalar, or null roots.
|
||||
* Infer the TypeScript value accepted by an author-facing value schema. Exact
|
||||
* inference is bounded to 16 container levels, then falls back to `JsonValue`.
|
||||
*/
|
||||
export type InferValue<S extends ValueSchemaSpec, D extends readonly unknown[] = readonly []> =
|
||||
D['length'] extends 12 ? JsonValue :
|
||||
S extends StringValueSchemaSpec ? InferScalar<S, string> :
|
||||
S extends NumberValueSchemaSpec | IntegerValueSchemaSpec ? InferScalar<S, number> :
|
||||
S extends BooleanValueSchemaSpec ? InferScalar<S, boolean> :
|
||||
S extends NullValueSchemaSpec ? null :
|
||||
S extends ArrayValueSchemaSpec
|
||||
? S extends { items: infer I extends ValueSchemaSpec } ? InferValue<I, NextDepth<D>>[] : JsonValue[]
|
||||
: S extends ObjectValueSchemaSpec ? InferObject<S, NextDepth<D>> :
|
||||
S extends JsonValueSchemaSpec ? JsonValue :
|
||||
S extends OneOfValueSchemaSpec ? InferValue<S['oneOf'][number], NextDepth<D>> :
|
||||
never
|
||||
export type InferValue<S> = InferValueAt<S, []>
|
||||
|
||||
/** Infer the TypeScript argument object for an implicit parameter schema. */
|
||||
export type InferArgs<S extends ParameterSchemaSpec> = InferProperties<S, readonly []>
|
||||
export type InferArgs<S> = InferProperties<S, []>
|
||||
|
||||
const ANNOTATION_KEYS = ['description', 'title', 'default', 'examples'] as const
|
||||
|
||||
@@ -278,11 +288,11 @@ function runSchemaCompiler(initial: CompileTask): void {
|
||||
continue
|
||||
}
|
||||
if (task.kind === 'property') {
|
||||
if (!isPlainJsonRecord(task.property)) authorError(`${task.path} must be a value schema object`)
|
||||
if (!isJsonSchemaRecord(task.property)) authorError(`${task.path} must be a value schema object`)
|
||||
if (Object.hasOwn(task.property, 'required') && task.property.required !== true) {
|
||||
authorError(`${task.path}.required must be true when present`)
|
||||
}
|
||||
if (task.property.required === true) task.required.push(task.key)
|
||||
if (Object.hasOwn(task.property, 'required') && task.property.required === true) task.required.push(task.key)
|
||||
tasks.push({
|
||||
kind: 'value',
|
||||
input: task.property,
|
||||
@@ -293,7 +303,7 @@ function runSchemaCompiler(initial: CompileTask): void {
|
||||
continue
|
||||
}
|
||||
if (task.kind === 'property-map') {
|
||||
if (!isPlainJsonRecord(task.input)) authorError(`${task.path} must be an object of value schemas`)
|
||||
if (!isJsonSchemaRecord(task.input)) authorError(`${task.path} must be an object of value schemas`)
|
||||
if (seen.has(task.input)) authorError(`${task.path} is circular`)
|
||||
seen.add(task.input)
|
||||
const compiled: CompiledPropertyMap = { properties: {} }
|
||||
@@ -319,7 +329,7 @@ function runSchemaCompiler(initial: CompileTask): void {
|
||||
}
|
||||
|
||||
const { input, path } = task
|
||||
if (!isPlainJsonRecord(input)) authorError(`${path} must be a value schema object`)
|
||||
if (!isJsonSchemaRecord(input)) authorError(`${path} must be a value schema object`)
|
||||
if (seen.has(input)) authorError(`${path} is circular`)
|
||||
seen.add(input)
|
||||
const authorKeys = [...ANNOTATION_KEYS, ...(task.allowRequired ? ['required'] : [])]
|
||||
@@ -330,7 +340,7 @@ function runSchemaCompiler(initial: CompileTask): void {
|
||||
if (Object.hasOwn(input, 'oneOf')) {
|
||||
assertAuthorKeys(input, path, [...authorKeys, 'oneOf', 'type'])
|
||||
if (Object.hasOwn(input, 'type')) authorError(`${path} cannot declare both type and oneOf`)
|
||||
if (!Array.isArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`)
|
||||
if (!isPlainJsonArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`)
|
||||
const branches: JsonSchemaNode[] = []
|
||||
node.oneOf = branches
|
||||
copyAnnotations(input, node)
|
||||
@@ -346,7 +356,8 @@ function runSchemaCompiler(initial: CompileTask): void {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (input.type) {
|
||||
const inputType = Object.hasOwn(input, 'type') ? input.type : undefined
|
||||
switch (inputType) {
|
||||
case 'json':
|
||||
assertAuthorKeys(input, path, [...authorKeys, 'type'])
|
||||
copyAnnotations(input, node)
|
||||
@@ -388,12 +399,11 @@ function runSchemaCompiler(initial: CompileTask): void {
|
||||
case 'boolean':
|
||||
case 'null':
|
||||
assertAuthorKeys(input, path, [...authorKeys, 'type', 'enum', 'const'])
|
||||
node.type = input.type
|
||||
node.type = inputType
|
||||
copyAnnotations(input, node)
|
||||
if (Object.hasOwn(input, 'enum')) {
|
||||
node.enum = Array.isArray(input.enum)
|
||||
? Array.from(input.enum as unknown[], entry => entry as JsonSchemaScalar)
|
||||
: input.enum as JsonSchemaScalar[]
|
||||
if (!isPlainJsonArray(input.enum)) authorError(`${path}.enum must be a non-empty array of scalar values`)
|
||||
node.enum = Array.from(input.enum, entry => entry as JsonSchemaScalar)
|
||||
}
|
||||
if (Object.hasOwn(input, 'const')) node.const = input.const as JsonSchemaScalar
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user