Claude Code Plugins

Community-maintained marketplace

Feedback

fsharp-validation

@heimeshoff/Cinemarco
1
0

|

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name fsharp-validation
description Create validation logic for F# backends with error accumulation and async validation. Use when: "add validation", "validate X", "input validation", "complex validation", "business rules", "check email", "required fields", "async validation", "database uniqueness", "cross-field validation", "validation errors". Creates validators in src/Server/Validation.fs. Use for complex scenarios; basic validation patterns are in fsharp-backend skill.
allowed-tools Read, Edit, Write, Grep

F# Validation Patterns

When to Use This Skill

Activate when:

  • User requests "add validation for X"
  • Implementing API endpoints (always validate at boundary)
  • Need complex validation rules
  • Validating create/update requests
  • Checking business rules or constraints

Core Principle

Validate at the API boundary, before any processing.

Basic Validator Helpers

Location: src/Server/Validation.fs

module Validation

open System
open System.Text.RegularExpressions

// Single field validators return Option<string>
// None = valid, Some errorMessage = invalid

let validateRequired (fieldName: string) (value: string) : string option =
    if String.IsNullOrWhiteSpace(value) then
        Some $"{fieldName} is required"
    else
        None

let validateLength (fieldName: string) (minLen: int) (maxLen: int) (value: string) : string option =
    let len = value.Length
    if len < minLen then
        Some $"{fieldName} must be at least {minLen} characters"
    elif len > maxLen then
        Some $"{fieldName} must be at most {maxLen} characters"
    else
        None

let validateRange (fieldName: string) (min: int) (max: int) (value: int) : string option =
    if value < min || value > max then
        Some $"{fieldName} must be between {min} and {max}"
    else
        None

let validateEmail (email: string) : string option =
    let emailPattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$"
    if Regex.IsMatch(email, emailPattern) then None
    else Some "Invalid email format"

let validateUrl (url: string) : string option =
    match Uri.TryCreate(url, UriKind.Absolute) with
    | true, _ -> None
    | false, _ -> Some "Invalid URL format"

let validatePositive (fieldName: string) (value: int) : string option =
    if value > 0 then None else Some $"{fieldName} must be positive"

let validateNonNegative (fieldName: string) (value: int) : string option =
    if value >= 0 then None else Some $"{fieldName} cannot be negative"

let validatePattern (fieldName: string) (pattern: string) (value: string) : string option =
    if Regex.IsMatch(value, pattern) then None
    else Some $"{fieldName} has invalid format"

Entity Validation

Multiple Errors (Accumulate All)

let validateTodoItem (item: TodoItem) : Result<TodoItem, string list> =
    let errors = [
        validateRequired "Title" item.Title
        validateLength "Title" 1 100 item.Title

        match item.Description with
        | Some desc -> validateLength "Description" 0 500 desc
        | None -> None

        validatePositive "Id" item.Id
    ] |> List.choose id

    if errors.IsEmpty then Ok item else Error errors

// Convert to single error string for API
let validateTodoItemString (item: TodoItem) : Result<TodoItem, string> =
    match validateTodoItem item with
    | Ok item -> Ok item
    | Error errors -> Error (String.concat "; " errors)

Conditional Validation

let validateUser (user: User) : Result<User, string list> =
    let errors = [
        validateRequired "Name" user.Name
        validateEmail (EmailAddress.value user.Email)

        // Only validate password if it's being changed
        if user.IsPasswordChange then
            yield! [
                validateLength "Password" 8 100 user.Password
                if not (Regex.IsMatch(user.Password, @"[A-Z]")) then
                    Some "Password must contain uppercase letter"
                if not (Regex.IsMatch(user.Password, @"[0-9]")) then
                    Some "Password must contain number"
            ] |> List.choose id
    ] |> List.choose id

    if errors.IsEmpty then Ok user else Error errors

Cross-Field Validation

let validateDateRange (start: DateTime) (endDate: DateTime) : string option =
    if endDate < start then
        Some "End date must be after start date"
    else
        None

let validateEvent (event: Event) : Result<Event, string list> =
    let errors = [
        validateRequired "Title" event.Title
        validateDateRange event.StartDate event.EndDate

        // Custom business rule
        if event.MaxParticipants < event.CurrentParticipants then
            Some "Max participants cannot be less than current participants"
        else
            None
    ] |> List.choose id

    if errors.IsEmpty then Ok event else Error errors

Request Validation

Create Request

type CreateTodoRequest = {
    Title: string
    Description: string option
    Priority: Priority
}

