Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/fix-schema-encoding-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"effect": patch
---

Fix Schema handling of encoded-side checks for container ASTs.

Checks added after `flip` are now preserved as `encodingChecks` across
`Declaration`, `Arrays`, `Objects`, and `Union`, even when rebuilding the AST
does not change child nodes. `toType` now projects those checks consistently,
and parsing applies encoded-side checks to the local encoded value when an
encoding chain is present without allowing encoded-side `parseOptions`
annotations to affect the current parser side.
29 changes: 11 additions & 18 deletions packages/effect/src/SchemaAST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ export class Declaration extends Base {
}
private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) {
const tps = mapOrSame(this.typeParameters, recur)
return tps === this.typeParameters ?
return tps === this.typeParameters && checks === this.checks && encodingChecks === this.encodingChecks ?
this :
new Declaration(tps, this.run, this.annotations, checks, undefined, this.context, encodingChecks)
}
Expand Down Expand Up @@ -1704,7 +1704,8 @@ export class Arrays extends Base {
private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) {
const elements = mapOrSame(this.elements, recur)
const rest = mapOrSame(this.rest, recur)
return elements === this.elements && rest === this.rest ?
return elements === this.elements && rest === this.rest && checks === this.checks &&
encodingChecks === this.encodingChecks ?
this :
new Arrays(
this.isMutable,
Expand Down Expand Up @@ -2245,7 +2246,8 @@ export class Objects extends Base {
: new IndexSignature(p, t, merge)
})

return props === this.propertySignatures && indexes === this.indexSignatures
return props === this.propertySignatures && indexes === this.indexSignatures && checks === this.checks &&
encodingChecks === this.encodingChecks
? this
: new Objects(
props,
Expand Down Expand Up @@ -2661,7 +2663,7 @@ export class Union<A extends AST = AST> extends Base {
}
private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) {
const types = mapOrSame(this.types, recur)
return types === this.types ?
return types === this.types && checks === this.checks && encodingChecks === this.encodingChecks ?
this :
new Union(types, this.mode, this.annotations, checks, undefined, this.context, encodingChecks)
}
Expand Down Expand Up @@ -2860,19 +2862,6 @@ export class Suspend extends Base {
}
}

/** @internal */
export function getEncodingChecks(ast: AST): Checks | undefined {
switch (ast._tag) {
case "Declaration":
case "Arrays":
case "Objects":
case "Union":
return ast.encodingChecks
default:
return undefined
}
}

