My agentic slop goes here. Not intended for anyone else!
1# Parser Implementation Guide 2 3This guide will help you complete the JSON parser implementations throughout the JMAP codebase. All type definitions are complete - only the parsing logic needs to be filled in. 4 5## Overview 6 7**Status**: All `of_json` and `to_json` functions have stub implementations that raise "not yet implemented" errors. 8 9**Goal**: Implement these functions using the provided test JSON files as specifications. 10 11**Tools**: Use `Jmap_parser.Helpers` module for common parsing operations. 12 13## Implementation Strategy 14 15### Step 1: Start with Primitives (Easiest) 16 17These are already complete, but review them as examples: 18 19```ocaml 20(* jmap-core/jmap_id.ml - COMPLETE *) 21let of_json json = 22 match json with 23 | `String s -> of_string s 24 | _ -> raise (Jmap_error.Parse_error "Id must be a JSON string") 25 26(* jmap-core/jmap_primitives.ml - COMPLETE *) 27module UnsignedInt = struct 28 let of_json = function 29 | `Float f -> of_int (int_of_float f) 30 | `Int i -> if i >= 0 then of_int i else raise (Parse_error "...") 31 | _ -> raise (Parse_error "Expected number") 32end 33``` 34 35### Step 2: Implement Core Parsers 36 37#### 2.1 Comparator (Simple Object) 38 39**File**: `jmap-core/jmap_comparator.ml` 40**Test**: `test/data/core/request_query.json` (sort field) 41 42```ocaml 43let of_json json = 44 let open Jmap_parser.Helpers in 45 let fields = expect_object json in 46 let property = get_string "property" fields in 47 let is_ascending = get_bool_opt "isAscending" fields true in 48 let collation = get_string_opt "collation" fields in 49 { property; is_ascending; collation } 50``` 51 52#### 2.2 Filter (Recursive Type) 53 54**File**: `jmap-core/jmap_filter.ml` 55**Test**: `test/data/core/request_query.json` (filter field) 56 57The generic `of_json` function is already complete. You need to implement condition parsers for each type (Mailbox, Email, etc.). 58 59#### 2.3 Session (Complex Nested Object) 60 61**File**: `jmap-core/jmap_session.ml` 62**Test**: `test/data/core/session.json` 63 64```ocaml 65(* Account parser *) 66module Account = struct 67 let of_json json = 68 let open Jmap_parser.Helpers in 69 let fields = expect_object json in 70 { 71 name = get_string "name" fields; 72 is_personal = get_bool "isPersonal" fields; 73 is_read_only = get_bool "isReadOnly" fields; 74 account_capabilities = parse_map (fun v -> v) 75 (require_field "accountCapabilities" fields); 76 } 77end 78 79(* Session parser *) 80let of_json json = 81 let open Jmap_parser.Helpers in 82 let fields = expect_object json in 83 { 84 capabilities = parse_map (fun v -> v) (require_field "capabilities" fields); 85 accounts = parse_map Account.of_json (require_field "accounts" fields); 86 primary_accounts = parse_map expect_string (require_field "primaryAccounts" fields); 87 username = get_string "username" fields; 88 api_url = get_string "apiUrl" fields; 89 download_url = get_string "downloadUrl" fields; 90 upload_url = get_string "uploadUrl" fields; 91 event_source_url = get_string "eventSourceUrl" fields; 92 state = get_string "state" fields; 93 } 94``` 95 96#### 2.4 Invocation (3-tuple Array) 97 98**File**: `jmap-core/jmap_invocation.ml` 99**Test**: Any request or response file (methodCalls/methodResponses field) 100 101```ocaml 102let of_json json = 103 let open Jmap_parser.Helpers in 104 match json with 105 | `A [method_name_json; arguments_json; call_id_json] -> 106 let method_name = expect_string method_name_json in 107 let call_id = expect_string call_id_json in 108 109 (* Parse based on method name *) 110 begin match witness_of_method_name method_name with 111 | Packed template -> 112 (* Parse arguments based on witness type *) 113 (* Return properly typed invocation *) 114 (* TODO: Complete this logic *) 115 raise (Parse_error "Invocation parsing not complete") 116 end 117 118 | _ -> raise (Parse_error "Invocation must be 3-element array") 119``` 120 121#### 2.5 Request and Response 122 123**File**: `jmap-core/jmap_request.ml` and `jmap_response.ml` 124**Test**: All `test/data/core/request_*.json` and `response_*.json` 125 126```ocaml 127(* Request *) 128let of_json json = 129 let open Jmap_parser.Helpers in 130 let fields = expect_object json in 131 { 132 using = parse_array Jmap_capability.of_json 133 (require_field "using" fields); 134 method_calls = parse_array Jmap_invocation.of_json 135 (require_field "methodCalls" fields); 136 created_ids = match find_field "createdIds" fields with 137 | Some obj -> Some (parse_map Jmap_id.of_json obj) 138 | None -> None; 139 } 140 141(* Response - similar pattern *) 142``` 143 144### Step 3: Implement Standard Method Parsers 145 146These follow predictable patterns. Example for Get: 147 148**File**: `jmap-core/jmap_standard_methods.ml` 149**Tests**: `test/data/core/request_get.json`, `response_get.json` 150 151```ocaml 152module Get = struct 153 let request_of_json parse_obj json = 154 let open Jmap_parser.Helpers in 155 let fields = expect_object json in 156 { 157 account_id = Jmap_id.of_json (require_field "accountId" fields); 158 ids = parse_array_opt Jmap_id.of_json (find_field "ids" fields); 159 properties = parse_array_opt expect_string (find_field "properties" fields); 160 } 161 162 let response_of_json parse_obj json = 163 let open Jmap_parser.Helpers in 164 let fields = expect_object json in 165 { 166 account_id = Jmap_id.of_json (require_field "accountId" fields); 167 state = get_string "state" fields; 168 list = parse_array parse_obj (require_field "list" fields); 169 not_found = parse_array Jmap_id.of_json (require_field "notFound" fields); 170 } 171end 172 173(* Repeat for Changes, Set, Copy, Query, QueryChanges *) 174``` 175 176### Step 4: Implement Mail Type Parsers 177 178#### 4.1 Mailbox (Simple Mail Type) 179 180**File**: `jmap-mail/jmap_mailbox.ml` 181**Tests**: `test/data/mail/mailbox_get_response.json` 182 183```ocaml 184module Rights = struct 185 let of_json json = 186 let open Jmap_parser.Helpers in 187 let fields = expect_object json in 188 { 189 may_read_items = get_bool "mayReadItems" fields; 190 may_add_items = get_bool "mayAddItems" fields; 191 may_remove_items = get_bool "mayRemoveItems" fields; 192 may_set_seen = get_bool "maySetSeen" fields; 193 may_set_keywords = get_bool "maySetKeywords" fields; 194 may_create_child = get_bool "mayCreateChild" fields; 195 may_rename = get_bool "mayRename" fields; 196 may_delete = get_bool "mayDelete" fields; 197 may_submit = get_bool "maySubmit" fields; 198 } 199end 200 201let of_json json = 202 let open Jmap_parser.Helpers in 203 let fields = expect_object json in 204 { 205 id = Jmap_id.of_json (require_field "id" fields); 206 name = get_string "name" fields; 207 parent_id = Option.map Jmap_id.of_json (find_field "parentId" fields); 208 role = get_string_opt "role" fields; 209 sort_order = Jmap_primitives.UnsignedInt.of_json 210 (require_field "sortOrder" fields); 211 total_emails = Jmap_primitives.UnsignedInt.of_json 212 (require_field "totalEmails" fields); 213 unread_emails = Jmap_primitives.UnsignedInt.of_json 214 (require_field "unreadEmails" fields); 215 total_threads = Jmap_primitives.UnsignedInt.of_json 216 (require_field "totalThreads" fields); 217 unread_threads = Jmap_primitives.UnsignedInt.of_json 218 (require_field "unreadThreads" fields); 219 my_rights = Rights.of_json (require_field "myRights" fields); 220 is_subscribed = get_bool "isSubscribed" fields; 221 } 222``` 223 224#### 4.2 Email (Most Complex) 225 226**File**: `jmap-mail/jmap_email.ml` 227**Tests**: 228- `test/data/mail/email_get_response.json` (basic) 229- `test/data/mail/email_get_full_response.json` (with body structure) 230 231Start with submodules: 232 233```ocaml 234module EmailAddress = struct 235 let of_json json = 236 let open Jmap_parser.Helpers in 237 let fields = expect_object json in 238 { 239 name = get_string_opt "name" fields; 240 email = get_string "email" fields; 241 } 242end 243 244module BodyPart = struct 245 (* Recursive parser for MIME structure *) 246 let rec of_json json = 247 let open Jmap_parser.Helpers in 248 let fields = expect_object json in 249 { 250 part_id = get_string_opt "partId" fields; 251 blob_id = Option.map Jmap_id.of_json (find_field "blobId" fields); 252 size = Jmap_primitives.UnsignedInt.of_json (require_field "size" fields); 253 headers = parse_array parse_header (require_field "headers" fields); 254 name = get_string_opt "name" fields; 255 type_ = get_string "type" fields; 256 charset = get_string_opt "charset" fields; 257 disposition = get_string_opt "disposition" fields; 258 cid = get_string_opt "cid" fields; 259 language = parse_array_opt expect_string (find_field "language" fields); 260 location = get_string_opt "location" fields; 261 sub_parts = parse_array_opt of_json (find_field "subParts" fields); 262 } 263 264 and parse_header json = 265 let open Jmap_parser.Helpers in 266 let fields = expect_object json in 267 (get_string "name" fields, get_string "value" fields) 268end 269 270(* Main Email parser *) 271let of_json json = 272 let open Jmap_parser.Helpers in 273 let fields = expect_object json in 274 { 275 (* Parse all 24 fields *) 276 id = Jmap_id.of_json (require_field "id" fields); 277 blob_id = Jmap_id.of_json (require_field "blobId" fields); 278 thread_id = Jmap_id.of_json (require_field "threadId" fields); 279 mailbox_ids = parse_map (fun _ -> true) (require_field "mailboxIds" fields); 280 keywords = parse_map (fun _ -> true) (require_field "keywords" fields); 281 (* ... continue for all fields ... *) 282 from = parse_array_opt EmailAddress.of_json (find_field "from" fields); 283 to_ = parse_array_opt EmailAddress.of_json (find_field "to" fields); 284 body_structure = Option.map BodyPart.of_json (find_field "bodyStructure" fields); 285 (* ... etc ... *) 286 } 287``` 288 289### Step 5: Testing Pattern 290 291For each parser you implement: 292 293```ocaml 294(* In test/test_jmap.ml *) 295 296let test_mailbox_parse () = 297 (* Load test JSON *) 298 let json = load_json "test/data/mail/mailbox_get_response.json" in 299 300 (* Parse response *) 301 let response = Jmap_mail.Jmap_mailbox.Get.response_of_json 302 Jmap_mailbox.Parser.of_json json in 303 304 (* Validate *) 305 check int "Mailbox count" 3 (List.length response.list); 306 307 let inbox = List.hd response.list in 308 check string "Inbox name" "Inbox" inbox.name; 309 check (option string) "Inbox role" (Some "inbox") inbox.role; 310 check bool "Can read" true inbox.my_rights.may_read_items; 311 312let () = 313 run "JMAP" [ 314 "Mailbox", [ 315 test_case "Parse mailbox response" `Quick test_mailbox_parse; 316 ]; 317 ] 318``` 319 320## Common Patterns 321 322### Optional Fields 323 324```ocaml 325(* Option with default *) 326let is_ascending = get_bool_opt "isAscending" fields true 327 328(* Option without default *) 329let collation = get_string_opt "collation" fields 330 331(* Map with option *) 332parent_id = Option.map Jmap_id.of_json (find_field "parentId" fields) 333``` 334 335### Arrays 336 337```ocaml 338(* Required array *) 339ids = parse_array Jmap_id.of_json (require_field "ids" fields) 340 341(* Optional array (null or array) *) 342properties = parse_array_opt expect_string (find_field "properties" fields) 343``` 344 345### Maps (JSON Objects) 346 347```ocaml 348(* String -> value *) 349keywords = parse_map (fun v -> true) (require_field "keywords" fields) 350 351(* Id -> value *) 352mailbox_ids = parse_map Jmap_id.of_json (require_field "mailboxIds" fields) 353``` 354 355### Recursive Types 356 357```ocaml 358(* Mutually recursive *) 359let rec parse_filter parse_condition json = 360 match json with 361 | `O fields -> 362 match List.assoc_opt "operator" fields with 363 | Some op -> (* FilterOperator *) 364 let conditions = parse_array (parse_filter parse_condition) ... in 365 Operator (op, conditions) 366 | None -> (* FilterCondition *) 367 Condition (parse_condition json) 368 | _ -> raise (Parse_error "...") 369``` 370 371## Helper Reference 372 373```ocaml 374(* From Jmap_parser.Helpers *) 375 376(* Type expectations *) 377expect_object : json -> (string * json) list 378expect_array : json -> json list 379expect_string : json -> string 380expect_int : json -> int 381expect_bool : json -> bool 382 383(* Field access *) 384find_field : string -> fields -> json option 385require_field : string -> fields -> json 386 387(* Typed getters *) 388get_string : string -> fields -> string 389get_string_opt : string -> fields -> string option 390get_bool : string -> fields -> bool 391get_bool_opt : string -> fields -> bool -> bool (* with default *) 392get_int : string -> fields -> int 393get_int_opt : string -> fields -> int option 394 395(* Parsers *) 396parse_map : (json -> 'a) -> json -> (string * 'a) list 397parse_array : (json -> 'a) -> json -> 'a list 398parse_array_opt : (json -> 'a) -> json option -> 'a list option 399``` 400 401## Order of Implementation 402 403Recommended order (easiest to hardest): 404 4051. ✅ Primitives (already done) 4062. `Jmap_comparator` - Simple object 4073. `Jmap_capability.CoreCapability` - Nested object 4084. `Jmap_session` - Complex nested object with maps 4095. `Jmap_standard_methods.Get.request` - Simple with optionals 4106. `Jmap_standard_methods.Get.response` - With generic list 4117. Other standard methods (Changes, Query, etc.) 4128. `Jmap_invocation` - Array tuple with GADT dispatch 4139. `Jmap_request` and `Jmap_response` - Top-level protocol 41410. `Jmap_mailbox` - Simplest mail type 41511. `Jmap_thread` - Very simple (2 fields) 41612. `Jmap_identity` - Medium complexity 41713. `Jmap_vacation_response` - Singleton pattern 41814. `Jmap_search_snippet` - Search results 41915. `Jmap_email_submission` - With enums and envelope 42016. `Jmap_email` - Most complex (save for last) 421 422## Validation Strategy 423 424For each parser: 425 4261. **Parse test file**: Ensure no exceptions 4272. **Check required fields**: Verify non-optional fields are present 4283. **Validate values**: Check actual values match test file 4294. **Round-trip**: Serialize and parse again, compare 4305. **Error cases**: Try malformed JSON, missing fields 431 432## Serialization (to_json) 433 434After parsing is complete, implement serialization: 435 436```ocaml 437let to_json t = 438 `O [ 439 ("id", Jmap_id.to_json t.id); 440 ("name", `String t.name); 441 ("sortOrder", Jmap_primitives.UnsignedInt.to_json t.sort_order); 442 (* ... *) 443 ] 444``` 445 446Remove fields that are None: 447 448```ocaml 449let fields = [ 450 ("id", Jmap_id.to_json t.id); 451 ("name", `String t.name); 452] in 453let fields = match t.parent_id with 454 | Some pid -> ("parentId", Jmap_id.to_json pid) :: fields 455 | None -> fields 456in 457`O fields 458``` 459 460## Common Pitfalls 461 4621. **Case sensitivity**: JSON field names are case-sensitive 463 - Use `"receivedAt"` not `"receivedat"` 464 4652. **Null vs absent**: Distinguish between `null` and field not present 466 ```ocaml 467 | Some `Null -> None (* null *) 468 | Some value -> Some (parse value) (* present *) 469 | None -> None (* absent *) 470 ``` 471 4723. **Empty arrays**: `[]` is different from `null` 473 ```ocaml 474 parse_array_opt (* Returns None for null, Some [] for [] *) 475 ``` 476 4774. **Number types**: JSON doesn't distinguish int/float 478 ```ocaml 479 | `Float f -> int_of_float f 480 | `Int i -> i 481 ``` 482 4835. **Boolean maps**: Many fields are `Id[Boolean]` 484 ```ocaml 485 mailbox_ids = parse_map (fun _ -> true) field 486 ``` 487 488## Getting Help 489 4901. **Check test files**: They contain the exact JSON structure 4912. **Look at existing parsers**: Id and primitives are complete 4923. **Use the helpers**: They handle most common cases 4934. **Follow the types**: Type errors will guide you 494 495## Success Criteria 496 497Parser implementation is complete when: 498 499- [ ] All test files parse without errors 500- [ ] All required fields are extracted 501- [ ] Optional fields handled correctly 502- [ ] Round-trip works (parse -> serialize -> parse) 503- [ ] All 50 test files pass 504- [ ] No TODO comments remain in parser code 505 506Good luck! Start simple and build up to the complex types. The type system will guide you.