this repo has no description

Add ResultReference module for composing JMAP requests

- Created a new ResultReference module to simplify creating and using result references as described in RFC8620 Section 3.7
- Added helper functions to create common reference patterns
- Implemented demo functionality in fastmail_list to show how to chain multiple JMAP requests efficiently
- Added a new -demo-refs command line option to demonstrate this functionality
- Updated AGENT.md to mark task 11 as completed

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+213 -1
bin
lib
+1 -1
AGENT.md
··· 45 45 9. DONE Read the mailbox attribute spec in specs/ and add a typed interface to the 46 46 JMAP labels defined in there. 47 47 10. DONE Integrate the human-readable keyword and label printing into fastmail-list. 48 - 11. Add an OCaml interface to compose result references together explicitly into a 48 + 11. DONE Add an OCaml interface to compose result references together explicitly into a 49 49 single request, from reading the specs.
+132
bin/fastmail_list.ml
··· 64 64 in 65 65 is_unread_keyword || is_not_seen 66 66 67 + (** Example function demonstrating how to use result references for chained requests *) 68 + let demo_result_references conn account_id = 69 + let open Jmap.Types in 70 + 71 + (* Create a request that chains the following operations: 72 + 1. Get mailboxes 73 + 2. Query emails in the first mailbox found 74 + 3. Get the full email objects for those IDs 75 + *) 76 + 77 + (* Create method call IDs *) 78 + let mailbox_get_id = "mailboxGet" in 79 + let email_query_id = "emailQuery" in 80 + let email_get_id = "emailGet" in 81 + 82 + (* First call: Get mailboxes *) 83 + let mailbox_get_call = { 84 + name = "Mailbox/get"; 85 + arguments = `O [ 86 + ("accountId", `String account_id); 87 + ]; 88 + method_call_id = mailbox_get_id; 89 + } in 90 + 91 + (* Second call: Query emails in the first mailbox using result reference *) 92 + (* Create reference to the first mailbox ID from the previous result *) 93 + let mailbox_id_ref = Jmap.ResultReference.create 94 + ~result_of:mailbox_get_id 95 + ~name:"Mailbox/get" 96 + ~path:"/list/0/id" in 97 + 98 + (* Use the reference to create the query arguments *) 99 + let (mailbox_id_ref_key, mailbox_id_ref_value) = 100 + Jmap.ResultReference.reference_arg "inMailbox" mailbox_id_ref in 101 + 102 + let email_query_call = { 103 + name = "Email/query"; 104 + arguments = `O [ 105 + ("accountId", `String account_id); 106 + ("filter", `O [ 107 + (mailbox_id_ref_key, mailbox_id_ref_value) 108 + ]); 109 + ("limit", `Float 10.0); 110 + ]; 111 + method_call_id = email_query_id; 112 + } in 113 + 114 + (* Third call: Get full email objects using the query result *) 115 + (* Create reference to the email IDs from the query result *) 116 + let email_ids_ref = Jmap.ResultReference.create 117 + ~result_of:email_query_id 118 + ~name:"Email/query" 119 + ~path:"/ids" in 120 + 121 + (* Use the reference to create the get arguments *) 122 + let (email_ids_ref_key, email_ids_ref_value) = 123 + Jmap.ResultReference.reference_arg "ids" email_ids_ref in 124 + 125 + let email_get_call = { 126 + name = "Email/get"; 127 + arguments = `O [ 128 + ("accountId", `String account_id); 129 + (email_ids_ref_key, email_ids_ref_value) 130 + ]; 131 + method_call_id = email_get_id; 132 + } in 133 + 134 + (* Create the complete request with all three method calls *) 135 + let request = { 136 + using = [ 137 + Jmap.Capability.to_string Jmap.Capability.Core; 138 + Jmap_mail.Capability.to_string Jmap_mail.Capability.Mail 139 + ]; 140 + method_calls = [ 141 + mailbox_get_call; 142 + email_query_call; 143 + email_get_call 144 + ]; 145 + created_ids = None; 146 + } in 147 + 148 + (* Make the request *) 149 + let* response_result = Jmap.Api.make_request conn.config request in 150 + Printf.printf "\nResult Reference Demo:\n"; 151 + Printf.printf "=====================\n"; 152 + 153 + match response_result with 154 + | Error err -> 155 + Printf.printf "Error executing chained request: %s\n" 156 + (match err with 157 + | Jmap.Api.Connection_error msg -> "Connection error: " ^ msg 158 + | Jmap.Api.HTTP_error (code, body) -> Printf.sprintf "HTTP error %d: %s" code body 159 + | Jmap.Api.Parse_error msg -> "Parse error: " ^ msg 160 + | Jmap.Api.Authentication_error -> "Authentication error"); 161 + Lwt.return_unit 162 + | Ok response -> 163 + (* Process the response *) 164 + try 165 + (* Look for the Email/get method response *) 166 + let email_get_result = List.find (fun (inv : Ezjsonm.value invocation) -> 167 + inv.name = "Email/get" 168 + ) response.method_responses in 169 + 170 + (* Extract the email list from the response *) 171 + let list = Ezjsonm.find email_get_result.arguments ["list"] in 172 + match list with 173 + | `A emails -> 174 + Printf.printf "Successfully retrieved %d emails using chained result references!\n" 175 + (List.length emails); 176 + Lwt.return_unit 177 + | _ -> 178 + Printf.printf "Unexpected email list format in response.\n"; 179 + Lwt.return_unit 180 + with 181 + | Not_found -> 182 + Printf.printf "No Email/get result found in response.\n"; 183 + Lwt.return_unit 184 + | e -> 185 + Printf.printf "Error processing response: %s\n" (Printexc.to_string e); 186 + Lwt.return_unit 187 + 67 188 (** Main function *) 68 189 let main () = 69 190 (* Parse command-line arguments *) 70 191 let unread_only = ref false in 71 192 let show_labels = ref false in 72 193 let debug_level = ref 0 in 194 + let demo_refs = ref false in 73 195 74 196 let args = [ 75 197 ("-unread", Arg.Set unread_only, "List only unread messages"); 76 198 ("-labels", Arg.Set show_labels, "Show labels/keywords associated with messages"); 77 199 ("-debug", Arg.Int (fun level -> debug_level := level), "Set debug level (0-4, where 4 is most verbose)"); 200 + ("-demo-refs", Arg.Set demo_refs, "Demonstrate result references"); 78 201 ] in 79 202 80 203 let usage_msg = "Usage: JMAP_API_TOKEN=your_token fastmail_list [options]" in ··· 91 214 Printf.eprintf " -unread List only unread messages\n"; 92 215 Printf.eprintf " -labels Show labels/keywords associated with messages\n"; 93 216 Printf.eprintf " -debug LEVEL Set debug level (0-4, where 4 is most verbose)\n"; 217 + Printf.eprintf " -demo-refs Demonstrate result references\n"; 94 218 exit 1 95 219 | Some token -> 96 220 (* Only print token info at Info level or higher *) ··· 143 267 | [] -> 144 268 Printf.eprintf "No accounts found\n"; 145 269 exit 1 270 + in 271 + 272 + (* Run result references demo if requested *) 273 + let* () = 274 + if !demo_refs then 275 + demo_result_references conn primary_account_id 276 + else 277 + Lwt.return_unit 146 278 in 147 279 148 280 (* Get the Inbox mailbox *)
+48
lib/jmap.ml
··· 381 381 } 382 382 end 383 383 384 + (** Module for working with ResultReferences as described in Section 3.7 of RFC8620 *) 385 + module ResultReference = struct 386 + open Types 387 + 388 + (** Create a reference to a previous method result *) 389 + let create ~result_of ~name ~path = 390 + { result_of; name; path } 391 + 392 + (** Create a JSON pointer path to access a specific property *) 393 + let property_path property = 394 + "/" ^ property 395 + 396 + (** Create a JSON pointer path to access all items in an array with a specific property *) 397 + let array_items_path ?(property="") array_property = 398 + let base = "/" ^ array_property ^ "/*" in 399 + if property = "" then base 400 + else base ^ "/" ^ property 401 + 402 + (** Create argument with result reference. 403 + Returns string key prefixed with # and ResultReference value. *) 404 + let reference_arg arg_name ref_obj = 405 + (* Prefix argument name with # *) 406 + let prefixed_name = "#" ^ arg_name in 407 + 408 + (* Convert reference object to JSON *) 409 + let json_value = `O [ 410 + ("resultOf", `String ref_obj.result_of); 411 + ("name", `String ref_obj.name); 412 + ("path", `String ref_obj.path) 413 + ] in 414 + 415 + (prefixed_name, json_value) 416 + 417 + (** Create a reference to all IDs returned by a query method *) 418 + let query_ids ~result_of = 419 + create 420 + ~result_of 421 + ~name:"Foo/query" 422 + ~path:"/ids" 423 + 424 + (** Create a reference to properties of objects returned by a get method *) 425 + let get_property ~result_of ~property = 426 + create 427 + ~result_of 428 + ~name:"Foo/get" 429 + ~path:("/list/*/" ^ property) 430 + end 431 + 384 432 module Api = struct 385 433 open Lwt.Syntax 386 434 open Types
+32
lib/jmap.mli
··· 342 342 343 343 (** {1 API Client} *) 344 344 345 + (** Module for working with ResultReferences as described in Section 3.7 of RFC8620. 346 + Provides utilities to create and compose results from previous methods. *) 347 + module ResultReference : sig 348 + (** Create a reference to a previous method result *) 349 + val create : 350 + result_of:string -> 351 + name:string -> 352 + path:string -> 353 + Types.result_reference 354 + 355 + (** Create a JSON pointer path to access a specific property *) 356 + val property_path : string -> string 357 + 358 + (** Create a JSON pointer path to access all items in an array with a specific property *) 359 + val array_items_path : ?property:string -> string -> string 360 + 361 + (** Create argument with result reference. 362 + Returns string key prefixed with # and ResultReference value. *) 363 + val reference_arg : string -> Types.result_reference -> string * Ezjsonm.value 364 + 365 + (** Create a reference to all IDs returned by a query method *) 366 + val query_ids : 367 + result_of:string -> 368 + Types.result_reference 369 + 370 + (** Create a reference to properties of objects returned by a get method *) 371 + val get_property : 372 + result_of:string -> 373 + property:string -> 374 + Types.result_reference 375 + end 376 + 345 377 (** Module for making JMAP API requests over HTTP. 346 378 Provides functionality to interact with JMAP servers according to RFC8620. *) 347 379 module Api : sig