// -----------------------------------------------------------------------------
// Checks
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -3392,9 +3381,13 @@ export const toType = memoize(<A extends AST>(ast: A): A => {
}
const out: any = ast
const type = out.recur?.(toType) ?? out
if (getEncodingChecks(type)) {
const encodingChecks = type.encodingChecks
if (encodingChecks) {
return modifyOwnPropertyDescriptors(type, (d) => {
d.encodingChecks.value = undefined
if (type === ast) {
d.checks.value = combineChecks(type.checks, encodingChecks)
}
})
}
return type
Expand Down
106 changes: 57 additions & 49 deletions packages/effect/src/SchemaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,11 +1007,14 @@ export interface Parser {
const recur = memoize(
(ast: SchemaAST.AST): Parser => {
let parser: Parser
const encodingChecks = SchemaAST.getEncodingChecks(ast)
const resolvedChecks = ast.checks ?? encodingChecks
const astOptions = (resolvedChecks ? resolvedChecks[resolvedChecks.length - 1].annotations : ast.annotations)
const checks = ast.checks
const encoding = ast.encoding
const links = encoding
const len = links?.length ?? 0
const encodingChecks = (ast as any).encodingChecks
const astOptions = (checks ? checks[checks.length - 1].annotations : ast.annotations)
?.["parseOptions"]
if (!ast.context && !ast.encoding && !ast.checks && !encodingChecks) {
if (!ast.context && !encoding && !checks && !encodingChecks) {
return (ou, options) => {
parser ??= ast.getParser(recur)
if (astOptions) {
Expand All @@ -1022,15 +1025,15 @@ const recur = memoize(
}
const isStructural = SchemaAST.isArrays(ast) || SchemaAST.isObjects(ast) ||
(SchemaAST.isDeclaration(ast) && ast.typeParameters.length > 0)
const structuralChecks = checks && isStructural ?
checks.filter((check) => check.annotations?.[SchemaAST.STRUCTURAL_ANNOTATION_KEY]) :
undefined
return (ou, options) => {
if (astOptions) {
options = { ...options, ...astOptions }
}
const encoding = ast.encoding
let srou: Effect.Effect<Option.Option<unknown>, SchemaIssue.Issue, unknown> | undefined
if (encoding) {
const links = encoding
const len = links.length
if (links) {
for (let i = len - 1; i >= 0; i--) {
const link = links[i]
const to = link.to
Expand All @@ -1047,58 +1050,63 @@ const recur = memoize(
}

parser ??= ast.getParser(recur)
let sroa = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options)
const parseLocal = (localOu: Option.Option<unknown>) => {
let sroa = parser(localOu, options)

if (encodingChecks && !options?.disableChecks) {
sroa = Effect.flatMapEager(sroa, (oa) => {
if (Option.isSome(ou) && Option.isSome(oa)) {
const issues: Array<SchemaIssue.Issue> = []
if (encodingChecks && !options?.disableChecks) {
sroa = Effect.flatMapEager(sroa, (oa) => {
if (Option.isSome(localOu) && Option.isSome(oa)) {
const issues: Array<SchemaIssue.Issue> = []

SchemaAST.collectIssues(encodingChecks, ou.value, issues, ast, options)
SchemaAST.collectIssues(encodingChecks, localOu.value, issues, ast, options)

if (Arr.isArrayNonEmpty(issues)) {
return Effect.fail(new SchemaIssue.Composite(ast, ou, issues))
if (Arr.isArrayNonEmpty(issues)) {
return Effect.fail(new SchemaIssue.Composite(ast, localOu, issues))
}
}
}
return Effect.succeed(oa)
})
}

if (ast.checks && !options?.disableChecks) {
const checks = ast.checks
if (options?.errors === "all" && isStructural && Option.isSome(ou)) {
sroa = mapSchemaIssueEffect(sroa, (issue) => {
const issues: Array<SchemaIssue.Issue> = []
SchemaAST.collectIssues(
checks.filter((check) => check.annotations?.[SchemaAST.STRUCTURAL_ANNOTATION_KEY]),
ou.value,
issues,
ast,
options
)
const out: SchemaIssue.Issue = Arr.isArrayNonEmpty(issues)
? issue._tag === "Composite" && issue.ast === ast
? new SchemaIssue.Composite(ast, issue.actual, [...issue.issues, ...issues])
: new SchemaIssue.Composite(ast, ou, [issue, ...issues])
: issue
return out
return Effect.succeed(oa)
})
}
sroa = Effect.flatMapEager(sroa, (oa) => {
if (Option.isSome(oa)) {
const value = oa.value
const issues: Array<SchemaIssue.Issue> = []

SchemaAST.collectIssues(checks, value, issues, ast, options)
if (checks && !options?.disableChecks) {
if (options?.errors === "all" && structuralChecks && structuralChecks.length > 0 && Option.isSome(localOu)) {
sroa = mapSchemaIssueEffect(sroa, (issue) => {
const issues: Array<SchemaIssue.Issue> = []
SchemaAST.collectIssues(
structuralChecks,
localOu.value,
issues,
ast,
options
)
const out: SchemaIssue.Issue = Arr.isArrayNonEmpty(issues)
? issue._tag === "Composite" && issue.ast === ast
? new SchemaIssue.Composite(ast, issue.actual, [...issue.issues, ...issues])
: new SchemaIssue.Composite(ast, localOu, [issue, ...issues])
: issue
return out
})
}
sroa = Effect.flatMapEager(sroa, (oa) => {
if (Option.isSome(oa)) {
const value = oa.value
const issues: Array<SchemaIssue.Issue> = []

SchemaAST.collectIssues(checks, value, issues, ast, options)

if (Arr.isArrayNonEmpty(issues)) {
return Effect.fail(new SchemaIssue.Composite(ast, oa, issues))
if (Arr.isArrayNonEmpty(issues)) {
return Effect.fail(new SchemaIssue.Composite(ast, oa, issues))
}
}
}
return Effect.succeed(oa)
})
return Effect.succeed(oa)
})
}

return sroa
}

const sroa = srou ? Effect.flatMapEager(srou, parseLocal) : parseLocal(ou)

return sroa
}
}
Expand Down
Loading
Loading