taler-rust

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

commit 512a4592c5e721191356835fecd0317bb5a543d6
parent f5e3bb2d417428ccba9a23e3556d7ad11a0b3d4a
Author: Antoine A <>
Date:   Fri,  4 Sep 2026 12:43:58 +0200

common: improve taler timestamp db compatibility with Kotlin implementation

Diffstat:
Madapters/taler-cyclos/src/api.rs | 2+-
Madapters/taler-magnet-bank/src/api.rs | 2+-
Madapters/taler-wise/src/api.rs | 2+-
Mcommon/taler-api/src/db.rs | 2+-
Mcommon/taler-api/src/test/api.rs | 2+-
Mcommon/taler-common/benches/encoding.rs | 7++-----
Mcommon/taler-common/src/api/prepared.rs | 2+-
Mcommon/taler-common/src/api/revenue.rs | 2+-
Mcommon/taler-common/src/api/wire.rs | 2+-
Mcommon/taler-common/src/signature.rs | 2+-
Mcommon/taler-common/src/types.rs | 2+-
Acommon/taler-common/src/types/time.rs | 241+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcommon/taler-common/src/types/timestamp.rs | 241-------------------------------------------------------------------------------
Mcommon/taler-test-utils/src/routine.rs | 2+-
14 files changed, 254 insertions(+), 257 deletions(-)

