Hyperlinkv0.8.0-beta.28

Schema

Source effect/Schema.ts:12487347 lines
export interface Class<Self, S extends Constraint & { readonly fields: Struct.Fields }, Inherited> extends
  BottomLazy<
    SchemaAST.Declaration,
    decodeTo<declareConstructor<Self, S["Encoded"], readonly [S], S["Iso"]>, S>,
    readonly [S],
    S["~type.mutability"],
    S["~type.optionality"],
    S["~type.constructor.default"],
    S["~encoded.mutability"],
    S["~encoded.optionality"]
  >
{
  readonly "Type": Self
  readonly "Encoded": S["Encoded"]
  readonly "DecodingServices": S["DecodingServices"]
  readonly "EncodingServices": S["EncodingServices"]
  readonly "~type.make.in": RequiredKeys<S["~type.make.in"]> extends never ? void | S["~type.make.in"]
    : S["~type.make.in"]
  readonly "~type.make": Self
  readonly "Iso": S["Iso"]
  new(
    ...args: {} extends S["~type.make.in"] ? [props?: S["~type.make.in"], options?: MakeOptions]
      : [props: S["~type.make.in"], options?: MakeOptions]
  ): S["Type"] & Inherited
  readonly identifier: string
  readonly fields: S["fields"]

  /**
   * Returns a new struct with the fields modified by the provided function.
   *
   * **Details**
   *
   * Options:
   *
   * - `unsafePreserveChecks` - if `true`, keep any `.check(...)` constraints
   *   that were attached to the original struct. Defaults to `false`.
   *
   *   **Warning**: This is an unsafe operation. Since `mapFields`
   *   transformations change the schema type, the original refinement functions
   *   may no longer be valid or safe to apply to the transformed schema. Only
   *   use this option if you have verified that your refinements remain correct
   *   after the transformation.
   */
  mapFields<To extends Struct.Fields>(
    f: (fields: S["fields"]) => To,
    options?: {
      readonly unsafePreserveChecks?: boolean | undefined
    } | undefined
  ): Struct<Simplify<Readonly<To>>>

  /**
   * Returns a function that creates a schema-backed subclass with this class's
   * fields plus additional fields.
   *
   * **When to use**
   *
   * Use when you need a subclass whose constructor validates both inherited
   * fields and newly added fields.
   *
   * **Details**
   *
   * The returned function accepts either a field map or a `Struct`. When you
   * pass a `Struct`, checks attached to that extension schema are preserved and
   * combined with checks from the base class schema.
   *
   * **Gotchas**
   *
   * Checks from a `Struct` argument are evaluated against the full subclass
   * value after inherited and extension fields are merged. Object-wide checks
   * such as `isMaxProperties` count inherited fields too.
   */
  extend<Extended = never, Static = {}, Brand = {}>(
    identifier: string
  ): {
    <NewFields extends Struct.Fields>(
      fields: NewFields,
      annotations?: Annotations.Declaration<Extended, readonly [Struct<Simplify<Assign<S["fields"], NewFields>>>]>
    ): [Extended] extends [never] ? MissingSelfGeneric<"Base.extend"> : InheritStaticMembers<
      Class<Extended, Struct<Simplify<Assign<S["fields"], NewFields>>>, Self & Brand>,
      Static
    >
    <Extension extends Struct<Struct.Fields>>(
      schema: Extension,
      annotations?: Annotations.Declaration<
        Extended,
        readonly [Struct<Simplify<Assign<S["fields"], Extension["fields"]>>>]
      >
    ): [Extended] extends [never] ? MissingSelfGeneric<"Base.extend"> : InheritStaticMembers<
      Class<Extended, Struct<Simplify<Assign<S["fields"], Extension["fields"]>>>, Self & Brand>,
      Static
    >
  }
}

// Merges custom static members from a parent class onto the extended class,
// giving priority to the extended class's own members (e.g. schema-generated statics).
type InheritStaticMembers<C, Static> = C & Pick<Static, Exclude<keyof Static, keyof C>>

const immerable: unique symbol = globalThis.Symbol.for("immer-draftable") as any

const payloadToken = {}

function makeClass<
  Self,
  S extends Struct<Struct.Fields>,
  Inherited extends new(...args: ReadonlyArray<any>) => any
