Model Validation & Time Utilities Sprint: From Basic Models to Proper Validation Layer

A comprehensive weekend sprint retrospective documenting the transformation from basic OCaml models to proper validation infrastructure, enhanced field management, time utility implementation, and cryptographic security improvements.

Project: Chaufr – Personal drivers, on demand, in your own vehicle

This sprint documents the journey from basic models with minimal validation to sophisticated validation infrastructure with proper time handling, enhanced security, and proper field management.


Sprint Overview: The Model Validation & Time Infrastructure Transformation

Initial State (Pre-Sprint)

The model layer had fundamental architectural gaps:

Final State (Post-Sprint)

By sprint's end, we achieved:


Key Accomplishments

1. 🔍 Comprehensive Model Validation Infrastructure

Challenge: Models lacked proper validation, leading to potential data quality issues and runtime errors.

Solution: Implemented comprehensive validation functions for all domain models with standardized error handling.

User Model Enhancement

Before:

type t = {
  id : string option;
  name : string;  (* Single name field *)
  email : string;
  phone : string option;
}

After:

type t = {
  id : string option;
  first_name : string;
  last_name : string;  (* Split for better data modeling *)
  email : string;
  phone : string option;
  vehicle_description : string option;  (* New field for car owners *)
}
[@@deriving show, yojson]

(* Comprehensive validation functions *)
let validate_email email =
  let at_index = String.index_opt email '@' in
  match at_index with
  | Some idx ->
      let domain = String.sub email (idx + 1) (String.length email - idx - 1) in
      if String.contains domain '.' then Ok ()
      else Error "Invalid email format: missing '.' in domain"
  | None -> Error "Invalid email format: missing '@'"

let validate_phone phone =
  if String.length phone < 7 then
    Error "Phone number too short (must be at least 7 characters)"
  else if
    not
      (String.for_all
         (fun c ->
           (c >= '0' && c <= '9') || c = ' ' || c = '-' || c = '(' || c = ')')
         phone)
  then
    Error
      "Invalid phone format: only digits, spaces, hyphens, and parentheses allowed"
  else Ok ()

Driver Model Validation

type t = {
  id : string option;
  first_name : string;  (* Split from single name *)
  last_name : string;
  drivers_license_number : string;
  experience_years : int option;
  location : string option;
  is_available : bool;
  verified : bool;  (* New field for driver verification *)
  created_at : string;
}

(* Enhanced validation *)
let validate_license license =
  if String.trim license = "" then
    Error "License number cannot be empty"
  else if String.length license < 5 then
    Error "License number too short (minimum 5 characters)"
  else Ok ()

let validate (d : t) =
  let errors = [] in
  let errors =
    if String.trim d.first_name = "" then "First name cannot be empty" :: errors
    else errors
  in
  (* Additional validation logic... *)

Enhanced Password Model with Validation:

type t = {
  id : string option;
  owner_id : string;
  owner_type : string; (* "user" or "driver" *)
  password_hash : string;
  salt : string option;
  last_used : string option;
}

(* Security-focused validation *)
let validate_owner_type owner_type =
  match String.lowercase_ascii (String.trim owner_type) with
  | "user" | "driver" -> Ok ()
  | _ -> Error "Owner type must be either 'user' or 'driver'"

let validate_password_hash hash =
  if String.trim hash = "" then Error "Password hash cannot be empty"
  else if String.length hash < 32 then
    Error "Password hash too short (must be at least 32 characters)"
  else Ok ()

Benefits Achieved:

2. ⏰ Centralized Time Utility Infrastructure

Challenge: Inconsistent time handling across services with duplicate timestamp generation functions.

Solution: Implemented centralized time utilities with RFC3339 support and standardized timestamp handling.

New Time Module Implementation

(* lib/server/util/time.ml *)
let posix_now_ms () : Int64.t = 
  Int64.of_float (Unix.gettimeofday () *. 1000.0)

let timestamp_now_rfc3339 () : string = 
  Ptime.to_rfc3339 (Ptime_clock.now ())

let parse_rfc3339 (s : string) : Ptime.t option =
  match Ptime.of_rfc3339 s with
  | Ok (t, _tz, _frac) -> Some t
  | Error _ -> None

