schema.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { Schema } from "effect"
  2. import {
  3. AbsolutePath,
  4. DateTimeUtcFromMillis,
  5. NonNegativeInt,
  6. optional,
  7. PositiveInt,
  8. RelativePath,
  9. statics,
  10. } from "@opencode-ai/schema/schema"
  11. export { AbsolutePath, DateTimeUtcFromMillis, NonNegativeInt, optional, PositiveInt, RelativePath, statics }
  12. /**
  13. * Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable`
  14. * until `effect:core/x228my` ("Types.DeepMutable widens unknown to `{}`") lands.
  15. *
  16. * The upstream version falls through `unknown` into `{ -readonly [K in keyof T]: ... }`
  17. * where `keyof unknown = never`, so `unknown` collapses to `{}`. This local
  18. * version gates the object branch on `extends object` (which `unknown` does
  19. * not) so `unknown` passes through untouched.
  20. *
  21. * Primitive bailout matches upstream — without it, branded strings like
  22. * `string & Brand<"SessionID">` fall into the object branch and get their
  23. * prototype methods walked.
  24. *
  25. * Tuple branch preserves readonly tuples (e.g. `ConfigPlugin.Spec`'s
  26. * `readonly [string, Options]`); the general array branch would otherwise
  27. * widen them to unbounded arrays.
  28. */
  29. // eslint-disable-next-line @typescript-eslint/ban-types
  30. export type DeepMutable<T> = T extends string | number | boolean | bigint | symbol | Function
  31. ? T
  32. : T extends readonly [unknown, ...unknown[]]
  33. ? { -readonly [K in keyof T]: DeepMutable<T[K]> }
  34. : T extends readonly (infer U)[]
  35. ? DeepMutable<U>[]
  36. : T extends object
  37. ? { -readonly [K in keyof T]: DeepMutable<T[K]> }
  38. : T
  39. /**
  40. * Nominal wrapper for scalar types. The class itself is a valid schema —
  41. * pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc.
  42. *
  43. * Overrides `~type.make` on the derived `Schema.Opaque` so `Schema.Schema.Type`
  44. * of a field using this newtype resolves to `Self` rather than the underlying
  45. * branded phantom. Without that override, passing a class instance to code
  46. * typed against `Schema.Schema.Type<FieldSchema>` would require a cast even
  47. * though the values are structurally equivalent at runtime.
  48. *
  49. * @example
  50. * class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
  51. * static make(id: string): QuestionID {
  52. * return this.make(id)
  53. * }
  54. * }
  55. *
  56. * Schema.decodeEffect(QuestionID)(input)
  57. */
  58. export function Newtype<Self>() {
  59. return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S) => {
  60. abstract class Base {
  61. declare readonly _newtype: Tag
  62. static make(value: Schema.Schema.Type<S>): Self {
  63. return value as unknown as Self
  64. }
  65. }
  66. Object.setPrototypeOf(Base, schema)
  67. return Base as unknown as (abstract new (_: never) => { readonly _newtype: Tag }) & {
  68. readonly make: (value: Schema.Schema.Type<S>) => Self
  69. } & Omit<Schema.Opaque<Self, S, {}>, "make" | "~type.make"> & {
  70. readonly "~type.make": Self
  71. }
  72. }
  73. }