>(
  Inherited: Inherited,
  identifier: string,
  struct: S,
  annotations: Annotations.Declaration<Self, readonly [S]> | undefined,
  proto: ((identifier: string) => object) | undefined
): any {
  const getClassSchema = getClassSchemaFactory(struct, identifier, annotations)
  const ClassTypeId = getClassTypeId(identifier) // HMR support

  const out = class extends Inherited {
    constructor(...[input, options]: ReadonlyArray<any>) {
      const internalOptions = options as MakeOptions | undefined
      const payload = internalOptions?.["~payload"]
      const value = payload?.token === payloadToken
        ? payload.value
        : struct.make(input ?? {}, options)
      super(value, { ...options, disableChecks: true, "~payload": { token: payloadToken, value } })
    }

    static readonly [TypeId] = TypeId

    get [ClassTypeId]() {
      return ClassTypeId
    }

    static readonly [immerable] = true

    static readonly identifier = identifier
    static readonly fields = struct.fields

    static get ast(): SchemaAST.Declaration {
      return getClassSchema(this).ast
    }
    static pipe() {
      return Pipeable.pipeArguments(this, arguments)
    }
    static rebuild(ast: SchemaAST.Declaration) {
      return getClassSchema(this).rebuild(ast)
    }
    static make(input: S["~type.make.in"], options?: MakeOptions): Self {
      return new this(input, options)
    }
    static makeOption(input: S["~type.make.in"], options?: MakeOptions): Option_.Option<Self> {
      return SchemaParser.makeOption(getClassSchema(this) as any)(input ?? {}, options) as any
    }
    static makeEffect(input: S["~type.make.in"], options?: MakeOptions): Effect.Effect<Self, SchemaError> {
      return (getClassSchema(this) as any).makeEffect(input ?? {}, options)
    }
    static annotate(annotations: Annotations.Declaration<Self, readonly [S]>) {
      return this.rebuild(SchemaAST.annotate(this.ast, annotations))
    }
    static annotateKey(annotations: Annotations.Key<Self>) {
      return this.rebuild(SchemaAST.annotateKey(this.ast, annotations))
    }
    static check(...checks: readonly [SchemaAST.Check<Self>, ...Array<SchemaAST.Check<Self>>]) {
      return this.rebuild(SchemaAST.appendChecks(this.ast, checks))
    }
    static extend(
      identifier: string
    ) {
      return (
        schema: Struct.Fields | Struct<Struct.Fields>,
        annotations?: Annotations.Declaration<any, readonly [any]>
      ) => {
        const extension = isStruct(schema) ? schema : Struct(schema)
        const fields = { ...struct.fields, ...extension.fields }
        const ast = SchemaAST.struct(fields, struct.ast.checks, { identifier })
        return makeClass(
          this,
          identifier,
          makeStruct(SchemaAST.appendChecks(ast, extension.ast.checks), fields),
          annotations,
          proto
        )
      }
    }
    static mapFields<To extends Struct.Fields>(
      f: (fields: S["fields"]) => To,
      options?: {
        readonly unsafePreserveChecks?: boolean | undefined
      } | undefined
    ): Struct<Simplify<Readonly<To>>> {
      return struct.mapFields(f, options)
    }
  }

  if (proto !== undefined) {
    Object.assign(out.prototype, proto(identifier))
  }

  return out
}

function getClassTransformation(self: new(...args: ReadonlyArray<any>) => any) {
  return new SchemaTransformation.Transformation<any, any, never, never>(
    SchemaGetter.transform((input) => new self(input)),
    SchemaGetter.passthrough()
  )
}

function getClassTypeId(identifier: string) {
  return `~effect/Schema/Class/${identifier}`
}

