(** JMAP Standard Method Types This module defines the request and response types for all standard JMAP methods that work across different object types. These types are polymorphic over the object type 'a. Reference: RFC 8620 Sections 5.1-5.6 *) (** Local helper functions to avoid circular dependency with Jmap_parser *) module Helpers = struct let expect_object = function | `O fields -> fields | _ -> raise (Jmap_error.Parse_error "Expected JSON object") let expect_string = function | `String s -> s | _ -> raise (Jmap_error.Parse_error "Expected JSON string") let find_field name fields = List.assoc_opt name fields let require_field name fields = match find_field name fields with | Some v -> v | None -> raise (Jmap_error.Parse_error (Printf.sprintf "Missing required field: %s" name)) let get_string name fields = match require_field name fields with | `String s -> s | _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name)) let get_string_opt name fields = match find_field name fields with | Some (`String s) -> Some s | Some _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a string" name)) | None -> None let get_bool name fields = match require_field name fields with | `Bool b -> b | _ -> raise (Jmap_error.Parse_error (Printf.sprintf "Field %s must be a boolean" name)) let parse_array parse_elem = function | `A items -> List.map parse_elem items | `Null -> [] | _ -> raise (Jmap_error.Parse_error "Expected JSON array") end (** Standard /get method (RFC 8620 Section 5.1) *) module Get = struct type 'a request = { account_id : Jmap_id.t; ids : Jmap_id.t list option; (** null = fetch all *) properties : string list option; (** null = fetch all properties *) } type 'a response = { account_id : Jmap_id.t; state : string; list : 'a list; not_found : Jmap_id.t list; } (** Accessors for request *) let account_id (r : 'a request) = r.account_id let ids (r : 'a request) = r.ids let properties (r : 'a request) = r.properties (** Constructor for request *) let v ~account_id ?ids ?properties () = { account_id; ids; properties } (** Accessors for response *) let response_account_id (r : 'a response) = r.account_id let state (r : 'a response) = r.state let list (r : 'a response) = r.list let not_found (r : 'a response) = r.not_found (** Constructor for response *) let response_v ~account_id ~state ~list ~not_found = { account_id; state; list; not_found } (** Parse request from JSON. Test files: test/data/core/request_get.json *) let request_of_json parse_obj json = ignore parse_obj; let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let ids = match find_field "ids" fields with | Some `Null | None -> None | Some v -> Some (parse_array Jmap_id.of_json v) in let properties = match find_field "properties" fields with | Some `Null | None -> None | Some v -> Some (parse_array expect_string v) in { account_id; ids; properties } (** Convert request to JSON *) let request_to_json (req : 'a request) = let fields = [ ("accountId", Jmap_id.to_json req.account_id); ] in let fields = match req.ids with | Some ids -> ("ids", `A (List.map Jmap_id.to_json ids)) :: fields | None -> fields in let fields = match req.properties with | Some props -> ("properties", `A (List.map (fun s -> `String s) props)) :: fields | None -> fields in `O fields (** Parse response from JSON. Test files: test/data/core/response_get.json *) let response_of_json parse_obj json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let state = get_string "state" fields in let list = parse_array parse_obj (require_field "list" fields) in let not_found = match find_field "notFound" fields with | Some v -> parse_array Jmap_id.of_json v | None -> [] in { account_id; state; list; not_found } end (** Standard /changes method (RFC 8620 Section 5.2) *) module Changes = struct type request = { account_id : Jmap_id.t; since_state : string; max_changes : Jmap_primitives.UnsignedInt.t option; } type response = { account_id : Jmap_id.t; old_state : string; new_state : string; has_more_changes : bool; created : Jmap_id.t list; updated : Jmap_id.t list; destroyed : Jmap_id.t list; } (** Accessors for request *) let account_id (r : request) = r.account_id let since_state (r : request) = r.since_state let max_changes (r : request) = r.max_changes (** Constructor for request *) let v ~account_id ~since_state ?max_changes () = { account_id; since_state; max_changes } (** Accessors for response *) let response_account_id (r : response) = r.account_id let old_state (r : response) = r.old_state let new_state (r : response) = r.new_state let has_more_changes (r : response) = r.has_more_changes let created (r : response) = r.created let updated (r : response) = r.updated let destroyed (r : response) = r.destroyed (** Constructor for response *) let response_v ~account_id ~old_state ~new_state ~has_more_changes ~created ~updated ~destroyed = { account_id; old_state; new_state; has_more_changes; created; updated; destroyed } (** Parse request from JSON. Test files: test/data/core/request_changes.json *) let request_of_json json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let since_state = get_string "sinceState" fields in let max_changes = match find_field "maxChanges" fields with | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v) | None -> None in { account_id; since_state; max_changes } (** Parse response from JSON. Test files: test/data/core/response_changes.json *) let response_of_json json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let old_state = get_string "oldState" fields in let new_state = get_string "newState" fields in let has_more_changes = get_bool "hasMoreChanges" fields in let created = parse_array Jmap_id.of_json (require_field "created" fields) in let updated = parse_array Jmap_id.of_json (require_field "updated" fields) in let destroyed = parse_array Jmap_id.of_json (require_field "destroyed" fields) in { account_id; old_state; new_state; has_more_changes; created; updated; destroyed } end (** Standard /set method (RFC 8620 Section 5.3) *) module Set = struct (** PatchObject - JSON Pointer paths to values *) type patch_object = (string * Ezjsonm.value option) list type 'a request = { account_id : Jmap_id.t; if_in_state : string option; create : (Jmap_id.t * 'a) list option; update : (Jmap_id.t * patch_object) list option; destroy : Jmap_id.t list option; } type 'a response = { account_id : Jmap_id.t; old_state : string option; new_state : string; created : (Jmap_id.t * 'a) list option; updated : (Jmap_id.t * 'a option) list option; destroyed : Jmap_id.t list option; not_created : (Jmap_id.t * Jmap_error.set_error_detail) list option; not_updated : (Jmap_id.t * Jmap_error.set_error_detail) list option; not_destroyed : (Jmap_id.t * Jmap_error.set_error_detail) list option; } (** Accessors for request *) let account_id (r : 'a request) = r.account_id let if_in_state (r : 'a request) = r.if_in_state let create (r : 'a request) = r.create let update (r : 'a request) = r.update let destroy (r : 'a request) = r.destroy (** Constructor for request *) let v ~account_id ?if_in_state ?create ?update ?destroy () = { account_id; if_in_state; create; update; destroy } (** Accessors for response *) let response_account_id (r : 'a response) = r.account_id let old_state (r : 'a response) = r.old_state let new_state (r : 'a response) = r.new_state let created (r : 'a response) = r.created let updated (r : 'a response) = r.updated let destroyed (r : 'a response) = r.destroyed let not_created (r : 'a response) = r.not_created let not_updated (r : 'a response) = r.not_updated let not_destroyed (r : 'a response) = r.not_destroyed (** Constructor for response *) let response_v ~account_id ?old_state ~new_state ?created ?updated ?destroyed ?not_created ?not_updated ?not_destroyed () = { account_id; old_state; new_state; created; updated; destroyed; not_created; not_updated; not_destroyed } (** Parse request from JSON. Test files: - test/data/core/request_set_create.json - test/data/core/request_set_update.json - test/data/core/request_set_destroy.json *) let request_of_json parse_obj json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let if_in_state = get_string_opt "ifInState" fields in let create = match find_field "create" fields with | Some `Null | None -> None | Some (`O pairs) -> Some (List.map (fun (k, v) -> (Jmap_id.of_string k, parse_obj v)) pairs) | Some _ -> raise (Jmap_error.Parse_error "create must be an object") in let update = match find_field "update" fields with | Some `Null | None -> None | Some (`O pairs) -> Some (List.map (fun (k, v) -> let id = Jmap_id.of_string k in let patch = match v with | `O patch_fields -> List.map (fun (pk, pv) -> match pv with | `Null -> (pk, None) | _ -> (pk, Some pv) ) patch_fields | _ -> raise (Jmap_error.Parse_error "update value must be an object") in (id, patch) ) pairs) | Some _ -> raise (Jmap_error.Parse_error "update must be an object") in let destroy = match find_field "destroy" fields with | Some `Null | None -> None | Some v -> Some (parse_array Jmap_id.of_json v) in { account_id; if_in_state; create; update; destroy } (** Parse response from JSON. Test files: - test/data/core/response_set_create.json - test/data/core/response_set_update.json - test/data/core/response_set_destroy.json *) let response_of_json parse_obj json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let old_state = get_string_opt "oldState" fields in let new_state = get_string "newState" fields in let created = match find_field "created" fields with | Some `Null | None -> None | Some (`O pairs) -> Some (List.map (fun (k, v) -> (Jmap_id.of_string k, parse_obj v)) pairs) | Some _ -> raise (Jmap_error.Parse_error "created must be an object") in let updated = match find_field "updated" fields with | Some `Null | None -> None | Some (`O pairs) -> Some (List.map (fun (k, v) -> let id = Jmap_id.of_string k in match v with | `Null -> (id, None) | _ -> (id, Some (parse_obj v)) ) pairs) | Some _ -> raise (Jmap_error.Parse_error "updated must be an object") in let destroyed = match find_field "destroyed" fields with | Some `Null | None -> None | Some v -> Some (parse_array Jmap_id.of_json v) in let not_created = match find_field "notCreated" fields with | Some `Null | None -> None | Some (`O pairs) -> Some (List.map (fun (k, v) -> (Jmap_id.of_string k, Jmap_error.parse_set_error_detail v) ) pairs) | Some _ -> raise (Jmap_error.Parse_error "notCreated must be an object") in let not_updated = match find_field "notUpdated" fields with | Some `Null | None -> None | Some (`O pairs) -> Some (List.map (fun (k, v) -> (Jmap_id.of_string k, Jmap_error.parse_set_error_detail v) ) pairs) | Some _ -> raise (Jmap_error.Parse_error "notUpdated must be an object") in let not_destroyed = match find_field "notDestroyed" fields with | Some `Null | None -> None | Some (`O pairs) -> Some (List.map (fun (k, v) -> (Jmap_id.of_string k, Jmap_error.parse_set_error_detail v) ) pairs) | Some _ -> raise (Jmap_error.Parse_error "notDestroyed must be an object") in { account_id; old_state; new_state; created; updated; destroyed; not_created; not_updated; not_destroyed } end (** Standard /copy method (RFC 8620 Section 5.4) *) module Copy = struct type 'a request = { from_account_id : Jmap_id.t; if_from_in_state : string option; account_id : Jmap_id.t; if_in_state : string option; create : (Jmap_id.t * 'a) list; (** Each object must include source id *) on_success_destroy_original : bool option; destroy_from_if_in_state : string option; } type 'a response = { from_account_id : Jmap_id.t; account_id : Jmap_id.t; old_state : string option; new_state : string; created : (Jmap_id.t * 'a) list option; not_created : (Jmap_id.t * Jmap_error.set_error_detail) list option; } (** Accessors for request *) let from_account_id (r : 'a request) = r.from_account_id let if_from_in_state (r : 'a request) = r.if_from_in_state let account_id (r : 'a request) = r.account_id let if_in_state (r : 'a request) = r.if_in_state let create (r : 'a request) = r.create let on_success_destroy_original (r : 'a request) = r.on_success_destroy_original let destroy_from_if_in_state (r : 'a request) = r.destroy_from_if_in_state (** Constructor for request *) let v ~from_account_id ?if_from_in_state ~account_id ?if_in_state ~create ?on_success_destroy_original ?destroy_from_if_in_state () = { from_account_id; if_from_in_state; account_id; if_in_state; create; on_success_destroy_original; destroy_from_if_in_state } (** Accessors for response *) let response_from_account_id (r : 'a response) = r.from_account_id let response_account_id (r : 'a response) = r.account_id let old_state (r : 'a response) = r.old_state let new_state (r : 'a response) = r.new_state let created (r : 'a response) = r.created let not_created (r : 'a response) = r.not_created (** Constructor for response *) let response_v ~from_account_id ~account_id ?old_state ~new_state ?created ?not_created () = { from_account_id; account_id; old_state; new_state; created; not_created } (** Parse request from JSON. Test files: test/data/core/request_copy.json *) let request_of_json _parse_obj _json = (* TODO: Implement JSON parsing *) raise (Jmap_error.Parse_error "Copy.request_of_json not yet implemented") (** Parse response from JSON. Test files: test/data/core/response_copy.json *) let response_of_json _parse_obj _json = (* TODO: Implement JSON parsing *) raise (Jmap_error.Parse_error "Copy.response_of_json not yet implemented") end (** Standard /query method (RFC 8620 Section 5.5) *) module Query = struct type 'filter request = { account_id : Jmap_id.t; filter : 'filter Jmap_filter.t option; sort : Jmap_comparator.t list option; position : Jmap_primitives.Int53.t option; anchor : Jmap_id.t option; anchor_offset : Jmap_primitives.Int53.t option; limit : Jmap_primitives.UnsignedInt.t option; calculate_total : bool option; } type response = { account_id : Jmap_id.t; query_state : string; can_calculate_changes : bool; position : Jmap_primitives.UnsignedInt.t; ids : Jmap_id.t list; total : Jmap_primitives.UnsignedInt.t option; (** Only if calculateTotal=true *) limit : Jmap_primitives.UnsignedInt.t option; (** If server limited results *) } (** Accessors for request *) let account_id (r : 'f request) = r.account_id let filter (r : 'f request) = r.filter let sort (r : 'f request) = r.sort let position (r : 'f request) = r.position let anchor (r : 'f request) = r.anchor let anchor_offset (r : 'f request) = r.anchor_offset let limit (r : 'f request) = r.limit let calculate_total (r : 'f request) = r.calculate_total (** Constructor for request *) let v ~account_id ?filter ?sort ?position ?anchor ?anchor_offset ?limit ?calculate_total () = { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total } (** Accessors for response *) let response_account_id (r : response) = r.account_id let query_state (r : response) = r.query_state let can_calculate_changes (r : response) = r.can_calculate_changes let response_position (r : response) = r.position let ids (r : response) = r.ids let total (r : response) = r.total let response_limit (r : response) = r.limit (** Constructor for response *) let response_v ~account_id ~query_state ~can_calculate_changes ~position ~ids ?total ?limit () = { account_id; query_state; can_calculate_changes; position; ids; total; limit } (** Parse request from JSON. Test files: test/data/core/request_query.json *) let request_of_json parse_filter json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let filter = match find_field "filter" fields with | Some v -> Some (Jmap_filter.of_json parse_filter v) | None -> None in let sort = match find_field "sort" fields with | Some v -> Some (parse_array Jmap_comparator.of_json v) | None -> None in let position = match find_field "position" fields with | Some v -> Some (Jmap_primitives.Int53.of_json v) | None -> None in let anchor = match find_field "anchor" fields with | Some v -> Some (Jmap_id.of_json v) | None -> None in let anchor_offset = match find_field "anchorOffset" fields with | Some v -> Some (Jmap_primitives.Int53.of_json v) | None -> None in let limit = match find_field "limit" fields with | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v) | None -> None in let calculate_total = match find_field "calculateTotal" fields with | Some (`Bool b) -> Some b | Some _ -> raise (Jmap_error.Parse_error "calculateTotal must be a boolean") | None -> None in { account_id; filter; sort; position; anchor; anchor_offset; limit; calculate_total } (** Parse response from JSON. Test files: test/data/core/response_query.json *) let response_of_json json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let query_state = get_string "queryState" fields in let can_calculate_changes = get_bool "canCalculateChanges" fields in let position = Jmap_primitives.UnsignedInt.of_json (require_field "position" fields) in let ids = parse_array Jmap_id.of_json (require_field "ids" fields) in let total = match find_field "total" fields with | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v) | None -> None in let limit = match find_field "limit" fields with | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v) | None -> None in { account_id; query_state; can_calculate_changes; position; ids; total; limit } end (** Standard /queryChanges method (RFC 8620 Section 5.6) *) module QueryChanges = struct (** Item added to query results *) type added_item = { id : Jmap_id.t; index : Jmap_primitives.UnsignedInt.t; } type 'filter request = { account_id : Jmap_id.t; filter : 'filter Jmap_filter.t option; sort : Jmap_comparator.t list option; since_query_state : string; max_changes : Jmap_primitives.UnsignedInt.t option; up_to_id : Jmap_id.t option; calculate_total : bool option; } type response = { account_id : Jmap_id.t; old_query_state : string; new_query_state : string; total : Jmap_primitives.UnsignedInt.t option; removed : Jmap_id.t list; added : added_item list; } (** Accessors for added_item *) let added_item_id a = a.id let added_item_index a = a.index (** Constructor for added_item *) let added_item_v ~id ~index = { id; index } (** Accessors for request *) let account_id (r : 'f request) = r.account_id let filter (r : 'f request) = r.filter let sort (r : 'f request) = r.sort let since_query_state (r : 'f request) = r.since_query_state let max_changes (r : 'f request) = r.max_changes let up_to_id (r : 'f request) = r.up_to_id let calculate_total (r : 'f request) = r.calculate_total (** Constructor for request *) let v ~account_id ?filter ?sort ~since_query_state ?max_changes ?up_to_id ?calculate_total () = { account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total } (** Accessors for response *) let response_account_id (r : response) = r.account_id let old_query_state (r : response) = r.old_query_state let new_query_state (r : response) = r.new_query_state let total (r : response) = r.total let removed (r : response) = r.removed let added (r : response) = r.added (** Constructor for response *) let response_v ~account_id ~old_query_state ~new_query_state ?total ~removed ~added () = { account_id; old_query_state; new_query_state; total; removed; added } (** Parse request from JSON. Test files: test/data/core/request_query_changes.json *) let request_of_json parse_filter json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let filter = match find_field "filter" fields with | Some v -> Some (Jmap_filter.of_json parse_filter v) | None -> None in let sort = match find_field "sort" fields with | Some v -> Some (parse_array Jmap_comparator.of_json v) | None -> None in let since_query_state = get_string "sinceQueryState" fields in let max_changes = match find_field "maxChanges" fields with | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v) | None -> None in let up_to_id = match find_field "upToId" fields with | Some v -> Some (Jmap_id.of_json v) | None -> None in let calculate_total = match find_field "calculateTotal" fields with | Some (`Bool b) -> Some b | Some _ -> raise (Jmap_error.Parse_error "calculateTotal must be a boolean") | None -> None in { account_id; filter; sort; since_query_state; max_changes; up_to_id; calculate_total } (** Parse response from JSON. Test files: test/data/core/response_query_changes.json *) let response_of_json json = let open Helpers in let fields = expect_object json in let account_id = Jmap_id.of_json (require_field "accountId" fields) in let old_query_state = get_string "oldQueryState" fields in let new_query_state = get_string "newQueryState" fields in let total = match find_field "total" fields with | Some v -> Some (Jmap_primitives.UnsignedInt.of_json v) | None -> None in let removed = parse_array Jmap_id.of_json (require_field "removed" fields) in let added = match require_field "added" fields with | `A items -> List.map (fun item -> match item with | `O item_fields -> let id = Jmap_id.of_json (require_field "id" item_fields) in let index = Jmap_primitives.UnsignedInt.of_json (require_field "index" item_fields) in { id; index } | _ -> raise (Jmap_error.Parse_error "Added item must be an object") ) items | _ -> raise (Jmap_error.Parse_error "added must be an array") in { account_id; old_query_state; new_query_state; total; removed; added } end (** Core/echo method (RFC 8620 Section 7.3) *) module Echo = struct (** Echo simply returns the arguments unchanged *) type t = Ezjsonm.value (** Test files: - test/data/core/request_echo.json - test/data/core/response_echo.json *) let of_json json = json let to_json t = t end