Zulip bots with Eio

eio.exn

+256 -345
+1
.gitignore
··· 1 1 _build 2 + third_party
+1
dune
··· 1 + (data_only_dirs third_party)
+12 -17
lib/zulip/auth.ml
··· 74 74 in 75 75 { server_url; email; api_key } 76 76 | _ -> 77 - let err = 78 - Zulip_types.create_error ~code:(Other "config_missing") 79 - ~msg:"Missing required fields: email, key, site in zuliprc" () 80 - in 81 - raise (Eio.Exn.add_context (Zulip_types.err err) "reading %s" path) 77 + Error.raise_with_context 78 + (Error.make ~code:(Other "config_missing") 79 + ~message:"Missing required fields: email, key, site in zuliprc" ()) 80 + "reading %s" path 82 81 with 83 82 | Eio.Exn.Io _ as ex -> raise ex 84 83 | Sys_error msg -> 85 - let err = 86 - Zulip_types.create_error ~code:(Other "file_error") 87 - ~msg:("Cannot read zuliprc file: " ^ msg) 88 - () 89 - in 90 - raise (Eio.Exn.add_context (Zulip_types.err err) "reading %s" path) 84 + Error.raise_with_context 85 + (Error.make ~code:(Other "file_error") 86 + ~message:("Cannot read zuliprc file: " ^ msg) ()) 87 + "reading %s" path 91 88 | exn -> 92 - let err = 93 - Zulip_types.create_error ~code:(Other "parse_error") 94 - ~msg:("Error parsing zuliprc: " ^ Printexc.to_string exn) 95 - () 96 - in 97 - raise (Eio.Exn.add_context (Zulip_types.err err) "reading %s" path) 89 + Error.raise_with_context 90 + (Error.make ~code:(Other "parse_error") 91 + ~message:("Error parsing zuliprc: " ^ Printexc.to_string exn) ()) 92 + "reading %s" path 98 93 99 94 let server_url t = t.server_url 100 95 let email t = t.email
+1 -1
lib/zulip/auth.mli
··· 1 1 (** Authentication for the Zulip API. 2 2 3 3 This module handles authentication credentials for connecting to a Zulip server. 4 - @raise Eio.Io with [Zulip_types.E error] on authentication/config errors *) 4 + @raise Eio.Io with [Error.E error] on authentication/config errors *) 5 5 6 6 type t 7 7
+3 -2
lib/zulip/channels.ml
··· 28 28 match Encode.from_json response_codec json with 29 29 | Ok channels -> channels 30 30 | Error msg -> 31 - let err = Zulip_types.create_error ~code:(Other "json_parse") ~msg () in 32 - raise (Eio.Exn.add_context (Zulip_types.err err) "parsing channels list") 31 + Error.raise_with_context 32 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 33 + "parsing channels list" 33 34 34 35 (* Request types with jsont codecs *) 35 36 module Subscribe_request = struct
+1 -1
lib/zulip/channels.mli
··· 1 1 (** Channel (stream) operations for the Zulip API. 2 2 3 - All functions raise [Eio.Io] with [Zulip_types.E error] on failure. 3 + All functions raise [Eio.Io] with [Error.E error] on failure. 4 4 Context is automatically added indicating the operation being performed. *) 5 5 6 6 val create_channel : Client.t -> Channel.t -> unit
+25 -32
lib/zulip/client.ml
··· 100 100 | Error e -> 101 101 let msg = Jsont.Error.to_string e in 102 102 Log.err (fun m -> m "JSON parse error: %s" msg); 103 - let err = 104 - Zulip_types.create_error ~code:(Other "json_parse") ~msg () 105 - in 106 - raise (Eio.Exn.add_context (Zulip_types.err err) "%s %s" (method_to_string method_) path) 103 + Error.raise_with_context 104 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 105 + "%s %s" (method_to_string method_) path 107 106 in 108 107 109 108 (* Check for Zulip error response *) ··· 117 116 | Some (Jsont.String (s, _)) -> s 118 117 | _ -> "Unknown error" 119 118 in 120 - let code = 119 + let code : Error.code = 121 120 match List.assoc_opt "code" assoc with 122 - | Some (Jsont.String (s, _)) -> 123 - (match s with 124 - | "INVALID_API_KEY" -> Zulip_types.Invalid_api_key 125 - | "REQUEST_VARIABLE_MISSING" -> Zulip_types.Request_variable_missing 126 - | "BAD_REQUEST" -> Zulip_types.Bad_request 127 - | "USER_DEACTIVATED" -> Zulip_types.User_deactivated 128 - | "REALM_DEACTIVATED" -> Zulip_types.Realm_deactivated 129 - | "RATE_LIMIT_HIT" -> Zulip_types.Rate_limit_hit 130 - | s -> Zulip_types.Other s) 131 - | _ -> Zulip_types.Other "unknown" 121 + | Some (Jsont.String (s, _)) -> ( 122 + match s with 123 + | "INVALID_API_KEY" -> Invalid_api_key 124 + | "REQUEST_VARIABLE_MISSING" -> Request_variable_missing 125 + | "BAD_REQUEST" -> Bad_request 126 + | "USER_DEACTIVATED" -> User_deactivated 127 + | "REALM_DEACTIVATED" -> Realm_deactivated 128 + | "RATE_LIMIT_HIT" -> Rate_limit_hit 129 + | s -> Other s) 130 + | _ -> Other "unknown" 132 131 in 133 - (* Extract extra fields *) 134 132 let extra = 135 133 List.filter 136 134 (fun (k, _) -> k <> "code" && k <> "msg" && k <> "result") 137 135 assoc 138 136 in 139 - Log.warn (fun m -> 140 - m "API error: %s (code: %a)" msg Zulip_types.pp_error_code code); 141 - let err = Zulip_types.create_error ~code ~msg ~extra () in 142 - raise (Eio.Exn.add_context (Zulip_types.err err) "%s %s" (method_to_string method_) path) 137 + Log.warn (fun m -> m "API error: %s (code: %a)" msg Error.pp_code code); 138 + Error.raise_with_context 139 + (Error.make ~code ~message:msg ~extra ()) 140 + "%s %s" (method_to_string method_) path 143 141 | _ -> 144 142 if status >= 200 && status < 300 then json 145 143 else ( 146 144 Log.warn (fun m -> m "HTTP error: %d" status); 147 - let err = 148 - Zulip_types.create_error 149 - ~code:(Zulip_types.Other (string_of_int status)) 150 - ~msg:("HTTP error: " ^ string_of_int status) 151 - () 152 - in 153 - raise (Eio.Exn.add_context (Zulip_types.err err) "%s %s" (method_to_string method_) path))) 145 + Error.raise_with_context 146 + (Error.make ~code:(Other (string_of_int status)) 147 + ~message:("HTTP error: " ^ string_of_int status) ()) 148 + "%s %s" (method_to_string method_) path)) 154 149 | _ -> 155 150 if status >= 200 && status < 300 then json 156 151 else ( 157 152 Log.err (fun m -> m "Invalid JSON response"); 158 - let err = 159 - Zulip_types.create_error ~code:(Zulip_types.Other "json_parse") 160 - ~msg:"Invalid JSON response" () 161 - in 162 - raise (Eio.Exn.add_context (Zulip_types.err err) "%s %s" (method_to_string method_) path)) 153 + Error.raise_with_context 154 + (Error.make ~code:(Other "json_parse") ~message:"Invalid JSON response" ()) 155 + "%s %s" (method_to_string method_) path) 163 156 164 157 let pp fmt t = 165 158 Format.fprintf fmt "Client(server=%s)" (Auth.server_url t.auth)
+4 -4
lib/zulip/client.mli
··· 2 2 3 3 This module provides the low-level HTTP client for communicating with 4 4 the Zulip API. All API errors are raised as [Eio.Io] exceptions with 5 - [Zulip_types.E] error codes, following the Eio error pattern. 5 + [Error.E] error codes, following the Eio error pattern. 6 6 7 - @raise Eio.Io with [Zulip_types.E error] for API errors *) 7 + @raise Eio.Io with [Error.E error] for API errors *) 8 8 9 9 type t 10 10 (** Type representing a Zulip HTTP client *) ··· 39 39 ?body:string -> 40 40 ?content_type:string -> 41 41 unit -> 42 - Zulip_types.json 42 + Jsont.json 43 43 (** Make an HTTP request to the Zulip API. 44 44 @param content_type Optional Content-Type header 45 45 (default: application/x-www-form-urlencoded for POST/PUT, none for GET/DELETE) 46 - @raise Eio.Io with [Zulip_types.E error] on API errors. The exception 46 + @raise Eio.Io with [Error.E error] on API errors. The exception 47 47 includes context about the request method and path. *) 48 48 49 49 val pp : Format.formatter -> t -> unit
+93
lib/zulip/error.ml
··· 1 + type code = 2 + | Invalid_api_key 3 + | Request_variable_missing 4 + | Bad_request 5 + | User_deactivated 6 + | Realm_deactivated 7 + | Rate_limit_hit 8 + | Other of string 9 + 10 + type t = { 11 + code : code; 12 + message : string; 13 + extra : (string * Jsont.json) list; 14 + } 15 + 16 + type Eio.Exn.err += E of t 17 + 18 + let pp_code fmt = function 19 + | Invalid_api_key -> Format.fprintf fmt "Invalid_api_key" 20 + | Request_variable_missing -> Format.fprintf fmt "Request_variable_missing" 21 + | Bad_request -> Format.fprintf fmt "Bad_request" 22 + | User_deactivated -> Format.fprintf fmt "User_deactivated" 23 + | Realm_deactivated -> Format.fprintf fmt "Realm_deactivated" 24 + | Rate_limit_hit -> Format.fprintf fmt "Rate_limit_hit" 25 + | Other s -> Format.fprintf fmt "Other(%s)" s 26 + 27 + let pp fmt t = Format.fprintf fmt "%a: %s" pp_code t.code t.message 28 + 29 + let () = 30 + Eio.Exn.register_pp (fun f -> function 31 + | E e -> 32 + Format.fprintf f "Zulip %a" pp e; 33 + true 34 + | _ -> false) 35 + 36 + let code_to_api_string = function 37 + | Invalid_api_key -> "INVALID_API_KEY" 38 + | Request_variable_missing -> "REQUEST_VARIABLE_MISSING" 39 + | Bad_request -> "BAD_REQUEST" 40 + | User_deactivated -> "USER_DEACTIVATED" 41 + | Realm_deactivated -> "REALM_DEACTIVATED" 42 + | Rate_limit_hit -> "RATE_LIMIT_HIT" 43 + | Other s -> s 44 + 45 + let code_of_api_string = function 46 + | "INVALID_API_KEY" -> Invalid_api_key 47 + | "REQUEST_VARIABLE_MISSING" -> Request_variable_missing 48 + | "BAD_REQUEST" -> Bad_request 49 + | "USER_DEACTIVATED" -> User_deactivated 50 + | "REALM_DEACTIVATED" -> Realm_deactivated 51 + | "RATE_LIMIT_HIT" -> Rate_limit_hit 52 + | s -> Other s 53 + 54 + let make ~code ~message ?(extra = []) () = { code; message; extra } 55 + let code t = t.code 56 + let message t = t.message 57 + let extra t = t.extra 58 + let raise e = Stdlib.raise (Eio.Exn.create (E e)) 59 + 60 + let raise_with_context e fmt = 61 + Format.kasprintf (fun context -> 62 + Stdlib.raise (Eio.Exn.add_context (Eio.Exn.create (E e)) "%s" context)) 63 + fmt 64 + 65 + let code_jsont = 66 + let of_string s = Ok (code_of_api_string s) in 67 + Jsont.of_of_string ~kind:"ErrorCode" of_string ~enc:code_to_api_string 68 + 69 + let jsont = 70 + let kind = "ZulipError" in 71 + let make' code msg = { code = code_of_api_string code; message = msg; extra = [] } in 72 + let code' t = code_to_api_string t.code in 73 + let msg t = t.message in 74 + Jsont.Object.( 75 + map ~kind make' 76 + |> mem "code" Jsont.string ~enc:code' 77 + |> mem "msg" Jsont.string ~enc:msg 78 + |> finish) 79 + 80 + let of_json json = 81 + match Encode.from_json jsont json with 82 + | Ok err -> ( 83 + match json with 84 + | Jsont.Object (fields, _) -> 85 + let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in 86 + let extra = 87 + List.filter 88 + (fun (k, _) -> k <> "code" && k <> "msg" && k <> "result") 89 + assoc 90 + in 91 + Some { err with extra } 92 + | _ -> Some err) 93 + | Error _ -> None
+79
lib/zulip/error.mli
··· 1 + (** Zulip API error handling. 2 + 3 + This module defines protocol-level errors for the Zulip API, 4 + following the Eio error pattern for context-aware error handling. 5 + 6 + Errors are raised as [Eio.Io] exceptions: 7 + {[ 8 + try 9 + Zulip.Messages.send client msg 10 + with 11 + | Eio.Io (Zulip.Error.E { code = Invalid_api_key; message; _ }, _) -> 12 + Printf.eprintf "Authentication failed: %s\n" message 13 + | Eio.Io (Zulip.Error.E err, _) -> 14 + Printf.eprintf "API error: %a\n" Zulip.Error.pp err 15 + ]} *) 16 + 17 + (** {1 Error Codes} 18 + 19 + These error codes correspond to the error codes returned by the Zulip API 20 + in the "code" field of error responses. *) 21 + 22 + type code = 23 + | Invalid_api_key (** Authentication failure - invalid API key *) 24 + | Request_variable_missing (** Required parameter is missing *) 25 + | Bad_request (** Malformed request *) 26 + | User_deactivated (** User account has been deactivated *) 27 + | Realm_deactivated (** Organization (realm) has been deactivated *) 28 + | Rate_limit_hit (** API rate limit exceeded *) 29 + | Other of string (** Other/unknown error code *) 30 + 31 + (** {1 Error Type} *) 32 + 33 + type t = { 34 + code : code; (** The error code from the API *) 35 + message : string; (** Human-readable error message *) 36 + extra : (string * Jsont.json) list; (** Additional fields from the error response *) 37 + } 38 + (** The protocol-level error type. *) 39 + 40 + (** {1 Eio Integration} *) 41 + 42 + type Eio.Exn.err += E of t 43 + (** Extend [Eio.Exn.err] with Zulip protocol errors. *) 44 + 45 + val raise : t -> 'a 46 + (** [raise e] raises an [Eio.Io] exception for error [e]. 47 + Equivalent to [Stdlib.raise (Eio.Exn.create (E e))]. *) 48 + 49 + val raise_with_context : t -> ('a, Format.formatter, unit, 'b) format4 -> 'a 50 + (** [raise_with_context e fmt ...] raises an [Eio.Io] exception with context. 51 + Equivalent to [Stdlib.raise (Eio.Exn.add_context (Eio.Exn.create (E e)) fmt ...)]. *) 52 + 53 + (** {1 Error Construction} *) 54 + 55 + val make : code:code -> message:string -> ?extra:(string * Jsont.json) list -> unit -> t 56 + (** [make ~code ~message ?extra ()] creates an error value. *) 57 + 58 + (** {1 Accessors} *) 59 + 60 + val code : t -> code 61 + val message : t -> string 62 + val extra : t -> (string * Jsont.json) list 63 + 64 + (** {1 Pretty Printing} *) 65 + 66 + val pp_code : Format.formatter -> code -> unit 67 + val pp : Format.formatter -> t -> unit 68 + 69 + (** {1 JSON Parsing} *) 70 + 71 + val code_jsont : code Jsont.t 72 + (** Jsont codec for error codes. *) 73 + 74 + val jsont : t Jsont.t 75 + (** Jsont codec for errors. *) 76 + 77 + val of_json : Jsont.json -> t option 78 + (** [of_json json] attempts to parse a Zulip API error response. 79 + Returns [None] if the JSON does not represent an error. *)
+1 -1
lib/zulip/event.ml
··· 1 1 type t = { 2 2 id : int; 3 3 type_ : Event_type.t; 4 - data : Zulip_types.json; 4 + data : Jsont.json; 5 5 } 6 6 7 7 let id t = t.id
+1 -1
lib/zulip/event.mli
··· 7 7 8 8 val id : t -> int 9 9 val type_ : t -> Event_type.t 10 - val data : t -> Zulip_types.json 10 + val data : t -> Jsont.json 11 11 12 12 (** Jsont codec for event *) 13 13 val jsont : t Jsont.t
+6 -7
lib/zulip/event_queue.ml
··· 48 48 match Encode.from_json Register_response.codec json with 49 49 | Ok response -> { id = response.queue_id } 50 50 | Error msg -> 51 - let err = Zulip_types.create_error ~code:(Other "json_parse") ~msg () in 52 - raise 53 - (Eio.Exn.add_context (Zulip_types.err err) "parsing register response") 51 + Error.raise_with_context 52 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 53 + "parsing register response" 54 54 55 55 let id t = t.id 56 56 ··· 105 105 response.events 106 106 | Error msg -> 107 107 Log.warn (fun m -> m "Failed to parse events response: %s" msg); 108 - let err = Zulip_types.create_error ~code:(Other "json_parse") ~msg () in 109 - raise 110 - (Eio.Exn.add_context (Zulip_types.err err) "parsing events from queue %s" 111 - t.id) 108 + Error.raise_with_context 109 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 110 + "parsing events from queue %s" t.id 112 111 113 112 let delete t client = 114 113 let params = [ ("queue_id", t.id) ] in
+1 -1
lib/zulip/event_queue.mli
··· 1 1 (** Event queue for receiving Zulip events in real-time. 2 2 3 - All functions raise [Eio.Io] with [Zulip_types.E error] on failure. *) 3 + All functions raise [Eio.Io] with [Error.E error] on failure. *) 4 4 5 5 type t 6 6
+6 -9
lib/zulip/messages.ml
··· 9 9 match Encode.from_json Message_response.jsont response with 10 10 | Ok msg_response -> msg_response 11 11 | Error msg -> 12 - let err = 13 - Zulip_types.create_error ~code:(Other "json_parse") ~msg () 14 - in 15 - raise (Eio.Exn.add_context (Zulip_types.err err) "parsing message response") 12 + Error.raise_with_context 13 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 14 + "parsing message response" 16 15 17 16 let edit client ~message_id ?content ?topic () = 18 17 let params = ··· 77 76 78 77 let upload_file _client ~filename:_ = 79 78 (* TODO: Implement file upload using multipart/form-data *) 80 - let err = 81 - Zulip_types.create_error ~code:(Other "not_implemented") 82 - ~msg:"File upload not yet implemented" () 83 - in 84 - raise (Zulip_types.err err) 79 + Error.raise 80 + (Error.make ~code:(Other "not_implemented") 81 + ~message:"File upload not yet implemented" ())
+3 -3
lib/zulip/messages.mli
··· 1 1 (** Message operations for the Zulip API. 2 2 3 - All functions raise [Eio.Io] with [Zulip_types.E error] on failure. 3 + All functions raise [Eio.Io] with [Error.E error] on failure. 4 4 Context is automatically added indicating the operation being performed. *) 5 5 6 6 val send : Client.t -> Message.t -> Message_response.t ··· 16 16 (** Delete a message. 17 17 @raise Eio.Io on failure *) 18 18 19 - val get : Client.t -> message_id:int -> Zulip_types.json 19 + val get : Client.t -> message_id:int -> Jsont.json 20 20 (** Get a single message by ID. 21 21 @raise Eio.Io on failure *) 22 22 ··· 27 27 ?num_after:int -> 28 28 ?narrow:string list -> 29 29 unit -> 30 - Zulip_types.json 30 + Jsont.json 31 31 (** Get multiple messages with optional filtering. 32 32 @raise Eio.Io on failure *) 33 33
+9 -12
lib/zulip/users.ml
··· 10 10 match Encode.from_json response_codec json with 11 11 | Ok users -> users 12 12 | Error msg -> 13 - let err = Zulip_types.create_error ~code:(Other "json_parse") ~msg () in 14 - raise (Eio.Exn.add_context (Zulip_types.err err) "parsing users list") 13 + Error.raise_with_context 14 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 15 + "parsing users list" 15 16 16 17 let get client ~email = 17 18 (* Define a codec for the response that wraps the user in a "user" field *) ··· 31 32 (match Encode.from_json User.jsont json with 32 33 | Ok user -> user 33 34 | Error msg -> 34 - let err = 35 - Zulip_types.create_error ~code:(Other "json_parse") ~msg () 36 - in 37 - raise (Eio.Exn.add_context (Zulip_types.err err) "parsing user %s" email)) 35 + Error.raise_with_context 36 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 37 + "parsing user %s" email) 38 38 39 39 let get_by_id client ~user_id = 40 40 (* Define a codec for the response that wraps the user in a "user" field *) ··· 56 56 (match Encode.from_json User.jsont json with 57 57 | Ok user -> user 58 58 | Error msg -> 59 - let err = 60 - Zulip_types.create_error ~code:(Other "json_parse") ~msg () 61 - in 62 - raise 63 - (Eio.Exn.add_context (Zulip_types.err err) "parsing user id %d" 64 - user_id)) 59 + Error.raise_with_context 60 + (Error.make ~code:(Other "json_parse") ~message:msg ()) 61 + "parsing user id %d" user_id) 65 62 66 63 (* Request type for create_user *) 67 64 module Create_user_request = struct
+1 -1
lib/zulip/users.mli
··· 1 1 (** User operations for the Zulip API. 2 2 3 - All functions raise [Eio.Io] with [Zulip_types.E error] on failure. 3 + All functions raise [Eio.Io] with [Error.E error] on failure. 4 4 Context is automatically added indicating the operation being performed. *) 5 5 6 6 val list : Client.t -> User.t list
+2 -28
lib/zulip/zulip.ml
··· 1 1 (** Main module for Zulip OCaml API bindings *) 2 2 3 - (* Re-export core types from Zulip_types *) 4 - type json = Zulip_types.json 5 - 6 - type error_code = Zulip_types.error_code = 7 - | Invalid_api_key 8 - | Request_variable_missing 9 - | Bad_request 10 - | User_deactivated 11 - | Realm_deactivated 12 - | Rate_limit_hit 13 - | Other of string 14 - 15 - type error = Zulip_types.error = { 16 - code : error_code; 17 - message : string; 18 - extra : (string * json) list; 19 - } 20 - 21 - type Eio.Exn.err += E of error 22 - 23 - let err = Zulip_types.err 24 - let create_error = Zulip_types.create_error 25 - let error_code = Zulip_types.error_code 26 - let error_message = Zulip_types.error_message 27 - let error_extra = Zulip_types.error_extra 28 - let pp_error_code = Zulip_types.pp_error_code 29 - let pp_error = Zulip_types.pp_error 30 - let error_of_json = Zulip_types.error_of_json 3 + type json = Jsont.json 31 4 32 5 (** Re-export all submodules *) 6 + module Error = Error 33 7 module Auth = Auth 34 8 module Client = Client 35 9 module Message = Message
+6 -47
lib/zulip/zulip.mli
··· 5 5 6 6 {1 Error Handling} 7 7 8 - Errors are raised as [Eio.Io] exceptions with [Zulip_types.E error], 8 + Errors are raised as [Eio.Io] exceptions with [Error.E error], 9 9 following the Eio error pattern (like [Eio.Net.E] and [Eio.Fs.E]). 10 10 This provides context-aware error handling with automatic context 11 11 accumulation as errors propagate up the call stack. ··· 15 15 try 16 16 Client.request client ~method_:`GET ~path:"/api/v1/users" () 17 17 with 18 - | Eio.Io (Zulip_types.E { code = Invalid_api_key; message; _ }, _) -> 18 + | Eio.Io (Error.E { code = Invalid_api_key; message; _ }, _) -> 19 19 (* Handle authentication error *) 20 20 Log.err (fun m -> m "Auth failed: %s" message) 21 21 | Eio.Io _ as ex -> ··· 28 28 (** {1 Core Types} *) 29 29 30 30 (** JSON type used throughout the API *) 31 - type json = Zulip_types.json 32 - 33 - (** {2 Protocol Error Types} 31 + type json = Jsont.json 34 32 35 - Protocol errors are raised as [Eio.Io] exceptions. See {!Zulip_types} 36 - for the full error type definitions and helper functions. *) 33 + (** {1 Submodules} *) 37 34 38 - (** Error codes returned by Zulip API *) 39 - type error_code = Zulip_types.error_code = 40 - | Invalid_api_key 41 - | Request_variable_missing 42 - | Bad_request 43 - | User_deactivated 44 - | Realm_deactivated 45 - | Rate_limit_hit 46 - | Other of string 47 - 48 - (** Zulip protocol error type *) 49 - type error = Zulip_types.error = { 50 - code : error_code; 51 - message : string; 52 - extra : (string * json) list; 53 - } 54 - 55 - (** Extend [Eio.Exn.err] with Zulip protocol errors *) 56 - type Eio.Exn.err += E of error 57 - 58 - (** {3 Error Functions} *) 59 - 60 - val err : error -> exn 61 - (** [err e] creates an [Eio.Io] exception for error [e] *) 62 - 63 - val create_error : 64 - code:error_code -> 65 - msg:string -> 66 - ?extra:(string * json) list -> 67 - unit -> 68 - error 69 - 70 - val error_code : error -> error_code 71 - val error_message : error -> string 72 - val error_extra : error -> (string * json) list 73 - val pp_error_code : Format.formatter -> error_code -> unit 74 - val pp_error : Format.formatter -> error -> unit 75 - val error_of_json : json -> error option 76 - 77 - (** {1 Submodules} *) 35 + (** API error handling *) 36 + module Error = Error 78 37 79 38 (** Authentication management *) 80 39 module Auth = Auth
-105
lib/zulip/zulip_types.ml
··· 1 - (** Core types and errors for Zulip API *) 2 - 3 - type json = Jsont.json 4 - 5 - type error_code = 6 - | Invalid_api_key 7 - | Request_variable_missing 8 - | Bad_request 9 - | User_deactivated 10 - | Realm_deactivated 11 - | Rate_limit_hit 12 - | Other of string 13 - 14 - type error = { 15 - code : error_code; 16 - message : string; 17 - extra : (string * json) list; 18 - } 19 - 20 - (** Extend Eio.Exn.err with Zulip protocol errors *) 21 - type Eio.Exn.err += E of error 22 - 23 - let err e = Eio.Exn.create (E e) 24 - 25 - (** Pretty printing for error codes *) 26 - let pp_error_code fmt = function 27 - | Invalid_api_key -> Format.fprintf fmt "Invalid_api_key" 28 - | Request_variable_missing -> Format.fprintf fmt "Request_variable_missing" 29 - | Bad_request -> Format.fprintf fmt "Bad_request" 30 - | User_deactivated -> Format.fprintf fmt "User_deactivated" 31 - | Realm_deactivated -> Format.fprintf fmt "Realm_deactivated" 32 - | Rate_limit_hit -> Format.fprintf fmt "Rate_limit_hit" 33 - | Other s -> Format.fprintf fmt "Other(%s)" s 34 - 35 - let pp_error fmt t = 36 - Format.fprintf fmt "%a: %s" pp_error_code t.code t.message 37 - 38 - (** Register the pretty printer with Eio.Exn *) 39 - let () = 40 - Eio.Exn.register_pp (fun f -> function 41 - | E e -> 42 - Format.fprintf f "Zulip %a" pp_error e; 43 - true 44 - | _ -> false 45 - ) 46 - 47 - (** Internal: convert error_code to API string representation *) 48 - let error_code_to_api_string = function 49 - | Invalid_api_key -> "INVALID_API_KEY" 50 - | Request_variable_missing -> "REQUEST_VARIABLE_MISSING" 51 - | Bad_request -> "BAD_REQUEST" 52 - | User_deactivated -> "USER_DEACTIVATED" 53 - | Realm_deactivated -> "REALM_DEACTIVATED" 54 - | Rate_limit_hit -> "RATE_LIMIT_HIT" 55 - | Other s -> s 56 - 57 - (** Internal: parse error_code from API string representation *) 58 - let error_code_of_api_string s = 59 - match s with 60 - | "INVALID_API_KEY" -> Invalid_api_key 61 - | "REQUEST_VARIABLE_MISSING" -> Request_variable_missing 62 - | "BAD_REQUEST" -> Bad_request 63 - | "USER_DEACTIVATED" -> User_deactivated 64 - | "REALM_DEACTIVATED" -> Realm_deactivated 65 - | "RATE_LIMIT_HIT" -> Rate_limit_hit 66 - | s -> Other s 67 - 68 - let create_error ~code ~msg ?(extra = []) () = { code; message = msg; extra } 69 - let error_code t = t.code 70 - let error_message t = t.message 71 - let error_extra t = t.extra 72 - 73 - (** Jsont codec for error_code *) 74 - let error_code_jsont = 75 - let of_string s = Ok (error_code_of_api_string s) in 76 - Jsont.of_of_string ~kind:"ErrorCode" of_string ~enc:error_code_to_api_string 77 - 78 - (** Jsont codec for error *) 79 - let error_jsont = 80 - let kind = "ZulipError" in 81 - let make code msg = 82 - { code = error_code_of_api_string code; message = msg; extra = [] } 83 - in 84 - let code t = error_code_to_api_string t.code in 85 - let msg t = t.message in 86 - Jsont.Object.( 87 - map ~kind make 88 - |> mem "code" Jsont.string ~enc:code 89 - |> mem "msg" Jsont.string ~enc:msg 90 - |> finish 91 - ) 92 - 93 - let error_of_json json = 94 - match Encode.from_json error_jsont json with 95 - | Ok err -> 96 - (* Extract extra fields by getting all fields except code, msg, result *) 97 - (match json with 98 - | Jsont.Object (fields, _) -> 99 - let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in 100 - let extra = 101 - List.filter (fun (k, _) -> k <> "code" && k <> "msg" && k <> "result") assoc 102 - in 103 - Some { err with extra } 104 - | _ -> Some err) 105 - | Error _ -> None
-73
lib/zulip/zulip_types.mli
··· 1 - (** Core types and errors for Zulip API. 2 - 3 - This module defines the protocol-level error types for the Zulip API, 4 - following the Eio.Io error pattern for context-aware error handling. *) 5 - 6 - (** JSON type used throughout the API *) 7 - type json = Jsont.json 8 - 9 - (** {1 Protocol Error Codes} 10 - 11 - These error codes correspond to the error codes returned by the Zulip API 12 - in the "code" field of error responses. *) 13 - 14 - type error_code = 15 - | Invalid_api_key (** Authentication failure - invalid API key *) 16 - | Request_variable_missing (** Required parameter is missing *) 17 - | Bad_request (** Malformed request *) 18 - | User_deactivated (** User account has been deactivated *) 19 - | Realm_deactivated (** Organization (realm) has been deactivated *) 20 - | Rate_limit_hit (** API rate limit exceeded *) 21 - | Other of string (** Other/unknown error code *) 22 - 23 - (** {1 Zulip Protocol Errors} 24 - 25 - Protocol errors are raised as [Eio.Io] exceptions with context, 26 - following the same pattern as [Eio.Net.E] and [Eio.Fs.E]. *) 27 - 28 - (** The protocol-level error type, containing the error code, message, 29 - and any extra fields returned by the API. *) 30 - type error = { 31 - code : error_code; (** The error code from the API *) 32 - message : string; (** Human-readable error message *) 33 - extra : (string * json) list; (** Additional fields from the error response *) 34 - } 35 - 36 - (** Extend [Eio.Exn.err] with Zulip protocol errors. *) 37 - type Eio.Exn.err += E of error 38 - 39 - (** [err e] creates an [Eio.Io] exception for error [e]. 40 - Equivalent to [Eio.Exn.create (E e)]. *) 41 - val err : error -> exn 42 - 43 - (** {2 Error Construction} *) 44 - 45 - val create_error : 46 - code:error_code -> 47 - msg:string -> 48 - ?extra:(string * json) list -> 49 - unit -> 50 - error 51 - (** [create_error ~code ~msg ?extra ()] creates an error value. *) 52 - 53 - (** {2 Error Accessors} *) 54 - 55 - val error_code : error -> error_code 56 - val error_message : error -> string 57 - val error_extra : error -> (string * json) list 58 - 59 - (** {2 Pretty Printing} *) 60 - 61 - val pp_error_code : Format.formatter -> error_code -> unit 62 - val pp_error : Format.formatter -> error -> unit 63 - 64 - (** {1 Jsont Codecs} 65 - 66 - These codecs are used for parsing API error responses from JSON. *) 67 - 68 - val error_code_jsont : error_code Jsont.t 69 - val error_jsont : error Jsont.t 70 - 71 - (** [error_of_json json] attempts to parse a Zulip API error response. 72 - Returns [None] if the JSON does not represent an error. *) 73 - val error_of_json : json -> error option