t3code 仓库中 effect-smol 的 Schema.FilterOutput 扩展:自定义过滤器失败上报机制的完整解析
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本文以 t3code 仓库内 vendored 的参考仓库.repos/effect-smol中的 changeset 文档为骨架,完整解析 effect 包 Schema 模块中Schema.makeFilter谓词返回类型FilterOutput的扩展与新增类型Schema.FilterIssue。读完本文,你将掌握过滤器失败上报的全部合法返回形状、{ path, issue }与旧{ path, message }的破坏性迁移方式,以及多字段校验结果如何被归一化为Issue.Composite的源码级实现。
变更来源与在 t3code 仓库中的位置
t3code 主仓库通过.repos/目录内置了若干参考仓库,其中.repos/effect-smol是 effect 生态的精简版 monorepo,包含effect、schema相关的核心实现包。本次主题对应的变更说明文件位于:
.repos/effect-smol/.changeset/pre/expand-schema-filter-output.md
该 changeset 的 frontmatter 标注"effect": patch,即对effect包进行补丁级版本递增。其核心声明如下:
Schema: expand
FilterOutputand addFilterIssuefor richer filter failures.
也就是说,这是一次围绕「自定义校验过滤器如何向 Schema 解码失败系统上报错误」的 API 扩展:谓词函数可以返回的取值空间变大了,同时旧的{ path, message }对象形状被破坏性地重命名为{ path, issue }。
FilterOutput 的完整取值空间
在 effect-smol 的源码中,FilterOutput与FilterIssue定义在effect包的 Schema 模块里,见 Schema.ts:
/** 单个失败项,可单独返回,也是数组分支的元素类型 */ export type FilterIssue = string | SchemaIssue.Issue | { readonly path: ReadonlyArray<PropertyKey> readonly issue: string | SchemaIssue.Issue } /** makeFilter 谓词函数可返回的全部形状 */ export type FilterOutput = | undefined | boolean | FilterIssue | ReadonlyArray<FilterIssue>谓词函数本身由 makeFilter 构造:
export const makeFilter: <T>( filter: (input: T, ast: SchemaAST.AST, options: SchemaAST.ParseOptions) => FilterOutput, annotations?: Annotations.Filter | undefined, abort?: boolean ) => SchemaAST.Filter<T> = SchemaAST.makeFilter谓词接收已解码的输入值、当前 Schema AST和解析选项三参,返回值必须落在FilterOutput之中。各形状的语义(结合 Schema.ts 中的文档注释)可归纳为:
| 返回形状 | 语义 |
|---|---|
undefined/true | 校验通过 |
false | 通用失败,无自定义消息 |
string | 以该字符串作为失败消息 |
SchemaIssue.Issue | 直接返回一个完整构造好的 Issue |
{ path, issue } | 在嵌套路径处挂载失败(本次扩展后issue支持string \| SchemaIssue.Issue) |
ReadonlyArray<Schema.FilterIssue> | 一次性上报多个失败(本次新增的数组分支) |
其中「单失败形状(undefined、true、false、string、SchemaIssue.Issue)保持不变」这一点是 changeset 中明确声明的兼容性边界:所有旧代码若只使用这些形状,无需任何修改。
新增形状一:{ path, issue }支持挂载完整 Issue
changeset 指出,{ path, issue }这一分支中issue的取值从「仅字符串」扩展为string | SchemaIssue.Issue。源码文档注释(Schema.ts)给出了两种取值的归一化差异:
issue为string:会被包装进一个SchemaIssue.InvalidValue,且该包装遵循reportInput选项(即开启reportInput时会在错误中附带输入值);issue为SchemaIssue.Issue:原样返回,不做reportInput增强;- 无论哪种,最终结果都会再被包进一个位于给定
path的SchemaIssue.Pointer。
这个扩展的实际价值在于:以前想在嵌套字段路径上挂一个结构化的 Issue(例如带expected标注的InvalidValue),需要手动构造Pointer逐层包裹;现在直接在{ path, issue }里传入完整 Issue 即可。
makeFilter文档注释中的官方示例(Schema.ts)展示了「在嵌套路径上报失败」的用法:
import { Result, Schema } from "effect" const schema = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String }).check( Schema.makeFilter((o) => o.password === o.confirmPassword ? undefined : { path: ["password"], issue: "password and confirmPassword must match" } ) ) const result = Schema.decodeUnknownResult(schema)({ password: "123456", confirmPassword: "1234567" }) if (Result.isFailure(result) && result.failure.issue._tag === "Filter" && result.failure.issue.issue._tag === "Pointer") { result.failure.issue.issue.path // => ["password"] }解码失败时,错误树呈现为Filter包装Pointer,Pointer.path即谓词中指定的嵌套路径,这是校验结果可定位到具体字段的基础。
新增形状二:ReadonlyArray<FilterIssue>批量上报
changeset 对数组分支的三条规则做了精确约定,与源码实现逐条对应:
- 空数组 = 校验通过;
- 单元素数组 = 等价于直接返回该元素;
- 多元素数组 = 聚合为一个
Issue.Composite。
官方示例(Schema.ts)演示了跨字段的条件校验(「若a > 0则要求b、c同时大于 0」),并一次性收集所有违规:
import { Result, Schema } from "effect" const schema = Schema.Struct({ a: Schema.Finite, b: Schema.Finite, c: Schema.Finite }).check( Schema.makeFilter((o) => { const issues: Array<Schema.FilterIssue> = [] if (o.a > 0) { if (o.b <= 0) issues.push({ path: ["b"], issue: "b must be greater than 0" }) if (o.c <= 0) issues.push({ path: ["c"], issue: "c must be greater than 0" }) } return issues }) ) const result = Schema.decodeUnknownResult(schema)({ a: 1, b: 0, c: 0 }) if (Result.isFailure(result) && result.failure.issue._tag === "Filter" && result.failure.issue.issue._tag === "Composite") { result.failure.issue.issue.issues.map((issue) => issue._tag === "Pointer" ? issue.path : []) // => [["b"], ["c"]] }changeset 强调的动机是:此前实现多字段校验器需要显式导入SchemaIssue、手工new Composite(...);数组分支把这些样板代码吸收进了归一化逻辑,谓词只需要「push 失败项、return 数组」。
源码级实现:normalizeFilterOutput 的归一化路径
makeFilter只是薄封装,真正的工作发生在 AST 层。SchemaAST.makeFilter 把谓词输出直接委托给SchemaIssue.normalizeFilterOutput:
export function makeFilter<T>( filter: (input: T, ast: AST, options: ParseOptions) => Schema.FilterOutput, annotations?: Schema.Annotations.Filter | undefined, aborted: boolean = false ): Filter<T> { return new Filter( (input, ast, options) => SchemaIssue.normalizeFilterOutput(ast, filter(input, ast, options), input, options), annotations, aborted ) }归一化核心实现见 SchemaIssue.normalizeFilterOutput,其分支结构与 changeset 的三条规则一一对应:
export function normalizeFilterOutput( ast: SchemaAST.AST, out: Schema.FilterOutput, input?: unknown, options?: SchemaAST.ParseOptions ): Issue | undefined { if (Array.isArray(out)) { if (!Arr.isReadonlyArrayNonEmpty(out)) { return undefined // 空数组 => 成功 } return out.length === 1 ? makeFilterIssue(out[0], input, options) // 单元素 => 展开 : new Composite(ast, Arr.map(out, (entry) => makeFilterIssue(entry, input, options)), input, options) } return makeSingle(out as undefined | boolean | Schema.FilterIssue, input, options) }从源码结构看,数组分支先判空(非空数组才进入失败路径),长度为一时走单元素归一化,否则逐项归一化后构造Composite。单值分支则由makeSingle处理undefined/boolean/FilterIssue三种形状,{ path, issue }在此阶段被展开为Pointer包装——这与前文「挂载嵌套路径失败」示例中错误树出现_tag === "Pointer"完全吻合。
另外两个与makeFilter行为相关的细节同样来自文档注释(Schema.ts):
annotations参数作用于过滤器本身;默认格式化器对失败的取用顺序是message优先、expected次之,两者都缺省时显示<filter>;abort为true时,该过滤器失败后解析立即停止,不再收集后续 check 的失败。
破坏性重命名:{ path, message }→{ path, issue }
changeset 明确标注Breaking的部分是对象形状的字段重命名,迁移是机械式的:
// before Schema.makeFilter((o) => ({ path: ["a"], message: "bad" })) // after Schema.makeFilter((o) => ({ path: ["a"], issue: "bad" }))需要改动的是所有使用旧对象形状的调用点:把返回对象里的message键改名为issue,若原本就是字符串消息,值本身可以原样保留。
changeset 末尾还指出,同一重命名同步应用到了SchemaGetter.checkEffect的合法返回类型上。源码中该约束体现在 SchemaGetter.ts:
export function checkEffect<T, R = never>( f: (input: T, options: SchemaAST.ParseOptions) => Effect.Effect< undefined | boolean | Schema.FilterIssue, never, R > ): Getter<T, T, R> {可以看到checkEffect的 Effect 成功通道直接以Schema.FilterIssue为元素类型(外加undefined | boolean),即异步 getter 校验复用了同一套失败项定义,{ path, message }旧形状在两条 API 路径上必须一致迁移。
迁移与验证清单
结合 changeset 与源码,升级到新 API 时可以按以下清单操作:
- 全局搜索返回对象中的
message:键(限定在makeFilter谓词与SchemaGetter.checkEffect上下文),将其改为issue:; - 纯字符串/布尔返回值的谓词无需改动;
- 若旧代码曾手工导入
SchemaIssue并new Composite(...)聚合多字段失败,可以改写为ReadonlyArray<Schema.FilterIssue>数组返回,删除手工聚合逻辑; - 验证时可用
Schema.decodeUnknownResult检查失败 Issue 的_tag是否为预期的Filter→Pointer(单路径失败)或Filter→Composite(数组多失败),与上文官方示例的断言方式一致; - 注意
{ path, issue }中直接传SchemaIssue.Issue时不再享受reportInput增强——若依赖错误输出携带输入值,应保持issue为字符串或自行构造含输入信息的 Issue。
参考文件
- 变更说明:.repos/effect-smol/.changeset/pre/expand-schema-filter-output.md
FilterIssue/FilterOutput类型与makeFilter:.repos/effect-smol/packages/effect/src/Schema.ts- AST 层过滤器构造:.repos/effect-smol/packages/effect/src/SchemaAST.ts
- 输出归一化实现:.repos/effect-smol/packages/effect/src/SchemaIssue.ts
- 异步 getter 校验返回类型:.repos/effect-smol/packages/effect/src/SchemaGetter.ts
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考