let ptime_to_rfc3339 ?(frac_s = 3) (t : Ptime.t) : string =
  Ptime.to_rfc3339 ~frac_s t

Service Layer Refactoring

Before (Duplicated across services):

(* In user_service.ml *)
let posix_now_ms () = Int64.of_float (Unix.gettimeofday () *. 1000.0)

(* In driver_service.ml *)
let posix_now_ms () = Int64.of_float (Unix.gettimeofday () *. 1000.0)

(* In ride_service.ml *)
let posix_now_ms () = Int64.of_float (Unix.gettimeofday () *. 1000.0)

After (Centralized):

(* All services now use: *)
let v7_monotonic = Uuidm.v7_monotonic_gen ~now_ms:Time.posix_now_ms rand_state

Enhanced Dependencies

(library
 (public_name chaufr.server.util)
 (name util)
 (libraries
  lwt
  argon2
  mirage-crypto-rng
  mirage-crypto
  uuidm
  ptime           ; RFC3339 time parsing/formatting
  ptime.clock.os) ; System time access
 (modules constants password_hash time))

Time Infrastructure Benefits:

3. 🔐 Enhanced Cryptographic Security

Challenge: Password hashing used basic random number generation with security vulnerabilities.

Solution: Implemented proper cryptographic RNG with mirage-crypto and enhanced Argon2 configuration.

Cryptographic RNG Implementation

Before (Insecure):

let random_bytes n =
  let b = Bytes.create n in
  (* NOTE: For development use a cryptographically secure RNG *)
  for i = 0 to n - 1 do
    Bytes.set b i (Char.chr (Random.int 256))
  done;
  Bytes.unsafe_to_string b

After (Secure):

(* Use mirage-crypto for RNG *)
let random_bytes n = Mirage_crypto_rng.generate n

Enhanced Argon2 Configuration

Security Parameter Updates:

let default_params =
  { m_cost = 19456;     (* Memory cost - optimized for security *)
    t_cost = 2;         (* Time cost - balanced performance *)
    parallelism = 1;    (* Single-threaded for consistency *)
    hash_len = 32;      (* 256-bit output *)
  }

Password Service Integration

let hash_password ~params ~password =
  Lwt.catch
    (fun () ->
      let salt_len = 32 in  (* Doubled salt length *)
      let salt = random_bytes salt_len in
      let encoded_len =
        Argon2.encoded_len ~t_cost:params.t_cost ~m_cost:params.m_cost
          ~parallelism:params.parallelism ~salt_len ~hash_len:params.hash_len
          ~kind
      in
      match
        Argon2.hash ~t_cost:params.t_cost ~m_cost:params.m_cost
          ~parallelism:params.parallelism ~pwd:password ~salt ~kind
          ~hash_len:params.hash_len ~encoded_len ~version
      with
      | Ok (_raw, encoded) -> Lwt.return (Ok encoded)
      | Error e ->
          Lwt.return (Error (Argon2_error (Argon2.ErrorCodes.message e))))
    (fun exn -> Lwt.return (Error (Internal (Printexc.to_string exn))))

Security Improvements:

4. 📊 Enhanced Model Mapping Infrastructure

Challenge: Model to database mapping lacked proper validation integration and robust error handling.

Solution: Implemented comprehensive model mapping with integrated validation and improved type safety.

Model Mapping Evolution

Enhanced Model Mappers with Validation:

(* lib/server/database/model_mappers.ml *)
type conversion_error = string list

let user_model_to_insert_params (u : User.t) :
    ( Uuidm.t * string * string * string * string option * string option,
      conversion_error )
    result =
  match User.validate u with
  | Ok () ->
      let id =
        match u.id with
        | Some s -> Option.get (Uuidm.of_string s)
        | None -> gen_uuid ()
      in
      Ok (id, u.first_name, u.last_name, u.email, u.phone, u.vehicle_description)
  | Error errs -> Error errs

Comprehensive Password Mapping

let password_model_to_insert_params (p : Password.t) :
    ( Uuidm.t * Uuidm.t * string * string * string option,
      conversion_error )
    result =
  match Password.validate p with
  | Ok () ->
      let id =
        match p.id with
        | Some s -> Option.get (Uuidm.of_string s)
        | None -> gen_uuid ()
      in
      let owner_id = Option.get (Uuidm.of_string p.owner_id) in
      Ok (id, owner_id, p.owner_type, p.password_hash, p.salt)
  | Error errs -> Error errs

