jmap-metadata-client 0.2.0

JMAP Object Metadata extension client helpers (draft-ietf-jmap-metadata-02) — property constants, filter-condition builders, and /changes helpers for metadata-augmented requests
Documentation

jmap-metadata-client

What it is

Typed client methods for the JMAP Object Metadata extension (draft-ietf-jmap-metadata-01). Depends on jmap-base-client for transport, authentication, and session management, and on jmap-metadata-types for wire types.

What it's for

Implements draft-ietf-jmap-metadata-01 method bindings on top of jmap-base-client: Metadata/get|changes|set|query|queryChanges. Sibling of jmap-mail-client in the extension-client family — mirrors that crate's shape. Depends on jmap-base-client for transport and session, and on jmap-metadata-types for the wire types.

How to use

use jmap_base_client::{BearerAuth, ClientConfig, JmapClient};
use jmap_metadata_client::{JmapMetadataExt, MetadataChangesParams};
use jmap_types::{Id, State};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Build a base client (handles auth, HTTP, session fetch).
    let auth = BearerAuth::new("my-token")?;
    let client = JmapClient::new_plain(auth, "https://jmap.example.com", ClientConfig::default())?;

    // 2. Fetch the JMAP session document.
    let session = client.fetch_session().await?;

    // 3. Bind the session to a metadata client.
    let metadata = client.with_metadata_session(session);

    // 4. Fetch all Metadata in the account.
    let response = metadata.metadata_get(None, None).await?;
    for m in &response.list {
        // `related_type()` returns `Option<&str>` because the §4.1
        // extended-`/get` partial-response shape can omit it; for full
        // Metadata records from a plain `Metadata/get` the value is
        // always present.
        println!(
            "{} on {} {}",
            m.type_name(),
            m.related_type().unwrap_or("<unspecified>"),
            m.related_id(),
        );
    }

    // 5. Fetch only Annotation changes for Email objects.
    let params = MetadataChangesParams {
        filter_related_type: Some("Email".into()),
        filter_metadata_type: Some(vec!["Annotation".into()]),
        extra: serde_json::Map::new(),
    };
    let changes = metadata
        .metadata_changes(&State::from("s1"), None, Some(params))
        .await?;
    println!(
        "{} created / {} updated / {} destroyed",
        changes.created.len(),
        changes.updated.len(),
        changes.destroyed.len(),
    );

    Ok(())
}

After constructing a SessionClient via with_metadata_session, all JMAP Metadata methods are available without passing &Session on every call. If the session expires, re-fetch with JmapClient::fetch_session and construct a new SessionClient.

Id parameters are typed &jmap_types::Id (or &[jmap_types::Id] for slices) to make invalid Ids unrepresentable. State tokens use &jmap_types::State. Construct Ids with Id::new_validated(s) to enforce RFC 8620 §1.2 syntax at the boundary, or with Id::from(s) when the value is known-valid (e.g. already came back from a server response).

Registered methods

All method implementations live on SessionClient in src/methods/metadata.rs.

Method Function Returns
Metadata/get metadata_get GetResponse<Metadata>
Metadata/changes metadata_changes ChangesResponse
Metadata/set metadata_set SetResponse<Metadata>
Metadata/query metadata_query QueryResponse
Metadata/queryChanges metadata_query_changes QueryChangesResponse

Metadata/changes — optional filter args

Per draft §3.3, Metadata/changes accepts two extra optional arguments:

  • filterRelatedType — restrict to Metadata whose relatedType equals the value.
  • filterMetadataType — restrict to Metadata whose @type is in the array.

Pass them via MetadataChangesParams. Per §3.3 the state token in the response represents the complete account state and is independent of the filters; callers MUST re-use the same filter values across subsequent Metadata/changes calls to ensure consistent synchronisation.

Metadata/set

metadata_set accepts create, update, and destroy per RFC 8620 §5.3. Server-side uniqueness, quota, maySetPrivate gating, and related-object validation are reported back via the standard SetError catalogue (alreadyExists, forbidden, overQuota, invalidProperties); see draft §3.1 for details.

How it works

Every method follows the same five-step pattern:

  1. Validate arguments (empty-string guards return InvalidArgument before any network call).
  2. Call session_parts() to extract (api_url, account_id) from the bound session.
  3. Build the JMAP method arguments as a serde_json::Value.
  4. Call build_request(method_name, args, USING_METADATA) to construct a single-method JmapRequest.
  5. POST to the API URL and extract the typed response via jmap_base_client::extract_response.

The capability using array for all metadata requests is: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:metadata"].

Gotchas

  • Pinned to draft-01. This crate tracks draft-ietf-jmap-metadata-01; later draft revisions (or a final RFC) may rename methods, alter argument shapes, or change response fields.
  • Metadata/changes filter consistency. Per draft §3.3 the state token returned represents the complete account state and is independent of filterRelatedType / filterMetadataType. Callers MUST re-use the same filter values across subsequent Metadata/changes calls; mixing different filters against the same state token will produce inconsistent synchronisation results.

Crate family

jmap-types
    └── jmap-base-client          transport, auth, session
            └── jmap-metadata-client  ← this crate
                    (also depends on jmap-metadata-types for response types)

References

  • draft-ietf-jmap-metadata-01 — JMAP Object Metadata (normative for all method names, argument shapes, and response formats).
  • RFC 8620 — JMAP Core (request format, response shapes, /get, /set, /changes, /query, /queryChanges).