schema.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { Option, Schema, SchemaGetter } from "effect"
  2. export const AbsolutePath = Schema.String.pipe(Schema.brand("AbsolutePath"))
  3. export type AbsolutePath = typeof AbsolutePath.Type
  4. export const RelativePath = Schema.String.pipe(Schema.brand("RelativePath"))
  5. export type RelativePath = typeof RelativePath.Type
  6. /**
  7. * Integer greater than zero.
  8. */
  9. export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
  10. /**
  11. * Integer greater than or equal to zero.
  12. */
  13. export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
  14. /**
  15. * Optional public JSON field that can hold explicit `undefined` on the type
  16. * side but encodes it as an omitted key, matching legacy `JSON.stringify`.
  17. */
  18. export const optionalOmitUndefined = <S extends Schema.Top>(schema: S) =>
  19. Schema.optionalKey(schema).pipe(
  20. Schema.decodeTo(Schema.optional(schema), {
  21. decode: SchemaGetter.passthrough({ strict: false }),
  22. encode: SchemaGetter.transformOptional(Option.filter((value) => value !== undefined)),
  23. }),
  24. )
  25. /**
  26. * Strip `readonly` from a nested type. Stand-in for `effect`'s `Types.DeepMutable`
  27. * until `effect:core/x228my` ("Types.DeepMutable widens unknown to `{}`") lands.
  28. *
  29. * The upstream version falls through `unknown` into `{ -readonly [K in keyof T]: ... }`
  30. * where `keyof unknown = never`, so `unknown` collapses to `{}`. This local
  31. * version gates the object branch on `extends object` (which `unknown` does
  32. * not) so `unknown` passes through untouched.
  33. *
  34. * Primitive bailout matches upstream — without it, branded strings like
  35. * `string & Brand<"SessionID">` fall into the object branch and get their
  36. * prototype methods walked.
  37. *
  38. * Tuple branch preserves readonly tuples (e.g. `ConfigPlugin.Spec`'s
  39. * `readonly [string, Options]`); the general array branch would otherwise
  40. * widen them to unbounded arrays.
  41. */
  42. // eslint-disable-next-line @typescript-eslint/ban-types
  43. export type DeepMutable<T> = T extends string | number | boolean | bigint | symbol | Function
  44. ? T
  45. : T extends readonly [unknown, ...unknown[]]
  46. ? { -readonly [K in keyof T]: DeepMutable<T[K]> }
  47. : T extends readonly (infer U)[]
  48. ? DeepMutable<U>[]
  49. : T extends object
  50. ? { -readonly [K in keyof T]: DeepMutable<T[K]> }
  51. : T
  52. /**
  53. * Attach static methods to a schema object. Designed to be used with `.pipe()`:
  54. *
  55. * @example
  56. * export const Foo = fooSchema.pipe(
  57. * withStatics((schema) => ({
  58. * zero: schema.make(0),
  59. * from: Schema.decodeUnknownOption(schema),
  60. * }))
  61. * )
  62. */
  63. export const withStatics =
  64. <S extends object, M extends Record<string, unknown>>(methods: (schema: S) => M) =>
  65. (schema: S): S & M =>
  66. Object.assign(schema, methods(schema))
  67. /**
  68. * Nominal wrapper for scalar types. The class itself is a valid schema —
  69. * pass it directly to `Schema.decode`, `Schema.decodeEffect`, etc.
  70. *
  71. * Overrides `~type.make` on the derived `Schema.Opaque` so `Schema.Schema.Type`
  72. * of a field using this newtype resolves to `Self` rather than the underlying
  73. * branded phantom. Without that override, passing a class instance to code
  74. * typed against `Schema.Schema.Type<FieldSchema>` would require a cast even
  75. * though the values are structurally equivalent at runtime.
  76. *
  77. * @example
  78. * class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
  79. * static make(id: string): QuestionID {
  80. * return this.make(id)
  81. * }
  82. * }
  83. *
  84. * Schema.decodeEffect(QuestionID)(input)
  85. */
  86. export function Newtype<Self>() {
  87. return <const Tag extends string, S extends Schema.Top>(tag: Tag, schema: S) => {
  88. abstract class Base {
  89. declare readonly _newtype: Tag
  90. static make(value: Schema.Schema.Type<S>): Self {
  91. return value as unknown as Self
  92. }
  93. }
  94. Object.setPrototypeOf(Base, schema)
  95. return Base as unknown as (abstract new (_: never) => { readonly _newtype: Tag }) & {
  96. readonly make: (value: Schema.Schema.Type<S>) => Self
  97. } & Omit<Schema.Opaque<Self, S, {}>, "make" | "~type.make"> & {
  98. readonly "~type.make": Self
  99. }
  100. }
  101. }