Hyperlinkv0.8.0-beta.28

SchemaAST

SchemaAST.Objectsclasseffect/SchemaAST.ts:2038
Objects

AST node for object-like schemas, including structs and records.

When to use

Use when constructing or inspecting AST nodes for structs or records rather than array-like schemas.

Details

  • propertySignatures — named properties with their types (struct fields).
  • indexSignatures — index signature entries (record patterns), each with a parameter AST for matching keys and a type AST for values.

An Objects node with no properties and no index signatures performs only a non-nullish check: it accepts any value except null and undefined, including primitive values.

Gotchas

Duplicate property names throw at construction time.

Example (Inspecting a struct AST)

import { Schema, SchemaAST } from "effect"

const schema = Schema.Struct({ name: Schema.String })
const ast = schema.ast

if (SchemaAST.isObjects(ast)) {
  for (const ps of ast.propertySignatures) {
    console.log(ps.name, ps.type._tag)
  }
  // "name" "String"
}
Source effect/SchemaAST.ts:2038250 lines
export class Objects extends Base {
  readonly _tag = "Objects"
  readonly propertySignatures: ReadonlyArray<PropertySignature>
  readonly indexSignatures: ReadonlyArray<IndexSignature>
  readonly encodingChecks: Checks | undefined

  constructor(
    propertySignatures: ReadonlyArray<PropertySignature>,
    indexSignatures: ReadonlyArray<IndexSignature>,
    annotations?: Schema.Annotations.Annotations,
    checks?: Checks,
    encoding?: Encoding,
    context?: Context,
    encodingChecks?: Checks
  ) {
    super(annotations, checks, encoding, context)
    this.propertySignatures = propertySignatures
    this.indexSignatures = indexSignatures
    this.encodingChecks = encodingChecks

    // Duplicate property signatures
    const duplicates = propertySignatures.map((ps) => ps.name).filter((name, i, arr) => arr.indexOf(name) !== i)
    if (duplicates.length > 0) {
      throw new Error(`Duplicate identifiers: ${JSON.stringify(duplicates)}. ts(2300)`)
    }
  }
  /** @internal */
  getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser {
    // oxlint-disable-next-line @typescript-eslint/no-this-alias
    const ast = this
    const expectedKeys: Array<PropertyKey> = []
    const expectedKeysSet = new Set<PropertyKey>()
    const properties: Array<{
      readonly ps: PropertySignature | IndexSignature
      readonly parser: SchemaParser.Parser
      readonly name: PropertyKey
      readonly type: AST
    }> = []
    for (const ps of ast.propertySignatures) {
      expectedKeys.push(ps.name)
      expectedKeysSet.add(ps.name)
      properties.push({
        ps,
        parser: recur(ps.type),
        name: ps.name,
        type: ps.type
      })
    }
    const indexCount = ast.indexSignatures.length
    // ---------------------------------------------
    // handle empty struct
    // ---------------------------------------------
    if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
      return fromRefinement(ast, Predicate.isNotNullish)
    }

    const parseIndexes = indexCount > 0 ?
      iterateEager<{
        readonly oinput: Option.Option<unknown>
        readonly input: Record<PropertyKey, unknown>
        readonly options: ParseOptions
        readonly out: Record<PropertyKey, unknown>
        issues: Array<SchemaIssue.Issue> | undefined
      }, [key: PropertyKey, is: IndexSignature]>()({
        onItem: Effect.fnUntracedEager(function*(
          s,
          [key, is]
        ) {
          const parserKey = recur(parameterFromPropertyKey(is.parameter))
          const effKey = parserKey(Option.some(key), s.options)
          const exitKey = (effectIsExit(effKey) ? effKey : yield* Effect.exit(effKey)) as Exit.Exit<
            Option.Option<PropertyKey>,
            SchemaIssue.Issue
          >
          if (exitKey._tag === "Failure") {
            const eff = wrapPropertyKeyIssue(s, ast, key, exitKey)
            if (eff) yield* eff
            return
          }

          const value: Option.Option<unknown> = Option.some(s.input[key])
          const parserValue = recur(is.type)
          const effValue = parserValue(value, s.options)
          const exitValue = effectIsExit(effValue) ? effValue : yield* Effect.exit(effValue)
          if (exitValue._tag === "Failure") {
            const eff = wrapPropertyKeyIssue(s, ast, key, exitValue)
            if (eff) yield* eff
            return
          } else if (exitKey.value._tag === "Some" && exitValue.value._tag === "Some") {
            const k2 = exitKey.value.value
            if (expectedKeysSet.has(key) || expectedKeysSet.has(k2)) {
              return
            }
            const v2 = exitValue.value.value
            if (is.merge && is.merge.decode && Object.hasOwn(s.out, k2)) {
              const [k, v] = is.merge.decode.combine([k2, s.out[k2]], [k2, v2])
              internalRecord.set(s.out, k, v)
            } else {
              internalRecord.set(s.out, k2, v2)
            }
          }
        }),
        step: (_s, _, exit: Exit.Exit<void, SchemaIssue.Issue>) => exit._tag === "Failure" ? exit : undefined
      }) :
      undefined

