# Zulip Library Architecture ## Overview The Zulip OCaml library follows a clean, layered architecture that separates protocol types, encoding concerns, and HTTP communication. ## Architecture Layers ``` ┌─────────────────────────────────────┐ │ API Modules (Messages, Channels) │ ← High-level operations ├─────────────────────────────────────┤ │ Protocol Types (Message, Channel) │ ← Business logic types with Jsont codecs ├─────────────────────────────────────┤ │ Encode Module │ ← JSON/Form encoding utilities ├─────────────────────────────────────┤ │ Client Module │ ← HTTP request/response handling ├─────────────────────────────────────┤ │ Requests Library (EIO-based) │ ← Low-level HTTP └─────────────────────────────────────┘ ``` ## Key Design Principles ### 1. **Protocol Types with Jsont Codecs** Each Zulip API type (Message, Channel, User, etc.) has: - A clean OCaml record type - A `jsont` codec that defines bidirectional JSON conversion - Accessor functions - Pretty printer Example from `channel.ml`: ```ocaml type t = { name : string; description : string; invite_only : bool; history_public_to_subscribers : bool; } let jsont = Jsont.Object.map ~kind:"Channel" make |> Jsont.Object.mem "name" Jsont.string ~enc:name |> Jsont.Object.mem "description" Jsont.string ~enc:description |> Jsont.Object.mem "invite_only" Jsont.bool ~enc:invite_only |> Jsont.Object.mem "history_public_to_subscribers" Jsont.bool ~enc:history_public_to_subscribers |> Jsont.Object.finish ``` ### 2. **Encode Module: Separation of Encoding Concerns** The `Encode` module provides clean utilities for converting between OCaml types and wire formats: ```ocaml (** Convert using a jsont codec *) val to_json_string : 'a Jsont.t -> 'a -> string val to_form_urlencoded : 'a Jsont.t -> 'a -> string val from_json : 'a Jsont.t -> Jsont.json -> ('a, string) result ``` This eliminates the need for: - ❌ Manual JSON tree walking - ❌ Round-trip encode→decode conversions - ❌ Per-type encoding functions ### 3. **Request/Response Types with Codecs** API operations define request/response types locally with their codecs: ```ocaml (* In channels.ml *) module Subscribe_request = struct type t = { subscriptions : string list } let codec = Jsont.Object.( map ~kind:"SubscribeRequest" (fun subscriptions -> { subscriptions }) |> mem "subscriptions" (Jsont.list Jsont.string) ~enc:(fun r -> r.subscriptions) |> finish ) end let subscribe client ~channels = let req = Subscribe_request.{ subscriptions = channels } in let body = Encode.to_form_urlencoded Subscribe_request.codec req in let content_type = "application/x-www-form-urlencoded" in match Client.request client ~method_:`POST ~path:"/api/v1/users/me/subscriptions" ~body ~content_type () with | Ok _json -> Ok () | Error err -> Error err ``` ### 4. **Type-Safe Decoding** Response parsing uses codecs directly instead of manual pattern matching: ```ocaml (* OLD - manual JSON walking *) match json with | Jsont.Object (fields, _) -> let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in (match List.assoc_opt "streams" assoc with | Some (Jsont.Array (channel_list, _)) -> ... (* NEW - type-safe codec *) let response_codec = Jsont.Object.( map ~kind:"StreamsResponse" (fun streams -> streams) |> mem "streams" (Jsont.list Channel.jsont) ~enc:(fun x -> x) |> finish ) in match Encode.from_json response_codec json with | Ok channels -> Ok channels | Error msg -> Error (...) ``` ## Benefits ### ✅ Type Safety - Jsont codecs ensure correct JSON structure - Compilation errors catch schema mismatches - No runtime type confusion ### ✅ Maintainability - Protocol changes only require updating codecs - No manual JSON manipulation scattered through code - Clear separation of concerns ### ✅ Reusability - Codecs can be composed and reused - Encode module works for any jsont-encoded type - Request/response types are self-documenting ### ✅ Testability - Easy to test encoding/decoding in isolation - Mock responses can be type-checked - Round-trip property testing possible ## Migration Pattern When adding new API endpoints: 1. **Define the protocol type with codec**: ```ocaml type my_request = { field1: string; field2: int } let my_request_codec = Jsont.Object.( map ~kind:"MyRequest" (fun field1 field2 -> { field1; field2 }) |> mem "field1" Jsont.string ~enc:(fun r -> r.field1) |> mem "field2" Jsont.int ~enc:(fun r -> r.field2) |> finish ) ``` 2. **Encode using Encode module**: ```ocaml let body = Encode.to_form_urlencoded my_request_codec req in (* or *) let json = Encode.to_json_string my_request_codec req in ``` 3. **Decode responses with codec**: ```ocaml match Client.request client ~method_:`POST ~path:"/api/..." ~body () with | Ok json -> (match Encode.from_json response_codec json with | Ok data -> Ok data | Error msg -> Error ...) ``` ## Comparison with Old Approach ### Old (Manual JSON Manipulation): ```ocaml let send client message = let json = Message.to_json message in (* Round-trip conversion *) let params = match json with | Jsont.Object (fields, _) -> (* Manual pattern matching *) List.fold_left (fun acc ((key, _), value) -> let str_value = match value with (* More pattern matching *) | Jsont.String (s, _) -> s | Jsont.Bool (true, _) -> "true" | _ -> "" in (key, str_value) :: acc ) [] fields | _ -> [] in (* ... *) ``` ### New (Codec-Based): ```ocaml let send client message = let body = Message.to_form_urlencoded message in (* Clean encoding *) let content_type = "application/x-www-form-urlencoded" in match Client.request client ~method_:`POST ~path:"/api/v1/messages" ~body ~content_type () with | Ok response -> Message_response.of_json response | Error err -> Error err ``` ## Future Enhancements - **Validation**: Add validation layers on top of codecs - **Versioning**: Support multiple API versions with codec variants - **Documentation**: Generate API docs from codec definitions - **Testing**: Property-based testing with codec round-trips - **Code Generation**: Consider generating codecs from OpenAPI specs ## References - Jsont library: https://erratique.ch/software/jsont - Zulip REST API: https://zulip.com/api/rest - Original design doc: `CLAUDE.md`