vld-leptos 0.3.0

Leptos integration for the vld validation library — shared validation for server functions and WASM clients
Documentation

vld-leptos

Leptos integration for the vld validation library.

Define validation rules once — use them on both server and client (WASM).

No direct dependency on leptos — works with any Leptos version (0.6, 0.7, 0.8+) and compiles for WASM targets.

Installation

[dependencies]
vld = "0.1"
vld-leptos = "0.1"
leptos = "0.7"  # or your version

Quick Start

1. Shared Validation Schemas

Define schema factories in a shared module compiled for both server and WASM:

// shared.rs
pub fn name_schema() -> vld::primitives::ZString {
    vld::string().min(2).max(50)
}

pub fn email_schema() -> vld::primitives::ZString {
    vld::string().email()
}

pub fn age_schema() -> vld::primitives::ZInt {
    vld::number().int().min(0).max(150)
}

2. Server Function Validation

Use validate_args! inside #[server] functions:

use leptos::prelude::*;

#[server]
async fn create_user(
    name: String,
    email: String,
    age: i64,
) -> Result<(), ServerFnError> {
    vld_leptos::validate_args! {
        name  => shared::name_schema(),
        email => shared::email_schema(),
        age   => shared::age_schema(),
    }
    .map_err(|e| ServerFnError::new(e.to_string()))?;

    // ... insert into database
    Ok(())
}

3. Client-Side Reactive Validation

Use check_field inside Leptos memos for instant feedback:

#[component]
fn CreateUserForm() -> impl IntoView {
    let (name, set_name) = signal(String::new());
    let (email, set_email) = signal(String::new());

    let name_err = Memo::new(move |_| {
        let v = name.get();
        if v.is_empty() { return None; } // don't validate empty
        vld_leptos::check_field(&v, &shared::name_schema())
    });

    let email_err = Memo::new(move |_| {
        let v = email.get();
        if v.is_empty() { return None; }
        vld_leptos::check_field(&v, &shared::email_schema())
    });

    view! {
        <form>
            <input
                type="text"
                placeholder="Name"
                on:input=move |ev| set_name.set(event_target_value(&ev))
            />
            <Show when=move || name_err.get().is_some()>
                <span class="error">{move || name_err.get().unwrap_or_default()}</span>
            </Show>

            <input
                type="email"
                placeholder="Email"
                on:input=move |ev| set_email.set(event_target_value(&ev))
            />
            <Show when=move || email_err.get().is_some()>
                <span class="error">{move || email_err.get().unwrap_or_default()}</span>
            </Show>

            <button type="submit">"Create"</button>
        </form>
    }
}

4. Server → Client Error Display

Parse structured errors returned from server functions:

#[component]
fn CreateUserForm() -> impl IntoView {
    let action = ServerAction::<CreateUser>::new();

    let server_errors = Memo::new(move |_| {
        action.value().get().and_then(|result| {
            result.err().and_then(|e| {
                vld_leptos::VldServerError::from_json(&e.to_string())
            })
        })
    });

    view! {
        <ActionForm action>
            <input type="text" name="name" />
            <Show when=move || {
                server_errors.get()
                    .map(|e| e.has_field_error("name"))
                    .unwrap_or(false)
            }>
                <span class="error">
                    {move || server_errors.get()
                        .and_then(|e| e.field_error("name").map(String::from))
                        .unwrap_or_default()}
                </span>
            </Show>

            <input type="email" name="email" />
            <button type="submit">"Create"</button>
        </ActionForm>
    }
}

API Reference

Error Types

Type Description
VldServerError Structured error with per-field messages, serializable for transport
FieldError Single field error: { field, message }

VldServerError Methods

Method Returns Description
validation(fields) VldServerError Create from a list of FieldError
internal(msg) VldServerError Create an internal error
field_error(name) Option<&str> First error message for a field
field_errors(name) Vec<&str> All error messages for a field
has_field_error(name) bool Check if a field has errors
error_fields() Vec<&str> All field names with errors
from_json(s) Option<Self> Parse from JSON string
to_string() String Serialize to JSON (via Display)

Validation Functions

Function Use Case
validate::<Schema, T>(data) Validate a Serialize struct against a vld::schema! type
validate_value::<Schema>(json) Validate a serde_json::Value directly
check_field(value, schema) Single-field check → Option<String> error
check_field_all(value, schema) Single-field check → Vec<String> all errors
check_all_fields::<Schema, T>(data) Multi-field check → Vec<FieldError>

Macros

Macro Description
validate_args! { field => schema, ... } Inline validation of server function arguments

Custom Error Type

For advanced use cases, wrap VldServerError in your own error enum:

use serde::{Deserialize, Serialize};
use server_fn::codec::JsonEncoding;
use leptos::server_fn::error::{FromServerFnError, ServerFnErrorErr};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AppError {
    Validation(vld_leptos::VldServerError),
    NotFound(String),
    Internal(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::Validation(e) => write!(f, "{}", e),
            AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
            AppError::Internal(msg) => write!(f, "Internal: {}", msg),
        }
    }
}

impl std::error::Error for AppError {}

impl FromServerFnError for AppError {
    type Encoder = JsonEncoding;
    fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
        AppError::Internal(value.to_string())
    }
}

#[server]
async fn create_user(name: String) -> Result<(), AppError> {
    vld_leptos::validate_args! {
        name => vld::string().min(2),
    }
    .map_err(AppError::Validation)?;
    Ok(())
}

Schema-Based Alternative

Instead of individual schema functions, use a vld::schema! struct:

vld::schema! {
    struct CreateUserSchema {
        name: String => vld::string().min(2).max(50),
        email: String => vld::string().email(),
    }
}

#[derive(Serialize)]
struct FormData { name: String, email: String }

// Server: validate full struct
let data = FormData { name, email };
vld_leptos::validate::<CreateUserSchema, _>(&data)?;

// Client: check all fields at once
let errors = vld_leptos::check_all_fields::<CreateUserSchema, _>(&data);

Running the Example

cargo run -p vld-leptos --example leptos_basic

License

MIT