    return Effect.fnUntracedEager(function*(oinput, options) {
      if (oinput._tag === "None") {
        return oinput
      }
      const input = oinput.value as Record<PropertyKey, unknown>

      // If the input is not a record, return early with an error
      if (!(typeof input === "object" && input !== null && !Array.isArray(input))) {
        return yield* Effect.fail(new SchemaIssue.InvalidType(ast, oinput))
      }

      const out: Record<PropertyKey, unknown> = {}
      const state = {
        ast,
        oinput,
        input,
        out,
        issues: undefined as Arr.NonEmptyArray<SchemaIssue.Issue> | undefined,
        options
      }
      const errorsAllOption = options.errors === "all"
      const onExcessPropertyError = options.onExcessProperty === "error"
      const onExcessPropertyPreserve = options.onExcessProperty === "preserve"

      // ---------------------------------------------
      // handle excess properties
      // ---------------------------------------------
      let inputKeys: Array<PropertyKey> | undefined
      if (ast.indexSignatures.length === 0 && (onExcessPropertyError || onExcessPropertyPreserve)) {
        inputKeys = Reflect.ownKeys(input)
        for (let i = 0; i < inputKeys.length; i++) {
          const key = inputKeys[i]
          if (!expectedKeysSet.has(key)) {
            // key is unexpected
            if (onExcessPropertyError) {
              const issue = new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(ast, input[key]))
              if (errorsAllOption) {
                if (state.issues) {
                  state.issues.push(issue)
                } else {
                  state.issues = [issue]
                }
                continue
              } else {
                return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, [issue]))
              }
            } else {
              // preserve key
              internalRecord.set(out, key, input[key])
            }
          }
        }
      }

      const concurrency = resolveConcurrency(options?.concurrency)

      // ---------------------------------------------
      // handle property signatures
      // ---------------------------------------------
      const eff = parseProperties(state, properties, concurrency)
      if (eff) yield* eff

      // ---------------------------------------------
      // handle index signatures
      // ---------------------------------------------
      if (parseIndexes) {
        const keyPairs = Arr.empty<[PropertyKey, IndexSignature]>()
        for (let i = 0; i < indexCount; i++) {
          const is = ast.indexSignatures[i]
          const keys = getIndexSignatureKeys(input, is.parameter, options)
          for (let j = 0; j < keys.length; j++) {
            const key = keys[j]
            keyPairs.push([key, is])
          }
        }
        const eff = parseIndexes(state, keyPairs, concurrency)
        if (eff) yield* eff
      }

      if (state.issues) {
        return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, state.issues))
      }
      if (options.propertyOrder === "original") {
        // preserve input keys order
        const keys = (inputKeys ?? Reflect.ownKeys(input)).concat(expectedKeys)
        const preserved: Record<PropertyKey, unknown> = {}
        for (const key of keys) {
          if (Object.hasOwn(out, key)) {
            internalRecord.set(preserved, key, out[key])
          }
        }
        return Option.some(preserved)
      }
      return Option.some(out)
    })
  }
  private _rebuild(
    recur: (ast: AST) => AST,
    recurParameter: (ast: AST) => AST,
    flipMerge: boolean,
    checks: Checks | undefined,
    encodingChecks: Checks | undefined
  ): Objects {
    const props = mapOrSame(this.propertySignatures, (ps) => {
      const t = recur(ps.type)
      return t === ps.type ? ps : new PropertySignature(ps.name, t)
    })

    const indexes = mapOrSame(this.indexSignatures, (is) => {
      const p = recurParameter(is.parameter)
      const t = recur(is.type)
      const merge = flipMerge ? is.merge?.flip() : is.merge
      return p === is.parameter && t === is.type && merge === is.merge
        ? is
        : new IndexSignature(p, t, merge)
    })

    return props === this.propertySignatures && indexes === this.indexSignatures && checks === this.checks &&
        encodingChecks === this.encodingChecks
      ? this
      : new Objects(
        props,
        indexes,
        this.annotations,
        checks,
        undefined,
        this.context,
        encodingChecks
      )
  }
  /** @internal */
  flip(recur: (ast: AST) => AST): AST {
    return this._rebuild(recur, recur, true, this.encodingChecks, this.checks)
  }
  /** @internal */
  recur(recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST = recur): AST {
    return this._rebuild(recur, recurParameter, false, this.checks, this.encodingChecks)
  }
  /** @internal */
  getExpected(): string {
    if (this.propertySignatures.length === 0 && this.indexSignatures.length === 0) return "object | array"
    return "object"
  }
}
Referenced by 6 symbols