let password_row_to_model
    { id; owner_id; owner_type; password_hash; salt; last_used; _ } : Password.t
    =
  {
    id = Some (Uuidm.to_string id);
    owner_id = Uuidm.to_string owner_id;
    owner_type;
    password_hash;
    salt;
    last_used;
  }

Database Query Integration

Updated Repository with Enhanced Queries:

(* lib/server/repository/password_queries.ml *)
let insert_password_q =
  Caqti_type.(t5 uuid uuid string string (option string) ->! uuid)
  @@ "INSERT INTO passwords (id, owner_id, owner_type, password_hash, salt) \
      VALUES ($1,$2,$3,$4,$5) RETURNING id"

let select_password_by_owner_q =
  Caqti_type.(
    t2 uuid string
    ->! t7 uuid uuid string string (option string) (option string) string)
  @@ "SELECT id, owner_id, owner_type, password_hash, salt, \
      to_char(last_used,'YYYY-MM-DD HH24:MI:SS'), \
      to_char(created_at,'YYYY-MM-DD HH24:MI:SS') FROM passwords WHERE \
      owner_id = $1 AND owner_type = $2"

Service Layer Integration

Password Service with Validation:

let create_password ~owner_id ~owner_type ~plain_password =
  match Uuidm.of_string owner_id with
  | None -> Lwt.return (Error (`Query_error "Invalid owner UUID"))
  | Some owner_uuid -> (
      owner_exists ~owner_id ~owner_type >>= fun exists ->
      if not exists then Lwt.return (Error (`Query_error "Owner not found"))
      else
        let id =
          match v7_monotonic () with
          | Some u -> u
          | None ->
              Uuidm.v7_non_monotonic_gen ~now_ms:Time.posix_now_ms rand_state ()
        in
        hash_password ~password:plain_password >>= fun hash_res ->
        match hash_res with
        | Ok encoded ->
            let password_hash = encoded in
            let salt = None in  (* Argon2 embeds salt *)
            PasswordQ.create ~id ~owner_id:owner_uuid ~owner_type ~password_hash ~salt
        (* Error handling continues... *)

Model Mapping Benefits:


Technical Deep Dive

Dependency Management Evolution

Before: Minimal Dependencies

depends: [
  "ocaml"
  "dune" {>= "3.20"}
  "dream"
  "argon2"
  "caqti-lwt"
  (* Basic dependencies *)
]

After: Enhanced Dependencies for Security & Time

depends: [
  "ocaml"
  "dune" {>= "3.20"}
  "dream"
  "mirage-crypto-rng"    (* Secure cryptographic RNG *)
  "argon2"               (* Password hashing *)
  "caqti-lwt"            (* Database connectivity *)
  "ptime"                (* RFC3339 time handling *)
  "caqti-driver-postgresql"
  "tyxml"
  "uuidm"
  "lwt"
  "lwt_ppx"
  "simple_dotenv"
  "yojson"
  "ppx_deriving"
  "ppx_deriving_yojson"
  "opentelemetry"
  "opentelemetry-client-ocurl"
  "opentelemetry-lwt"
  "alcotest"
  "alcotest-lwt"
]

Database Schema Evolution

New Migration (004_add_missing_columns):

-- 004_add_missing_columns_up.sql
ALTER TABLE users 
  DROP COLUMN IF EXISTS name,
  ADD COLUMN IF NOT EXISTS first_name VARCHAR(100),
  ADD COLUMN IF NOT EXISTS last_name VARCHAR(100),
  ADD COLUMN IF NOT EXISTS vehicle_description TEXT;

ALTER TABLE drivers 
  DROP COLUMN IF EXISTS name,
  ADD COLUMN IF NOT EXISTS first_name VARCHAR(100),
  ADD COLUMN IF NOT EXISTS last_name VARCHAR(100),
  ADD COLUMN IF NOT EXISTS verified BOOLEAN DEFAULT FALSE;

ALTER TABLE rides 
  ADD COLUMN IF NOT EXISTS vehicle_notes TEXT;

