6c405e142d
crate::lock replaces three poison-tolerant lock helpers; the rate limiter reuses auth::bearer_token; the landing page reuses invites::display_code. Removed: AppError::not_implemented (D-004 placeholder, only its own test used it) and the unused Mutation::mutation_id/entity_type. tasks::start never awaited or failed: now a plain fn returning AppState. Milestone-ownership comments (M2a/M3/M4) rewritten to say what the code does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
227 lines
7.6 KiB
Rust
227 lines
7.6 KiB
Rust
//! 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<User>,
|
|
pub categories: Vec<Category>,
|
|
pub accounts: Vec<Account>,
|
|
pub recurring_templates: Vec<RecurringTemplate>,
|
|
pub expenses: Vec<Expense>,
|
|
pub settlements: Vec<Settlement>,
|
|
pub audit_entries: Vec<AuditEntry>,
|
|
}
|
|
|
|
/// 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<Split>,
|
|
#[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<TimestampSchema>, required = true)]
|
|
pub deleted_at: Option<String>,
|
|
}
|
|
|
|
/// 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<Allocation>,
|
|
#[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<TimestampSchema>, required = true)]
|
|
pub deleted_at: Option<String>,
|
|
}
|
|
|
|
/// 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<Mutation>,
|
|
}
|
|
|
|
/// 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<SyncEntity>,
|
|
}
|
|
|
|
/// 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<MutationResult>,
|
|
}
|