a database layer insipred by caqti and ecto
at main 12 kB view raw
1type change = 2 | StringChange of string 3 | IntChange of int 4 | FloatChange of float 5 | BoolChange of bool 6 | NullChange 7 8module StringMap = Map.Make (String) 9module StringSet = Set.Make (String) 10 11type 'a t = { 12 data : 'a; 13 changes : change StringMap.t; 14 errors : Error.validation_error list; 15 valid : bool; 16 action : action option; 17 constraints : constraint_def list; 18} 19 20and action = Insert | Update | Delete 21 22and constraint_def = { 23 constraint_name : string; 24 constraint_field : string; 25 constraint_type : constraint_type; 26} 27 28and constraint_type = 29 | UniqueConstraint 30 | ForeignKeyConstraint of { table : string; column : string } 31 | CheckConstraint of string 32 33let create data = 34 { 35 data; 36 changes = StringMap.empty; 37 errors = []; 38 valid = true; 39 action = None; 40 constraints = []; 41 } 42 43let change data = create data 44 45let for_insert data = 46 { 47 data; 48 changes = StringMap.empty; 49 errors = []; 50 valid = true; 51 action = Some Insert; 52 constraints = []; 53 } 54 55let for_update data = 56 { 57 data; 58 changes = StringMap.empty; 59 errors = []; 60 valid = true; 61 action = Some Update; 62 constraints = []; 63 } 64 65let cast params ~fields t = 66 let allowed = 67 List.fold_left 68 (fun acc f -> StringSet.add (Field.name f) acc) 69 StringSet.empty fields 70 in 71 let changes = 72 List.fold_left 73 (fun acc (k, v) -> 74 if StringSet.mem k allowed then StringMap.add k (StringChange v) acc 75 else acc) 76 StringMap.empty params 77 in 78 { t with changes } 79 80let cast_assoc assoc ~fields t = 81 let allowed = 82 List.fold_left 83 (fun acc f -> StringSet.add (Field.name f) acc) 84 StringSet.empty fields 85 in 86 let changes = 87 List.fold_left 88 (fun acc (k, v) -> 89 if StringSet.mem k allowed then StringMap.add k v acc else acc) 90 StringMap.empty assoc 91 in 92 { t with changes } 93 94let put_change field value t = 95 let name = Field.name field in 96 { t with changes = StringMap.add name (StringChange value) t.changes } 97 98let delete_change field t = 99 let name = Field.name field in 100 { t with changes = StringMap.remove name t.changes } 101 102let get_change t field = 103 let name = Field.name field in 104 match StringMap.find_opt name t.changes with 105 | Some (StringChange s) -> Some s 106 | _ -> None 107 108let get_field t field = Field.get field t.data 109 110let add_error ~field ~message ~validation t = 111 let error = Error.{ field; message; validation } in 112 { t with errors = error :: t.errors; valid = false } 113 114let validate_required fields t = 115 List.fold_left 116 (fun acc field -> 117 let name = Field.name field in 118 match StringMap.find_opt name acc.changes with 119 | Some (StringChange s) when s <> "" -> acc 120 | Some (StringChange _) | Some NullChange | None -> 121 add_error ~field:name ~message:"can't be blank" ~validation:"required" 122 acc 123 | Some _ -> acc) 124 t fields 125 126let validate_format field ~pattern t = 127 let name = Field.name field in 128 match StringMap.find_opt name t.changes with 129 | None -> t 130 | Some (StringChange value) -> 131 let re = Re.Pcre.regexp pattern in 132 if Re.execp re value then t 133 else 134 add_error ~field:name ~message:"has invalid format" ~validation:"format" 135 t 136 | _ -> t 137 138let validate_length field ?min ?max ?is t = 139 let name = Field.name field in 140 match StringMap.find_opt name t.changes with 141 | None -> t 142 | Some (StringChange value) -> ( 143 let len = String.length value in 144 let t = 145 match min with 146 | Some m when len < m -> 147 add_error ~field:name 148 ~message:(Printf.sprintf "should be at least %d character(s)" m) 149 ~validation:"length" t 150 | _ -> t 151 in 152 let t = 153 match max with 154 | Some m when len > m -> 155 add_error ~field:name 156 ~message:(Printf.sprintf "should be at most %d character(s)" m) 157 ~validation:"length" t 158 | _ -> t 159 in 160 match is with 161 | Some expected when len <> expected -> 162 add_error ~field:name 163 ~message:(Printf.sprintf "should be %d character(s)" expected) 164 ~validation:"length" t 165 | _ -> t) 166 | _ -> t 167 168let validate_inclusion field ~values t = 169 let name = Field.name field in 170 match StringMap.find_opt name t.changes with 171 | None -> t 172 | Some (StringChange value) -> 173 if List.mem value values then t 174 else add_error ~field:name ~message:"is invalid" ~validation:"inclusion" t 175 | _ -> t 176 177let validate_exclusion field ~values t = 178 let name = Field.name field in 179 match StringMap.find_opt name t.changes with 180 | None -> t 181 | Some (StringChange value) -> 182 if not (List.mem value values) then t 183 else 184 add_error ~field:name ~message:"is reserved" ~validation:"exclusion" t 185 | _ -> t 186 187let validate_number field ?greater_than ?less_than ?greater_than_or_equal 188 ?less_than_or_equal t = 189 let name = Field.name field in 190 match StringMap.find_opt name t.changes with 191 | None -> t 192 | Some (StringChange value) -> ( 193 match int_of_string_opt value with 194 | None -> 195 add_error ~field:name ~message:"is not a valid number" 196 ~validation:"number" t 197 | Some n -> ( 198 let t = 199 match greater_than with 200 | Some m when n <= m -> 201 add_error ~field:name 202 ~message:(Printf.sprintf "must be greater than %d" m) 203 ~validation:"number" t 204 | _ -> t 205 in 206 let t = 207 match less_than with 208 | Some m when n >= m -> 209 add_error ~field:name 210 ~message:(Printf.sprintf "must be less than %d" m) 211 ~validation:"number" t 212 | _ -> t 213 in 214 let t = 215 match greater_than_or_equal with 216 | Some m when n < m -> 217 add_error ~field:name 218 ~message: 219 (Printf.sprintf "must be greater than or equal to %d" m) 220 ~validation:"number" t 221 | _ -> t 222 in 223 match less_than_or_equal with 224 | Some m when n > m -> 225 add_error ~field:name 226 ~message:(Printf.sprintf "must be less than or equal to %d" m) 227 ~validation:"number" t 228 | _ -> t)) 229 | Some (IntChange n) -> 230 let t = 231 match greater_than with 232 | Some m when n <= m -> 233 add_error ~field:name 234 ~message:(Printf.sprintf "must be greater than %d" m) 235 ~validation:"number" t 236 | _ -> t 237 in 238 let t = 239 match less_than with 240 | Some m when n >= m -> 241 add_error ~field:name 242 ~message:(Printf.sprintf "must be less than %d" m) 243 ~validation:"number" t 244 | _ -> t 245 in 246 t 247 | _ -> t 248 249let validate_acceptance field t = 250 let name = Field.name field in 251 match StringMap.find_opt name t.changes with 252 | None -> 253 add_error ~field:name ~message:"must be accepted" ~validation:"acceptance" 254 t 255 | Some (BoolChange true) -> t 256 | Some (StringChange "true") -> t 257 | Some (StringChange "1") -> t 258 | _ -> 259 add_error ~field:name ~message:"must be accepted" ~validation:"acceptance" 260 t 261 262let validate_confirmation field ~confirmation_field t = 263 let name = Field.name field in 264 let conf_name = Field.name confirmation_field in 265 let value = StringMap.find_opt name t.changes in 266 let conf_value = StringMap.find_opt conf_name t.changes in 267 match (value, conf_value) with 268 | Some v1, Some v2 when v1 = v2 -> t 269 | Some _, Some _ -> 270 add_error ~field:conf_name ~message:"does not match" 271 ~validation:"confirmation" t 272 | _ -> t 273 274let validate_change field validator t = 275 match get_change t field with 276 | None -> t 277 | Some value -> ( 278 match validator value with 279 | Ok () -> t 280 | Error msg -> 281 add_error ~field:(Field.name field) ~message:msg ~validation:"custom" 282 t) 283 284let validate t f = f t 285 286let unique_constraint field t = 287 let name = Field.name field in 288 let constraint_def = 289 { 290 constraint_name = name ^ "_unique"; 291 constraint_field = name; 292 constraint_type = UniqueConstraint; 293 } 294 in 295 { t with constraints = constraint_def :: t.constraints } 296 297let foreign_key_constraint field ~references:(table, column) t = 298 let name = Field.name field in 299 let constraint_def = 300 { 301 constraint_name = name ^ "_fkey"; 302 constraint_field = name; 303 constraint_type = ForeignKeyConstraint { table; column }; 304 } 305 in 306 { t with constraints = constraint_def :: t.constraints } 307 308let check_constraint ~name field ~expression t = 309 let field_name = Field.name field in 310 let constraint_def = 311 { 312 constraint_name = name; 313 constraint_field = field_name; 314 constraint_type = CheckConstraint expression; 315 } 316 in 317 { t with constraints = constraint_def :: t.constraints } 318 319let is_valid t = t.valid 320let errors t = t.errors 321let data t = t.data 322let changes t = StringMap.bindings t.changes 323let changes_map t = t.changes 324let action t = t.action 325 326let get_error t field = 327 let name = Field.name field in 328 List.find_opt (fun e -> e.Error.field = name) t.errors 329 330let has_error t field = Option.is_some (get_error t field) 331 332let traverse_errors t f = 333 List.iter (fun err -> f err.Error.field err.Error.message) t.errors 334 335let error_messages t = 336 List.map 337 (fun err -> Printf.sprintf "%s %s" err.Error.field err.Error.message) 338 t.errors 339 340let apply_action t = if t.valid then Ok t.data else Error t.errors 341 342type 'a assoc_changeset = { 343 assoc_name : string; 344 assoc_changesets : 'a t list; 345 on_replace : [ `Raise | `Mark_as_invalid | `Delete | `Update ]; 346} 347 348let cast_assoc_one ~assoc_name ~params ~cast_fn t = 349 match List.assoc_opt assoc_name params with 350 | None -> t 351 | Some assoc_params -> 352 let child_changeset = cast_fn assoc_params in 353 if is_valid child_changeset then t 354 else 355 let child_errors = errors child_changeset in 356 let prefixed_errors = 357 List.map 358 (fun err -> 359 Error. 360 { 361 field = assoc_name ^ "." ^ err.field; 362 message = err.message; 363 validation = err.validation; 364 }) 365 child_errors 366 in 367 { t with errors = prefixed_errors @ t.errors; valid = false } 368 369let cast_assoc_many ~assoc_name ~params_list ~cast_fn t = 370 let process_one idx params = 371 let child_changeset = cast_fn params in 372 if is_valid child_changeset then [] 373 else 374 let child_errors = errors child_changeset in 375 List.map 376 (fun err -> 377 Error. 378 { 379 field = Printf.sprintf "%s[%d].%s" assoc_name idx err.field; 380 message = err.message; 381 validation = err.validation; 382 }) 383 child_errors 384 in 385 let all_errors = List.concat (List.mapi process_one params_list) in 386 if all_errors = [] then t 387 else { t with errors = all_errors @ t.errors; valid = false } 388 389let put_assoc ~assoc_name ~json_string t = 390 let change = StringChange json_string in 391 { t with changes = StringMap.add assoc_name change t.changes } 392 393let cast_embed ~embed_name ~params ~parse t = 394 match List.assoc_opt embed_name params with 395 | None -> t 396 | Some embed_params -> ( 397 match parse embed_params with 398 | Ok _embedded -> t 399 | Error msg -> 400 add_error ~field:embed_name ~message:msg ~validation:"embedded" t) 401 402let put_embed ~embed_name ~json_string t = 403 let change = StringChange json_string in 404 { t with changes = StringMap.add embed_name change t.changes } 405 406let merge_errors parent_changeset child_changeset ~prefix = 407 let child_errors = errors child_changeset in 408 let prefixed_errors = 409 List.map 410 (fun err -> 411 Error. 412 { 413 field = prefix ^ "." ^ err.field; 414 message = err.message; 415 validation = err.validation; 416 }) 417 child_errors 418 in 419 { 420 parent_changeset with 421 errors = prefixed_errors @ parent_changeset.errors; 422 valid = parent_changeset.valid && is_valid child_changeset; 423 }