Claude-skill-registry fsharp-validation
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fsharp-validation" ~/.claude/skills/majiayu000-claude-skill-registry-fsharp-validation && rm -rf "$T"
manifest:
skills/data/fsharp-validation/SKILL.mdsource content
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