OCaml bindings to the Typesense embeddings search API
at main 107 lines 3.4 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** Typesense API error handling. 7 8 Typesense returns errors in a simple format: {"message": "..."} *) 9 10(** Typesense error response *) 11type t = { 12 message : string; 13 status_code : int; 14} 15 16let error_jsont = 17 Jsont.Object.map ~kind:"TypesenseError" 18 (fun message -> message) 19 |> Jsont.Object.mem "message" Jsont.string ~enc:Fun.id 20 |> Jsont.Object.skip_unknown 21 |> Jsont.Object.finish 22 23(** Parse an API error into a structured Typesense error. *) 24let of_api_error (e : Openapi.Runtime.api_error) : t option = 25 match Jsont_bytesrw.decode_string error_jsont e.body with 26 | Ok message -> Some { message; status_code = e.status } 27 | Error _ -> None 28 29(** {1 Styled Output Helpers} *) 30 31(** Style for error labels (red, bold) *) 32let error_style = Fmt.(styled (`Fg `Red) (styled `Bold string)) 33 34(** Style for status codes *) 35let status_style status = 36 if status >= 500 then Fmt.(styled (`Fg `Red) int) 37 else if status >= 400 then Fmt.(styled (`Fg `Yellow) int) 38 else Fmt.(styled (`Fg `Green) int) 39 40(** Pretty-print a Typesense API error with colors. *) 41let pp ppf (e : t) = 42 Fmt.pf ppf "%s [%a]" e.message (status_style e.status_code) e.status_code 43 44(** Convert to a human-readable string (without colors). *) 45let to_string (e : t) : string = 46 Printf.sprintf "%s [%d]" e.message e.status_code 47 48(** Check if this is an authentication/authorization error. *) 49let is_auth_error (e : t) = 50 e.status_code = 401 || e.status_code = 403 51 52(** Check if this is a "not found" error. *) 53let is_not_found (e : t) = 54 e.status_code = 404 55 56(** Handle an exception, printing a nice error message if it's an API error. 57 58 Returns an exit code: 59 - 1 for most API errors 60 - 77 for authentication errors (permission denied) 61 - 69 for not found errors *) 62let handle_exn exn = 63 match exn with 64 | Openapi.Runtime.Api_error e -> 65 (match of_api_error e with 66 | Some err -> 67 Fmt.epr "%a %a@." error_style "Error:" pp err; 68 if is_auth_error err then 77 69 else if is_not_found err then 69 70 else 1 71 | None -> 72 (* Not a Typesense error, show raw response *) 73 Fmt.epr "%a %s %s returned %a@.%s@." 74 error_style "API Error:" 75 e.method_ e.url 76 (status_style e.status) e.status 77 e.body; 78 1) 79 | Failure msg -> 80 Fmt.epr "%a %s@." error_style "Error:" msg; 81 1 82 | exn -> 83 (* Re-raise unknown exceptions *) 84 raise exn 85 86(** Wrap a function to handle API errors gracefully. *) 87let run f = 88 try f (); 0 89 with exn -> handle_exn exn 90 91(** Exception to signal desired exit code without calling [exit] directly. 92 This avoids issues when running inside Eio's event loop. *) 93exception Exit_code of int 94 95(** Wrap a command action to handle API errors gracefully. *) 96let wrap f = 97 try f () 98 with 99 | Stdlib.Exit -> 100 (* exit() was called somewhere - treat as success *) 101 () 102 | Eio.Cancel.Cancelled Stdlib.Exit -> 103 (* Eio wraps Exit in Cancelled - treat as success *) 104 () 105 | exn -> 106 let code = handle_exn exn in 107 raise (Exit_code code)