Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
make sure validation happens at the boundary (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Mar 23, 2023
1 parent c3a1230 commit dbb0a59
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 227 deletions.
2 changes: 1 addition & 1 deletion .changeset/breezy-eyes-wink.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@effect/schema": patch
"@effect/schema": minor
---

Only run effects when allowed
5 changes: 5 additions & 0 deletions .changeset/two-socks-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

Optimize internal validations
89 changes: 79 additions & 10 deletions benchmark/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,34 @@ import * as D from "@effect/io/Debug"
import * as P from "@effect/schema/Parser"
import * as t from "@effect/schema/Schema"
import * as Benchmark from "benchmark"
import { z } from "zod"

D.runtimeDebug.tracingEnabled = true

/*
io-ts
space-object (good) x 476,424 ops/sec ±0.45% (92 runs sampled)
space-object (bad) x 434,563 ops/sec ±0.58% (87 runs sampled)
0.3.0
parseEither (good) x 84,398 ops/sec ±1.93% (88 runs sampled)
parseEither (bad) x 205,431 ops/sec ±5.29% (80 runs sampled)
parseEither (good) x 96,300 ops/sec ±0.45% (91 runs sampled)
zod (good) x 168,570 ops/sec ±7.15% (80 runs sampled)
parseEither (bad) x 76,757 ops/sec ±2.38% (87 runs sampled)
zod (bad) x 56,287 ops/sec ±4.53% (84 runs sampled)
parseEither (bad2) x 75,451 ops/sec ±7.84% (84 runs sampled)
zod (bad2) x 77,259 ops/sec ±5.32% (84 runs sampled)
*/

const suite = new Benchmark.Suite()

const Vector = t.tuple(t.number, t.number, t.number)
const VectorZod = z.tuple([z.number(), z.number(), z.number()])

const Asteroid = t.struct({
type: t.literal("asteroid"),
location: Vector,
mass: t.number
})
const AsteroidZod = z.object({
type: z.literal("asteroid"),
location: VectorZod,
mass: z.number()
}).strict()

const Planet = t.struct({
type: t.literal("planet"),
Expand All @@ -31,20 +38,39 @@ const Planet = t.struct({
population: t.number,
habitable: t.boolean
})
const PlanetZod = z.object({
type: z.literal("planet"),
location: VectorZod,
mass: z.number(),
population: z.number(),
habitable: z.boolean()
}).strict()

const Rank = t.union(
t.literal("captain"),
t.literal("first mate"),
t.literal("officer"),
t.literal("ensign")
)
const RankZod = z.union([
z.literal("captain"),
z.literal("first mate"),
z.literal("officer"),
z.literal("ensign")
])

const CrewMember = t.struct({
name: t.string,
age: t.number,
rank: Rank,
home: Planet
})
const CrewMemberZod = z.object({
name: z.string(),
age: z.number(),
rank: RankZod,
home: PlanetZod
}).strict()

const Ship = t.struct({
type: t.literal("ship"),
Expand All @@ -53,10 +79,19 @@ const Ship = t.struct({
name: t.string,
crew: t.array(CrewMember)
})
const ShipZod = z.object({
type: z.literal("ship"),
location: VectorZod,
mass: z.number(),
name: z.string(),
crew: z.array(CrewMemberZod)
}).strict()

export const T = t.union(Asteroid, Planet, Ship)
export const schema = t.union(Asteroid, Planet, Ship)
export const schemaZod = z.discriminatedUnion("type", [AsteroidZod, PlanetZod, ShipZod])

export const parseEither = P.parseEither(T)
export const parseEither = P.parseEither(schema)
const options = { allErrors: true }

const good = {
type: "ship",
Expand Down Expand Up @@ -100,15 +135,49 @@ const bad = {
]
}

const bad2 = {
type: "ship",
location: [1, 2, 3],
mass: 4,
name: "foo",
crew: [
{
name: "bar",
age: 44,
rank: "captain",
home: {
type: "planet",
location: [5, 6, 7],
mass: 8,
population: 1000,
habitable: true
}
}
],
excess: 1
}

// console.log(parseEither(good))
// console.log(parseEither(bad))

suite
.add("parseEither (good)", function() {
parseEither(good)
parseEither(good, options)
})
.add("zod (good)", function() {
schemaZod.safeParse(good)
})
.add("parseEither (bad)", function() {
parseEither(bad)
parseEither(bad, options)
})
.add("zod (bad)", function() {
schemaZod.safeParse(bad)
})
.add("parseEither (bad2)", function() {
parseEither(bad2, options)
})
.add("zod (bad2)", function() {
schemaZod.safeParse(bad2)
})
.on("cycle", function(event: any) {
console.log(String(event.target))
Expand Down
59 changes: 22 additions & 37 deletions benchmark/union.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import type * as E from "@effect/data/Either"
import * as RA from "@effect/data/ReadonlyArray"
import * as D from "@effect/io/Debug"
import type { ParseError } from "@effect/schema/ParseResult"
import * as S from "@effect/schema/Schema"
import * as Benchmark from "benchmark"
import { z } from "zod"

D.runtimeDebug.tracingEnabled = true

/*
n = 3
parseEither (good) x 406,821 ops/sec ±0.48% (88 runs sampled)
parseManual (good) x 376,234 ops/sec ±4.45% (81 runs sampled)
parseEither (bad) x 407,671 ops/sec ±2.12% (87 runs sampled)
parseManual (bad) x 514,905 ops/sec ±0.51% (88 runs sampled)
n = 10
parseEither (good) x 403,275 ops/sec ±0.46% (88 runs sampled)
parseManual (good) x 369,469 ops/sec ±4.57% (79 runs sampled)
parseEither (bad) x 383,222 ops/sec ±0.57% (84 runs sampled)
parseManual (bad) x 473,157 ops/sec ±3.27% (85 runs sampled)
n = 100
parseEither (good) x 366,276 ops/sec ±1.94% (85 runs sampled)
parseManual (good) x 373,495 ops/sec ±4.38% (80 runs sampled)
parseEither (bad) x 322,847 ops/sec ±0.64% (86 runs sampled)
parseManual (bad) x 408,789 ops/sec ±2.73% (85 runs sampled)
parseEither (good) x 423,411 ops/sec ±0.82% (84 runs sampled)
zod (good) x 773,350 ops/sec ±7.44% (78 runs sampled)
parseEither (bad) x 363,213 ops/sec ±2.59% (89 runs sampled)
zod (bad) x 853,607 ops/sec ±1.29% (89 runs sampled)
*/

const suite = new Benchmark.Suite()
Expand All @@ -37,22 +26,18 @@ const members = RA.makeBy(n, (i) =>
}))
const schema = S.union(...members)

const parseEither = S.parseEither(schema)
const x = RA.makeBy(n, (i) =>
z.object({
kind: z.literal(i),
a: z.string(),
b: z.number(),
c: z.boolean()
}).strict())

const parseManual = (input: unknown): E.Either<ParseError, {
readonly kind: number
readonly a: string
readonly b: number
readonly c: boolean
}> => {
if (
typeof input === "object" && input !== null && "kind" in input && typeof input.kind === "number"
) {
const kind = input.kind
return S.parseEither(members[kind])(input)
}
return parseEither(input)
}
const schemaZod = z.discriminatedUnion("kind", x)

const parseEither = S.parseEither(schema)
const options = { allErrors: true }

const good = {
kind: n - 1,
Expand All @@ -73,16 +58,16 @@ const bad = {

suite
.add("parseEither (good)", function() {
parseEither(good)
parseEither(good, options)
})
.add("parseManual (good)", function() {
parseManual(good)
.add("zod (good)", function() {
schemaZod.safeParse(good)
})
.add("parseEither (bad)", function() {
parseEither(bad)
parseEither(bad, options)
})
.add("parseManual (bad)", function() {
parseManual(bad)
.add("zod (bad)", function() {
schemaZod.safeParse(good)
})
.on("cycle", function(event: any) {
console.log(String(event.target))
Expand Down
17 changes: 9 additions & 8 deletions benchmark/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import * as Benchmark from "benchmark"
import { z } from "zod"

/*
schema (good) x 401,457 ops/sec ±0.44% (89 runs sampled)
zod (good) x 400,149 ops/sec ±6.77% (81 runs sampled)
schema (bad) x 3,559,130 ops/sec ±2.83% (86 runs sampled)
zod (bad) x 45,989 ops/sec ±2.33% (88 runs sampled)
schema (good) x 107,508 ops/sec ±0.47% (87 runs sampled)
zod (good) x 407,693 ops/sec ±6.17% (77 runs sampled)
schema (bad) x 104,383 ops/sec ±2.04% (88 runs sampled)
zod (bad) x 102,018 ops/sec ±5.56% (79 runs sampled)
*/

const suite = new Benchmark.Suite()
Expand All @@ -23,7 +23,7 @@ const UserZod = z.object({
country: z.string().min(3).max(200),
zip: z.string().min(3).max(200)
})
})
}).strict()

const schema = S.struct({
name: pipe(S.string, S.minLength(3), S.maxLength(20)),
Expand Down Expand Up @@ -61,7 +61,8 @@ const bad = {
}
}

const parseEither = S.validateEither(schema)
const parseEither = S.parseEither(schema)
const options = { allErrors: true }

// console.log(UserZod.safeParse(good))
// console.log(parseEither(good))
Expand All @@ -70,13 +71,13 @@ const parseEither = S.validateEither(schema)

suite
.add("schema (good)", function() {
parseEither(good)
parseEither(good, options)
})
.add("zod (good)", function() {
UserZod.safeParse(good)
})
.add("schema (bad)", function() {
parseEither(bad)
parseEither(bad, options)
})
.add("zod (bad)", function() {
UserZod.safeParse(bad)
Expand Down
80 changes: 80 additions & 0 deletions benchmark/zod2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { pipe } from "@effect/data/Function"
// import * as D from "@effect/io/Debug"
import * as S from "@effect/schema/Schema"
import * as Benchmark from "benchmark"
import { z } from "zod"

/*
1)
schema (good) x 338,786 ops/sec ±0.55% (88 runs sampled)
zod (good) x 1,312,221 ops/sec ±6.42% (79 runs sampled)
schema (bad) x 373,497 ops/sec ±1.22% (89 runs sampled)
zod (bad) x 126,029 ops/sec ±3.76% (85 runs sampled)
2)
schema (good) x 616,053 ops/sec ±0.55% (89 runs sampled)
zod (good) x 1,237,098 ops/sec ±7.91% (76 runs sampled)
schema (bad) x 546,779 ops/sec ±0.63% (86 runs sampled)
zod (bad) x 127,494 ops/sec ±5.93% (83 runs sampled)
*/

const suite = new Benchmark.Suite()

const UserZod = z.object({
name: z.string().min(3).max(20),
age: z.number().min(0).max(120)
})

const schema = S.struct({
name: pipe(S.string, S.minLength(3), S.maxLength(20)),
age: pipe(S.number, S.greaterThanOrEqualTo(0), S.lessThanOrEqualTo(120))
})

// const UserZod = z.object({
// name: z.string().min(3),
// age: z.number()
// }).strict()

// const schema = S.struct({
// name: pipe(S.string, S.minLength(3)),
// age: pipe(S.number)
// })

const good = {
name: "Joe",
age: 13
}

const bad = {
name: "Jo",
age: 13
}

const parseEither = S.parseEither(schema)
const options = { allErrors: true }

// parseEither(good, options)
// console.log(UserZod.safeParse(good))
// console.log(parseEither(good))
// console.log(JSON.stringify(UserZod.safeParse(bad), null, 2))
// console.log(JSON.stringify(parseEither(bad), null, 2))

suite
.add("schema (good)", function() {
parseEither(good, options)
})
.add("zod (good)", function() {
UserZod.safeParse(good)
})
.add("schema (bad)", function() {
parseEither(bad, options)
})
.add("zod (bad)", function() {
UserZod.safeParse(bad)
})
.on("cycle", function(event: any) {
console.log(String(event.target))
})
.on("complete", function(this: any) {
console.log("Fastest is " + this.filter("fastest").map("name"))
})
.run({ async: true })
Loading

0 comments on commit dbb0a59

Please sign in to comment.