jmap-types 0.1.0

Shared JMAP wire types (RFC 8620) for the jmap-* crate family
Documentation
# jmap-types

Shared JMAP wire types for the `jmap-*` crate family.

Implements the core data structures defined in [RFC 8620] (JMAP Core): identifiers,
timestamps, state tokens, method-level errors, request/response envelopes, and
result references. All types serialize to and from the exact JSON wire format the
spec requires.

## What

| Type | RFC | Description |
|------|-----|-------------|
| `Id` | §1.2 | Opaque server-assigned record identifier |
| `UTCDate` | §1.4 | RFC 3339 UTC timestamp string |
| `State` | §1.2 | Opaque server state token |
| `JmapError` | §3.6.2 | Method-level error with typed constructors |
| `Invocation` | §3.2 | `(method_name, arguments, call_id)` 3-tuple |
| `JmapRequest` | §3.3 | HTTP request envelope |
| `JmapResponse` | §3.4 | HTTP response envelope |
| `ResultReference` | §9 | Reference to a prior method's result |
| `Argument<T>` | §9 | A method argument that is either a value or a `ResultReference` |

## Why

Client crates and server crates both need these types, but neither should pull in
the other's dependencies. `jmap-types` has exactly three dependencies — `serde`,
`serde_json`, and `thiserror` — and no async runtime, no HTTP framework, and no
application logic. Any crate in the `jmap-*` family can depend on it without cost.

## Usage

Add to `Cargo.toml`:

```toml
[dependencies]
jmap-types = "0.1"
```

### Identifiers

```rust
use jmap_types::{Id, UTCDate, State};

// Construct from any string source
let id = Id::from("abc123");
let date = UTCDate::from("2014-10-30T06:12:00Z");
let state = State::from("75128aab4b1b");

// Use as a string reference without allocating
println!("{}", id);          // "abc123"
let s: &str = id.as_ref();  // borrow the inner string

// Consume to get the inner String
let raw: String = id.into_inner();
```

### Building a request

```rust
use jmap_types::{JmapRequest, Invocation};
use serde_json::json;

let req = JmapRequest {
    using: vec![
        "urn:ietf:params:jmap:core".into(),
        "urn:ietf:params:jmap:mail".into(),
    ],
    method_calls: vec![
        ("Email/get".into(), json!({"accountId": "u1", "ids": ["m1"]}), "c1".into()),
    ],
    created_ids: None,
};

let json = serde_json::to_string(&req)?;
// {"using":[...],"methodCalls":[["Email/get",{"accountId":"u1","ids":["m1"]},"c1"]]}
```

### Parsing a response

```rust
use jmap_types::JmapResponse;

let raw = r#"{
  "methodResponses": [["Email/get", {"list": []}, "c1"]],
  "sessionState": "75128aab4b1b"
}"#;

let resp: JmapResponse = serde_json::from_str(raw)?;
println!("{}", resp.session_state); // "75128aab4b1b"
```

### Method-level errors

```rust
use jmap_types::JmapError;

// Emit a typed error into a methodResponses array
let err = JmapError::invalid_arguments("ids must not be empty");
let err = JmapError::unknown_method();
let err = JmapError::server_fail("unexpected database error");

// alreadyExists requires the existing record's Id (RFC 8620 §5.4 MUST)
use jmap_types::Id;
let err = JmapError::already_exists(Id::from("existing-record-id"));

// Serialize for the wire — key is "type", not "error_type"
// {"type":"invalidArguments","description":"ids must not be empty"}
let json = serde_json::to_string(&err)?;
```

### Result references

```rust
use jmap_types::{Argument, ResultReference};

// A plain value argument
let arg: Argument<Vec<jmap_types::Id>> = Argument::Value(vec![Id::from("m1")]);

// A result reference — resolves at dispatch time
let rr = ResultReference::new("c0", "Email/query", "/ids");
let arg: Argument<Vec<jmap_types::Id>> = Argument::Ref(rr);
```

## How it works

### Wire format

All types use `serde` with the JSON field names and structure RFC 8620 mandates:

- `JmapRequest.method_calls` serializes as `"methodCalls"` (camelCase)
- `JmapResponse.session_state` serializes as `"sessionState"`
- `JmapError.error_type` serializes as `"type"` (the RFC field name)
- `JmapError.existing_id` serializes as `"existingId"` (present only for `alreadyExists`)
- `Invocation` is a type alias for `(String, serde_json::Value, String)`, which
  serde serializes as a 3-element JSON array — exactly the RFC 8620 §3.2 format

### `Argument<T>` and the sealed trait

`Argument<T>` uses `#[serde(untagged)]` to deserialize either as the value type `T`
or as a `ResultReference` (a JSON object). This works correctly only if `T` cannot
itself deserialize from a JSON object — otherwise serde would match `T` first and
silently swallow any `ResultReference` payload.

To prevent this, `T` is constrained to a sealed set of types that are guaranteed
to not deserialize from objects: `String`, `Vec<String>`, `Id`, `Vec<Id>`, `u32`,
`u64`, `bool`. `serde_json::Value` is deliberately excluded — `Argument<Value>`
fails to compile.

The sealed trait lives in a private module and cannot be implemented outside this
crate. To add a new type, open a PR and verify the invariant holds.

### `#[non_exhaustive]` and field privacy

Public structs are `#[non_exhaustive]` so that adding fields in future versions is
not a breaking change. The `Id`, `UTCDate`, and `State` newtypes also carry
`#[non_exhaustive]` specifically to prevent callers from pattern-matching the inner
field (e.g. `let Id(s) = id;`), which would otherwise lock in the single-field
layout as a semver guarantee. Use `as_ref()` or `into_inner()` instead.

### What this crate does not do

- **No validation**: `Id::from("")` succeeds. RFC 8620 requires non-empty Ids, but
  enforcement belongs in the consumer (e.g. `jmap-server`), not in the wire type.
- **No dispatch**: method routing, argument resolution, and `#`-prefixed key
  handling are the dispatcher's job.
- **No async**: this crate has no runtime dependency.

## Crate family

```
jmap-types          ← this crate
    ├── jmap-server     dispatcher, axum, tokio
    ├── jmap-mail-types RFC 8621 data types
    └── jmap-chat-types Chat extension data types
```

## References

- [RFC 8620] — JMAP Core (normative)
- [RFC 7807] — Problem Details for HTTP APIs (request-level errors, §3.6.1)
- [RFC 6901] — JSON Pointer (used in `ResultReference.path`)

[RFC 8620]: https://www.rfc-editor.org/rfc/rfc8620
[RFC 7807]: https://www.rfc-editor.org/rfc/rfc7807
[RFC 6901]: https://www.rfc-editor.org/rfc/rfc6901

## License

Licensed under the MIT License. See [LICENSE](LICENSE) for details.