//! Sync protocol objects (SPEC §14.4.2, §14.4.3, §14.4.6). use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; use super::account::Account; use super::audit::{AuditEntry, EntityType}; use super::category::Category; use super::expense::{Expense, Split, SplitMode}; use super::recurring::RecurringTemplate; use super::settlement::{Allocation, Settlement}; use super::user::User; use super::{DateSchema, OreSchema, ServerVersionSchema, TimestampSchema, UuidSchema}; /// E-31 `GET /sync` response: one page; every array present (possibly empty) and sorted by `server_version`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct SyncPullResponse { /// `meta.sync_epoch`, 32 lowercase hex characters. #[schema(pattern = "^[0-9a-f]{32}$")] pub epoch: String, pub since: i64, /// The highest `server_version` in the page, or `since` when the page is empty. pub next_since: i64, pub has_more: bool, #[schema(value_type = TimestampSchema)] pub server_time: String, pub users: Vec, pub categories: Vec, pub accounts: Vec, pub recurring_templates: Vec, pub expenses: Vec, pub settlements: Vec, pub audit_entries: Vec, } /// Exactly the client-writable fields of an expense (SPEC §14.4.3); server-owned fields are ignored if sent. A /// delete is the full entity with `deleted_at` set. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct PushedExpense { #[schema(value_type = UuidSchema)] pub id: Uuid, #[schema(value_type = UuidSchema)] pub account_id: Uuid, #[schema(value_type = UuidSchema)] pub category_id: Uuid, #[schema(value_type = OreSchema)] pub amount_ore: i64, pub description: String, pub notes: String, #[schema(value_type = DateSchema)] pub expense_date: String, #[schema(value_type = UuidSchema)] pub payer_user_id: Uuid, pub split_mode: SplitMode, pub splits: Vec, #[schema(value_type = TimestampSchema)] pub created_at: String, #[schema(value_type = TimestampSchema)] pub updated_at: String, #[serde(deserialize_with = "super::required_nullable")] #[schema(value_type = Option, required = true)] pub deleted_at: Option, } /// Exactly the client-writable fields of a settlement (SPEC §14.4.3). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct PushedSettlement { #[schema(value_type = UuidSchema)] pub id: Uuid, #[schema(value_type = UuidSchema)] pub from_user_id: Uuid, #[schema(value_type = UuidSchema)] pub to_user_id: Uuid, #[schema(value_type = OreSchema)] pub amount_ore: i64, #[schema(value_type = DateSchema)] pub settlement_date: String, pub note: String, pub allocations: Vec, #[schema(value_type = TimestampSchema)] pub created_at: String, #[schema(value_type = TimestampSchema)] pub updated_at: String, #[serde(deserialize_with = "super::required_nullable")] #[schema(value_type = Option, required = true)] pub deleted_at: Option, } /// The constant `"expense"` (the discriminator value of [`ExpenseMutation`]). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub enum ExpenseTag { #[serde(rename = "expense")] Expense, } /// The constant `"settlement"` (the discriminator value of [`SettlementMutation`]). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub enum SettlementTag { #[serde(rename = "settlement")] Settlement, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct ExpenseMutation { #[schema(value_type = UuidSchema)] pub mutation_id: Uuid, #[schema(inline)] pub entity_type: ExpenseTag, pub entity: PushedExpense, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct SettlementMutation { #[schema(value_type = UuidSchema)] pub mutation_id: Uuid, #[schema(inline)] pub entity_type: SettlementTag, pub entity: PushedSettlement, } /// One pushed change, discriminated by `entity_type`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(untagged)] #[schema(discriminator(property_name = "entity_type", mapping( ("expense" = "#/components/schemas/ExpenseMutation"), ("settlement" = "#/components/schemas/SettlementMutation"), )))] pub enum Mutation { Expense(ExpenseMutation), Settlement(SettlementMutation), } /// E-32 `POST /sync` request: 1 to 100 mutations (more is 422 `batch_too_large`). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct SyncPushRequest { pub mutations: Vec, } /// A full wire object of the type named by `entity_type`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(untagged)] pub enum SyncEntity { Expense(Expense), Settlement(Settlement), } /// The constant `"accepted"`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub enum AcceptedTag { #[serde(rename = "accepted")] Accepted, } /// The constant `"rejected"`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub enum RejectedTag { #[serde(rename = "rejected")] Rejected, } /// Exhaustive (SPEC §14.4.6). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum RejectionReason { Stale, Deleted, ValidationFailed, ReferenceNotFound, ClockSkew, } /// An accepted mutation. Accepted and rejected results have different key sets (SPEC §14.4.3). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct AcceptedResult { #[schema(value_type = UuidSchema)] pub mutation_id: Uuid, pub entity_type: EntityType, #[schema(value_type = UuidSchema)] pub entity_id: Uuid, #[schema(inline)] pub status: AcceptedTag, #[schema(value_type = ServerVersionSchema)] pub server_version: i64, // The full wire object after processing. (A doc comment here would make utoipa wrap the schema in `allOf`.) #[schema(inline)] pub entity: SyncEntity, } /// A rejected mutation (SPEC §14.4.6). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct RejectedResult { #[schema(value_type = UuidSchema)] pub mutation_id: Uuid, pub entity_type: EntityType, #[schema(value_type = UuidSchema)] pub entity_id: Uuid, #[schema(inline)] pub status: RejectedTag, pub reason: RejectionReason, /// For validation failures, the domain error token of SPEC §14.5.3. pub message: String, // The stored row, or null when the server has no row with that id. #[schema(inline, required = true)] pub current: Option, } /// One result per mutation, discriminated by `status`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(untagged)] #[schema(discriminator(property_name = "status", mapping( ("accepted" = "#/components/schemas/AcceptedResult"), ("rejected" = "#/components/schemas/RejectedResult"), )))] pub enum MutationResult { Accepted(AcceptedResult), Rejected(RejectedResult), } /// E-32 response: one result per mutation, in request order. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] pub struct SyncPushResponse { #[schema(value_type = TimestampSchema)] pub server_time: String, pub results: Vec, }