this repo has no description

Compare changes

Choose any two refs to compare.

+20 -1
.gitignore
··· 1 - _build 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Third-party sources (fetch locally with opam source) 7 + third_party/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/ 18 + 19 + # Environment and secrets 2 20 .env 3 21 .api-key 22 + .api-key-rw 4 23 .api-url
+1
.ocamlformat
··· 1 + version=0.28.1
+53
.tangled/workflows/build.yml
··· 1 + when: 2 + - event: ["push", "pull_request"] 3 + branch: ["main"] 4 + 5 + engine: nixery 6 + 7 + dependencies: 8 + nixpkgs: 9 + - shell 10 + - stdenv 11 + - findutils 12 + - binutils 13 + - libunwind 14 + - ncurses 15 + - opam 16 + - git 17 + - gawk 18 + - gnupatch 19 + - gnum4 20 + - gnumake 21 + - gnutar 22 + - gnused 23 + - gnugrep 24 + - diffutils 25 + - gzip 26 + - bzip2 27 + - gcc 28 + - ocaml 29 + - pkg-config 30 + 31 + steps: 32 + - name: opam 33 + command: | 34 + opam init --disable-sandboxing -a -y 35 + - name: repo 36 + command: | 37 + opam repo add aoah https://tangled.org/anil.recoil.org/aoah-opam-repo.git 38 + - name: switch 39 + command: | 40 + opam install . --confirm-level=unsafe-yes --deps-only 41 + - name: build 42 + command: | 43 + opam exec -- dune build -p jmap 44 + - name: switch-test 45 + command: | 46 + opam install . --confirm-level=unsafe-yes --deps-only --with-test 47 + - name: test 48 + command: | 49 + opam exec -- dune runtest --verbose 50 + - name: doc 51 + command: | 52 + opam install -y odoc 53 + opam exec -- dune build @doc
+15
LICENSE.md
··· 1 + ISC License 2 + 3 + Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org> 4 + 5 + Permission to use, copy, modify, and distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+54
README.md
··· 1 + # ocaml-jmap - JMAP Protocol Implementation for OCaml 2 + 3 + A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail). 4 + 5 + ## Packages 6 + 7 + - **jmap** - Core JMAP protocol types and serialization 8 + - **jmap-eio** - JMAP client using Eio for async I/O 9 + - **jmap-brr** - JMAP client for browsers using js_of_ocaml 10 + 11 + ## Key Features 12 + 13 + - Full RFC 8620 (JMAP Core) support: sessions, accounts, method calls, and error handling 14 + - Full RFC 8621 (JMAP Mail) support: mailboxes, emails, threads, identities, and submissions 15 + - Type-safe API with comprehensive type definitions 16 + - Multiple backends: Eio for native async, Brr for browser-based clients 17 + - JSON serialization via jsont 18 + 19 + ## Usage 20 + 21 + ```ocaml 22 + (* Query emails from a mailbox *) 23 + open Jmap 24 + 25 + let query_emails ~client ~account_id ~mailbox_id = 26 + let filter = Email.Query.Filter.(in_mailbox mailbox_id) in 27 + let query = Email.Query.make ~account_id ~filter () in 28 + Client.call client query 29 + ``` 30 + 31 + ## Installation 32 + 33 + ``` 34 + opam install jmap jmap-eio 35 + ``` 36 + 37 + For browser-based applications: 38 + 39 + ``` 40 + opam install jmap jmap-brr 41 + ``` 42 + 43 + ## Documentation 44 + 45 + API documentation is available via: 46 + 47 + ``` 48 + opam install jmap 49 + odig doc jmap 50 + ``` 51 + 52 + ## License 53 + 54 + ISC
+6 -4
bin/dune
··· 1 1 (executable 2 2 (name jmap) 3 3 (public_name jmap) 4 - (package jmap-eio) 4 + (package jmap) 5 + (optional) 5 6 (modules jmap) 6 - (libraries jmap-eio eio_main)) 7 + (libraries jmap.eio eio_main)) 7 8 8 9 (executable 9 10 (name jmapq) 10 11 (public_name jmapq) 11 - (package jmap-eio) 12 + (package jmap) 13 + (optional) 12 14 (modules jmapq) 13 - (libraries jmap-eio eio_main re jsont.bytesrw)) 15 + (libraries jmap.eio eio_main re jsont.bytesrw))
+233 -5
bin/jmapq.ml
··· 308 308 let doc = "Email IDs to mark as processed" in 309 309 Arg.(non_empty & pos_all string [] & info [] ~docv:"EMAIL_ID" ~doc) 310 310 in 311 - let run cfg email_id_strs = 311 + let verbose_term = 312 + let doc = "Show the raw JMAP server response" in 313 + Arg.(value & flag & info ["v"; "verbose"] ~doc) 314 + in 315 + let run cfg verbose email_id_strs = 312 316 Eio_main.run @@ fun env -> 313 317 Eio.Switch.run @@ fun sw -> 314 318 let client = Jmap_eio.Cli.create_client ~sw env cfg in ··· 318 322 Jmap_eio.Cli.debug cfg "Marking %d email(s) with '%s' keyword" 319 323 (List.length email_ids) zulip_processed_keyword; 320 324 321 - (* Build patch to add the zulip-processed keyword *) 325 + (* Build patch to add the zulip-processed keyword and mark as read *) 322 326 let patch = 323 327 let open Jmap_eio.Chain in 324 - json_obj [("keywords/" ^ zulip_processed_keyword, json_bool true)] 328 + json_obj [ 329 + ("keywords/" ^ zulip_processed_keyword, json_bool true); 330 + ("keywords/$seen", json_bool true); 331 + ] 325 332 in 326 333 327 334 (* Build updates list: each email ID gets the same patch *) ··· 341 348 Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e); 342 349 exit 1 343 350 | Ok response -> 351 + (* Print raw response if verbose *) 352 + if verbose then begin 353 + Fmt.pr "@[<v>%a:@," Fmt.(styled `Bold string) "Server Response"; 354 + (match Jsont_bytesrw.encode_string' ~format:Jsont.Indent 355 + Jmap.Proto.Response.jsont response with 356 + | Ok json_str -> Fmt.pr "%s@,@]@." json_str 357 + | Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e)) 358 + end; 344 359 (* Check for JMAP method-level errors first *) 345 360 let call_id = Jmap_eio.Chain.call_id set_h in 346 361 (match Jmap.Proto.Response.find_response call_id response with ··· 370 385 |> List.map (fun (id, _) -> Jmap.Proto.Id.to_string id) 371 386 in 372 387 if List.length updated_ids > 0 then begin 373 - Fmt.pr "@[<v>%a %d email(s) with '%s':@," 388 + Fmt.pr "@[<v>%a %d email(s) as read with '%s':@," 374 389 Fmt.(styled `Green string) "Marked" 375 390 (List.length updated_ids) 376 391 zulip_processed_keyword; ··· 411 426 `Pre " jmapq zulip-timeout StrrDTS_WEa3 StrsGZ7P8Dpc StrsGuCSXJ3Z"; 412 427 ] in 413 428 let info = Cmd.info "zulip-timeout" ~doc ~man in 414 - Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ email_ids_term) 429 + Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ verbose_term $ email_ids_term) 430 + 431 + (** {1 Zulip View Command} *) 432 + 433 + let zulip_view_cmd = 434 + let json_term = 435 + let doc = "Output as JSON" in 436 + Arg.(value & flag & info ["json"] ~doc) 437 + in 438 + let limit_term = 439 + let doc = "Maximum number of messages to fetch (default: all)" in 440 + Arg.(value & opt (some int) None & info ["limit"; "n"] ~docv:"N" ~doc) 441 + in 442 + let verbose_term = 443 + let doc = "Show the raw JMAP request and response" in 444 + Arg.(value & flag & info ["v"; "verbose"] ~doc) 445 + in 446 + let run cfg json_output limit verbose = 447 + Eio_main.run @@ fun env -> 448 + Eio.Switch.run @@ fun sw -> 449 + let client = Jmap_eio.Cli.create_client ~sw env cfg in 450 + let account_id = Jmap_eio.Cli.get_account_id cfg client in 451 + 452 + Jmap_eio.Cli.debug cfg "Searching for Zulip emails marked as processed"; 453 + 454 + (* Build filter for emails from noreply@zulip.com with zulip-processed keyword *) 455 + let cond : Jmap.Proto.Email.Filter_condition.t = { 456 + in_mailbox = None; in_mailbox_other_than = None; 457 + before = None; after = None; 458 + min_size = None; max_size = None; 459 + all_in_thread_have_keyword = None; 460 + some_in_thread_have_keyword = None; 461 + none_in_thread_have_keyword = None; 462 + has_keyword = Some zulip_processed_keyword; 463 + not_keyword = None; 464 + has_attachment = None; 465 + text = None; 466 + from = Some "noreply@zulip.com"; 467 + to_ = None; 468 + cc = None; bcc = None; subject = None; 469 + body = None; header = None; 470 + } in 471 + let filter = Jmap.Proto.Filter.Condition cond in 472 + let sort = [Jmap.Proto.Filter.comparator ~is_ascending:false "receivedAt"] in 473 + 474 + (* Query for processed Zulip emails *) 475 + let query_limit = match limit with 476 + | Some n -> Int64.of_int n 477 + | None -> Int64.of_int 10000 478 + in 479 + let query_inv = Jmap_eio.Client.Build.email_query 480 + ~call_id:"q1" 481 + ~account_id 482 + ~filter 483 + ~sort 484 + ~limit:query_limit 485 + () 486 + in 487 + 488 + let req = Jmap_eio.Client.Build.( 489 + make_request 490 + ~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail] 491 + [query_inv] 492 + ) in 493 + 494 + (* Print request if verbose *) 495 + if verbose then begin 496 + Fmt.pr "@[<v>%a:@," Fmt.(styled `Bold string) "Request"; 497 + (match Jsont_bytesrw.encode_string' ~format:Jsont.Indent 498 + Jmap.Proto.Request.jsont req with 499 + | Ok json_str -> Fmt.pr "%s@,@]@." json_str 500 + | Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e)) 501 + end; 502 + 503 + match Jmap_eio.Client.request client req with 504 + | Error e -> 505 + Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e); 506 + exit 1 507 + | Ok response -> 508 + (* Print response if verbose *) 509 + if verbose then begin 510 + Fmt.pr "@[<v>%a:@," Fmt.(styled `Bold string) "Response"; 511 + (match Jsont_bytesrw.encode_string' ~format:Jsont.Indent 512 + Jmap.Proto.Response.jsont response with 513 + | Ok json_str -> Fmt.pr "%s@,@]@." json_str 514 + | Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e)) 515 + end; 516 + match Jmap_eio.Client.Parse.parse_email_query ~call_id:"q1" response with 517 + | Error e -> 518 + Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e); 519 + exit 1 520 + | Ok query_result -> 521 + let email_ids = query_result.ids in 522 + Jmap_eio.Cli.debug cfg "Found %d processed Zulip email IDs" (List.length email_ids); 523 + 524 + if List.length email_ids = 0 then ( 525 + if json_output then 526 + Fmt.pr "[]@." 527 + else 528 + Fmt.pr "No Zulip emails marked as processed.@." 529 + ) else ( 530 + (* Fetch email details *) 531 + let get_inv = Jmap_eio.Client.Build.email_get 532 + ~call_id:"g1" 533 + ~account_id 534 + ~ids:email_ids 535 + ~properties:["id"; "blobId"; "threadId"; "mailboxIds"; "keywords"; 536 + "size"; "receivedAt"; "subject"; "from"] 537 + () 538 + in 539 + let req2 = Jmap_eio.Client.Build.( 540 + make_request 541 + ~capabilities:[Jmap.Proto.Capability.core; Jmap.Proto.Capability.mail] 542 + [get_inv] 543 + ) in 544 + 545 + match Jmap_eio.Client.request client req2 with 546 + | Error e -> 547 + Fmt.epr "Error: %s@." (Jmap_eio.Client.error_to_string e); 548 + exit 1 549 + | Ok response2 -> 550 + match Jmap_eio.Client.Parse.parse_email_get ~call_id:"g1" response2 with 551 + | Error e -> 552 + Fmt.epr "Parse error: %s@." (Jsont.Error.to_string e); 553 + exit 1 554 + | Ok get_result -> 555 + (* Parse Zulip subjects and filter successful parses *) 556 + let zulip_messages = 557 + get_result.list 558 + |> List.filter_map Zulip_message.of_email 559 + in 560 + 561 + Jmap_eio.Cli.debug cfg "Parsed %d Zulip messages from %d emails" 562 + (List.length zulip_messages) 563 + (List.length get_result.list); 564 + 565 + if json_output then ( 566 + (* Output as JSON *) 567 + match Jsont_bytesrw.encode_string' ~format:Jsont.Indent Zulip_message.list_jsont zulip_messages with 568 + | Ok json_str -> Fmt.pr "%s@." json_str 569 + | Error e -> Fmt.epr "JSON encoding error: %s@." (Jsont.Error.to_string e) 570 + ) else ( 571 + (* Human-readable output *) 572 + Fmt.pr "@[<v>%a (%d messages)@,@," 573 + Fmt.(styled `Bold string) "Processed Zulip Notifications" 574 + (List.length zulip_messages); 575 + 576 + (* Group by server, then by channel *) 577 + let by_server = Hashtbl.create 8 in 578 + List.iter (fun (msg : Zulip_message.t) -> 579 + let existing = try Hashtbl.find by_server msg.server with Not_found -> [] in 580 + Hashtbl.replace by_server msg.server (msg :: existing) 581 + ) zulip_messages; 582 + 583 + Hashtbl.iter (fun server msgs -> 584 + Fmt.pr "%a [%s]@," 585 + Fmt.(styled `Bold string) "Server:" 586 + server; 587 + 588 + (* Group by channel within server *) 589 + let by_channel = Hashtbl.create 8 in 590 + List.iter (fun (msg : Zulip_message.t) -> 591 + let existing = try Hashtbl.find by_channel msg.channel with Not_found -> [] in 592 + Hashtbl.replace by_channel msg.channel (msg :: existing) 593 + ) msgs; 594 + 595 + Hashtbl.iter (fun channel channel_msgs -> 596 + Fmt.pr " %a #%s (%d)@," 597 + Fmt.(styled `Cyan string) "Channel:" 598 + channel 599 + (List.length channel_msgs); 600 + 601 + (* Sort by date descending *) 602 + let sorted = List.sort (fun a b -> 603 + Ptime.compare b.Zulip_message.date a.Zulip_message.date 604 + ) channel_msgs in 605 + 606 + List.iter (fun (msg : Zulip_message.t) -> 607 + let read_marker = if msg.is_read then " " else "*" in 608 + let labels_str = match msg.labels with 609 + | [] -> "" 610 + | ls -> " [" ^ String.concat ", " ls ^ "]" 611 + in 612 + Fmt.pr " %s %s %a %s%s@," 613 + read_marker 614 + (ptime_to_string msg.date) 615 + Fmt.(styled `Yellow string) (truncate_string 40 msg.topic) 616 + (truncate_string 12 msg.id) 617 + labels_str 618 + ) sorted; 619 + Fmt.pr "@," 620 + ) by_channel 621 + ) by_server; 622 + 623 + Fmt.pr "@]@." 624 + ) 625 + ) 626 + in 627 + let doc = "List Zulip emails that have been marked as processed" in 628 + let man = [ 629 + `S Manpage.s_description; 630 + `P (Printf.sprintf "Lists all Zulip notification emails that have the '%s' keyword." 631 + zulip_processed_keyword); 632 + `S Manpage.s_examples; 633 + `P "List all processed Zulip notifications:"; 634 + `Pre " jmapq zulip-view"; 635 + `P "Output as JSON:"; 636 + `Pre " jmapq zulip-view --json"; 637 + `P "Limit to 50 most recent:"; 638 + `Pre " jmapq zulip-view -n 50"; 639 + ] in 640 + let info = Cmd.info "zulip-view" ~doc ~man in 641 + Cmd.v info Term.(const run $ Jmap_eio.Cli.config_term $ json_term $ limit_term $ verbose_term) 415 642 416 643 (** {1 Main Command Group} *) 417 644 ··· 427 654 Cmd.group info [ 428 655 zulip_list_cmd; 429 656 zulip_timeout_cmd; 657 + zulip_view_cmd; 430 658 ] 431 659 432 660 let () =
+11 -27
dune-project
··· 6 6 7 7 (generate_opam_files true) 8 8 9 - (source 10 - (github avsm/ocaml-jmap)) 9 + (license ISC) 11 10 12 11 (authors "Anil Madhavapeddy <anil@recoil.org>") 13 12 14 13 (maintainers "Anil Madhavapeddy <anil@recoil.org>") 15 14 16 - (license ISC) 15 + (homepage "https://tangled.org/@anil.recoil.org/ocaml-jmap") 17 16 18 - (documentation https://avsm.github.io/ocaml-jmap) 17 + (bug_reports "https://tangled.org/@anil.recoil.org/ocaml-jmap/issues") 18 + 19 + (maintenance_intent "(latest)") 19 20 20 21 (package 21 22 (name jmap) 22 23 (synopsis "JMAP protocol implementation for OCaml") 23 24 (description 24 - "A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail).") 25 + "A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail). Includes subpackages for Eio (jmap.eio) and browser (jmap.brr) clients.") 25 26 (depends 26 27 (ocaml (>= 5.4.0)) 27 28 (jsont (>= 0.2.0)) 28 29 json-pointer 29 - (ptime (>= 1.0.0)))) 30 - 31 - (package 32 - (name jmap-eio) 33 - (synopsis "JMAP client for Eio") 34 - (description "High-level JMAP client using Eio for async I/O and the Requests HTTP library.") 35 - (depends 36 - (ocaml (>= 5.4.0)) 37 - (jmap (= :version)) 38 - (jsont (>= 0.2.0)) 39 - eio 40 - requests)) 41 - 42 - (package 43 - (name jmap-brr) 44 - (synopsis "JMAP client for browsers") 45 - (description "JMAP client using Brr for browser-based email clients with js_of_ocaml.") 46 - (depends 47 - (ocaml (>= 5.4.0)) 48 - (jmap (= :version)) 49 - (jsont (>= 0.2.0)) 50 - (brr (>= 0.0.6)))) 30 + (ptime (>= 1.0.0)) 31 + (eio :with-test) 32 + (requests :with-test) 33 + (brr :with-test)) 34 + (depopts eio requests brr))
+2 -1
eio/dune
··· 1 1 (library 2 2 (name jmap_eio) 3 - (public_name jmap-eio) 3 + (public_name jmap.eio) 4 + (optional) 4 5 (libraries jmap jsont jsont.bytesrw eio requests uri str cmdliner fmt.tty) 5 6 (modules jmap_eio codec client cli))
-35
jmap-brr.opam
··· 1 - # This file is generated by dune, edit dune-project instead 2 - opam-version: "2.0" 3 - synopsis: "JMAP client for browsers" 4 - description: 5 - "JMAP client using Brr for browser-based email clients with js_of_ocaml." 6 - maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 - authors: ["Anil Madhavapeddy <anil@recoil.org>"] 8 - license: "ISC" 9 - homepage: "https://github.com/avsm/ocaml-jmap" 10 - doc: "https://avsm.github.io/ocaml-jmap" 11 - bug-reports: "https://github.com/avsm/ocaml-jmap/issues" 12 - depends: [ 13 - "dune" {>= "3.20"} 14 - "ocaml" {>= "5.4.0"} 15 - "jmap" {= version} 16 - "jsont" {>= "0.2.0"} 17 - "brr" {>= "0.0.6"} 18 - "odoc" {with-doc} 19 - ] 20 - build: [ 21 - ["dune" "subst"] {dev} 22 - [ 23 - "dune" 24 - "build" 25 - "-p" 26 - name 27 - "-j" 28 - jobs 29 - "@install" 30 - "@runtest" {with-test} 31 - "@doc" {with-doc} 32 - ] 33 - ] 34 - dev-repo: "git+https://github.com/avsm/ocaml-jmap.git" 35 - x-maintenance-intent: ["(latest)"]
-36
jmap-eio.opam
··· 1 - # This file is generated by dune, edit dune-project instead 2 - opam-version: "2.0" 3 - synopsis: "JMAP client for Eio" 4 - description: 5 - "High-level JMAP client using Eio for async I/O and the Requests HTTP library." 6 - maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 - authors: ["Anil Madhavapeddy <anil@recoil.org>"] 8 - license: "ISC" 9 - homepage: "https://github.com/avsm/ocaml-jmap" 10 - doc: "https://avsm.github.io/ocaml-jmap" 11 - bug-reports: "https://github.com/avsm/ocaml-jmap/issues" 12 - depends: [ 13 - "dune" {>= "3.20"} 14 - "ocaml" {>= "5.4.0"} 15 - "jmap" {= version} 16 - "jsont" {>= "0.2.0"} 17 - "eio" 18 - "requests" 19 - "odoc" {with-doc} 20 - ] 21 - build: [ 22 - ["dune" "subst"] {dev} 23 - [ 24 - "dune" 25 - "build" 26 - "-p" 27 - name 28 - "-j" 29 - jobs 30 - "@install" 31 - "@runtest" {with-test} 32 - "@doc" {with-doc} 33 - ] 34 - ] 35 - dev-repo: "git+https://github.com/avsm/ocaml-jmap.git" 36 - x-maintenance-intent: ["(latest)"]
+7 -5
jmap.opam
··· 2 2 opam-version: "2.0" 3 3 synopsis: "JMAP protocol implementation for OCaml" 4 4 description: 5 - "A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail)." 5 + "A complete implementation of the JSON Meta Application Protocol (JMAP) as specified in RFC 8620 (core) and RFC 8621 (mail). Includes subpackages for Eio (jmap.eio) and browser (jmap.brr) clients." 6 6 maintainer: ["Anil Madhavapeddy <anil@recoil.org>"] 7 7 authors: ["Anil Madhavapeddy <anil@recoil.org>"] 8 8 license: "ISC" 9 - homepage: "https://github.com/avsm/ocaml-jmap" 10 - doc: "https://avsm.github.io/ocaml-jmap" 11 - bug-reports: "https://github.com/avsm/ocaml-jmap/issues" 9 + homepage: "https://tangled.org/@anil.recoil.org/ocaml-jmap" 10 + bug-reports: "https://tangled.org/@anil.recoil.org/ocaml-jmap/issues" 12 11 depends: [ 13 12 "dune" {>= "3.20"} 14 13 "ocaml" {>= "5.4.0"} 15 14 "jsont" {>= "0.2.0"} 16 15 "json-pointer" 17 16 "ptime" {>= "1.0.0"} 17 + "eio" {with-test} 18 + "requests" {with-test} 19 + "brr" {with-test} 18 20 "odoc" {with-doc} 19 21 ] 22 + depopts: ["eio" "requests" "brr"] 20 23 build: [ 21 24 ["dune" "subst"] {dev} 22 25 [ ··· 31 34 "@doc" {with-doc} 32 35 ] 33 36 ] 34 - dev-repo: "git+https://github.com/avsm/ocaml-jmap.git" 35 37 x-maintenance-intent: ["(latest)"]
+2 -1
lib/js/dune
··· 2 2 3 3 (library 4 4 (name jmap_brr) 5 - (public_name jmap-brr) 5 + (public_name jmap.brr) 6 + (optional) 6 7 (libraries jmap brr jsont.brr) 7 8 (modes byte))
+18 -8
lib/proto/proto_method.ml
··· 132 132 133 133 let set_response_jsont (type a) (obj_jsont : a Jsont.t) : a set_response Jsont.t = 134 134 let kind = "SetResponse" in 135 + (* All map/list fields in SetResponse can be null per RFC 8620 Section 5.3 *) 136 + (* opt_mem handles missing keys, Jsont.option handles explicit null values *) 137 + (* Option.join flattens option option -> option *) 138 + let join = Option.join in 135 139 let make account_id old_state new_state created updated destroyed 136 140 not_created not_updated not_destroyed = 137 - { account_id; old_state; new_state; created; updated; destroyed; 138 - not_created; not_updated; not_destroyed } 141 + { account_id; old_state; new_state; 142 + created = join created; 143 + updated = join updated; 144 + destroyed = join destroyed; 145 + not_created = join not_created; 146 + not_updated = join not_updated; 147 + not_destroyed = join not_destroyed } 139 148 in 140 149 (* For updated values, the server may return null or an object - RFC 8620 Section 5.3 *) 141 150 (* "Id[Foo|null]" means map values can be null, use Jsont.option to handle this *) 142 151 let nullable_obj = Jsont.(option obj_jsont) in 152 + let opt enc = Option.map Option.some enc in 143 153 Jsont.Object.map ~kind make 144 154 |> Jsont.Object.mem "accountId" Proto_id.jsont ~enc:(fun r -> r.account_id) 145 155 |> Jsont.Object.opt_mem "oldState" Jsont.string ~enc:(fun r -> r.old_state) 146 156 |> Jsont.Object.mem "newState" Jsont.string ~enc:(fun r -> r.new_state) 147 - |> Jsont.Object.opt_mem "created" (Proto_json_map.of_id obj_jsont) ~enc:(fun r -> r.created) 148 - |> Jsont.Object.opt_mem "updated" (Proto_json_map.of_id nullable_obj) ~enc:(fun r -> r.updated) 149 - |> Jsont.Object.opt_mem "destroyed" (Jsont.list Proto_id.jsont) ~enc:(fun r -> r.destroyed) 150 - |> Jsont.Object.opt_mem "notCreated" (Proto_json_map.of_id Proto_error.set_error_jsont) ~enc:(fun r -> r.not_created) 151 - |> Jsont.Object.opt_mem "notUpdated" (Proto_json_map.of_id Proto_error.set_error_jsont) ~enc:(fun r -> r.not_updated) 152 - |> Jsont.Object.opt_mem "notDestroyed" (Proto_json_map.of_id Proto_error.set_error_jsont) ~enc:(fun r -> r.not_destroyed) 157 + |> Jsont.Object.opt_mem "created" Jsont.(option (Proto_json_map.of_id obj_jsont)) ~enc:(fun r -> opt r.created) 158 + |> Jsont.Object.opt_mem "updated" Jsont.(option (Proto_json_map.of_id nullable_obj)) ~enc:(fun r -> opt r.updated) 159 + |> Jsont.Object.opt_mem "destroyed" Jsont.(option (list Proto_id.jsont)) ~enc:(fun r -> opt r.destroyed) 160 + |> Jsont.Object.opt_mem "notCreated" Jsont.(option (Proto_json_map.of_id Proto_error.set_error_jsont)) ~enc:(fun r -> opt r.not_created) 161 + |> Jsont.Object.opt_mem "notUpdated" Jsont.(option (Proto_json_map.of_id Proto_error.set_error_jsont)) ~enc:(fun r -> opt r.not_updated) 162 + |> Jsont.Object.opt_mem "notDestroyed" Jsont.(option (Proto_json_map.of_id Proto_error.set_error_jsont)) ~enc:(fun r -> opt r.not_destroyed) 153 163 |> Jsont.Object.finish 154 164 155 165 (* Foo/copy *)