function getClassSchemaFactory<S extends Constraint>(
  from: S,
  identifier: string,
  annotations: Annotations.Declaration<any, readonly [S]> | undefined
) {
  let memo: decodeTo<declareConstructor<any, S["Encoded"], readonly [S]>, S> | undefined
  return <Self extends (new(...args: ReadonlyArray<any>) => any) & { readonly identifier: string }>(
    self: Self
  ): decodeTo<declareConstructor<Self, S["Encoded"], readonly [S]>, S> => {
    if (memo !== undefined) {
      return memo
    }
    const transformation = getClassTransformation(self)
    const to = make<declareConstructor<Self, S["Encoded"], readonly [S]>>(
      new SchemaAST.Declaration(
        [from.ast],
        () => (input, ast) => {
          return input instanceof self ||
              Predicate.hasProperty(input, getClassTypeId(identifier)) ?
            Effect.succeed(input) :
            Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input)))
        },
        {
          identifier,
          [SchemaAST.ClassTypeId]: ([from]: readonly [SchemaAST.AST]) => new SchemaAST.Link(from, transformation),
          toCodec: ([from]: readonly [ConstraintCodec<S["Encoded"], S["Encoded"]>]) =>
            new SchemaAST.Link(from.ast, transformation),
          toArbitrary: ([from]: readonly [Annotations.ToArbitrary.TypeParameter<S["Type"]>]) => () => ({
            arbitrary: from.arbitrary.map((args: S["Type"]) => new self(args)),
            terminal: from.terminal?.map((args: S["Type"]) => new self(args))
          }),
          toFormatter: ([from]: readonly [Formatter<S["Type"]>]) => (t: Self) => `${self.identifier}(${from(t)})`,
          "~sentinels": SchemaAST.collectSentinels(from.ast),
          ...annotations
        }
      )
    )
    return memo = decodeTo<declareConstructor<Self, S["Encoded"], readonly [S]>, S>(to, transformation)(from)
  }
}

function isStruct(schema: Struct.Fields | Struct<Struct.Fields>): schema is Struct<Struct.Fields> {
  return isSchema(schema)
}

type MissingSelfGeneric<Usage extends string> =
  `Missing \`Self\` generic - use \`class Self extends ${Usage}<Self>(...)\``

/**
 * Creates a schema-backed class whose constructor validates input against a
 * {@link Struct} schema. Construction throws a {@link SchemaError} on invalid
 * input.
 *
 * **When to use**
 *
 * Use when you need a schema-backed data class with validated construction,
 * schema-derived decoding/encoding, and class-style methods or inheritance.
 *
 * **Details**
 *
 * Pass the desired class type as the first type parameter. The second optional
 * type parameter can be used to add nominal brands.
 *
 * **Gotchas**
 *
 * Passing `disableChecks` in the options skips constructor validation.
 *
 * **Example** (Defining a basic class)
 *
 * ```ts
 * import { Schema } from "effect"
 *
 * class Person extends Schema.Class<Person>("Person")({
 *   name: Schema.String,
 *   age: Schema.Number
 * }) {}
 *
 * const alice = new Person({ name: "Alice", age: 30 })
 * console.log(alice.name) // "Alice"
 * console.log(`${alice}`) // "Person({ name: Alice, age: 30 })"
 * ```
 *
 * **Example** (Extending a class)
 *
 * ```ts
 * import { Schema } from "effect"
 *
 * class Animal extends Schema.Class<Animal>("Animal")({
 *   name: Schema.String
 * }) {}
 *
 * class Dog extends Animal.extend<Dog>("Dog")({
 *   breed: Schema.String
 * }) {}
 *
 * const dog = new Dog({ name: "Rex", breed: "Labrador" })
 * console.log(dog.name) // "Rex"
 * console.log(dog.breed) // "Labrador"
 * ```
 *
 * @see {@link TaggedClass} for adding a `_tag` literal field to the class schema
 * @see {@link ErrorClass} for defining schema-backed error classes
 * @see {@link TaggedErrorClass} for defining tagged schema-backed error classes
 *
 * @category constructors
 * @since 3.10.0
 */
export const Class: {
  <Self = never, Brand = {}>(identifier: string): {
    <const Fields extends Struct.Fields>(
      fields: Fields,
      annotations?: Annotations.Declaration<Self, readonly [Struct<Fields>]>
    ): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class<Self, Struct<Fields>, Brand>
    <S extends Struct<Struct.Fields>>(
      schema: S,
      annotations?: Annotations.Declaration<Self, readonly [S]>
    ): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class<Self, S, Brand>
  }
} = <Self, Brand = {}>(identifier: string) =>
(
  schema: Struct.Fields | Struct<Struct.Fields>,
  annotations?: Annotations.Declaration<Self, readonly [Struct<Struct.Fields>]>
): [Self] extends [never] ? MissingSelfGeneric<"Schema.Class"> : Class<Self, Struct<Struct.Fields>, Brand> => {
  const struct = isStruct(schema) ? schema : Struct(schema)
  return makeClass(
    Data.Class,
    identifier,
    struct,
    annotations,
    (identifier) => ({
      toString() {
        return `${identifier}(${format({ ...this })})`
      }
    })
  )
}
Referenced by 16 symbols