已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

Schema Annotation

了解如何用 Annotation 增强 schema,以便在基于 Effect 的应用中更好地自定义、处理错误、编写文档并控制并发。

Schema AST 节点可以携带可选的元数据,称为 Annotation。解码侧使用 .annotate(...) 方法或 Schema.annotate(...),编码侧使用 Schema.annotateEncoded(...),而 struct 字段或 tuple 元素则使用 Schema.annotateKey(...)

示例(用 Annotation 自定义 Schema)

import { Schema } from "effect"

// Define a Password schema, starting with a string type
const Password = Schema.String
  // Add a custom error message for non-string values
  .annotate({ message: "not a string" })
  .pipe(
    // Enforce non-empty strings and provide a custom error message
    Schema.check(
      Schema.isNonEmpty({ message: "required" }),
      // Restrict the string length to 10 characters or fewer
      // with a custom error message for exceeding length
      Schema.makeFilter((s) =>
        s.length <= 10 ? undefined : "must be at most 10 characters long",
      ),
    ),
  )
  .annotate({
    // Add a unique identifier for the schema
    identifier: "Password",
    // Provide a title for the schema
    title: "password",
    // Include a description explaining what this schema represents
    description: "A password is a secret string used to authenticate a user",
    // Add examples for better clarity
    examples: ["1Ki77y", "jelly22fi$h"],
    // Include any additional documentation
    documentation: `...technical information on Password schema...`,
  })

内置 Annotation

可用的 Annotation 取决于 schema 节点的种类。下面是最常见的一些:

Annotation作用范围说明
identifierschemaschema 解释器使用的稳定名称,包括 JSON Schema 引用与期望值消息。
expectedschema 或 check默认错误格式化器使用的人类可读描述。
titleschema 或 key简短的显示标题,JSON Schema 工具也能识别。
descriptionschema 或 key针对所表示值的更详细文档。
documentationschema 或 key面向开发者的附加文档。
examplesschema 或 key示例解码值;它们只是元数据,不会被校验。
defaultschema 或 key一个有文档记录的默认值;它不会改变解码或构造行为。
messageschema 或 check替换匹配失败时的默认消息。
messageMissingKeykey当必需 key 缺失时替换错误消息。
messageUnexpectedKeyschemaonExcessProperty"error" 时,替换多余 key 的消息。
parseOptionsschema覆盖该 schema 节点的 parse options
toJsonSchemacheckJSON Schema 解释器描述一个自定义 check。
toArbitraryschema 或 declaration定制 Arbitrary 的生成。
toFormatterdeclaration定义自定义 declaration 的 Formatter 行为。
toEquivalencedeclaration定义自定义 declaration 的 Equivalence 行为。
toCodecJsondeclaration定义 JSON codec 解释器如何表示自定义 declaration。

并发 parse option

对于 StructArrayUnion 这类包含多个带 effect 的 schema,concurrency parse option 控制可以并发运行多少个解析 effect。

type Concurrency = number | "unbounded" | undefined

下面用表格给出更简洁的版本:

说明
number限制并发任务的最大数量。
"unbounded"所有任务并发运行,没有数量限制。
undefined同一时刻最多运行一个任务(默认值)。

示例(顺序执行)

在这个示例中,我们定义了三个任务,模拟耗时不同的异步操作。由于没有指定 concurrency,这些任务会一个接一个地顺序执行。

import { Schema, SchemaGetter } from "effect"
import type { Duration } from "effect"
import { Effect } from "effect"

// Simulates an async task
const item = (id: number, duration: Duration.Input) =>
  Schema.String.pipe(
    Schema.decode({
      decode: SchemaGetter.checkEffect(() =>
        Effect.gen(function* () {
          yield* Effect.sleep(duration)
          console.log(`Task ${id} done`)
          return true
        }),
      ),
      encode: SchemaGetter.passthrough(),
    }),
  )

const Sequential = Schema.Tuple([
  item(1, "30 millis"),
  item(2, "10 millis"),
  item(3, "20 millis"),
])

Effect.runPromise(Schema.decodeEffect(Sequential)(["a", "b", "c"]))
/*
Output:
Task 1 done
Task 2 done
Task 3 done
*/

示例(并发执行)

通过向解释器传入 { concurrency: "unbounded" },这些任务就可以并发运行,而不必互相等待。

import { Schema, SchemaGetter } from "effect"
import type { Duration } from "effect"
import { Effect } from "effect"

