taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 95f68d984566240e7606df863ec61460e1b8ae93
parent fcc09117ad3a761486922279845b3abec0efd024
Author: Antoine A <>
Date:   Tue, 17 Dec 2024 20:08:37 +0100

utils: improve timestamp type and sql logic

Diffstat:
MCargo.lock | 108+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mtaler-api/src/db.rs | 22++++++++++++++--------
Mtaler-api/src/lib.rs | 2+-
Mtaler-api/tests/api.rs | 7+------
Mtaler-api/tests/common/db.rs | 43+++++++++++++++++++++----------------------
Mtaler-api/tests/common/mod.rs | 21++-------------------
Mtaler-common/Cargo.toml | 1+
Mtaler-common/src/api_common.rs | 117+++-----------------------------------------------------------------------------
Mtaler-common/src/api_wire.rs | 4++--
Mtaler-common/src/lib.rs | 3+--
Ataler-common/src/types.rs | 25+++++++++++++++++++++++++
Rtaler-common/src/amount.rs -> taler-common/src/types/amount.rs | 0
Rtaler-common/src/payto.rs -> taler-common/src/types/payto.rs | 0
Ataler-common/src/types/timestamp.rs | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 245 insertions(+), 218 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -133,7 +133,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower", "tower-layer", @@ -156,7 +156,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -164,9 +164,9 @@ dependencies = [ [[package]] name = "axum-test" -version = "16.4.0" +version = "16.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "017cbca2776229a7100ebee44e065fcf5baccea6fc4cb9e5bea8328d83863a03" +checksum = "63e3a443d2608936a02a222da7b746eb412fede7225b3030b64fe9be99eab8dc" dependencies = [ "anyhow", "assert-json-diff", @@ -269,9 +269,9 @@ checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" [[package]] name = "cc" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] @@ -352,18 +352,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -721,11 +721,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -787,9 +787,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", @@ -1021,6 +1021,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] +name = "jiff" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db69f08d4fb10524cacdb074c10b296299d71274ddbc830a8ee65666867002e9" +dependencies = [ + "jiff-tzdb-platform", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329" +dependencies = [ + "jiff-tzdb", +] + +[[package]] name = "js-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1174,9 +1199,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] @@ -1450,9 +1475,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags", ] @@ -1583,9 +1608,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.19" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -1606,9 +1631,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" @@ -1641,18 +1666,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", @@ -2071,12 +2096,6 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" @@ -2119,13 +2138,14 @@ version = "0.1.0" dependencies = [ "base32", "fastrand", + "jiff", "rand", "serde", "serde_json", "serde_urlencoded", "serde_with", "sqlx", - "thiserror 2.0.6", + "thiserror 2.0.7", "url", ] @@ -2165,11 +2185,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.7", ] [[package]] @@ -2185,9 +2205,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" dependencies = [ "proc-macro2", "quote", @@ -2301,14 +2321,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -2430,9 +2450,9 @@ checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" diff --git a/taler-api/src/db.rs b/taler-api/src/db.rs @@ -16,13 +16,19 @@ use std::time::Duration; -use sqlx::{error::BoxDynError, postgres::PgRow, query::Query, Decode, Error, QueryBuilder, Type}; -use sqlx::{PgPool, Postgres, Row}; +use sqlx::{ + error::BoxDynError, postgres::PgRow, query::Query, Decode, Error, PgExecutor, PgPool, + QueryBuilder, Type, +}; +use sqlx::{Postgres, Row}; use taler_common::{ - amount::{Amount, Decimal}, - api_common::{Base32, SafeU64, Timestamp}, + api_common::{Base32, SafeU64}, api_params::{History, Page}, - payto::Payto, + types::{ + amount::{Amount, Decimal}, + payto::Payto, + timestamp::Timestamp, + }, }; use tokio::sync::watch::Receiver; use url::Url; @@ -40,8 +46,8 @@ pub enum IncomingType { /* ----- Routines ------ */ -pub async fn page<'a, R: Send + Unpin>( - pool: &PgPool, +pub async fn page<'a, 'b, R: Send + Unpin>( + pool: impl PgExecutor<'b>, id_col: &str, params: &Page, prepare: impl Fn() -> QueryBuilder<'a, Postgres>, @@ -70,7 +76,7 @@ pub async fn page<'a, R: Send + Unpin>( .await } -pub async fn history<'a, R: Send + Unpin>( +pub async fn history<'a, 'b, R: Send + Unpin>( pool: &PgPool, id_col: &str, params: &History, diff --git a/taler-api/src/lib.rs b/taler-api/src/lib.rs @@ -38,7 +38,6 @@ use error::{failure, failure_code, ApiError, ApiResult}; use http_body_util::BodyExt; use serde::de::DeserializeOwned; use taler_common::{ - amount::Amount, api_params::{History, HistoryParams, Page, TransferParams}, api_wire::{ AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddKycauthResponse, @@ -46,6 +45,7 @@ use taler_common::{ TransferState, TransferStatus, WireConfig, }, error_code::ErrorCode, + types::amount::Amount, }; use tokio::{net::TcpListener, signal}; use tracing::info; diff --git a/taler-api/tests/api.rs b/taler-api/tests/api.rs @@ -18,13 +18,13 @@ use common::sample_wire_gateway_api; use sqlx::PgPool; use taler_api::{auth::AuthMethod, db::IncomingType, standard_layer}; use taler_common::{ - amount::amount, api_common::{Base32, EddsaPublicKey, HashCode, ShortHashCode}, api_wire::{ IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList, TransferResponse, TransferState, TransferStatus, }, error_code::ErrorCode, + types::{amount::amount, url}, }; use test_utils::{ axum_test::TestServer, @@ -32,7 +32,6 @@ use test_utils::{ json, routine::{routine_history, routine_pagination}, }; -use url::Url; mod common; @@ -44,10 +43,6 @@ async fn setup(pool: PgPool) -> TestServer { .unwrap() } -fn url(url: &str) -> Url { - url.parse().expect("Invalid url") -} - #[sqlx::test] async fn errors(pool: PgPool) { let server = setup(pool).await; diff --git a/taler-api/tests/common/db.rs b/taler-api/tests/common/db.rs @@ -20,17 +20,16 @@ use sqlx::{ }; use taler_api::{ db::{history, page, BindHelper, IncomingType, TypeHelper}, - error::{ApiError, ApiResult}, + error::ApiError, }; use taler_common::{ - amount::Amount, - api_common::{EddsaPublicKey, SafeU64, Timestamp}, + api_common::{EddsaPublicKey, SafeU64}, api_params::{History, Page}, api_wire::{ IncomingBankTransaction, OutgoingBankTransaction, TransferListStatus, TransferRequest, TransferResponse, TransferState, TransferStatus, }, - payto::Payto, + types::{amount::Amount, payto::Payto, timestamp::Timestamp}, }; use tokio::sync::watch::{Receiver, Sender}; use tracing::debug; @@ -70,8 +69,8 @@ pub enum TransferResult { RequestUidReuse, } -pub async fn transfer(db: &PgPool, transfer: TransferRequest) -> ApiResult<TransferResult> { - Ok(sqlx::query( +pub async fn transfer(db: &PgPool, transfer: TransferRequest) -> sqlx::Result<TransferResult> { + sqlx::query( " SELECT out_request_uid_reuse, out_tx_row_id, out_timestamp FROM taler_transfer(($1, $2)::taler_amount, $3, $4, $5, $6, $7, $8) @@ -95,7 +94,7 @@ pub async fn transfer(db: &PgPool, transfer: TransferRequest) -> ApiResult<Trans }) }) .fetch_one(db) - .await?) + .await } pub async fn transfer_page( @@ -103,8 +102,8 @@ pub async fn transfer_page( status: &Option<TransferState>, params: &Page, currency: &str, -) -> ApiResult<Vec<TransferListStatus>> { - Ok(page( +) -> sqlx::Result<Vec<TransferListStatus>> { + page( db, "transfer_id", params, @@ -136,15 +135,15 @@ pub async fn transfer_page( }) }, ) - .await?) + .await } pub async fn transfer_by_id( db: &PgPool, id: u64, currency: &str, -) -> ApiResult<Option<TransferStatus>> { - Ok(sqlx::query( +) -> sqlx::Result<Option<TransferStatus>> { + sqlx::query( " SELECT status, @@ -171,7 +170,7 @@ pub async fn transfer_by_id( }) }) .fetch_optional(db) - .await?) + .await } pub async fn outgoing_page( @@ -179,8 +178,8 @@ pub async fn outgoing_page( params: &History, currency: &str, listen: impl FnOnce() -> Receiver<i64>, -) -> ApiResult<Vec<OutgoingBankTransaction>> { - Ok(history( +) -> sqlx::Result<Vec<OutgoingBankTransaction>> { + history( db, "transfer_id", params, @@ -211,7 +210,7 @@ pub async fn outgoing_page( }) }, ) - .await?) + .await } pub enum AddIncomingResult { @@ -227,8 +226,8 @@ pub async fn add_incoming( timestamp: &Timestamp, kind: IncomingType, key: &EddsaPublicKey, -) -> ApiResult<AddIncomingResult> { - Ok(sqlx::query( +) -> sqlx::Result<AddIncomingResult> { + sqlx::query( " SELECT out_reserve_pub_reuse, out_tx_row_id FROM add_incoming($1, $2, ($3, $4)::taler_amount, $5, $6, $7) @@ -248,7 +247,7 @@ pub async fn add_incoming( }) }) .fetch_one(db) - .await?) + .await } pub async fn incoming_page( @@ -256,8 +255,8 @@ pub async fn incoming_page( params: &History, currency: &str, listen: impl FnOnce() -> Receiver<i64>, -) -> ApiResult<Vec<IncomingBankTransaction>> { - Ok(history( +) -> sqlx::Result<Vec<IncomingBankTransaction>> { + history( db, "incoming_transaction_id", params, @@ -308,5 +307,5 @@ pub async fn incoming_page( }) }, ) - .await?) + .await } diff --git a/taler-api/tests/common/mod.rs b/taler-api/tests/common/mod.rs @@ -25,7 +25,6 @@ use taler_api::{ wire_gateway_api, WireGatewayImpl, }; use taler_common::{ - api_common::Timestamp, api_params::{History, Page}, api_wire::{ AddIncomingRequest, AddIncomingResponse, AddKycauthRequest, AddKycauthResponse, @@ -33,7 +32,7 @@ use taler_common::{ TransferState, TransferStatus, }, error_code::ErrorCode, - payto::payto, + types::{payto::payto, timestamp::Timestamp}, }; use tokio::sync::watch::Sender; @@ -79,7 +78,7 @@ impl WireGatewayImpl for SampleState { } async fn transfer_by_id(&self, id: u64) -> ApiResult<Option<TransferStatus>> { - db::transfer_by_id(&self.pool, id, &self.currency).await + Ok(db::transfer_by_id(&self.pool, id, &self.currency).await?) } async fn outgoing_history(&self, params: History) -> ApiResult<OutgoingHistory> { @@ -148,22 +147,6 @@ impl WireGatewayImpl for SampleState { )), } } - - fn check_currency(&self, amount: &taler_common::amount::Amount) -> ApiResult<()> { - let currency = self.currency(); - if amount.currency.as_ref() != currency { - Err(failure( - ErrorCode::GENERIC_CURRENCY_MISMATCH, - std::format!( - "Wrong currency: expected {} got {}", - currency, - amount.currency - ), - )) - } else { - Ok(()) - } - } } pub async fn sample_wire_gateway_api(pool: Option<PgPool>, currency: String) -> Router { diff --git a/taler-common/Cargo.toml b/taler-common/Cargo.toml @@ -9,6 +9,7 @@ serde_with = "3.11.0" rand = "0.8" fastrand = "2.2.0" serde_urlencoded = "0.7" +jiff = "0.1" serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["raw_value"] } url.workspace = true diff --git a/taler-common/src/api_common.rs b/taler-common/src/api_common.rs @@ -14,15 +14,10 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{ - fmt::Display, - ops::Deref, - str::FromStr, - time::{Duration, SystemTime}, -}; +use std::{fmt::Display, ops::Deref, str::FromStr}; -use serde::{de::Error, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::{value::RawValue, Value}; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::value::RawValue; /// <https://docs.taler.net/core/api-common.html#tsref-type-ErrorDetail> #[derive(Debug, Clone, Serialize, Deserialize)] @@ -41,112 +36,6 @@ pub struct ErrorDetail { pub extra: Option<Box<RawValue>>, } -/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp> -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Timestamp { - Never, - Time(SystemTime), -} - -#[derive(Serialize, Deserialize)] -struct TimestampImpl { - t_s: Value, -} - -impl Timestamp { - /** Timestamp corresponding to "now" */ - pub fn now() -> Self { - Self::Time(SystemTime::now()) - } - - /** Timestamp corresponding to now as it would be stored in db */ - pub fn now_stable() -> Self { - Self::from_sql_micros(Self::now().as_sql_micros()) - .expect("timestamp sql roundtrip must always succedd") - } - /** Timestamp corresponding to "never" */ - pub const fn never() -> Self { - Self::Never - } - - /** I64 equivalent of this timestamp for db storage */ - pub fn as_sql_micros(&self) -> i64 { - match self { - Timestamp::Never => i64::MAX, - Timestamp::Time(system_time) => system_time - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_micros() - .try_into() - .expect("timestamp must be a valid i64"), - } - } - - /** Timestamp equivalent of as i64 as stored in db */ - pub fn from_sql_micros(micros: i64) -> Result<Self, String> { - if micros == i64::MAX { - Ok(Self::Never) - } else { - let micros = u64::try_from(micros) - .map_err(|_| format!("expected timestamp micros got negative value {micros}"))?; - Ok(Self::Time( - SystemTime::UNIX_EPOCH - .checked_add(Duration::from_micros(micros)) - .ok_or_else(|| format!("expected timestamp micros got overflowing {micros}"))?, - )) - } - } -} - -impl<'de> Deserialize<'de> for Timestamp { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let tmp = TimestampImpl::deserialize(deserializer)?; - match tmp.t_s { - Value::Number(s) => { - if let Some(since_epoch_s) = s.as_u64() { - Ok(Self::Time( - SystemTime::UNIX_EPOCH + Duration::from_secs(since_epoch_s), - )) - } else { - Err(Error::custom("Expected epoch time")) - } - } - Value::String(str) if str == "never" => Ok(Self::Never), - _ => Err(Error::custom("Expected epoch time or 'never'")), - } - } -} - -impl Serialize for Timestamp { - fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - let mut se_struct = se.serialize_struct("Timestamp", 1)?; - match self { - Timestamp::Never => se_struct.serialize_field("t_s", "never")?, - Timestamp::Time(time) => se_struct.serialize_field( - "t_s", - &time - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(), - )?, - }; - - se_struct.end() - } -} - -impl From<SystemTime> for Timestamp { - fn from(time: SystemTime) -> Self { - Self::Time(time) - } -} - /// <https://docs.taler.net/core/api-common.html#tsref-type-SafeUint64> #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct SafeU64(u64); diff --git a/taler-common/src/api_wire.rs b/taler-common/src/api_wire.rs @@ -18,9 +18,9 @@ use url::Url; -use crate::{amount::Amount, payto::Payto}; +use crate::types::{amount::Amount, payto::Payto, timestamp::Timestamp}; -use super::api_common::{EddsaPublicKey, HashCode, SafeU64, ShortHashCode, Timestamp, WadId}; +use super::api_common::{EddsaPublicKey, HashCode, SafeU64, ShortHashCode, WadId}; use serde::{Deserialize, Serialize}; /// <https://docs.taler.net/core/api-bank-wire.html#tsref-type-WireConfig> diff --git a/taler-common/src/lib.rs b/taler-common/src/lib.rs @@ -14,12 +14,11 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -pub mod amount; pub mod api_common; pub mod api_params; pub mod api_wire; pub mod error_code; +pub mod types; pub mod config { // TODO } -pub mod payto; diff --git a/taler-common/src/types.rs b/taler-common/src/types.rs @@ -0,0 +1,25 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +pub mod amount; +pub mod payto; +pub mod timestamp; + +use url::Url; + +pub fn url(url: &str) -> Url { + url.parse().expect("Invalid url") +} diff --git a/taler-common/src/amount.rs b/taler-common/src/types/amount.rs diff --git a/taler-common/src/payto.rs b/taler-common/src/types/payto.rs diff --git a/taler-common/src/types/timestamp.rs b/taler-common/src/types/timestamp.rs @@ -0,0 +1,110 @@ +/* + This file is part of TALER + Copyright (C) 2024 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use serde::{de::Error, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; + +/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp> +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Timestamp { + Never, + Time(jiff::Timestamp), +} + +#[derive(Serialize, Deserialize)] +struct TimestampImpl { + t_s: Value, +} + +impl Timestamp { + /** Timestamp corresponding to "now" */ + pub fn now() -> Self { + Self::Time(jiff::Timestamp::now()) + } + + /** Timestamp corresponding to now as it would be stored in db */ + pub fn now_stable() -> Self { + Self::from_sql_micros(Self::now().as_sql_micros()) + .expect("timestamp sql roundtrip must always succedd") + } + + /** Timestamp corresponding to "never" */ + pub const fn never() -> Self { + Self::Never + } + + /** I64 equivalent of this timestamp for db storage */ + pub fn as_sql_micros(&self) -> i64 { + match self { + Timestamp::Never => i64::MAX, + Timestamp::Time(timestamp) => timestamp.as_microsecond(), + } + } + + /** Timestamp equivalent of as i64 as stored in db */ + pub fn from_sql_micros(micros: i64) -> Result<Self, String> { + if micros == i64::MAX { + Ok(Self::Never) + } else { + let timestamp = jiff::Timestamp::from_microsecond(micros) + .map_err(|e| format!("expected timestamp micros got overflowing {micros}: {e}"))?; + Ok(Self::Time(timestamp)) + } + } +} + +impl<'de> Deserialize<'de> for Timestamp { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let tmp = TimestampImpl::deserialize(deserializer)?; + match tmp.t_s { + Value::Number(s) => { + if let Some(since_epoch_s) = s.as_i64() { + jiff::Timestamp::from_second(since_epoch_s) + .map(Self::Time) + .map_err(Error::custom) + } else { + Err(Error::custom("Expected epoch time")) + } + } + Value::String(str) if str == "never" => Ok(Self::Never), + _ => Err(Error::custom("Expected epoch time or 'never'")), + } + } +} + +impl Serialize for Timestamp { + fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut se_struct = se.serialize_struct("Timestamp", 1)?; + match self { + Timestamp::Never => se_struct.serialize_field("t_s", "never")?, + Timestamp::Time(time) => se_struct.serialize_field("t_s", &time.as_second())?, + }; + + se_struct.end() + } +} + +impl From<jiff::Timestamp> for Timestamp { + fn from(time: jiff::Timestamp) -> Self { + Self::Time(time) + } +}