Skip to main content

cloudillo_contact/
types.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! JSON REST API types for contacts and address books.
5//!
6//! The shape is structured-only: server owns vCard generation and field extraction;
7//! clients never see raw vCard text. Custom vCard properties sent by external CardDAV
8//! clients are preserved in the stored blob but invisible to JSON responses.
9
10use serde::{Deserialize, Serialize};
11use serde_with::skip_serializing_none;
12
13use cloudillo_core::prelude::*;
14use cloudillo_types::types::serialize_timestamp_iso;
15
16// Structured sub-types
17//**********************
18
19/// vCard N (structured name) property.
20#[skip_serializing_none]
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct ContactName {
24	pub given: Option<String>,
25	pub family: Option<String>,
26	pub additional: Option<String>,
27	pub prefix: Option<String>,
28	pub suffix: Option<String>,
29}
30
31/// A value with TYPE and PREF parameters — covers EMAIL, TEL, URL, etc.
32#[skip_serializing_none]
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct TypedValue {
36	pub value: String,
37	#[serde(default, skip_serializing_if = "Vec::is_empty")]
38	pub r#type: Vec<String>,
39	pub pref: Option<u8>,
40}
41
42// Profile overlay
43//*****************
44
45/// Live profile data returned alongside a contact that has `profileIdTag` set.
46/// Fetched per-request from the tenant's `profiles` table; never written into storage.
47#[skip_serializing_none]
48#[derive(Debug, Clone, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct ProfileOverlay {
51	pub id_tag: String,
52	pub name: Option<String>,
53	pub r#type: Option<String>,
54	pub profile_pic: Option<String>,
55	pub status: Option<cloudillo_types::meta_adapter::ProfileStatus>,
56	pub connected: Option<bool>,
57	pub following: Option<bool>,
58}
59
60// Write types (input — POST/PUT/PATCH bodies)
61//*********************************************
62
63/// Body for `POST /api/address-books/{abId}/contacts` (create) and
64/// `PUT  /api/address-books/{abId}/contacts/{uid}` (replace).
65///
66/// Field absence means "leave empty"; no merge semantics on full replace.
67#[skip_serializing_none]
68#[derive(Debug, Clone, Default, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct ContactInput {
71	pub uid: Option<String>,
72	#[serde(rename = "fn")]
73	pub formatted_name: Option<String>,
74	pub n: Option<ContactName>,
75	#[serde(default)]
76	pub emails: Vec<TypedValue>,
77	#[serde(default)]
78	pub phones: Vec<TypedValue>,
79	pub org: Option<String>,
80	pub title: Option<String>,
81	pub note: Option<String>,
82	pub photo: Option<String>,
83	pub profile_id_tag: Option<String>,
84}
85
86/// Body for `PATCH /api/address-books/{abId}/contacts/{uid}`.
87/// Each field uses `Patch<T>` so clients can distinguish "leave alone" from "clear".
88#[derive(Debug, Default, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ContactPatch {
91	#[serde(default, rename = "fn")]
92	pub formatted_name: Patch<String>,
93	#[serde(default)]
94	pub n: Patch<ContactName>,
95	#[serde(default)]
96	pub emails: Patch<Vec<TypedValue>>,
97	#[serde(default)]
98	pub phones: Patch<Vec<TypedValue>>,
99	#[serde(default)]
100	pub org: Patch<String>,
101	#[serde(default)]
102	pub title: Patch<String>,
103	#[serde(default)]
104	pub note: Patch<String>,
105	#[serde(default)]
106	pub photo: Patch<String>,
107	#[serde(default)]
108	pub profile_id_tag: Patch<String>,
109}
110
111// Read types (output — response bodies)
112//***************************************
113
114/// Full contact response including server-side metadata and optional live profile overlay.
115#[skip_serializing_none]
116#[derive(Debug, Clone, Serialize)]
117#[serde(rename_all = "camelCase")]
118pub struct ContactOutput {
119	pub c_id: u64,
120	pub ab_id: u64,
121	pub uid: String,
122	pub etag: String,
123	#[serde(rename = "fn")]
124	pub formatted_name: Option<String>,
125	pub n: Option<ContactName>,
126	#[serde(default, skip_serializing_if = "Vec::is_empty")]
127	pub emails: Vec<TypedValue>,
128	#[serde(default, skip_serializing_if = "Vec::is_empty")]
129	pub phones: Vec<TypedValue>,
130	pub org: Option<String>,
131	pub title: Option<String>,
132	pub note: Option<String>,
133	pub photo: Option<String>,
134	pub profile_id_tag: Option<String>,
135	/// Live profile data merged from the `profiles` table (only present when linked and known).
136	pub profile: Option<ProfileOverlay>,
137	/// Set when the stored vCard blob could not be parsed. Clients should render
138	/// "record unreadable" rather than treating an empty projection as authoritative.
139	pub parse_error: Option<String>,
140	#[serde(serialize_with = "serialize_timestamp_iso")]
141	pub created_at: Timestamp,
142	#[serde(serialize_with = "serialize_timestamp_iso")]
143	pub updated_at: Timestamp,
144}
145
146/// Summary row for list endpoints (omits emails[]/phones[] detail to keep list responses small).
147#[skip_serializing_none]
148#[derive(Debug, Clone, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct ContactListItem {
151	pub c_id: u64,
152	pub ab_id: u64,
153	pub uid: String,
154	pub etag: String,
155	#[serde(rename = "fn")]
156	pub formatted_name: Option<String>,
157	pub email: Option<String>,
158	pub tel: Option<String>,
159	pub org: Option<String>,
160	pub photo: Option<String>,
161	pub profile_id_tag: Option<String>,
162	pub profile: Option<ProfileOverlay>,
163	#[serde(serialize_with = "serialize_timestamp_iso")]
164	pub updated_at: Timestamp,
165}
166
167// Address book
168//**************
169
170#[skip_serializing_none]
171#[derive(Debug, Clone, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct AddressBookOutput {
174	pub ab_id: u64,
175	pub name: String,
176	pub description: Option<String>,
177	pub ctag: String,
178	#[serde(serialize_with = "serialize_timestamp_iso")]
179	pub created_at: Timestamp,
180	#[serde(serialize_with = "serialize_timestamp_iso")]
181	pub updated_at: Timestamp,
182}
183
184#[skip_serializing_none]
185#[derive(Debug, Default, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct AddressBookCreate {
188	pub name: String,
189	pub description: Option<String>,
190}
191
192#[derive(Debug, Default, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct AddressBookPatch {
195	#[serde(default)]
196	pub name: Patch<String>,
197	#[serde(default)]
198	pub description: Patch<String>,
199}
200
201#[derive(Debug, Default, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct ListContactsQuery {
204	pub q: Option<String>,
205	pub cursor: Option<String>,
206	pub limit: Option<u32>,
207}
208
209// Import
210//********
211
212/// How to handle a vCard whose UID matches an existing contact in the address book.
213#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
214#[serde(rename_all = "kebab-case")]
215pub enum ImportConflictMode {
216	/// Keep the existing contact unchanged (safe default).
217	#[default]
218	Skip,
219	/// Replace the existing contact with the imported card (CardDAV-style).
220	Replace,
221	/// Always create a new contact, regenerating the UID so duplicates land side-by-side.
222	Add,
223}
224
225#[derive(Debug, Default, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct ImportContactsQuery {
228	#[serde(default)]
229	pub conflict: Option<ImportConflictMode>,
230}
231
232#[derive(Debug, Default, Serialize)]
233#[serde(rename_all = "camelCase")]
234pub struct ImportContactsError {
235	/// 0-based index of the card in the source file (counting valid BEGIN:VCARD blocks only).
236	pub index: u32,
237	pub uid: Option<String>,
238	pub message: String,
239}
240
241#[derive(Debug, Default, Serialize)]
242#[serde(rename_all = "camelCase")]
243pub struct ImportContactsResult {
244	/// Number of vCard blocks the parser found in the input (including unparseable ones).
245	pub total: u32,
246	/// New contacts created.
247	pub imported: u32,
248	/// Existing contacts overwritten (only with conflict=replace).
249	pub updated: u32,
250	/// Existing contacts left unchanged (only with conflict=skip).
251	pub skipped: u32,
252	/// Per-card parse / write failures.
253	pub errors: Vec<ImportContactsError>,
254}
255
256// vim: ts=4