// Simulates an async task
const item = (id: number, duration: Duration.Input) =>
  Schema.String.pipe(
    Schema.decode({
      decode: SchemaGetter.checkEffect(() =>
        Effect.gen(function* () {
          yield* Effect.sleep(duration)
          console.log(`Task ${id} done`)
          return true
        }),
      ),
      encode: SchemaGetter.passthrough(),
    }),
  )

const Concurrent = Schema.Tuple([
  item(1, "30 millis"),
  item(2, "10 millis"),
  item(3, "20 millis"),
])

Effect.runPromise(
  Schema.decodeEffect(Concurrent, { concurrency: "unbounded" })([
    "a",
    "b",
    "c",
  ]),
)
/*
Output:
Task 2 done
Task 3 done
Task 1 done
*/

用 fallback 处理解码错误

Schema.catchDecoding 让你可以用 fallback 逻辑从解码问题中恢复。

type DecodingFallback<T> = (
  issue: SchemaIssue.Issue,
) => Effect.Effect<Option.Option<T>, SchemaIssue.Issue>

这个 Annotation 让你能够在解码失败时指定 fallback 行为,从而优雅地从错误中恢复。

示例(基本 fallback)

在这个基本示例中,当解码失败时(例如输入为 null),会返回 fallback 值而不是报错。

import { Schema } from "effect"
import { Effect } from "effect"

// Schema with a fallback value
const schema = Schema.String.pipe(
  Schema.catchDecoding(() => Effect.succeedSome("<fallback>")),
)

console.log(Schema.decodeUnknownSync(schema)("valid input"))
// Output: valid input

console.log(Schema.decodeUnknownSync(schema)(null))
// Output: <fallback>

示例(带日志的进阶 fallback)

在这个进阶示例中,当发生解码错误时,schema 会记录该 issue,然后返回一个 fallback 值。这展示了如何在错误处理过程中加入日志和其他副作用。

import { Schema } from "effect"
import { Effect } from "effect"

// Schema with logging and fallback
const schemaWithLog = Schema.String.pipe(
  Schema.catchDecoding((issue) =>
    Effect.gen(function* () {
      // Log the error issue
      yield* Effect.log(issue._tag)
      // Simulate a delay
      yield* Effect.sleep(10)
      // Return a fallback value
      return yield* Effect.succeedSome("<fallback>")
    }),
  ),
)

// Run the effectful fallback logic
Effect.runPromise(Schema.decodeUnknownEffect(schemaWithLog)(null)).then(
  console.log,
)
/*
Output:
timestamp=... level=INFO fiber=#0 message=InvalidType
<fallback>
*/

自定义 Annotation

除了内置 Annotation 之外,你还可以定义自定义 Annotation 来满足特定需求。例如,下面演示如何创建一个 deprecated Annotation:

示例(定义一个自定义 Annotation)

import { Schema } from "effect"

// Define a unique identifier for your custom annotation
const DeprecatedId = Symbol.for(
  "some/unique/identifier/for/your/custom/annotation",
)

// Apply the custom annotation to the schema
const MyString = Schema.String.annotate({ [DeprecatedId]: true })

为了让新的自定义 Annotation 具备类型安全,你可以使用 module augmentation。在下一个示例中,我们希望自定义 Annotation 是一个 boolean。

示例(为自定义 Annotation 添加类型安全)

import { Schema } from "effect"

const DeprecatedId = Symbol.for(
  "some/unique/identifier/for/your/custom/annotation",
)

// Module augmentation
declare module "effect/Schema" {
  namespace Annotations {
    interface Annotations {
      [DeprecatedId]?: boolean
    }
  }
}

const MyString = Schema.String.annotate({
  // @errors: 2418
  [DeprecatedId]: "bad value",
})

你可以使用 Schema.resolveAnnotations 辅助函数读取自定义 Annotation。

示例(读取一个自定义 Annotation)

import { Schema } from "effect"

const DeprecatedId = Symbol.for(
  "some/unique/identifier/for/your/custom/annotation",
)

declare module "effect/Schema" {
  namespace Annotations {
    interface Annotations {
      [DeprecatedId]?: boolean
    }
  }
}

const MyString = Schema.String.annotate({ [DeprecatedId]: true })

// Helper function to check if a schema is marked as deprecated
const isDeprecated = (schema: Schema.Top): boolean =>
  Schema.resolveAnnotations(schema)?.[DeprecatedId] ?? false

console.log(isDeprecated(Schema.String))
// Output: false

console.log(isDeprecated(MyString))
// Output: true