+20
-1
.gitignore
+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
.ocamlformat
···
1
+
version=0.28.1
+53
.tangled/workflows/build.yml
+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
+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
+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
+233
-5
bin/jmapq.ml
+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 () =
+5
-4
dune-project
+5
-4
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")
16
+
17
+
(bug_reports "https://tangled.org/@anil.recoil.org/ocaml-jmap/issues")
17
18
18
-
(documentation https://avsm.github.io/ocaml-jmap)
19
+
(maintenance_intent "(latest)")
19
20
20
21
(package
21
22
(name jmap)
+2
-4
jmap-brr.opam
+2
-4
jmap-brr.opam
···
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"}
···
31
30
"@doc" {with-doc}
32
31
]
33
32
]
34
-
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
35
33
x-maintenance-intent: ["(latest)"]
+2
-4
jmap-eio.opam
+2
-4
jmap-eio.opam
···
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"}
···
32
31
"@doc" {with-doc}
33
32
]
34
33
]
35
-
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
36
34
x-maintenance-intent: ["(latest)"]
+2
-4
jmap.opam
+2
-4
jmap.opam
···
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"}
···
31
30
"@doc" {with-doc}
32
31
]
33
32
]
34
-
dev-repo: "git+https://github.com/avsm/ocaml-jmap.git"
35
33
x-maintenance-intent: ["(latest)"]
+18
-8
lib/proto/proto_method.ml
+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 *)