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:
-
Basic models with no validation infrastructure
(
User
,Driver
,Ride
,Password
) - Time handling inconsistencies across services with duplicate timestamp functions
- Cryptographic security gaps with basic random number generation
- Model field limitations with incomplete data representation
- Database mapping issues between models and database representations
Final State (Post-Sprint)
By sprint's end, we achieved:
- Comprehensive validation infrastructure with standardized error handling
- Centralized time utilities with RFC3339 support and standardized timestamps
- Enhanced cryptographic security with proper RNG and improved password hashing
- Extended model fields with better data representation
- Robust model mapping with validation integration
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:
- ✅ Type-safe validation with meaningful error messages
- ✅ Comprehensive field validation across all models
- ✅ Better data modeling with split names and additional fields
- ✅ Security-focused validation for sensitive data
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:
- ✅ Standardized RFC3339 timestamp handling
- ✅ Eliminated code duplication across services
- ✅ Proper timezone-aware time handling
- ✅ Consistent UUID v7 generation with monotonic ordering
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:
- ✅ Cryptographically secure random number generation
- ✅ Doubled salt length for enhanced security
- ✅ Proper Argon2 parameter configuration
- ✅ Enhanced error handling for cryptographic operations
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:
- ✅ Integrated validation at the mapping layer
- ✅ Type-safe conversions with proper error handling
- ✅ Enhanced password model with comprehensive fields
- ✅ Standardized conversion patterns across all models
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
-
Centralized Time Functions
- Single source of truth for timestamp generation
- Consistent UUID v7 monotonic ordering
- Reduced function call overhead across services
-
RFC3339 Standardization
- Timezone-aware timestamp handling
- Proper formatting for API responses
- Database compatibility with PostgreSQL timestamptz
Security Architecture Improvements
-
Cryptographic RNG Performance
- Hardware-accelerated random number generation
- Proper entropy for cryptographic operations
- Enhanced security for password salt generation
-
Argon2 Configuration Optimization
- Balanced security vs performance parameters
- Memory cost optimized for deployment environment
- Configurable via environment variables
Model Validation Impact
-
Type Safety Benefits
- Compile-time validation structure verification
- Runtime validation with meaningful error messages
- Reduced database constraint violations
-
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:
- Accumulator pattern for collecting multiple validation errors
- Separate validation functions for complex fields (email, phone)
- Integration of validation at the model mapping layer
- Meaningful error messages for client applications
⚠️ Challenges Faced:
- Initial approach of throwing exceptions vs returning Result types
- Balancing validation complexity with performance requirements
- Managing validation consistency across similar fields in different models
2. Time Utility Centralization
✅ What Worked:
- Single module for all time-related operations
- RFC3339 standardization for API compatibility
- Elimination of duplicated timestamp functions across services
- Consistent UUID v7 generation with proper time sources
⚠️ Lessons Learned:
- Early investment in time infrastructure prevents future refactoring
- Timezone considerations are crucial for global applications
- Centralized utilities improve maintainability significantly
3. Cryptographic Security Implementation
✅ Best Practices Established:
- Use dedicated cryptographic libraries (mirage-crypto-rng)
- Proper parameter configuration for Argon2
- Enhanced salt generation for improved security
- Environment-configurable security parameters
⚠️ Security Considerations:
- Balance between security and performance in hash parameters
- Proper RNG initialization and entropy management
- Regular security parameter review as computational power increases
4. Model Evolution Strategy
✅ Migration Approach:
- Database migrations handle existing data transformation
- Backward-compatible field additions where possible
- Comprehensive testing of model validation logic
- Clear migration scripts for deployment
Current State Assessment
✅ Completed This Sprint
- [x] Comprehensive Model Validation - User, Driver, Ride, Password models with full validation
- [x] Centralized Time Utilities - RFC3339 support, consistent timestamp handling
- [x] Enhanced Cryptographic Security - mirage-crypto-rng, improved Argon2 configuration
- [x] Model Field Enhancements - Split names, vehicle descriptions, verification fields
- [x] Database Schema Updates - Migration 004 with data transformation
- [x] Service Layer Integration - Updated all services to use new infrastructure
🎯 Next Sprint Priorities
-
Testing Infrastructure Enhancement
- Validation unit tests for all models
- Time utility integration testing
- Cryptographic security verification
-
API Layer Integration
- Request validation middleware
- Error response standardization
- API documentation updates
-
Performance Optimization
- Validation performance profiling
- Database query optimization
- Memory usage analysis
Key Takeaways
Technical Architecture Insights
-
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
-
Time and Security Centralization
- Centralized utilities eliminate code duplication and inconsistencies
- Proper cryptographic libraries are essential for security
- RFC3339 standardization improves API interoperability
-
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.