jmap-calendars-types 0.2.0

JMAP Calendars extension data types (draft-ietf-jmap-calendars-*)
Documentation
//! CalendarEventNotification, Person, and NotificationType.
//!
//! Normative reference: draft-ietf-jmap-calendars-26 §7.

use jmap_types::{impl_string_enum, Id, PatchObject, UTCDate};
use serde::{Deserialize, Serialize};

/// Who made the change that triggered a [`CalendarEventNotification`]
/// (draft-ietf-jmap-calendars-26 §7).
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Person {
    /// Display name of the person who made the change.
    pub name: String,

    /// Email address of the person, or `null` if not available.
    pub email: Option<String>,

    /// Id of the Principal corresponding to this person, or `null` if
    /// the change came from an external entity (e.g. an iTIP invitation).
    pub principal_id: Option<Id>,

    /// CalendarAddress URI of the person, or `null`.
    pub calendar_address: Option<String>,

    /// Catch-all for vendor / site / private extension fields not covered
    /// by the typed fields above. Preserves unknown fields across
    /// deserialize/serialize round-trip per workspace extras-preservation
    /// policy (see workspace AGENTS.md).
    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
    pub extra: serde_json::Map<String, serde_json::Value>,
}

/// Type of change recorded by a [`CalendarEventNotification`]
/// (draft-ietf-jmap-calendars-26 §7).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum NotificationType {
    /// A calendar event was created.
    Created,
    /// A calendar event was updated.
    Updated,
    /// A calendar event was destroyed.
    Destroyed,
    /// Any notification type string not recognised by this implementation.
    Other(String),
}

impl_string_enum!(NotificationType, "a JMAP CalendarEventNotification type string",
    "created"   => Created,
    "updated"   => Updated,
    "destroyed" => Destroyed,
);

/// A JMAP CalendarEventNotification object
/// (draft-ietf-jmap-calendars-26 §7).
///
/// Records a change made to a calendar event by an external entity or
/// another client.  CalendarEventNotifications have no per-user data; a
/// single notification object is shared across all users who have access
/// to the changed event.
///
/// Only `destroyed` notifications may be explicitly managed via
/// `CalendarEventNotification/set`; `created` and `updated` notifications
/// are server-set.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CalendarEventNotification {
    /// Server-assigned identifier for this notification.
    pub id: Id,

    /// UTC date-time when this notification was created.
    pub created: UTCDate,

    /// Who made the change.
    pub changed_by: Person,

    /// Comment sent along with the change (e.g. iTIP COMMENT property), if any.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub comment: Option<String>,

    /// Type of change recorded.
    ///
    /// Wire field name is `type`.
    #[serde(rename = "type")]
    pub notification_type: NotificationType,

    /// Id of the CalendarEvent this notification is about (always the base
    /// event id, even for single-instance changes).
    pub calendar_event_id: Id,

    /// Whether the event is a draft.  Present for `created` and `updated`
    /// notifications only.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_draft: Option<bool>,

    /// The CalendarEvent data before the change (for `updated` or `destroyed`),
    /// or the data after creation (for `created`).
    pub event: serde_json::Value,

    /// A PatchObject encoding the change between `event` and the state after
    /// the update.  Present for `updated` notifications only.
    ///
    /// Wire format is byte-identical to a plain JSON object via
    /// [`PatchObject`]'s `#[serde(transparent)]`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub event_patch: Option<PatchObject>,

    /// Catch-all for vendor / site / private extension fields not covered
    /// by the typed fields above. Preserves unknown fields across
    /// deserialize/serialize round-trip per workspace extras-preservation
    /// policy (see workspace AGENTS.md).
    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
    pub extra: serde_json::Map<String, serde_json::Value>,
}

// Serde-default function for the `@type` discriminator on
// CalendarAlert. draft-ietf-jmap-calendars-26 §6.4 marks the field as
// mandatory on the wire; we supply the literal as a default so
// deserialize is liberal in what it accepts (spec-violating vendor
// input missing `@type` does not fail). Mirrors the same pattern in
// `jmap-jscalendar-types` (bd:JMAP-ky8g.10) and `jmap-tasks-types`
// (bd:JMAP-ky8g.1).

fn calendar_alert_at_type_default() -> String {
    "CalendarAlert".to_owned()
}

