Skip to content
English

字段、检查与模型 Schema ​

策略的具体实现如下。

ts
export function defineCommand<
  Value extends object = Record<string, unknown>,
  Result = Value,
  Args extends unknown[] = [],
>(definition: CommandDefinition<Value, Result, Args>): Command<Value, Result, Args> {
  breakWhile(
    definition.category !== undefined && !INTERACTION_CATEGORIES.includes(definition.category),
    'Invalid interaction category',
  );
  const fields: Record<string, ProtocolField> = Object.fromEntries(
    Object.entries(fieldDefinitions(definition.fields)).map(([name, field]) => [
      name,
      {
        jsonSchema: field.jsonSchema === undefined ? undefined : structuredClone(field.jsonSchema),
        required: field.required,
        modelRequired: field.modelRequired,
        owned: field.owned,
      },
    ]),
  );
  for (const name of definition.ownedFields ?? []) {
    if (!Object.hasOwn(fields, name))
      Object.defineProperty(fields, name, { value: {}, enumerable: true });
  }
  const discriminator = definition.discriminator && {
    field: definition.discriminator.field,
    value: definition.discriminator.value,
  };
  breakWhile(
    discriminator && Object.hasOwn(fields, discriminator.field),
    'Duplicate protocol discriminator field',
  );
  const entries: [string, ProtocolField][] = [
    ...(discriminator
      ? [
          [
            discriminator.field,
            { required: true, jsonSchema: { type: 'string', enum: [discriminator.value] } },
          ] as [string, ProtocolField],
        ]
      : []),
    ...Object.entries(fields),
  ];
  const checks: readonly ProtocolCheck<Value, Args>[] = (definition.checks ?? []).map((check) => {
    if (!check || check.occur === undefined || 'condition' in check || 'conditions' in check)
      throw new TypeError('Protocol checks require occur');
    const bind = (predicate: ProtocolCheck<Value, Args>['when']) => predicate?.bind(check);
    if (
      typeof check.occur !== 'function' &&
      (!Array.isArray(check.occur) ||
        check.occur.some((predicate) => typeof predicate !== 'function'))
    )
      throw new TypeError('Protocol occur requires a predicate or predicate array');
    return Object.freeze({
      occur:
        typeof check.occur === 'function'
          ? check.occur.bind(check)
          : Object.freeze(check.occur.map((predicate) => predicate.bind(check))),
      when: bind(check.when),
      error: typeof check.error === 'function' ? check.error.bind(check) : check.error,
      tags: Object.freeze([...(check.tags ?? [])]),
    });
  });
  const tags = [...(definition.tags ?? [])];
  const additionalProperties = definition.additionalProperties ?? false;
  const optionalPresence = definition.optionalPresence ?? 'own';
  const error =
    typeof definition.error === 'function' ? definition.error.bind(definition) : definition.error;
  const refine = definition.refine?.bind(definition),
    format = definition.format?.bind(definition);
  const parameters = Object.fromEntries(
    Object.entries(fields).map(([name, field]) => [name, field.jsonSchema ?? {}]),
  );
  const schema = {
    type: 'object',
    properties: Object.fromEntries(entries.map(([name, field]) => [name, field.jsonSchema ?? {}])),
    required: entries
      .filter(([, field]) => field.required && field.modelRequired !== false)
      .map(([name]) => name),
    additionalProperties,
  };
  const inputJsonSchema = {
    ...schema,
    required: entries.filter(([, field]) => field.required).map(([name]) => name),
  };
  freezeSchema(inputJsonSchema);
  freezeSchema(fields);
  freezeSchema(parameters);
  freezeSchema(schema);

  const keys = (value: Record<string, unknown>) =>
    entries
      .filter(
        ([name, field]) =>
          field.required ||
          (optionalPresence === 'defined' ? value[name] !== undefined : Object.hasOwn(value, name)),
      )
      .map(([name]) => name);
  const validate = (value: Value, ...args: Args): void => {
    const fail = reason(error, value, args);
    breakWhile(!value || typeof value !== 'object' || Array.isArray(value), fail, tags);
    const record = value as Record<string, unknown>;
    const expected = keys(record);
    breakWhile(
      [
        {
          occur: () => expected.some((name) => !Object.hasOwn(record, name)),
          error: fail,
          tags: ['required-fields'],
        },
        {
          occur: () =>
            !additionalProperties && Object.keys(record).some((name) => !expected.includes(name)),
          error: fail,
          tags: ['unknown-fields'],
        },
        {
          occur: () => discriminator && record[discriminator.field] !== discriminator.value,
          error: fail,
          tags: ['discriminator'],
        },
        ...checks.map((check) => ({
          occur: () => {
            if (
              check.when &&
              !booleanResult(
                check.when(value, ...args),
                'Protocol when predicates must return boolean synchronously',
              )
            )
              return false;
            return typeof check.occur === 'function'
              ? synchronous(
                  check.occur(value, ...args),
                  'Protocol occur predicates must be synchronous',
                )
              : check.occur.some((occur) =>
                  synchronous(
                    occur(value, ...args),
                    'Protocol occur predicates must be synchronous',
                  ),
                );
          },
          error: reason(check.error ?? error, value, args),
          tags: check.tags,
        })),
      ],
      tags,
    );
    assertion(refine?.(value, ...args), 'Protocol refinements must return void synchronously');
  };
  return Object.freeze({
    ...(definition.category !== undefined ? { category: definition.category } : {}),
    ...(definition.version !== undefined ? { version: definition.version } : {}),
    ...(definition.description !== undefined ? { description: definition.description } : {}),
    ...(definition.terminal !== undefined ? { terminal: definition.terminal } : {}),
    fieldSchemas: parameters,
    modelJsonSchema: schema,
    inputJsonSchema,
    keys,
    assert: validate,
    modes: definition.modes && Object.freeze([...definition.modes]),
    ownedFields: Object.freeze([
      ...new Set([
        ...(definition.ownedFields ?? []),
        ...Object.entries(fields)
          .filter(([, field]) => field.owned)
          .map(([name]) => name),
      ]),
    ]),
    parse(value: Value, ...args: Args): Result {
      validate(value, ...args);
      return format
        ? synchronous(format(value, ...args), 'Protocol formatters must be synchronous')
        : (value as unknown as Result);
    },
  });
}

