commit 95f68d984566240e7606df863ec61460e1b8ae93
parent fcc09117ad3a761486922279845b3abec0efd024
Author: Antoine A <>
Date: Tue, 17 Dec 2024 20:08:37 +0100
utils: improve timestamp type and sql logic
Diffstat:
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)
+ }
+}