My agentic slop goes here. Not intended for anyone else!
1# Zulip Library Architecture 2 3## Overview 4 5The Zulip OCaml library follows a clean, layered architecture that separates protocol types, encoding concerns, and HTTP communication. 6 7## Architecture Layers 8 9``` 10┌─────────────────────────────────────┐ 11│ API Modules (Messages, Channels) │ ← High-level operations 12├─────────────────────────────────────┤ 13│ Protocol Types (Message, Channel) │ ← Business logic types with Jsont codecs 14├─────────────────────────────────────┤ 15│ Encode Module │ ← JSON/Form encoding utilities 16├─────────────────────────────────────┤ 17│ Client Module │ ← HTTP request/response handling 18├─────────────────────────────────────┤ 19│ Requests Library (EIO-based) │ ← Low-level HTTP 20└─────────────────────────────────────┘ 21``` 22 23## Key Design Principles 24 25### 1. **Protocol Types with Jsont Codecs** 26 27Each Zulip API type (Message, Channel, User, etc.) has: 28- A clean OCaml record type 29- A `jsont` codec that defines bidirectional JSON conversion 30- Accessor functions 31- Pretty printer 32 33Example from `channel.ml`: 34```ocaml 35type t = { 36 name : string; 37 description : string; 38 invite_only : bool; 39 history_public_to_subscribers : bool; 40} 41 42let jsont = 43 Jsont.Object.map ~kind:"Channel" make 44 |> Jsont.Object.mem "name" Jsont.string ~enc:name 45 |> Jsont.Object.mem "description" Jsont.string ~enc:description 46 |> Jsont.Object.mem "invite_only" Jsont.bool ~enc:invite_only 47 |> Jsont.Object.mem "history_public_to_subscribers" Jsont.bool ~enc:history_public_to_subscribers 48 |> Jsont.Object.finish 49``` 50 51### 2. **Encode Module: Separation of Encoding Concerns** 52 53The `Encode` module provides clean utilities for converting between OCaml types and wire formats: 54 55```ocaml 56(** Convert using a jsont codec *) 57val to_json_string : 'a Jsont.t -> 'a -> string 58val to_form_urlencoded : 'a Jsont.t -> 'a -> string 59val from_json : 'a Jsont.t -> Jsont.json -> ('a, string) result 60``` 61 62This eliminates the need for: 63- ❌ Manual JSON tree walking 64- ❌ Round-trip encode→decode conversions 65- ❌ Per-type encoding functions 66 67### 3. **Request/Response Types with Codecs** 68 69API operations define request/response types locally with their codecs: 70 71```ocaml 72(* In channels.ml *) 73module Subscribe_request = struct 74 type t = { subscriptions : string list } 75 76 let codec = 77 Jsont.Object.( 78 map ~kind:"SubscribeRequest" (fun subscriptions -> { subscriptions }) 79 |> mem "subscriptions" (Jsont.list Jsont.string) ~enc:(fun r -> r.subscriptions) 80 |> finish 81 ) 82end 83 84let subscribe client ~channels = 85 let req = Subscribe_request.{ subscriptions = channels } in 86 let body = Encode.to_form_urlencoded Subscribe_request.codec req in 87 let content_type = "application/x-www-form-urlencoded" in 88 match Client.request client ~method_:`POST ~path:"/api/v1/users/me/subscriptions" 89 ~body ~content_type () with 90 | Ok _json -> Ok () 91 | Error err -> Error err 92``` 93 94### 4. **Type-Safe Decoding** 95 96Response parsing uses codecs directly instead of manual pattern matching: 97 98```ocaml 99(* OLD - manual JSON walking *) 100match json with 101| Jsont.Object (fields, _) -> 102 let assoc = List.map (fun ((k, _), v) -> (k, v)) fields in 103 (match List.assoc_opt "streams" assoc with 104 | Some (Jsont.Array (channel_list, _)) -> ... 105 106(* NEW - type-safe codec *) 107let response_codec = 108 Jsont.Object.( 109 map ~kind:"StreamsResponse" (fun streams -> streams) 110 |> mem "streams" (Jsont.list Channel.jsont) ~enc:(fun x -> x) 111 |> finish 112 ) 113in 114match Encode.from_json response_codec json with 115| Ok channels -> Ok channels 116| Error msg -> Error (...) 117``` 118 119## Benefits 120 121### ✅ Type Safety 122- Jsont codecs ensure correct JSON structure 123- Compilation errors catch schema mismatches 124- No runtime type confusion 125 126### ✅ Maintainability 127- Protocol changes only require updating codecs 128- No manual JSON manipulation scattered through code 129- Clear separation of concerns 130 131### ✅ Reusability 132- Codecs can be composed and reused 133- Encode module works for any jsont-encoded type 134- Request/response types are self-documenting 135 136### ✅ Testability 137- Easy to test encoding/decoding in isolation 138- Mock responses can be type-checked 139- Round-trip property testing possible 140 141## Migration Pattern 142 143When adding new API endpoints: 144 1451. **Define the protocol type with codec**: 146 ```ocaml 147 type my_request = { field1: string; field2: int } 148 149 let my_request_codec = 150 Jsont.Object.( 151 map ~kind:"MyRequest" (fun field1 field2 -> { field1; field2 }) 152 |> mem "field1" Jsont.string ~enc:(fun r -> r.field1) 153 |> mem "field2" Jsont.int ~enc:(fun r -> r.field2) 154 |> finish 155 ) 156 ``` 157 1582. **Encode using Encode module**: 159 ```ocaml 160 let body = Encode.to_form_urlencoded my_request_codec req in 161 (* or *) 162 let json = Encode.to_json_string my_request_codec req in 163 ``` 164 1653. **Decode responses with codec**: 166 ```ocaml 167 match Client.request client ~method_:`POST ~path:"/api/..." ~body () with 168 | Ok json -> 169 (match Encode.from_json response_codec json with 170 | Ok data -> Ok data 171 | Error msg -> Error ...) 172 ``` 173 174## Comparison with Old Approach 175 176### Old (Manual JSON Manipulation): 177```ocaml 178let send client message = 179 let json = Message.to_json message in (* Round-trip conversion *) 180 let params = match json with 181 | Jsont.Object (fields, _) -> (* Manual pattern matching *) 182 List.fold_left (fun acc ((key, _), value) -> 183 let str_value = match value with (* More pattern matching *) 184 | Jsont.String (s, _) -> s 185 | Jsont.Bool (true, _) -> "true" 186 | _ -> "" 187 in 188 (key, str_value) :: acc 189 ) [] fields 190 | _ -> [] in 191 (* ... *) 192``` 193 194### New (Codec-Based): 195```ocaml 196let send client message = 197 let body = Message.to_form_urlencoded message in (* Clean encoding *) 198 let content_type = "application/x-www-form-urlencoded" in 199 match Client.request client ~method_:`POST ~path:"/api/v1/messages" 200 ~body ~content_type () with 201 | Ok response -> Message_response.of_json response 202 | Error err -> Error err 203``` 204 205## Future Enhancements 206 207- **Validation**: Add validation layers on top of codecs 208- **Versioning**: Support multiple API versions with codec variants 209- **Documentation**: Generate API docs from codec definitions 210- **Testing**: Property-based testing with codec round-trips 211- **Code Generation**: Consider generating codecs from OpenAPI specs 212 213## References 214 215- Jsont library: https://erratique.ch/software/jsont 216- Zulip REST API: https://zulip.com/api/rest 217- Original design doc: `CLAUDE.md`