Expand description
JSContact (RFC 9553) typed sub-types for the jmap-* crate family.
Normative reference: RFC 9553 (JSContact).
These are sub-object types that have no JMAP identity of their own.
They are embedded within ContactCard (from jmap-contacts-types).
§Crate family position
(no JMAP dep)
└── jmap-jscontact-types ← this crate (RFC 9553 typed sub-types)
└── jmap-contacts-types (consumes via path-dep + re-export)§Design: optional fields and Option<...>
RFC 9553 marks most fields optional, and JMAP properties arguments
permit partial responses. Every optional field is Option<...> with
#[serde(skip_serializing_if = "Option::is_none")] so partial inputs
round-trip unchanged.
Mandatory fields per the RFC are normally kept as bare types (not
Option) to express the requirement at the type level — callers
building a fresh sub-object must populate them. The exception is
the Resource-derived types (Calendar, CryptoKey,
Directory, Link, Media), whose RFC-mandatory kind and
uri fields are modelled as Option to permit partial-response
deserialization (a JMAP client requesting properties: ["kind"] on
a Calendar legitimately receives a JSON object with no uri and
must round-trip it unchanged).
The trade-off: callers can construct, e.g.,
Calendar { kind: None, uri: None, ... } and serialize a wire
object that no spec-conformant peer can validate. The type system
does not catch this; the kit’s posture is “types model the wire
shape; semantic validity is the consumer’s job” (see the kit-vs-jig
section in the workspace AGENTS.md). Callers building a fresh
value for emission MUST populate the mandatory-on-wire fields
themselves before serializing.
Mandatory-on-wire fields modelled as Option (8 sites across 5
types), gathered into one place so each consumer does not have to
re-derive them from the per-field rustdoc:
| Struct | Mandatory field | RFC section |
|---|---|---|
Calendar | kind | RFC 9553 §2.4.1 |
Calendar | uri | RFC 9553 §1.4.4 |
CryptoKey | uri | RFC 9553 §1.4.4 |
Directory | kind | RFC 9553 §2.6.2 |
Directory | uri | RFC 9553 §1.4.4 |
Link | uri | RFC 9553 §1.4.4 |
Media | kind | RFC 9553 §2.6.4 |
Media | uri | RFC 9553 §1.4.4 |
§Design: cross-field invariants are not type-enforced
Six structs carry a cross-field “at least one of X, Y must be set”
constraint at the rustdoc level that the Rust type system does not
enforce. The kit’s posture is “types model the wire shape; semantic
validity is the consumer’s job” (see the kit-vs-jig section in the
workspace AGENTS.md). Encoding these constraints in the type system
would diverge from that posture and force the partial-response
Option modelling into a corner.
Callers building a fresh value for emission MUST validate the constraint themselves before serializing. The constraints, gathered into one place so each consumer does not have to re-derive them from the per-struct rustdoc:
| Struct | Constraint |
|---|---|
Name | at least one of components or full |
Organization | at least one of name or units |
SpeakToAs | at least one of grammatical_gender or pronouns |
OnlineService | at least one of uri or user |
Address | at least one of components, coordinates, country_code, full, or time_zone |
Author | at least one property other than @type |
Deserialize does not reject inputs that violate these constraints —
the kit accepts partial-response inputs that legitimately omit
fields. The constraint applies only to emitting fresh values. See
bd:JMAP-sgrr.30.
§Design: numeric-range bounds are not type-enforced
RFC 9553 specifies bounded ranges for several numeric fields, but
the kit models them as bare u32 (or Option<u32>) without
type-level guarantees. Deserialize accepts out-of-range values
without complaint; callers building a fresh value MUST verify the
bound themselves before serializing. The kit’s posture — types
model the wire shape, semantic validity is the consumer’s job —
takes precedence over the NonZeroU32 / BoundedU8 patterns that
would push validation into the type system but require breaking
API changes to introduce later.
Affected fields, gathered into one place:
| Field(s) | RFC bound | RFC section |
|---|---|---|
.pref on Nickname, Pronouns, EmailAddress, OnlineService, Phone, LanguagePref, Calendar, SchedulingAddress, Address, CryptoKey, Directory, Link, Media (13 sites) | 1..=100, lower = more preferred | RFC 9553 §1.5.3 |
Directory::list_as | > 0 | RFC 9553 §2.6.2 |
PersonalInfo::list_as | > 0 | RFC 9553 §2.8.4 |
PartialDate::month | 1..=12 | RFC 9553 §2.8.1 |
PartialDate::day | 1..=31 (with month-specific cap) | RFC 9553 §2.8.1 |
Newtypes like Pref(NonZeroU8) capped at 100 or
Option<NonZeroU32> for list_as would express the contract at
the type level, but adopting them is a public-API break the kit
is not taking; see bd:JMAP-sgrr.14, bd:JMAP-sgrr.15, and
bd:JMAP-sgrr.22.
§Design: @type discriminator
Every RFC 9553 sub-object has an @type discriminator on the wire.
The Rust field is named at_type: Option<String> and renamed to
"@type" via serde attributes, with default and
skip_serializing_if = "Option::is_none". The field is modelled as
Option<String> (not bare String) because RFC 9553 §1.3.4 permits
omitting @type whenever the type is implied by context — most
notably when the value is in a defaultType position (see
Anniversary::date / AnniversaryDate for the worked example).
The value type is String (not an enum) to preserve forward-
compatibility with new sub-object types.
§Design: Resource-derived types
RFC 9553 §1.4.4 defines the abstract Resource common fields
(@type, kind, uri, mediaType, contexts, pref, label).
Five concrete types extend Resource:
Calendar, CryptoKey, Directory, Link, Media.
Each embeds the common fields directly because the RFC defines the
inheritance for documentation only — the wire format is a flat
object per concrete type.
Structs§
- Address
- A postal or geographic address (RFC 9553 §2.5.1).
- Address
Component - A single component of an
Address(RFC 9553 §2.5.1.2). - Anniversary
- A memorable date or event (RFC 9553 §2.8.1).
- Author
- The author of a
Note(RFC 9553 §2.8.3). - Calendar
- A calendaring resource (RFC 9553 §2.4.1).
- Crypto
Key - A cryptographic key or certificate associated with a Card (RFC 9553 §2.6.1). Extends the abstract Resource type.
- Directory
- A directory service associated with a Card (RFC 9553 §2.6.2).
- Email
Address - An email address (RFC 9553 §2.3.1).
- Language
Pref - A preferred language (RFC 9553 §2.3.4).
- Link
- A generic resource link associated with a Card (RFC 9553 §2.6.3).
- Media
- A media resource associated with a Card (RFC 9553 §2.6.4).
- Name
- The name of the entity represented by a Card (RFC 9553 §2.2.1).
- Name
Component - A single component of a
Name(RFC 9553 §2.2.1.2). - Nickname
- A nickname for the entity represented by a Card (RFC 9553 §2.2.2).
- Note
- A free-text note associated with a Card (RFC 9553 §2.8.3).
- Online
Service - An online service (messaging service, social media, etc.) (RFC 9553 §2.3.2).
- OrgUnit
- An organizational unit within an
Organization(RFC 9553 §2.2.3). - Organization
- A company or organization name associated with a Card (RFC 9553 §2.2.3).
- Partial
Date - A complete or partial Gregorian calendar date (RFC 9553 §2.8.1).
- Personal
Info - Personal information such as an expertise, hobby, or interest (RFC 9553 §2.8.4).
- Phone
- A phone number (RFC 9553 §2.3.3).
- Pronouns
- A pronouns entry (RFC 9553 §2.2.4).
- Relation
- A relationship to another Card (RFC 9553 §2.1.8).
- Scheduling
Address - An iTIP scheduling address (RFC 9553 §2.4.2).
- Speak
ToAs - How to address or refer to the entity represented by a Card (RFC 9553 §2.2.4).
- Timestamp
- A UTC point in time (RFC 9553 §2.8.1).
- Title
- A job title or functional position (RFC 9553 §2.2.5).
Enums§
- Anniversary
Date - The date value of an
Anniversary— either aPartialDateor aTimestamp(RFC 9553 §2.8.1).