Skip to content

Latest commit

 

History

History
250 lines (198 loc) · 6.41 KB

File metadata and controls

250 lines (198 loc) · 6.41 KB
id schema-basic-async
title Basic Async Validation with Schema.filterEffect
category async-validation
skillLevel beginner
tags
schema
async-validation
effects
filtering
validation-rules
lessonOrder 6
rule
description
Basic Async Validation with Schema.filterEffect.
summary You have sync validation rules (format, length) handled by schema. But some rules require async operations: checking a username isn't taken, verifying a discount code is valid, calling an external...

Problem

You have sync validation rules (format, length) handled by schema. But some rules require async operations: checking a username isn't taken, verifying a discount code is valid, calling an external API. You need schemas that perform async validation without leaving the schema layer.

Solution

import { Schema, Effect } from "effect"

// ============================================
// 1. Basic async filter
// ============================================

const ValidUsername = Schema.String.pipe(
  Schema.minLength(3),
  Schema.maxLength(20),
  Schema.pattern(/^[a-zA-Z0-9_-]+$/),
  Schema.filterEffect((username) =>
    Effect.gen(function* () {
      // Simulate async check
      yield* Effect.sleep(Duration.millis(100))

      // Check if username is taken
      const isTaken = ["admin", "root", "system"].includes(username)

      if (isTaken) {
        return yield* Effect.fail(
          new Error(`Username "${username}" is already taken`)
        )
      }

      return username
    })
  )
)

// ============================================
// 2. Email with async verification
// ============================================

const VerifiedEmail = Schema.String.pipe(
  Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
  Schema.filterEffect((email) =>
    Effect.gen(function* () {
      // Simulate async email verification
      yield* Effect.sleep(Duration.millis(200))

      // Check against a denylist
      const bannedDomains = ["temp-mail.com", "10minutemail.com"]
      const [, domain] = email.split("@")

      if (bannedDomains.includes(domain)) {
        return yield* Effect.fail(
          new Error(`Email domain "${domain}" not allowed`)
        )
      }

      return email
    })
  )
)

// ============================================
// 3. Discount code validation
// ============================================

const ValidDiscountCode = Schema.String.pipe(
  Schema.toUpperCase(),
  Schema.filterEffect((code) =>
    Effect.gen(function* () {
      // Simulate database lookup
      yield* Effect.sleep(Duration.millis(150))

      const validCodes: Record<string, number> = {
        WELCOME10: 10,
        SAVE20: 20,
        VIP50: 50,
      }

      if (!validCodes[code]) {
        return yield* Effect.fail(
          new Error(`Discount code "${code}" is invalid`)
        )
      }

      return code
    })
  )
)

// ============================================
// 4. SignUp form with async validation
// ============================================

const SignUpForm = Schema.Struct({
  username: ValidUsername,
  email: VerifiedEmail,
  password: Schema.String.pipe(Schema.minLength(8)),
  discountCode: Schema.optional(ValidDiscountCode),
})

type SignUpForm = typeof SignUpForm.Type

// ============================================
// 5. Processing async validation
// ============================================

const validateSignup = (
  data: unknown
): Effect.Effect<SignUpForm, Error> =>
  Effect.gen(function* () {
    console.log("Validating signup form...")

    const result = yield* Effect.tryPromise({
      try: () => Schema.decodeUnknown(SignUpForm)(data),
      catch: (error) => {
        const msg = error instanceof Error ? error.message : String(error)
        return new Error(`Validation failed: ${msg}`)
      },
    })

    console.log("✅ All validations passed")
    return result
  })

// ============================================
// 6. Application logic
// ============================================

const appLogic = Effect.gen(function* () {
  console.log("=== Basic Async Validation ===\n")

  console.log("1. Valid signup:\n")

  const validData = {
    username: "alice_dev",
    email: "alice@example.com",
    password: "SecurePassword123",
    discountCode: "WELCOME10",
  }

  const result1 = yield* validateSignup(validData)
  console.log(`✓ User: ${result1.username}`)
  console.log(`✓ Email: ${result1.email}`)

  console.log("\n2. Invalid username (taken):\n")

  const invalidUsername = {
    username: "admin",
    email: "admin@example.com",
    password: "SecurePassword123",
  }

  const result2 = yield* validateSignup(invalidUsername).pipe(
    Effect.either
  )

  if (result2._tag === "Left") {
    console.log(`✗ Error: ${result2.left.message}`)
  }

  console.log("\n3. Invalid email domain:\n")

  const invalidEmail = {
    username: "bob_coder",
    email: "bob@temp-mail.com",
    password: "SecurePassword123",
  }

  const result3 = yield* validateSignup(invalidEmail).pipe(
    Effect.either
  )

  if (result3._tag === "Left") {
    console.log(`✗ Error: ${result3.left.message}`)
  }

  console.log("\n4. Invalid discount code:\n")

  const invalidCode = {
    username: "charlie",
    email: "charlie@example.com",
    password: "SecurePassword123",
    discountCode: "INVALID99",
  }

  const result4 = yield* validateSignup(invalidCode).pipe(
    Effect.either
  )

  if (result4._tag === "Left") {
    console.log(`✗ Error: ${result4.left.message}`)
  }

  return result1
})

const { Duration } = require("effect")

// Run application
Effect.runPromise(appLogic)
  .then(() => console.log("\n✅ Async validation complete"))
  .catch((error) => console.error(`Error: ${error.message}`))

Why This Works

Concept Explanation
filterEffect Run Effect during schema validation
Composable Chain sync validation with async filters
Error handling Async errors become validation errors
Type safe Output type same as input schema
Readable Validation logic near schema definition
Reusable Each filter is independently composable

When to Use

  • Username availability checks
  • Email verification
  • Discount code validation
  • API-based validation rules
  • Database lookup during parse
  • Rate limit checks
  • License verification

Related Patterns