-- Update existing data with name splitting
UPDATE users SET 
  first_name = SPLIT_PART(name, ' ', 1),
  last_name = CASE 
    WHEN ARRAY_LENGTH(STRING_TO_ARRAY(name, ' '), 1) > 1 
    THEN SUBSTRING(name FROM POSITION(' ' IN name) + 1)
    ELSE ''
  END
WHERE first_name IS NULL OR last_name IS NULL;

Validation Strategy Patterns

1. Accumulator Pattern for Multiple Errors

let validate (u : User.t) =
  let errors = [] in
  let errors =
    if String.trim u.first_name = "" then "First name cannot be empty" :: errors
    else errors
  in
  let errors =
    if String.trim u.last_name = "" then "Last name cannot be empty" :: errors
    else errors
  in
  let errors =
    match validate_email u.email with
    | Ok () -> errors
    | Error e -> e :: errors
  in
  if errors = [] then Ok () else Error errors

2. Optional Field Validation

let errors =
  match u.phone with
  | Some phone -> (
      match validate_phone phone with
      | Ok () -> errors
      | Error e -> e :: errors)
  | None -> errors (* Phone is optional *)
in

3. UUID Validation Integration

let errors =
  match u.id with
  | Some id_str when Uuidm.of_string id_str = None ->
      "Invalid ID: must be a valid UUID" :: errors
  | _ -> errors
in

Performance and Architecture Considerations

Time Utility Performance

  1. Centralized Time Functions

    • Single source of truth for timestamp generation
    • Consistent UUID v7 monotonic ordering
    • Reduced function call overhead across services
  2. RFC3339 Standardization

    • Timezone-aware timestamp handling
    • Proper formatting for API responses
    • Database compatibility with PostgreSQL timestamptz

Security Architecture Improvements

  1. Cryptographic RNG Performance

    • Hardware-accelerated random number generation
    • Proper entropy for cryptographic operations
    • Enhanced security for password salt generation
  2. Argon2 Configuration Optimization

    • Balanced security vs performance parameters
    • Memory cost optimized for deployment environment
    • Configurable via environment variables

Model Validation Impact

  1. Type Safety Benefits

    • Compile-time validation structure verification
    • Runtime validation with meaningful error messages
    • Reduced database constraint violations
  2. Database Integrity

    • Field validation before database operations
    • Enhanced data quality through input validation
    • Reduced error handling complexity in upper layers

Lessons Learned and Best Practices

1. Validation Strategy Architecture

✅ What Worked:

⚠️ Challenges Faced:

2. Time Utility Centralization

✅ What Worked:

⚠️ Lessons Learned:

3. Cryptographic Security Implementation

✅ Best Practices Established:

⚠️ Security Considerations:

4. Model Evolution Strategy

✅ Migration Approach:


Current State Assessment

✅ Completed This Sprint

🎯 Next Sprint Priorities

  1. Testing Infrastructure Enhancement

    • Validation unit tests for all models
    • Time utility integration testing
    • Cryptographic security verification
  2. API Layer Integration

    • Request validation middleware
    • Error response standardization
    • API documentation updates
  3. Performance Optimization

    • Validation performance profiling
    • Database query optimization
    • Memory usage analysis

Key Takeaways

Technical Architecture Insights

  1. Validation Infrastructure Investment

    • Early validation infrastructure pays dividends in maintainability
    • Type-safe validation with meaningful errors improves developer experience
    • Integration at the model mapping layer catches issues before database operations
  2. Time and Security Centralization

    • Centralized utilities eliminate code duplication and inconsistencies
    • Proper cryptographic libraries are essential for security
    • RFC3339 standardization improves API interoperability
  3. Model Evolution Patterns

    • Database migrations enable safe model field evolution
    • Validation-first approach ensures data quality from the start
    • Comprehensive error handling improves system reliability

Conclusion

This weekend sprint transformed the Chaufr model layer from basic data structures to proper validation infrastructure. The integration of comprehensive validation, centralized time utilities, and enhanced cryptographic security creates a solid foundation for future development.

The approach to model enhancement—covering validation, field expansion, database migrations, and service integration—demonstrates the value of holistic infrastructure improvement. These changes not only improve current functionality but also establish patterns and utilities that will accelerate future development.


This sprint retrospective documents OCaml web development practices and can serve as a reference for similar model validation and infrastructure enhancement projects.

Hey, this site is part of ring.muhokama.fun!