Immich bindings and CLI in OCaml
at main 153 lines 4.8 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Anil Madhavapeddy. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** Immich API error handling using NestJS error format. *) 7 8(** Re-export NestJS error type for convenience. *) 9type t = Openapi.Nestjs.t = { 10 status_code : int; 11 error : string option; 12 message : string; 13 correlation_id : string option; 14} 15 16(** Parse an API error into a structured Immich/NestJS error. *) 17let of_api_error = Openapi.Nestjs.of_api_error 18 19(** {1 Styled Output Helpers} *) 20 21(** Style for error labels (red, bold) *) 22let error_style = Fmt.(styled (`Fg `Red) (styled `Bold string)) 23 24(** Style for status codes *) 25let status_style status = 26 if status >= 500 then Fmt.(styled (`Fg `Red) int) 27 else if status >= 400 then Fmt.(styled (`Fg `Yellow) int) 28 else Fmt.(styled (`Fg `Green) int) 29 30(** Style for correlation IDs (dim) *) 31let correlation_style = Fmt.(styled `Faint string) 32 33(** Style for error type (bold) *) 34let error_type_style = Fmt.(styled `Bold string) 35 36(** Pretty-print an Immich API error with colors. 37 38 Format: "Forbidden: Missing required permission [403] (correlationId: abc123)" *) 39let pp ppf (e : t) = 40 match e.error with 41 | Some err -> 42 Fmt.pf ppf "%a: %s [%a]" 43 error_type_style err 44 e.message 45 (status_style e.status_code) e.status_code; 46 (match e.correlation_id with 47 | Some cid -> Fmt.pf ppf " (%a)" correlation_style (Printf.sprintf "correlationId: %s" cid) 48 | None -> ()) 49 | None -> 50 Fmt.pf ppf "%s [%a]" 51 e.message 52 (status_style e.status_code) e.status_code; 53 (match e.correlation_id with 54 | Some cid -> Fmt.pf ppf " (%a)" correlation_style (Printf.sprintf "correlationId: %s" cid) 55 | None -> ()) 56 57(** Convert to a human-readable string (without colors). *) 58let to_string (e : t) : string = 59 let error_prefix = match e.error with 60 | Some err -> err ^ ": " 61 | None -> "" 62 in 63 match e.correlation_id with 64 | Some cid -> 65 Printf.sprintf "%s%s [%d] (correlationId: %s)" 66 error_prefix e.message e.status_code cid 67 | None -> 68 Printf.sprintf "%s%s [%d]" error_prefix e.message e.status_code 69 70(** Check if this is an authentication/authorization error. *) 71let is_auth_error = Openapi.Nestjs.is_auth_error 72 73(** Check if this is a "not found" error. *) 74let is_not_found = Openapi.Nestjs.is_not_found 75 76(** Handle an exception, printing a nice error message if it's an API error. 77 78 Returns an exit code: 79 - 0 if not an error (shouldn't happen, but for completeness) 80 - 1 for most API errors 81 - 77 for authentication errors (permission denied) 82 - 69 for not found errors *) 83let handle_exn exn = 84 match exn with 85 | Openapi.Runtime.Api_error e -> 86 (match of_api_error e with 87 | Some nestjs -> 88 Fmt.epr "%a %a@." error_style "Error:" pp nestjs; 89 if is_auth_error nestjs then 77 90 else if is_not_found nestjs then 69 91 else 1 92 | None -> 93 (* Not a NestJS error, show raw response *) 94 Fmt.epr "%a %s %s returned %a@.%s@." 95 error_style "API Error:" 96 e.method_ e.url 97 (status_style e.status) e.status 98 e.body; 99 1) 100 | Failure msg -> 101 Fmt.epr "%a %s@." error_style "Error:" msg; 102 1 103 | exn -> 104 (* Re-raise unknown exceptions *) 105 raise exn 106 107(** Wrap a function to handle API errors gracefully. 108 109 Usage: 110 {[ 111 let () = Immich_auth.Error.run (fun () -> 112 let albums = Immich.Albums.get_all_albums client () in 113 ... 114 ) 115 ]} *) 116let run f = 117 try f (); 0 118 with exn -> handle_exn exn 119 120(** Exception to signal desired exit code without calling [exit] directly. 121 This avoids issues when running inside Eio's event loop. *) 122exception Exit_code of int 123 124(** Wrap a command action to handle API errors gracefully. 125 126 This is designed to be used in cmdliner command definitions: 127 {[ 128 let list_action ~profile env = 129 Immich_auth.Error.wrap (fun () -> 130 let api = ... in 131 let albums = Immich.Albums.get_all_albums api () in 132 ... 133 ) 134 135 let list_cmd env fs = 136 Cmd.v info Term.(const list_action $ ...) 137 ]} 138 139 The wrapper catches API errors and prints a nice message, 140 then raises [Exit_code] with an appropriate code. This exception 141 should be caught by the main program outside the Eio event loop. *) 142let wrap f = 143 try f () 144 with 145 | Stdlib.Exit -> 146 (* exit() was called somewhere - treat as success *) 147 () 148 | Eio.Cancel.Cancelled Stdlib.Exit -> 149 (* Eio wraps Exit in Cancelled - treat as success *) 150 () 151 | exn -> 152 let code = handle_exn exn in 153 raise (Exit_code code)