My agentic slop goes here. Not intended for anyone else!

more

Changed files
+486 -209
stack
+208
stack/zulip/lib/zulip/lib/jsonu.ml
···
+
(** JSON utility functions for Zulip API *)
+
+
open Error
+
+
type json = Error.json
+
+
(** {1 Field extraction utilities} *)
+
+
let get_string fields key =
+
match List.assoc_opt key fields with
+
| Some (`String s) -> Ok s
+
| Some _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "Field '%s' is not a string" key) ())
+
| None -> Error (Error.create ~code:(Other "json_missing_field") ~msg:(Printf.sprintf "Field '%s' not found" key) ())
+
+
let get_string_default fields key default =
+
match get_string fields key with
+
| Ok s -> s
+
| Error _ -> default
+
+
let get_string_opt fields key =
+
match List.assoc_opt key fields with
+
| Some (`String s) -> Some s
+
| _ -> None
+
+
let to_int_flex = function
+
| `Float f -> int_of_float f
+
| `String s -> (try int_of_string s with _ -> failwith "Invalid integer string")
+
| json -> failwith (Printf.sprintf "Expected int or float, got %s" (match json with
+
| `Null -> "null"
+
| `Bool _ -> "bool"
+
| `O _ -> "object"
+
| `A _ -> "array"
+
| _ -> "unknown"))
+
+
let get_int fields key =
+
match List.assoc_opt key fields with
+
| Some json ->
+
(try Ok (to_int_flex json) with
+
| Failure msg -> Error (Error.create ~code:(Other "json_type_error") ~msg ()))
+
| None -> Error (Error.create ~code:(Other "json_missing_field") ~msg:(Printf.sprintf "Field '%s' not found" key) ())
+
+
let get_int_default fields key default =
+
match get_int fields key with
+
| Ok i -> i
+
| Error _ -> default
+
+
let get_int_opt fields key =
+
match List.assoc_opt key fields with
+
| Some json -> (try Some (to_int_flex json) with _ -> None)
+
| None -> None
+
+
let get_float fields key =
+
match List.assoc_opt key fields with
+
| Some (`Float f) -> Ok f
+
| Some (`String s) ->
+
(try Ok (float_of_string s) with
+
| _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "Field '%s' is not a valid float" key) ()))
+
| Some _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "Field '%s' is not a float" key) ())
+
| None -> Error (Error.create ~code:(Other "json_missing_field") ~msg:(Printf.sprintf "Field '%s' not found" key) ())
+
+
let get_float_default fields key default =
+
match get_float fields key with
+
| Ok f -> f
+
| Error _ -> default
+
+
let get_bool fields key =
+
match List.assoc_opt key fields with
+
| Some (`Bool b) -> Ok b
+
| Some _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "Field '%s' is not a boolean" key) ())
+
| None -> Error (Error.create ~code:(Other "json_missing_field") ~msg:(Printf.sprintf "Field '%s' not found" key) ())
+
+
let get_bool_default fields key default =
+
match get_bool fields key with
+
| Ok b -> b
+
| Error _ -> default
+
+
let get_bool_opt fields key =
+
match List.assoc_opt key fields with
+
| Some (`Bool b) -> Some b
+
| _ -> None
+
+
let get_object fields key =
+
match List.assoc_opt key fields with
+
| Some (`O obj) -> Ok obj
+
| Some _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "Field '%s' is not an object" key) ())
+
| None -> Error (Error.create ~code:(Other "json_missing_field") ~msg:(Printf.sprintf "Field '%s' not found" key) ())
+
+
let get_object_opt fields key =
+
match List.assoc_opt key fields with
+
| Some (`O obj) -> Some obj
+
| _ -> None
+
+
let get_array fields key =
+
match List.assoc_opt key fields with
+
| Some (`A arr) -> Ok arr
+
| Some _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "Field '%s' is not an array" key) ())
+
| None -> Error (Error.create ~code:(Other "json_missing_field") ~msg:(Printf.sprintf "Field '%s' not found" key) ())
+
+
let get_array_opt fields key =
+
match List.assoc_opt key fields with
+
| Some (`A arr) -> Some arr
+
| _ -> None
+
+
(** {1 Type conversion utilities} *)
+
+
let to_int_safe = function
+
| `Float f -> Some (int_of_float f)
+
| `String s -> (try Some (int_of_string s) with _ -> None)
+
| _ -> None
+
+
let to_string_safe = function
+
| `String s -> Some s
+
| _ -> None
+
+
let to_bool_safe = function
+
| `Bool b -> Some b
+
| _ -> None
+
+
let to_float_safe = function
+
| `Float f -> Some f
+
| `String s -> (try Some (float_of_string s) with _ -> None)
+
| _ -> None
+
+
(** {1 Object parsing utilities} *)
+
+
let with_object context f = function
+
| `O fields -> f fields
+
| _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "%s: expected JSON object" context) ())
+
+
let with_array context f json =
+
match json with
+
| `A items ->
+
let rec process acc = function
+
| [] -> Ok (List.rev acc)
+
| item :: rest ->
+
match f item with
+
| Ok v -> process (v :: acc) rest
+
| Error e -> Error e
+
in
+
process [] items
+
| _ -> Error (Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "%s: expected JSON array" context) ())
+
+
(** {1 Construction utilities} *)
+
+
let optional_field key encoder = function
+
| Some value -> Some (key, encoder value)
+
| None -> None
+
+
let optional_fields fields =
+
List.filter_map (fun x -> x) fields
+
+
let string_array strings =
+
`A (List.map (fun s -> `String s) strings)
+
+
let int_array ints =
+
`A (List.map (fun i -> `Float (float_of_int i)) ints)
+
+
(** {1 Error handling} *)
+
+
let json_error msg =
+
Error.create ~code:(Other "json_error") ~msg ()
+
+
let field_missing_error field =
+
Error.create ~code:(Other "json_missing_field") ~msg:(Printf.sprintf "Required field '%s' not found" field) ()
+
+
let type_mismatch_error field expected =
+
Error.create ~code:(Other "json_type_error") ~msg:(Printf.sprintf "Field '%s' type mismatch: expected %s" field expected) ()
+
+
let parse_with_error context f =
+
try f ()
+
with
+
| Failure msg -> Error (Error.create ~code:(Other "json_parse_error") ~msg:(Printf.sprintf "%s: %s" context msg) ())
+
| exn -> Error (Error.create ~code:(Other "json_parse_error") ~msg:(Printf.sprintf "%s: %s" context (Printexc.to_string exn)) ())
+
+
+
(** {1 Debugging utilities} *)
+
+
let to_string_pretty json =
+
let rec aux indent = function
+
| `Null -> "null"
+
| `Bool b -> string_of_bool b
+
| `Float f ->
+
if float_of_int (int_of_float f) = f then
+
string_of_int (int_of_float f)
+
else
+
string_of_float f
+
| `String s -> Printf.sprintf "%S" s
+
| `A [] -> "[]"
+
| `A lst ->
+
let items = List.map (aux (indent ^ " ")) lst in
+
Printf.sprintf "[\n%s%s\n%s]"
+
(indent ^ " ")
+
(String.concat (",\n" ^ indent ^ " ") items)
+
indent
+
| `O [] -> "{}"
+
| `O fields ->
+
let items = List.map (fun (k, v) ->
+
Printf.sprintf "%S: %s" k (aux (indent ^ " ") v)
+
) fields in
+
Printf.sprintf "{\n%s%s\n%s}"
+
(indent ^ " ")
+
(String.concat (",\n" ^ indent ^ " ") items)
+
indent
+
in
+
aux "" json
+
+
let pp fmt json =
+
Format.pp_print_string fmt (to_string_pretty json)
+117
stack/zulip/lib/zulip/lib/jsonu.mli
···
+
(** JSON utility functions for Zulip API
+
+
This module provides common utilities for working with JSON in the Zulip API,
+
reducing boilerplate and providing consistent error handling. *)
+
+
(** {1 Type definitions} *)
+
+
type json = Error.json
+
+
(** {1 Field extraction utilities} *)
+
+
(** Extract a string field from a JSON object *)
+
val get_string : (string * json) list -> string -> (string, Error.t) result
+
+
(** Extract a string field with a default value *)
+
val get_string_default : (string * json) list -> string -> string -> string
+
+
(** Extract an optional string field *)
+
val get_string_opt : (string * json) list -> string -> string option
+
+
(** Extract an integer field (handles both int and float representations) *)
+
val get_int : (string * json) list -> string -> (int, Error.t) result
+
+
(** Extract an integer field with a default value *)
+
val get_int_default : (string * json) list -> string -> int -> int
+
+
(** Extract an optional integer field *)
+
val get_int_opt : (string * json) list -> string -> int option
+
+
(** Extract a float field *)
+
val get_float : (string * json) list -> string -> (float, Error.t) result
+
+
(** Extract a float field with a default value *)
+
val get_float_default : (string * json) list -> string -> float -> float
+
+
(** Extract a boolean field *)
+
val get_bool : (string * json) list -> string -> (bool, Error.t) result
+
+
(** Extract a boolean field with a default value *)
+
val get_bool_default : (string * json) list -> string -> bool -> bool
+
+
(** Extract an optional boolean field *)
+
val get_bool_opt : (string * json) list -> string -> bool option
+
+
(** Extract a JSON object field *)
+
val get_object : (string * json) list -> string -> ((string * json) list, Error.t) result
+
+
(** Extract an optional JSON object field *)
+
val get_object_opt : (string * json) list -> string -> (string * json) list option
+
+
(** Extract a JSON array field *)
+
val get_array : (string * json) list -> string -> (json list, Error.t) result
+
+
(** Extract an optional JSON array field *)
+
val get_array_opt : (string * json) list -> string -> json list option
+
+
(** {1 Type conversion utilities} *)
+
+
(** Convert JSON to int, handling both int and float representations *)
+
val to_int_flex : json -> int
+
+
(** Safely convert JSON to int *)
+
val to_int_safe : json -> int option
+
+
(** Convert JSON to string *)
+
val to_string_safe : json -> string option
+
+
(** Convert JSON to bool *)
+
val to_bool_safe : json -> bool option
+
+
(** Convert JSON to float *)
+
val to_float_safe : json -> float option
+
+
(** {1 Object parsing utilities} *)
+
+
(** Parse a JSON value as an object, applying a function to its fields *)
+
val with_object : string -> ((string * json) list -> ('a, Error.t) result) -> json -> ('a, Error.t) result
+
+
(** Parse a JSON value as an array, applying a function to each element *)
+
val with_array : string -> (json -> ('a, Error.t) result) -> json -> ('a list, Error.t) result
+
+
(** {1 Construction utilities} *)
+
+
(** Create an optional field for JSON object construction *)
+
val optional_field : string -> ('a -> json) -> 'a option -> (string * json) option
+
+
(** Create a list of optional fields, filtering out None values *)
+
val optional_fields : (string * json) option list -> (string * json) list
+
+
(** Convert a string list to a JSON array *)
+
val string_array : string list -> json
+
+
(** Convert an int list to a JSON array *)
+
val int_array : int list -> json
+
+
(** {1 Error handling} *)
+
+
(** Create a JSON parsing error *)
+
val json_error : string -> Error.t
+
+
(** Create a field missing error *)
+
val field_missing_error : string -> Error.t
+
+
(** Create a type mismatch error *)
+
val type_mismatch_error : string -> string -> Error.t
+
+
(** Wrap a parsing function with exception handling *)
+
val parse_with_error : string -> (unit -> ('a, Error.t) result) -> ('a, Error.t) result
+
+
+
(** {1 Debugging utilities} *)
+
+
(** Convert JSON to a pretty-printed string *)
+
val to_string_pretty : json -> string
+
+
(** Print JSON value for debugging *)
+
val pp : Format.formatter -> json -> unit
+6 -21
stack/zulip/lib/zulip/lib/message_response.ml
···
let automatic_new_visibility_policy t = t.automatic_new_visibility_policy
let of_json json =
-
match json with
-
| `O fields ->
-
(try
-
let id = match List.assoc "id" fields with
-
| `Float f -> int_of_float f
-
| `String s -> int_of_string s
-
| _ -> failwith "id not found or not a number" in
-
let automatic_new_visibility_policy =
-
try Some (match List.assoc "automatic_new_visibility_policy" fields with
-
| `String s -> s
-
| _ -> failwith "invalid visibility policy")
-
with Not_found -> None in
+
Jsonu.with_object "message_response" (fun fields ->
+
match Jsonu.get_int fields "id" with
+
| Error e -> Error e
+
| Ok id ->
+
let automatic_new_visibility_policy = Jsonu.get_string_opt fields "automatic_new_visibility_policy" in
Ok { id; automatic_new_visibility_policy }
-
with
-
| Failure msg ->
-
Error (Error.create ~code:(Other "parse_error") ~msg:("Failed to parse message response: " ^ msg) ())
-
| Not_found ->
-
Error (Error.create ~code:(Other "parse_error") ~msg:"Failed to parse message response: missing field" ())
-
| _ ->
-
Error (Error.create ~code:(Other "parse_error") ~msg:"Failed to parse message response" ()))
-
| _ ->
-
Error (Error.create ~code:(Other "parse_error") ~msg:"Expected JSON object for message response" ())
+
) json
let pp fmt t = Format.fprintf fmt "MessageResponse{id=%d}" t.id
+9 -25
stack/zulip/lib/zulip/lib/user.ml
···
("is_bot", `Bool t.is_bot);
]
-
let of_json json =
-
try
-
match json with
-
| `O fields ->
-
let get_string key =
-
match List.assoc key fields with
-
| `String s -> s
-
| _ -> failwith ("Expected string for " ^ key) in
-
let get_bool key default =
-
match List.assoc_opt key fields with
-
| Some (`Bool b) -> b
-
| None -> default
-
| _ -> failwith ("Expected bool for " ^ key) in
-
-
let email = get_string "email" in
-
let full_name = get_string "full_name" in
-
let is_active = get_bool "is_active" true in
-
let is_admin = get_bool "is_admin" false in
-
let is_bot = get_bool "is_bot" false in
-
+
let of_json json =
+
Jsonu.with_object "user" (fun fields ->
+
match Jsonu.get_string fields "email", Jsonu.get_string fields "full_name" with
+
| Ok email, Ok full_name ->
+
let is_active = Jsonu.get_bool_default fields "is_active" true in
+
let is_admin = Jsonu.get_bool_default fields "is_admin" false in
+
let is_bot = Jsonu.get_bool_default fields "is_bot" false in
Ok { email; full_name; is_active; is_admin; is_bot }
-
| _ ->
-
Error (Error.create ~code:(Other "json_parse_error") ~msg:"User JSON must be an object" ())
-
with
-
| exn ->
-
Error (Error.create ~code:(Other "json_parse_error") ~msg:("User JSON parsing failed: " ^ Printexc.to_string exn) ())
+
| Error e, _ | _, Error e -> Error e
+
) json
let pp fmt t = Format.fprintf fmt "User{email=%s, full_name=%s}" t.email t.full_name
+3 -11
stack/zulip/lib/zulip_bot/lib/bot_runner.ml
···
(* Generator not initialized - this will be done by applications using the library *)
()
-
(* Convert Zulip.Error.json to Yojson.Safe.t *)
-
let rec convert_json : Zulip.Error.json -> Yojson.Safe.t = function
-
| `Null -> `Null
-
| `Bool b -> `Bool b
-
| `Float f -> `Float f
-
| `String s -> `String s
-
| `A lst -> `List (List.map convert_json lst)
-
| `O pairs -> `Assoc (List.map (fun (k, v) -> (k, convert_json v)) pairs)
type 'env t = {
client : Zulip.Client.t;
···
in
(* Parse the message JSON into Message.t *)
-
(match Message.of_json (convert_json message_json) with
+
(match Message.of_json message_json with
| Error err ->
Log.err (fun m -> m "Failed to parse message JSON: %s" err);
(* Show raw JSON for debugging *)
-
Log.debug (fun m -> m "@[%a@]" Message.pp_json_debug (convert_json message_json));
+
Log.debug (fun m -> m "@[%a@]" Message.pp_json_debug message_json);
| Ok message ->
(* Log the parsed message with colors *)
Log.info (fun m -> m "@[<h>%a@]" (Message.pp_ansi ~show_json:false) message);
···
let handle_webhook t ~webhook_data =
(* Process webhook data and route to handler *)
(* Parse the webhook data into Message.t first *)
-
match Message.of_json (convert_json webhook_data) with
+
match Message.of_json webhook_data with
| Error err -> Error (Zulip.Error.create ~code:(Zulip.Error.Other "parse_error") ~msg:("Failed to parse webhook message: " ^ err) ())
| Ok message ->
match Bot_handler.handle_message_with_env t.handler t.env message with
+22 -28
stack/zulip/lib/zulip_bot/lib/bot_storage.ml
···
~path:"/api/v1/bot_storage"
() with
| Ok json ->
-
(match json with
-
| `O fields ->
-
(match List.assoc_opt "storage" fields with
-
| Some (`O storage_fields) ->
-
List.iter (fun (k, v) ->
-
match v with
-
| `String value ->
-
Log.debug (fun m -> m "Loaded key from server: %s" k);
-
Hashtbl.add cache k value
-
| _ -> ()
-
) storage_fields
-
| _ -> ())
-
| _ -> ())
+
(match Zulip.Jsonu.get_object_opt (match json with `O f -> f | _ -> []) "storage" with
+
| Some storage_fields ->
+
List.iter (fun (k, v) ->
+
match Zulip.Jsonu.to_string_safe v with
+
| Some value ->
+
Log.debug (fun m -> m "Loaded key from server: %s" k);
+
Hashtbl.add cache k value
+
| None -> ()
+
) storage_fields
+
| None -> ())
| Error e ->
Log.warn (fun m -> m "Failed to load existing storage: %s" (Zulip.Error.message e)));
···
~path:"/api/v1/bot_storage"
~params () with
| Ok json ->
-
(match json with
-
| `O fields ->
-
(match List.assoc_opt "storage" fields with
-
| Some (`O storage_fields) ->
-
(match List.assoc_opt key storage_fields with
-
| Some (`String value) ->
-
(* Cache the value *)
-
Log.debug (fun m -> m "Retrieved key from API: %s" key);
-
Hashtbl.add t.cache key value;
-
Some value
-
| _ ->
-
Log.debug (fun m -> m "Key not found in API: %s" key);
-
None)
-
| _ -> None)
-
| _ -> None)
+
(match Zulip.Jsonu.get_object_opt (match json with `O f -> f | _ -> []) "storage" with
+
| Some storage_fields ->
+
(match Zulip.Jsonu.get_string_opt storage_fields key with
+
| Some value ->
+
(* Cache the value *)
+
Log.debug (fun m -> m "Retrieved key from API: %s" key);
+
Hashtbl.add t.cache key value;
+
Some value
+
| None ->
+
Log.debug (fun m -> m "Key not found in API: %s" key);
+
None)
+
| None -> None)
| Error e ->
Log.warn (fun m -> m "Error fetching key %s: %s" key (Zulip.Error.message e));
None
+116 -119
stack/zulip/lib/zulip_bot/lib/message.ml
···
-
open Yojson.Safe.Util
+
(* Use Jsonm exclusively via Zulip.Jsonu utilities *)
let logs_src = Logs.Src.create "zulip_bot.message"
module Log = (val Logs.src_log logs_src : Logs.LOG)
-
(** Helper to convert JSON number to int, handling float representation *)
-
let to_int_flex json =
-
match json with
-
| `Int i -> i
-
| `Float f -> int_of_float f
-
| _ -> raise (Type_error ("Expected int or float", json))
-
(** User representation *)
module User = struct
type t = {
···
let short_name t = t.short_name
let of_json json =
-
try
+
Zulip.Jsonu.with_object "user" (fun fields ->
(* Handle both "user_id" and "id" field names *)
-
let user_id =
-
try json |> member "user_id" |> to_int_flex
-
with _ -> json |> member "id" |> to_int_flex
-
in
-
let email = json |> member "email" |> to_string in
-
let full_name = json |> member "full_name" |> to_string in
-
let short_name =
-
try json |> member "short_name" |> to_string_option
-
with _ -> None
+
let user_id_result =
+
match Zulip.Jsonu.get_int fields "user_id" with
+
| Ok id -> Ok id
+
| Error _ -> Zulip.Jsonu.get_int fields "id"
in
-
Ok { user_id; email; full_name; short_name }
-
with
-
| Type_error (msg, _) -> Error ("User JSON parse error: " ^ msg)
-
| exn -> Error ("User JSON parse error: " ^ Printexc.to_string exn)
+
match user_id_result, Zulip.Jsonu.get_string fields "email", Zulip.Jsonu.get_string fields "full_name" with
+
| Ok user_id, Ok email, Ok full_name ->
+
let short_name = Zulip.Jsonu.get_string_opt fields "short_name" in
+
Ok { user_id; email; full_name; short_name }
+
| Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e
+
) json
end
(** Reaction representation *)
···
let user_id t = t.user_id
let of_json json =
-
try
-
let emoji_name = json |> member "emoji_name" |> to_string in
-
let emoji_code = json |> member "emoji_code" |> to_string in
-
let reaction_type = json |> member "reaction_type" |> to_string in
-
let user_id = json |> member "user" |> member "user_id" |> to_int_flex in
-
Ok { emoji_name; emoji_code; reaction_type; user_id }
-
with
-
| Type_error (msg, _) -> Error ("Reaction JSON parse error: " ^ msg)
-
| exn -> Error ("Reaction JSON parse error: " ^ Printexc.to_string exn)
+
Zulip.Jsonu.with_object "reaction" (fun fields ->
+
match Zulip.Jsonu.get_string fields "emoji_name",
+
Zulip.Jsonu.get_string fields "emoji_code",
+
Zulip.Jsonu.get_string fields "reaction_type" with
+
| Ok emoji_name, Ok emoji_code, Ok reaction_type ->
+
(* Try both "user_id" and "user" -> "user_id" patterns *)
+
let user_id_result =
+
match Zulip.Jsonu.get_int fields "user_id" with
+
| Ok id -> Ok id
+
| Error _ ->
+
match Zulip.Jsonu.get_object fields "user" with
+
| Ok user_obj -> Zulip.Jsonu.get_int user_obj "user_id"
+
| Error e -> Error e
+
in
+
(match user_id_result with
+
| Ok user_id -> Ok { emoji_name; emoji_code; reaction_type; user_id }
+
| Error e -> Error e)
+
| Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e
+
) json
end
(** Common message fields *)
···
content: string;
content_type: string;
reactions: Reaction.t list;
-
submessages: Yojson.Safe.t list;
+
submessages: Zulip.Error.json list;
flags: string list;
is_me_message: bool;
client: string;
···
}
| Unknown of {
common: common;
-
raw_json: Yojson.Safe.t;
+
raw_json: Zulip.Error.json;
}
(** Helper function to parse common fields *)
let parse_common json =
-
try
-
let id = json |> member "id" |> to_int_flex in
-
let sender_id = json |> member "sender_id" |> to_int_flex in
-
let sender_email = json |> member "sender_email" |> to_string in
-
let sender_full_name = json |> member "sender_full_name" |> to_string in
-
let sender_short_name = json |> member "sender_short_name" |> to_string_option in
-
let timestamp = json |> member "timestamp" |> to_float in
-
let content = json |> member "content" |> to_string in
-
let content_type = json |> member "content_type" |> to_string_option |> Option.value ~default:"text/html" in
+
Zulip.Jsonu.parse_with_error "common fields" (fun () ->
+
Zulip.Jsonu.with_object "message" (fun fields ->
+
match Zulip.Jsonu.get_int fields "id",
+
Zulip.Jsonu.get_int fields "sender_id",
+
Zulip.Jsonu.get_string fields "sender_email",
+
Zulip.Jsonu.get_string fields "sender_full_name" with
+
| Ok id, Ok sender_id, Ok sender_email, Ok sender_full_name ->
+
let sender_short_name = Zulip.Jsonu.get_string_opt fields "sender_short_name" in
+
let timestamp = Zulip.Jsonu.get_float_default fields "timestamp" 0.0 in
+
let content = Zulip.Jsonu.get_string_default fields "content" "" in
+
let content_type = Zulip.Jsonu.get_string_default fields "content_type" "text/html" in
-
let reactions =
-
try
-
json |> member "reactions" |> to_list |> List.map (fun r ->
-
match Reaction.of_json r with
-
| Ok reaction -> reaction
-
| Error err ->
-
Log.warn (fun m -> m "Failed to parse reaction: %s" err);
-
failwith "reaction parse error"
-
)
-
with
-
| Type_error _ -> []
-
| _ -> []
-
in
+
let reactions =
+
match Zulip.Jsonu.get_array_opt fields "reactions" with
+
| Some reactions_json ->
+
List.filter_map (fun r ->
+
match Reaction.of_json r with
+
| Ok reaction -> Some reaction
+
| Error err ->
+
Log.warn (fun m -> m "Failed to parse reaction: %s" (Zulip.Error.message err));
+
None
+
) reactions_json
+
| None -> []
+
in
-
let submessages =
-
try json |> member "submessages" |> to_list
-
with Type_error _ -> []
-
in
+
let submessages = Zulip.Jsonu.get_array_opt fields "submessages" |> Option.value ~default:[] in
-
let flags =
-
try json |> member "flags" |> to_list |> List.map to_string
-
with Type_error _ -> []
-
in
+
let flags =
+
match Zulip.Jsonu.get_array_opt fields "flags" with
+
| Some flags_json ->
+
List.filter_map Zulip.Jsonu.to_string_safe flags_json
+
| None -> []
+
in
-
let is_me_message =
-
try json |> member "is_me_message" |> to_bool
-
with Type_error _ -> false
-
in
+
let is_me_message = Zulip.Jsonu.get_bool_default fields "is_me_message" false in
+
let client = Zulip.Jsonu.get_string_default fields "client" "" in
+
let gravatar_hash = Zulip.Jsonu.get_string_default fields "gravatar_hash" "" in
+
let avatar_url = Zulip.Jsonu.get_string_opt fields "avatar_url" in
-
let client =
-
try json |> member "client" |> to_string
-
with Type_error _ -> ""
-
in
-
-
let gravatar_hash =
-
try json |> member "gravatar_hash" |> to_string
-
with Type_error _ -> ""
-
in
-
-
let avatar_url = json |> member "avatar_url" |> to_string_option in
-
-
Ok {
-
id; sender_id; sender_email; sender_full_name; sender_short_name;
-
timestamp; content; content_type; reactions; submessages; flags;
-
is_me_message; client; gravatar_hash; avatar_url;
-
}
-
with
-
| Type_error (msg, _) -> Error ("Common fields parse error: " ^ msg)
-
| exn -> Error ("Common fields parse error: " ^ Printexc.to_string exn)
+
Ok {
+
id; sender_id; sender_email; sender_full_name; sender_short_name;
+
timestamp; content; content_type; reactions; submessages;
+
flags; is_me_message; client; gravatar_hash; avatar_url
+
}
+
| Error e, _, _, _ | _, Error e, _, _ | _, _, Error e, _ | _, _, _, Error e -> Error e
+
) json
+
)
(** JSON parsing *)
let of_json json =
-
Log.debug (fun m -> m "Parsing message JSON: %s" (Yojson.Safe.pretty_to_string json));
+
Log.debug (fun m -> m "Parsing message JSON: %s" (Zulip.Jsonu.to_string_pretty json));
match parse_common json with
-
| Error err -> Error err
+
| Error err -> Error (Zulip.Error.message err)
| Ok common ->
-
try
-
let msg_type = json |> member "type" |> to_string in
-
match msg_type with
-
| "private" ->
-
let display_recipient_json = json |> member "display_recipient" |> to_list in
-
let users = List.map (fun user_json ->
-
Log.debug (fun m -> m "Parsing user in display_recipient: %s" (Yojson.Safe.to_string user_json));
-
match User.of_json user_json with
-
| Ok user -> user
-
| Error err ->
-
Log.warn (fun m -> m "Failed to parse user in display_recipient: %s" err);
-
Log.warn (fun m -> m "User JSON was: %s" (Yojson.Safe.to_string user_json));
-
failwith "user parse error"
-
) display_recipient_json in
-
Ok (Private { common; display_recipient = users })
+
Zulip.Jsonu.parse_with_error "message type" (fun () ->
+
Zulip.Jsonu.with_object "message" (fun fields ->
+
match Zulip.Jsonu.get_string fields "type" with
+
| Ok "private" ->
+
(match Zulip.Jsonu.get_array fields "display_recipient" with
+
| Ok recipient_json ->
+
let users_results = List.map User.of_json recipient_json in
+
let users = List.fold_left (fun acc result ->
+
match result with
+
| Ok user -> user :: acc
+
| Error err ->
+
Log.warn (fun m -> m "Failed to parse user in display_recipient: %s" (Zulip.Error.message err));
+
acc
+
) [] (List.rev users_results) in
-
| "stream" ->
-
let display_recipient = json |> member "display_recipient" |> to_string in
-
let stream_id = json |> member "stream_id" |> to_int_flex in
-
let subject = json |> member "subject" |> to_string in
-
Ok (Stream { common; display_recipient; stream_id; subject })
+
if List.length users = 0 && List.length users_results > 0 then
+
Error (Zulip.Jsonu.json_error "Failed to parse any users in display_recipient")
+
else
+
Ok (Private { common; display_recipient = users })
+
| Error e -> Error e)
-
| unknown_type ->
-
Log.warn (fun m -> m "Unknown message type: %s" unknown_type);
-
Ok (Unknown { common; raw_json = json })
+
| Ok "stream" ->
+
(match Zulip.Jsonu.get_string fields "display_recipient",
+
Zulip.Jsonu.get_int fields "stream_id",
+
Zulip.Jsonu.get_string fields "subject" with
+
| Ok display_recipient, Ok stream_id, Ok subject ->
+
Ok (Stream { common; display_recipient; stream_id; subject })
+
| Error e, _, _ | _, Error e, _ | _, _, Error e -> Error e)
+
+
| Ok unknown_type ->
+
Log.warn (fun m -> m "Unknown message type: %s" unknown_type);
+
Ok (Unknown { common; raw_json = json })
-
with
-
| Type_error (msg, _) -> Error ("Message type parse error: " ^ msg)
-
| exn -> Error ("Message type parse error: " ^ Printexc.to_string exn)
+
| Error _ ->
+
Log.warn (fun m -> m "No message type field found");
+
Ok (Unknown { common; raw_json = json })
+
) json
+
) |> Result.map_error Zulip.Error.message
(** Accessor functions *)
let get_common = function
···
| Private { display_recipient; _ } ->
display_recipient
|> List.map User.email
-
|> String.concat ","
+
|> String.concat ", "
| Stream { display_recipient; _ } -> display_recipient
| Unknown _ -> ""
···
let parse_command msg =
match extract_command msg with
| None -> None
-
| Some cmd_text ->
-
let parts = String.split_on_char ' ' cmd_text |> List.filter (fun s -> s <> "") in
+
| Some cmd_string ->
+
let parts = String.split_on_char ' ' (String.trim cmd_string) in
match parts with
| [] -> None
| cmd :: args -> Some (cmd, args)
···
(** Pretty print JSON for debugging *)
let pp_json_debug ppf json =
let open Fmt in
-
let json_str = Yojson.Safe.pretty_to_string json in
+
let json_str = Zulip.Jsonu.to_string_pretty json in
pf ppf "@[<v>%a@.%a@]"
(styled `Bold (styled (`Fg `Blue) string)) "Raw JSON:"
(styled (`Fg `Black) string) json_str
+5 -5
stack/zulip/lib/zulip_bot/lib/message.mli
···
content: string;
content_type: string;
reactions: Reaction.t list;
-
submessages: Yojson.Safe.t list;
+
submessages: Zulip.Error.json list;
flags: string list;
is_me_message: bool;
client: string;
···
}
| Unknown of {
common: common;
-
raw_json: Yojson.Safe.t;
+
raw_json: Zulip.Error.json;
}
(** Accessor functions *)
···
val content : t -> string
val content_type : t -> string
val reactions : t -> Reaction.t list
-
val submessages : t -> Yojson.Safe.t list
+
val submessages : t -> Zulip.Error.json list
val flags : t -> string list
val is_me_message : t -> bool
val client : t -> string
···
val parse_command : t -> (string * string list) option
(** JSON parsing *)
-
val of_json : Yojson.Safe.t -> (t, string) result
+
val of_json : Zulip.Error.json -> (t, string) result
(** Pretty printing *)
val pp : Format.formatter -> t -> unit
···
val pp_ansi : ?show_json:bool -> Format.formatter -> t -> unit
(** Pretty print JSON for debugging *)
-
val pp_json_debug : Format.formatter -> Yojson.Safe.t -> unit
+
val pp_json_debug : Format.formatter -> Zulip.Error.json -> unit