{"version":3,"file":"discriminated-union-validator.mjs","names":[],"sources":["../../../../../../../@warlock.js/seal/src/validators/discriminated-union-validator.ts"],"sourcesContent":["import { isPlainObject } from \"@mongez/supportive-is\";\nimport type { JsonSchemaResult, JsonSchemaTarget } from \"../standard-schema/json-schema\";\nimport { applyNullable, wrapNullableStrict } from \"../standard-schema/json-schema\";\nimport type { SchemaContext, ValidationResult } from \"../types\";\nimport { BaseValidator } from \"./base-validator\";\nimport { LiteralValidator } from \"./literal-validator\";\nimport { ObjectValidator } from \"./object-validator\";\n\n/**\n * Discriminated union validator — routes payloads by a shared discriminator field.\n *\n * Plain `v.union()` falls back to `matchesType()` to pick a branch, which is\n * coarse for object-vs-object unions (every branch matches \"is plain object\").\n * Discriminated union reads a known field's value and routes directly to the\n * matching branch, producing precise errors instead of confused mash from the\n * wrong branch.\n *\n * **Construction-time validation.** Every branch must:\n * - Be an `ObjectValidator`\n * - Declare the discriminator field\n * - Type the discriminator as `v.literal(...)` (single or multi-literal both work)\n * - Not collide with another branch's literal values\n *\n * Misconfigurations throw eagerly so tests catch them at schema-build time.\n *\n * @example\n * ```ts\n * const email = v.object({ type: v.literal(\"email\"), email: v.string().email() });\n * const sms   = v.object({ type: v.literal(\"sms\"),   phone: v.string() });\n * const push  = v.object({ type: v.literal(\"push\"),  deviceId: v.string() });\n *\n * const notif = v.discriminatedUnion(\"type\", [email, sms, push]);\n *\n * await validate(notif, { type: \"sms\", phone: \"555-1234\" });\n * // → routes to sms branch only; errors (if any) come from sms\n * ```\n *\n * @see `domains/seal/plans/2026-05-12-discriminated-union.md`\n */\nexport class DiscriminatedUnionValidator<\n  K extends string = string,\n  Branches extends ReadonlyArray<ObjectValidator<any>> = ReadonlyArray<ObjectValidator<any>>,\n> extends BaseValidator {\n  /** Map from discriminator literal value → matching branch validator. */\n  private branches: Map<string | number | boolean, ObjectValidator<any>>;\n\n  public constructor(\n    public discriminator: K,\n    public validators: Branches,\n  ) {\n    super();\n\n    this.branches = DiscriminatedUnionValidator.buildBranchMap(discriminator, validators);\n  }\n\n  /**\n   * Walk every branch, pull out the discriminator's literal values, and build\n   * the lookup map. Throws on misconfiguration (missing discriminator,\n   * non-literal discriminator, duplicate literal value).\n   */\n  private static buildBranchMap(\n    discriminator: string,\n    validators: ReadonlyArray<ObjectValidator<any>>,\n  ): Map<string | number | boolean, ObjectValidator<any>> {\n    const map = new Map<string | number | boolean, ObjectValidator<any>>();\n\n    for (const branch of validators) {\n      const discriminatorValidator = branch.schema?.[discriminator];\n\n      if (!discriminatorValidator) {\n        throw new Error(\n          `[Seal] discriminatedUnion: branch missing discriminator field \"${discriminator}\"`,\n        );\n      }\n\n      if (!(discriminatorValidator instanceof LiteralValidator)) {\n        throw new Error(\n          `[Seal] discriminatedUnion: discriminator \"${discriminator}\" must be v.literal(...) on every branch`,\n        );\n      }\n\n      for (const value of discriminatorValidator.values) {\n        if (map.has(value)) {\n          throw new Error(\n            `[Seal] discriminatedUnion: duplicate discriminator value \"${String(value)}\"`,\n          );\n        }\n        map.set(value, branch);\n      }\n    }\n\n    return map;\n  }\n\n  public override matchesType(value: any): boolean {\n    return isPlainObject(value);\n  }\n\n  public override async validate(data: any, context: SchemaContext): Promise<ValidationResult> {\n    if (data === null && this.isNullable) {\n      return { isValid: true, errors: [], data: null };\n    }\n\n    if (!isPlainObject(data)) {\n      return {\n        isValid: false,\n        errors: [\n          {\n            type: \"discriminatedUnion\",\n            error: `Expected object with discriminator field \"${this.discriminator}\"`,\n            input: context.key || context.path || \"value\",\n          },\n        ],\n        data: undefined,\n      };\n    }\n\n    const discriminatorValue = data[this.discriminator];\n    const branch = this.branches.get(discriminatorValue);\n\n    if (!branch) {\n      const allowed = [...this.branches.keys()].map((k) => String(k)).join(\", \");\n      return {\n        isValid: false,\n        errors: [\n          {\n            type: \"discriminatedUnion\",\n            error: `Field \"${this.discriminator}\" must be one of: ${allowed}`,\n            input: this.discriminator,\n          },\n        ],\n        data: undefined,\n      };\n    }\n\n    return branch.validate(data, context);\n  }\n\n  public override clone(): this {\n    const cloned = super.clone() as any;\n    cloned.discriminator = this.discriminator;\n    cloned.validators = this.validators.map((v: ObjectValidator<any>) => v.clone());\n    cloned.branches = DiscriminatedUnionValidator.buildBranchMap(\n      cloned.discriminator,\n      cloned.validators,\n    );\n    return cloned;\n  }\n\n  /**\n   * Emit `oneOf` of branch schemas. Each branch's own `toJsonSchema()` handles\n   * its `properties.{discriminator}.const` and the surrounding required/optional\n   * structure — we just enumerate.\n   *\n   * For `openai-strict`: the per-branch ObjectValidator already inflates\n   * `required` to include every field; `oneOf` is OpenAI-accepted as long as\n   * each branch passes strict rules independently.\n   */\n  public override toJsonSchema(target: JsonSchemaTarget = \"draft-2020-12\"): JsonSchemaResult {\n    const oneOf = this.validators.map((v) => v.toJsonSchema(target));\n    const schema: JsonSchemaResult = { oneOf };\n\n    if (this.isNullable) {\n      if (target === \"openai-strict\") {\n        return wrapNullableStrict(schema);\n      }\n      applyNullable(schema, target);\n    }\n\n    return schema;\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,8BAAb,MAAa,oCAGH,cAAc;CAItB,AAAO,YACL,AAAO,eACP,AAAO,YACP;EACA,MAAM;EAHC;EACA;EAIP,KAAK,WAAW,4BAA4B,eAAe,eAAe,UAAU;CACtF;;;;;;CAOA,OAAe,eACb,eACA,YACsD;EACtD,MAAM,sBAAM,IAAI,IAAqD;EAErE,KAAK,MAAM,UAAU,YAAY;GAC/B,MAAM,yBAAyB,OAAO,SAAS;GAE/C,IAAI,CAAC,wBACH,MAAM,IAAI,MACR,kEAAkE,cAAc,EAClF;GAGF,IAAI,EAAE,kCAAkC,mBACtC,MAAM,IAAI,MACR,6CAA6C,cAAc,yCAC7D;GAGF,KAAK,MAAM,SAAS,uBAAuB,QAAQ;IACjD,IAAI,IAAI,IAAI,KAAK,GACf,MAAM,IAAI,MACR,6DAA6D,OAAO,KAAK,EAAE,EAC7E;IAEF,IAAI,IAAI,OAAO,MAAM;GACvB;EACF;EAEA,OAAO;CACT;CAEA,AAAgB,YAAY,OAAqB;EAC/C,OAAO,cAAc,KAAK;CAC5B;CAEA,MAAsB,SAAS,MAAW,SAAmD;EAC3F,IAAI,SAAS,QAAQ,KAAK,YACxB,OAAO;GAAE,SAAS;GAAM,QAAQ,CAAC;GAAG,MAAM;EAAK;EAGjD,IAAI,CAAC,cAAc,IAAI,GACrB,OAAO;GACL,SAAS;GACT,QAAQ,CACN;IACE,MAAM;IACN,OAAO,6CAA6C,KAAK,cAAc;IACvE,OAAO,QAAQ,OAAO,QAAQ,QAAQ;GACxC,CACF;GACA,MAAM;EACR;EAGF,MAAM,qBAAqB,KAAK,KAAK;EACrC,MAAM,SAAS,KAAK,SAAS,IAAI,kBAAkB;EAEnD,IAAI,CAAC,QAAQ;GACX,MAAM,UAAU,CAAC,GAAG,KAAK,SAAS,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;GACzE,OAAO;IACL,SAAS;IACT,QAAQ,CACN;KACE,MAAM;KACN,OAAO,UAAU,KAAK,cAAc,oBAAoB;KACxD,OAAO,KAAK;IACd,CACF;IACA,MAAM;GACR;EACF;EAEA,OAAO,OAAO,SAAS,MAAM,OAAO;CACtC;CAEA,AAAgB,QAAc;EAC5B,MAAM,SAAS,MAAM,MAAM;EAC3B,OAAO,gBAAgB,KAAK;EAC5B,OAAO,aAAa,KAAK,WAAW,KAAK,MAA4B,EAAE,MAAM,CAAC;EAC9E,OAAO,WAAW,4BAA4B,eAC5C,OAAO,eACP,OAAO,UACT;EACA,OAAO;CACT;;;;;;;;;;CAWA,AAAgB,aAAa,SAA2B,iBAAmC;EAEzF,MAAM,SAA2B,EAAE,OADrB,KAAK,WAAW,KAAK,MAAM,EAAE,aAAa,MAAM,CACvB,EAAE;EAEzC,IAAI,KAAK,YAAY;GACnB,IAAI,WAAW,iBACb,OAAO,mBAAmB,MAAM;GAElC,cAAc,QAAQ,MAAM;EAC9B;EAEA,OAAO;CACT;AACF"}