export function compileCommands<
  Value extends object,
  Result = Value,
  Args extends unknown[] = [],
  Id extends string = string,
>(
  definitions: Readonly<Record<Id, CommandDefinition<Value, Result, Args>>>,
  defaults: Partial<CommandDefinition<Value, Result, Args>> = {},
): Readonly<Record<Id, Command<Value, Result, Args>>> {
  defaults = pinCommandDefinition(defaults);
  return Object.freeze(
    Object.fromEntries(
      Object.entries(definitions).map(([id, value]) => {
        const definition = pinCommandDefinition(value as CommandDefinition<Value, Result, Args>);
        const envelopeFields = { ...fieldDefinitions(defaults.fields ?? {}) };
        if (definition.discriminator) delete envelopeFields[definition.discriminator.field];
        return [
          id,
          defineCommand({
            ...defaults,
            ...definition,
            fields: { ...envelopeFields, ...fieldDefinitions(definition.fields!) },
            checks: [...(defaults.checks ?? []), ...(definition.checks ?? [])],
            tags: [...(defaults.tags ?? []), ...(definition.tags ?? [])],
          }),
        ];
      }),
    ),
  ) as Readonly<Record<Id, Command<Value, Result, Args>>>;
}

export function commandSchema(
  id: string,
  protocol: ProtocolShape,
  discriminator = 'type',
): Record<string, unknown> {
  if (protocol.modelJsonSchema) return protocol.modelJsonSchema;
  return {
    type: 'object',
    properties: { [discriminator]: { type: 'string', enum: [id] }, ...protocol.fieldSchemas },
    required: protocol.fieldSchemas ? protocol.keys({ [discriminator]: id }) : [discriminator],
    additionalProperties: !protocol.fieldSchemas,
  };
}

接口参考、使用示例与开发指南。