/// Push-notification payload emitted when a calendar alert fires
/// (draft-ietf-jmap-calendars-26 §6.4).
///
/// Sent over the JMAP push channel when the server determines that an alert
/// for a CalendarEvent should fire.  Clients receiving this payload should
/// trigger the appropriate local notification (e.g. an OS alert).
///
/// `recurrenceId` is `null` for non-recurring events and MUST serialize as
/// `null` (not be omitted) — the receiver uses its presence to distinguish
/// recurring from non-recurring.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CalendarAlert {
    /// Object type discriminator; always `"CalendarAlert"` on the wire.
    ///
    /// Deserialize is liberal: if `@type` is absent (spec-violating
    /// vendor input), this field defaults to `"CalendarAlert"` rather
    /// than failing the whole parent object's deserialize. Serialize
    /// always emits the field. See bd:JMAP-ky8g.10.
    #[serde(rename = "@type", default = "calendar_alert_at_type_default")]
    pub at_type: String,

    /// The account that owns the calendar event.
    pub account_id: Id,

    /// Id of the CalendarEvent for which this alert is firing.
    pub calendar_event_id: Id,

    /// The UID property of the iCalendar object underlying the calendar event.
    pub uid: String,

    /// The recurrenceId of the specific occurrence, or `null` for non-recurring
    /// events (draft-ietf-jmap-calendars-26 §6.4).
    ///
    /// Serializes as `null` when `None` — intentionally NOT marked
    /// `skip_serializing_if` so the receiver can distinguish recurring from
    /// non-recurring events.
    ///
    /// **Maintainer note: do NOT add `#[serde(skip_serializing_if = "Option::is_none")]`
    /// to this field.** Adding it would silently break the spec contract:
    /// per draft-ietf-jmap-calendars-26 §6.4, the receiver uses the
    /// **presence** of `recurrenceId` to determine whether the alert
    /// fired for a recurring or non-recurring event. An omitted field
    /// is indistinguishable from a sender that forgot to include it; an
    /// explicit `null` carries the "non-recurring" signal. This is the
    /// same "required-and-nullable" wire shape used by
    /// `PrincipalCalendarsCapability.account_id` in `capability.rs:121`.
    ///
    /// A "clippy clean-up", "consistency PR", or pattern-based code
    /// audit may flag this field as inconsistent with neighbours that
    /// use `skip_serializing_if` and propose to "fix" it. That is a
    /// silent wire-protocol break. The regression test
    /// `calendar_alert_recurrence_id_null_for_non_recurring` (in
    /// `tests/types_test.rs`) pins the current behavior. Design defense:
    /// bd:JMAP-1rwf.7.
    pub recurrence_id: Option<String>,

    /// Id of the [`Alert`](crate::Alert) object within the CalendarEvent that is firing.
    pub alert_id: String,

    /// Catch-all for vendor / site / private extension fields not covered
    /// by the typed fields above. Preserves unknown fields across
    /// deserialize/serialize round-trip per workspace extras-preservation
    /// policy (see workspace AGENTS.md).
    #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
    pub extra: serde_json::Map<String, serde_json::Value>,
}

/// Filter condition for `CalendarEventNotification/query`
/// (draft-ietf-jmap-calendars-26 §7.4.1).
///
/// All fields are optional; a condition with no fields set matches every
/// CalendarEventNotification.
///
/// # Excluded from extras preservation
///
/// This type is **out of scope** for the workspace extras-preservation
/// policy: it carries no flatten-extras `extra` field. Filter clauses the
/// server does not understand are a query-correctness hazard — silently
/// preserving an unrecognised clause and round-tripping it back to the
/// client can return the wrong set of records with no error signal.
///
/// ## What to do instead
///
/// **IETF-track path.** Vendors who need both capability-level declaration
/// and filterability for custom fields should use
/// `draft-ietf-jmap-metadata` (capability URI
/// `urn:ietf:params:jmap:metadata`), which defines a filterable
/// `Metadata` / `Annotation` companion object. Implemented in `jmap-metadata-types`,
/// `jmap-metadata-server`, and `jmap-metadata-client` (bd JMAP-06zp).
///
/// **Pre-IETF escape.** Vendors who cannot wait for the metadata draft can
/// either escape the filter tree to `serde_json::Value` or fork the
/// `FilterCondition` type. See `crate-jmap-calendars-types/PLAN.md` for
/// the hybrid sloppy-value pattern.
///
/// Cross-reference: bd JMAP-lbdy "Decision: filter algebra excluded".
#[non_exhaustive]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotificationFilterCondition {
    /// Notification `created` time must be on or after this UTCDate.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub after: Option<UTCDate>,

    /// Notification `created` time must be before this UTCDate.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub before: Option<UTCDate>,

    /// Notification `type` must equal this string.
    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
    pub notification_type: Option<String>,

    /// Notification `calendarEventId` must be in this list.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub calendar_event_ids: Option<Vec<Id>>,
}

/// Concrete filter type for CalendarEventNotification/query
/// (draft-ietf-jmap-calendars-26 §6).
///
/// Alias for `jmap_types::query::Filter<NotificationFilterCondition>`
/// provided so callers do not have to reach into `jmap-types` directly.
/// Mirrors the canonical [`jmap_mail_types::EmailFilter`] shape from the
/// workspace canonical extension-types template.
///
/// [`jmap_mail_types::EmailFilter`]: https://docs.rs/jmap-mail-types/latest/jmap_mail_types/query/type.EmailFilter.html
pub type NotificationFilter = jmap_types::query::Filter<NotificationFilterCondition>;