diff --git a/adapters/taler-cyclos/src/api.rs b/adapters/taler-cyclos/src/api.rs @@ -37,7 +37,7 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, - types::{amount::Currency, timestamp::TalerTimestamp}, + types::{amount::Currency, time::TalerTimestamp}, }; use tokio::sync::watch::Sender; diff --git a/adapters/taler-magnet-bank/src/api.rs b/adapters/taler-magnet-bank/src/api.rs @@ -36,7 +36,7 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, - types::{amount::Currency, timestamp::TalerTimestamp, utils::date_to_utc_ts}, + types::{amount::Currency, time::TalerTimestamp, utils::date_to_utc_ts}, }; use tokio::sync::watch::Sender; diff --git a/adapters/taler-wise/src/api.rs b/adapters/taler-wise/src/api.rs @@ -42,7 +42,7 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode, - types::{amount::Currency, payto::PaytoImpl, timestamp::TalerTimestamp}, + types::{amount::Currency, payto::PaytoImpl, time::TalerTimestamp}, }; use taler_test_utils::Router; use tokio::sync::watch::Sender; diff --git a/common/taler-api/src/db.rs b/common/taler-api/src/db.rs @@ -33,7 +33,7 @@ use taler_common::{ amount::{Amount, Currency, Decimal}, iban::IBAN, payto::PaytoURI, - timestamp::TalerTimestamp, + time::TalerTimestamp, utils::date_to_utc_ts, }, }; diff --git a/common/taler-api/src/test/api.rs b/common/taler-api/src/test/api.rs @@ -29,7 +29,7 @@ use taler_common::{ }, db::IncomingType, error_code::ErrorCode::{self}, - types::{amount::Currency, payto::FullIbanPayto, timestamp::TalerTimestamp}, + types::{amount::Currency, payto::FullIbanPayto, time::TalerTimestamp}, }; use tokio::sync::watch::Sender; diff --git a/common/taler-common/benches/encoding.rs b/common/taler-common/benches/encoding.rs @@ -14,14 +14,11 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::hint::black_box; -use std::str::FromStr; +use std::{hint::black_box, str::FromStr}; use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use taler_common::{ - encoding::base32, - encoding::base64, - encoding::hex, + encoding::{base32, base64, hex}, types::base32::Base32, }; diff --git a/common/taler-common/src/api/prepared.rs b/common/taler-common/src/api/prepared.rs @@ -29,7 +29,7 @@ use crate::{ types::{ amount::{Amount, Currency}, payto::PaytoURI, - timestamp::TalerTimestamp, + time::TalerTimestamp, }, }; diff --git a/common/taler-common/src/api/revenue.rs b/common/taler-common/src/api/revenue.rs @@ -22,7 +22,7 @@ use taler_macros::api_config; use crate::types::{ amount::{Amount, Currency}, payto::PaytoURI, - timestamp::TalerTimestamp, + time::TalerTimestamp, }; /// <https://docs.taler.net/core/api-bank-revenue.html#tsref-type-RevenueConfig> diff --git a/common/taler-common/src/api/wire.rs b/common/taler-common/src/api/wire.rs @@ -27,7 +27,7 @@ use crate::{ types::{ amount::{Amount, Currency}, payto::PaytoURI, - timestamp::TalerTimestamp, + time::TalerTimestamp, }, }; diff --git a/common/taler-common/src/signature.rs b/common/taler-common/src/signature.rs @@ -21,7 +21,7 @@ use aws_lc_rs::{ use crate::{ api::{EddsaPublicKey, EddsaSignature}, - types::{amount::Amount, payto::PaytoURI, timestamp::TalerTimestamp}, + types::{amount::Amount, payto::PaytoURI, time::TalerTimestamp}, }; pub trait Signature<const N: usize> { diff --git a/common/taler-common/src/types.rs b/common/taler-common/src/types.rs @@ -19,7 +19,7 @@ pub mod amount; pub mod base32; pub mod iban; pub mod payto; -pub mod timestamp; +pub mod time; pub mod utils; use url::Url; diff --git a/common/taler-common/src/types/time.rs b/common/taler-common/src/types/time.rs @@ -0,0 +1,241 @@ +/* + This file is part of TALER + Copyright (C) 2024, 2025, 2026 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 std::{fmt::Display, ops::Add, str::FromStr, time::Duration}; + +use jiff::{SignedDuration, Timestamp, civil::Time, tz::TimeZone}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error, ser::SerializeStruct}; // codespell:ignore +use serde_json::Value; + +/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp> +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum TalerTimestamp { + Never, + Timestamp(Timestamp), +} + +impl TalerTimestamp { + /** Encode timestamp for signature */ + pub fn signature_bytes(self) -> [u8; 8] { + match self { + TalerTimestamp::Never => u64::MAX, + // Truncate to second and then encode into microseconds as JSON format only support second precision + TalerTimestamp::Timestamp(timestamp) => (timestamp.as_second() as u64) * 1000 * 1000, + } + .to_be_bytes() + } + + /// Returns an absolute duration representing the elapsed time from this timestamp until the given other timestamp. + pub fn duration_until(self, other: Timestamp) -> SignedDuration { + match self { + TalerTimestamp::Never => SignedDuration::MAX, + TalerTimestamp::Timestamp(tm) => tm.duration_until(other), + } + } +} + +impl FromStr for TalerTimestamp { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s == "never" { + return Ok(Self::Never); + } + let s: i64 = s.parse()?; + + Ok(Self::Timestamp(jiff::Timestamp::from_second(s)?)) + } +} + +#[derive(Serialize, Deserialize)] +struct TimestampImpl { + t_s: Value, +} + +impl<'de> Deserialize<'de> for TalerTimestamp { + 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() { + jiff::Timestamp::from_second(since_epoch_s as i64) + .map(Self::Timestamp) + .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 TalerTimestamp { + 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 { + TalerTimestamp::Never => se_struct.serialize_field("t_s", "never")?, + TalerTimestamp::Timestamp(timestamp) => { + se_struct.serialize_field("t_s", &timestamp.as_second())? + } + } + se_struct.end() + } +} + +impl Display for TalerTimestamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TalerTimestamp::Never => f.write_str("never"), + TalerTimestamp::Timestamp(timestamp) => timestamp.fmt(f), + } + } +} + +impl From<jiff::Timestamp> for TalerTimestamp { + fn from(time: jiff::Timestamp) -> Self { + Self::Timestamp(time) + } +} + +impl From<jiff::civil::Date> for TalerTimestamp { + fn from(date: jiff::civil::Date) -> Self { + date.to_datetime(Time::midnight()) + .to_zoned(TimeZone::UTC) + .unwrap() + .timestamp() + .into() + } +} + +impl Add<jiff::Span> for TalerTimestamp { + type Output = Self; + + fn add(self, rhs: jiff::Span) -> Self::Output { + match self { + TalerTimestamp::Never => TalerTimestamp::Never, + TalerTimestamp::Timestamp(timestamp) => TalerTimestamp::Timestamp(timestamp + rhs), + } + } +} + +impl sqlx::Type<sqlx::Postgres> for TalerTimestamp { + fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo { + Option::<i64>::type_info() + } +} + +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for TalerTimestamp { + fn encode_by_ref( + &self, + buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>, + ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { + match self { + TalerTimestamp::Never => None, + TalerTimestamp::Timestamp(timestamp) => Some(timestamp.as_microsecond()), + } + .encode_by_ref(buf) + } +} + +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for TalerTimestamp { + fn decode( + value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>, + ) -> Result<Self, sqlx::error::BoxDynError> { + let micros = Option::<i64>::decode(value)?; + Ok(match micros { + None | Some(i64::MAX) => Self::Never, + Some(micros) => Self::Timestamp(Timestamp::from_microsecond(micros)?), + }) + } +} + +/// <https://docs.taler.net/core/api-common.html#tsref-type-RelativeTime> +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum RelativeTime { + Forever, + Duration(Duration), +} + +impl FromStr for RelativeTime { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s == "forever" { + return Ok(Self::Forever); + } + let micros: u64 = s.parse()?; + + Ok(Self::Duration(Duration::from_micros(micros))) + } +} + +#[derive(Serialize, Deserialize)] +struct RelativeTimeImpl { + d_us: Value, +} + +impl<'de> Deserialize<'de> for RelativeTime { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let tmp = RelativeTimeImpl::deserialize(deserializer)?; + match tmp.d_us { + Value::Number(s) => { + if let Some(micros) = s.as_u64() { + Ok(Self::Duration(Duration::from_micros(micros))) + } else { + Err(Error::custom("Expected microseconds")) + } + } + Value::String(str) if str == "forever" => Ok(Self::Forever), + _ => Err(Error::custom("Expected time or 'forever'")), + } + } +} + +impl Serialize for RelativeTime { + fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut se_struct = se.serialize_struct("RelativeTime", 1)?; + match self { + RelativeTime::Forever => se_struct.serialize_field("d_us", "forever")?, + RelativeTime::Duration(duration) => { + se_struct.serialize_field("d_us", &duration.as_micros())? + } + } + se_struct.end() + } +} + +impl Display for RelativeTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RelativeTime::Forever => f.write_str("forever"), + RelativeTime::Duration(duration) => write!(f, "{duration:?}"), + } + } +} diff --git a/common/taler-common/src/types/timestamp.rs b/common/taler-common/src/types/timestamp.rs @@ -1,241 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2024, 2025, 2026 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 std::{fmt::Display, ops::Add, str::FromStr, time::Duration}; - -use jiff::{SignedDuration, Timestamp, civil::Time, tz::TimeZone}; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error, ser::SerializeStruct}; // codespell:ignore -use serde_json::Value; - -/// <https://docs.taler.net/core/api-common.html#tsref-type-Timestamp> -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum TalerTimestamp { - Never, - Timestamp(Timestamp), -} - -impl TalerTimestamp { - /** Encode timestamp for signature */ - pub fn signature_bytes(self) -> [u8; 8] { - match self { - TalerTimestamp::Never => u64::MAX, - // Truncate to second and then encode into microseconds as JSON format only support second precision - TalerTimestamp::Timestamp(timestamp) => (timestamp.as_second() as u64) * 1000 * 1000, - } - .to_be_bytes() - } - - /// Returns an absolute duration representing the elapsed time from this timestamp until the given other timestamp. - pub fn duration_until(self, other: Timestamp) -> SignedDuration { - match self { - TalerTimestamp::Never => SignedDuration::MAX, - TalerTimestamp::Timestamp(tm) => tm.duration_until(other), - } - } -} - -impl FromStr for TalerTimestamp { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - if s == "never" { - return Ok(Self::Never); - } - let s: i64 = s.parse()?; - - Ok(Self::Timestamp(jiff::Timestamp::from_second(s)?)) - } -} - -#[derive(Serialize, Deserialize)] -struct TimestampImpl { - t_s: Value, -} - -impl<'de> Deserialize<'de> for TalerTimestamp { - 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() { - jiff::Timestamp::from_second(since_epoch_s as i64) - .map(Self::Timestamp) - .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 TalerTimestamp { - 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 { - TalerTimestamp::Never => se_struct.serialize_field("t_s", "never")?, - TalerTimestamp::Timestamp(timestamp) => { - se_struct.serialize_field("t_s", &timestamp.as_second())? - } - } - se_struct.end() - } -} - -impl Display for TalerTimestamp { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TalerTimestamp::Never => f.write_str("never"), - TalerTimestamp::Timestamp(timestamp) => timestamp.fmt(f), - } - } -} - -impl From<jiff::Timestamp> for TalerTimestamp { - fn from(time: jiff::Timestamp) -> Self { - Self::Timestamp(time) - } -} - -impl From<jiff::civil::Date> for TalerTimestamp { - fn from(date: jiff::civil::Date) -> Self { - date.to_datetime(Time::midnight()) - .to_zoned(TimeZone::UTC) - .unwrap() - .timestamp() - .into() - } -} - -impl Add<jiff::Span> for TalerTimestamp { - type Output = Self; - - fn add(self, rhs: jiff::Span) -> Self::Output { - match self { - TalerTimestamp::Never => TalerTimestamp::Never, - TalerTimestamp::Timestamp(timestamp) => TalerTimestamp::Timestamp(timestamp + rhs), - } - } -} - -impl sqlx::Type<sqlx::Postgres> for TalerTimestamp { - fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo { - Option::<i64>::type_info() - } -} - -impl<'q> sqlx::Encode<'q, sqlx::Postgres> for TalerTimestamp { - fn encode_by_ref( - &self, - buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'q>, - ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { - match self { - TalerTimestamp::Never => None, - TalerTimestamp::Timestamp(timestamp) => Some(timestamp.as_microsecond()), - } - .encode_by_ref(buf) - } -} - -impl<'r> sqlx::Decode<'r, sqlx::Postgres> for TalerTimestamp { - fn decode( - value: <sqlx::Postgres as sqlx::Database>::ValueRef<'r>, - ) -> Result<Self, sqlx::error::BoxDynError> { - let micros = Option::<i64>::decode(value)?; - Ok(match micros { - Some(micros) => Self::Timestamp(Timestamp::from_microsecond(micros)?), - None => Self::Never, - }) - } -} - -/// <https://docs.taler.net/core/api-common.html#tsref-type-RelativeTime> -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum RelativeTime { - Forever, - Duration(Duration), -} - -impl FromStr for RelativeTime { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - if s == "forever" { - return Ok(Self::Forever); - } - let micros: u64 = s.parse()?; - - Ok(Self::Duration(Duration::from_micros(micros))) - } -} - -#[derive(Serialize, Deserialize)] -struct RelativeTimeImpl { - d_us: Value, -} - -impl<'de> Deserialize<'de> for RelativeTime { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - let tmp = RelativeTimeImpl::deserialize(deserializer)?; - match tmp.d_us { - Value::Number(s) => { - if let Some(micros) = s.as_u64() { - Ok(Self::Duration(Duration::from_micros(micros))) - } else { - Err(Error::custom("Expected microseconds")) - } - } - Value::String(str) if str == "forever" => Ok(Self::Forever), - _ => Err(Error::custom("Expected epoch time or 'forever'")), - } - } -} - -impl Serialize for RelativeTime { - fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - let mut se_struct = se.serialize_struct("RelativeTime", 1)?; - match self { - RelativeTime::Forever => se_struct.serialize_field("d_us", "forever")?, - RelativeTime::Duration(duration) => { - se_struct.serialize_field("d_us", &duration.as_micros())? - } - } - se_struct.end() - } -} - -impl Display for RelativeTime { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RelativeTime::Forever => f.write_str("forever"), - RelativeTime::Duration(duration) => write!(f, "{duration:?}"), - } - } -} diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -47,7 +47,7 @@ use taler_common::{ amount::{Amount, Currency, amount}, base32::Base32, payto::PaytoURI, - timestamp::TalerTimestamp, + time::TalerTimestamp, url, }, };