let validateCreateRequest (req: CreateTodoRequest) : Result<CreateTodoRequest, string list> =
    let errors = [
        validateRequired "Title" req.Title
        validateLength "Title" 1 100 req.Title

        match req.Description with
        | Some desc when not (String.IsNullOrWhiteSpace(desc)) ->
            validateLength "Description" 1 500 desc
        | _ -> None
    ] |> List.choose id

    if errors.IsEmpty then Ok req else Error errors

Update Request

let validateUpdateRequest (req: UpdateTodoRequest) : Result<UpdateTodoRequest, string list> =
    let errors = [
        validatePositive "Id" req.Id

        match req.Title with
        | Some title ->
            yield! [
                validateRequired "Title" title
                validateLength "Title" 1 100 title
            ] |> List.choose id
        | None -> ()
    ] |> List.choose id

    if errors.IsEmpty then Ok req else Error errors

Business Rules

let validateBusinessRule (order: Order) : Result<Order, string list> =
    let errors = [
        // Check inventory
        if order.Quantity > order.AvailableStock then
            Some "Insufficient stock"

        // Check minimum order
        if order.TotalAmount < 10.0m then
            Some "Minimum order amount is $10"

        // Check business hours
        let now = DateTime.Now
        if now.Hour < 9 || now.Hour > 17 then
            Some "Orders can only be placed during business hours (9 AM - 5 PM)"

        // Check discount eligibility
        if order.DiscountPercent > 0 && not order.Customer.IsEligibleForDiscount then
            Some "Customer is not eligible for discount"
    ] |> List.choose id

    if errors.IsEmpty then Ok order else Error errors

Async Validation (Database Checks)

let checkEmailUnique (email: string) : Async<string option> =
    async {
        let! existing = Persistence.getUserByEmail email
        return
            match existing with
            | Some _ -> Some "Email already registered"
            | None -> None
    }

let validateUserRegistration (req: RegisterRequest) : Async<Result<RegisterRequest, string list>> =
    async {
        // Sync validations first
        let syncErrors = [
            validateRequired "Username" req.Username
            validateLength "Username" 3 20 req.Username
            validateEmail req.Email
            validateLength "Password" 8 100 req.Password
        ] |> List.choose id

        if not syncErrors.IsEmpty then
            return Error syncErrors
        else
            // Async validations
            let! emailCheck = checkEmailUnique req.Email

            let asyncErrors = [emailCheck] |> List.choose id

            if asyncErrors.IsEmpty then
                return Ok req
            else
                return Error asyncErrors
    }

Integration with API

// src/Server/Api.fs
let todoApi : ITodoApi = {
    create = fun request -> async {
        // Validate request
        match Validation.validateCreateRequest request with
        | Error errors ->
            return Error (String.concat "; " errors)
        | Ok validRequest ->
            let todo = Domain.processNewTodo validRequest
            let! saved = Persistence.insertTodo todo
            return Ok saved
    }

    update = fun request -> async {
        match Validation.validateUpdateRequest request with
        | Error errors ->
            return Error (String.concat "; " errors)
        | Ok validRequest ->
            match! Persistence.getTodoById validRequest.Id with
            | None -> return Error "Todo not found"
            | Some existing ->
                let updated = Domain.updateTodo existing validRequest
                do! Persistence.updateTodo updated
                return Ok updated
    }
}

Testing Validation

// src/Tests/Server.Tests/ValidationTests.fs
module ValidationTests

open Expecto
open Validation

[<Tests>]
let tests =
    testList "Validation" [
        testCase "Valid todo passes" <| fun () ->
            let todo = validTodo
            let result = validateTodoItem todo
            Expect.isOk result "Should be valid"

        testCase "Missing title fails" <| fun () ->
            let todo = { validTodo with Title = "" }
            let result = validateTodoItem todo
            Expect.isError result "Should fail"

        testCase "Multiple errors accumulated" <| fun () ->
            let todo = { validTodo with Title = ""; Id = -1 }
            match validateTodoItem todo with
            | Error errors ->
                Expect.isGreaterThan errors.Length 1 "Should have multiple errors"
            | Ok _ -> failtest "Should have failed"
    ]

Best Practices

✅ Do

  • Validate at API boundary
  • Accumulate all errors
  • Return specific error messages
  • Use reusable validators
  • Test validation thoroughly

❌ Don't

  • Skip validation on updates
  • Return generic errors
  • Validate in domain logic
  • Let invalid data reach persistence
  • Use exceptions for validation

Verification Checklist

  • Validation helpers defined
  • Entity validators created
  • Required fields validated
  • Length/range constraints checked
  • Format validation (email, URL, etc.)
  • Business rules validated
  • Async validation if needed
  • Errors accumulated
  • Clear error messages
  • Integrated with API layer
  • Tests written

Related Skills

  • fsharp-backend - Integration with API
  • fsharp-shared - Type definitions
  • fsharp-tests - Testing validation