Skip to main content

routex_client_common/
lib.rs

1#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
2
3use std::collections::HashMap;
4use std::future::Future;
5use std::sync::{Arc, RwLock};
6
7use anyhow::anyhow;
8use base64::prelude::*;
9use bytes::Bytes;
10use futures::lock::Mutex;
11use http::header::{
12    ACCEPT, CONTENT_LENGTH, CONTENT_TYPE, HeaderMap, HeaderValue, InvalidHeaderValue, USER_AGENT,
13};
14use http::{Method, Request, StatusCode};
15use routex_api::info::ConnectionInfo;
16#[cfg(feature = "uniffi")]
17use routex_api::info::CountryCode;
18use routex_api::{Authenticated, ConnectionId, Error as ServiceError, Service, ServiceId};
19#[cfg(feature = "error")]
20use routex_api::{
21    PaymentErrorCode, ProviderErrorCode, ServiceBlockedCode, TicketErrorCode,
22    UnsupportedProductReason,
23};
24use routex_settlement::KeySettlement;
25use serde::{Deserialize, Serialize};
26use url::Url;
27use uuid::Uuid;
28
29#[cfg(feature = "reqwest")]
30pub use reqwest;
31
32#[derive(thiserror::Error, Debug)]
33pub enum Error {
34    #[error(transparent)]
35    RequestError(anyhow::Error),
36    #[error("Service error")]
37    ServiceError(ServiceError),
38    #[error("Error response")]
39    ResponseError(Box<Response>),
40    #[error("Resource not found")]
41    NotFound,
42}
43
44#[derive(Clone, Debug)]
45pub struct Response {
46    pub url: Url,
47    pub status: StatusCode,
48    pub headers: HeaderMap,
49    pub body: Bytes,
50}
51
52pub fn json_decode<T: for<'de> Deserialize<'de>>(body: &[u8]) -> Result<T> {
53    serde_json::from_slice(body).map_err(|err| Error::RequestError(err.into()))
54}
55
56impl From<Response> for Error {
57    fn from(response: Response) -> Error {
58        match serde_json::from_slice::<ServiceError>(&response.body) {
59            Ok(payload) => Error::ServiceError(payload),
60            Err(_) => Error::ResponseError(Box::new(response)),
61        }
62    }
63}
64
65pub trait HttpClient {
66    fn execute(&self, req: Request<Vec<u8>>) -> impl Future<Output = Result<Response>>;
67}
68
69#[cfg(feature = "reqwest")]
70impl From<reqwest::Error> for Error {
71    fn from(err: reqwest::Error) -> Self {
72        Self::RequestError(anyhow!(err))
73    }
74}
75
76#[cfg(feature = "reqwest")]
77impl HttpClient for reqwest::Client {
78    async fn execute(&self, req: Request<Vec<u8>>) -> Result<Response> {
79        match self.execute(req.try_into()?).await {
80            Ok(r) => Ok(Response {
81                url: r.url().clone(),
82                status: r.status(),
83                headers: r.headers().clone(),
84                body: r.bytes().await?,
85            }),
86            Err(err) => Err(err.into()),
87        }
88    }
89}
90
91pub type Result<T> = std::result::Result<T, Error>;
92
93#[must_use]
94#[derive(Clone, Debug)]
95pub struct RoutexClientCore<C> {
96    url: Url,
97    user_agent: HeaderValue,
98    http_client: C,
99    keys: Arc<Mutex<HashMap<String, KeySettlement<sealed::RoutexKeySettlementEndpoint<C>>>>>,
100    redirect_uri: Option<HeaderValue>,
101    trace_id: Arc<RwLock<Option<Vec<u8>>>>,
102}
103
104mod sealed {
105    use http::HeaderValue;
106    use routex_settlement::KeySettlementCore;
107    use url::Url;
108    use uuid::Uuid;
109
110    use super::{Error, HttpClient, RequestBuilder};
111
112    #[derive(Debug)]
113    pub struct RoutexKeySettlementEndpoint<C> {
114        pub(super) url: Url,
115        pub(super) user_agent: HeaderValue,
116        pub(super) http_client: C,
117    }
118
119    impl<C> KeySettlementCore for RoutexKeySettlementEndpoint<C>
120    where
121        C: HttpClient,
122    {
123        type Data = Uuid;
124
125        async fn request(
126            &self,
127            public_key: [u8; 32],
128            ticket_id: &Self::Data,
129        ) -> anyhow::Result<routex_api::keys::Response> {
130            let request =
131                RequestBuilder::post(&self.url, &routex_api::keys::Request { public_key })
132                    .build(ticket_id, self.user_agent.clone());
133
134            let response = self.http_client.execute(request).await?;
135
136            if response.status.is_client_error() || response.status.is_server_error() {
137                Err(Error::from(response).into())
138            } else {
139                Ok(serde_json::from_slice(&response.body)?)
140            }
141        }
142    }
143}
144
145#[derive(Debug)]
146pub struct RequestBuilder {
147    request: Request<Vec<u8>>,
148}
149
150impl RequestBuilder {
151    fn get(url: &Url) -> Self {
152        let mut request = Request::new(Vec::new());
153
154        *request.method_mut() = Method::GET;
155        *request.uri_mut() = url.as_str().try_into().expect("URL to URI should work");
156
157        Self { request }
158    }
159
160    fn post(url: &Url, json: &impl Serialize) -> Self {
161        let mut request =
162            Request::new(serde_json::to_vec(&json).expect("Serialization should work"));
163
164        *request.method_mut() = Method::POST;
165        *request.uri_mut() = url.as_str().try_into().expect("URL to URI should work");
166
167        request
168            .headers_mut()
169            .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
170
171        Self { request }
172    }
173
174    fn build(mut self, ticket_id: &Uuid, user_agent: HeaderValue) -> Request<Vec<u8>> {
175        self.request.headers_mut().extend([
176            (
177                routex_api::headers::TICKET_ID.clone(),
178                HeaderValue::from_str(&ticket_id.to_string()).expect("ASCII"),
179            ),
180            (
181                ACCEPT,
182                HeaderValue::from_static(routex_api::CURRENT_MEDIA_TYPE),
183            ),
184            (USER_AGENT, user_agent),
185        ]);
186
187        self.request
188    }
189}
190
191#[must_use]
192pub struct SearchRequest<S, C>
193where
194    S: Service,
195{
196    inner: routex_api::info::Request,
197    ticket: Authenticated<routex_api::Ticket<S>>,
198    client: RoutexClientCore<C>,
199}
200
201impl<S, C> SearchRequest<S, C>
202where
203    S: Service,
204    C: HttpClient + Clone,
205{
206    /// If IBAN detection is enabled and the first value of a [`SearchFilter::Term`] is detected
207    /// to be a possible prefix of an IBAN that contains a national bank code,
208    /// the result might contain additional connections that match that bank code.
209    pub fn iban_detection(mut self, enable: bool) -> Self {
210        self.inner.iban_detection = enable;
211        self
212    }
213
214    /// Limit the number of results.
215    pub fn limit(mut self, limit: impl Into<Option<usize>>) -> Self {
216        self.inner.limit = limit.into();
217        self
218    }
219
220    /// Details to contain in search results.
221    pub fn details(mut self, details: impl IntoIterator<Item = routex_api::info::Details>) -> Self {
222        self.inner.details = details.into_iter().collect();
223        self
224    }
225
226    /// Send the request.
227    ///
228    /// # Errors
229    ///
230    /// Fails if the request fails.
231    pub async fn send(self) -> Result<Vec<ConnectionInfo>> {
232        json_decode(
233            &self
234                .client
235                .execute(
236                    &self.ticket,
237                    self.client.post(
238                        &routex_api::info::search_path().to_url(self.client.url()),
239                        &self.inner,
240                    ),
241                )
242                .await?,
243        )
244    }
245}
246
247pub const DEFAULT_URL: &str = "https://api.yaxi.tech/";
248
249impl<C> RoutexClientCore<C>
250where
251    C: HttpClient + Clone,
252{
253    /// Create a new client for a versioned distribution and with a custom URL
254    ///
255    /// # Panics
256    ///
257    /// Panics if it fails to build the [`reqwest::Client`].
258    pub fn for_distribution(distribution: &str, version: &str, url: Url, http_client: C) -> Self {
259        Self {
260            url,
261            user_agent: format!(
262                "RoutexClient/{} ({})",
263                version,
264                [distribution, std::env::consts::OS, std::env::consts::ARCH]
265                    .into_iter()
266                    .filter(|s| !s.is_empty())
267                    .collect::<Vec<_>>()
268                    .join("; "),
269            )
270            .try_into()
271            .expect("Invalid header value"),
272            http_client,
273            keys: Arc::default(),
274            redirect_uri: None,
275            trace_id: Arc::default(),
276        }
277    }
278
279    /// Run a key settlement with the routex service, verifying the attestation report.
280    ///
281    /// Stores a new random secret key and announces its public key to routex.
282    /// In return, routex responds with its own public key and an attestation report.
283    /// The attestation report allows verifying that routex created its corresponding secret key within the TEE
284    /// and that its creation happened in response to our client public key (freshness).
285    ///
286    /// # Errors
287    ///
288    /// Returns an error if the key settlement request fails or the attestation report fails verification.
289    pub async fn settle_key<S: routex_api::Service>(
290        &self,
291        ticket: &Authenticated<routex_api::Ticket<S>>,
292    ) -> Result<()> {
293        let ticket_id = ticket.to_data().id;
294
295        self.with_key_settlement(ticket_id, async |keys| {
296            keys.settle(&ticket_id).await.map_err(Error::RequestError)
297        })
298        .await
299    }
300
301    pub async fn with_key_settlement<U>(
302        &self,
303        ticket_id: Uuid,
304        f: impl AsyncFnOnce(&mut KeySettlement<sealed::RoutexKeySettlementEndpoint<C>>) -> U,
305    ) -> U {
306        f(self
307            .keys
308            .lock()
309            .await
310            .entry(ticket_id.into())
311            .or_insert_with(|| {
312                KeySettlement::new(sealed::RoutexKeySettlementEndpoint {
313                    url: routex_api::keys::settlement_path().to_url(&self.url),
314                    user_agent: self.user_agent.clone(),
315                    http_client: self.http_client.clone(),
316                })
317            }))
318        .await
319    }
320
321    pub async fn system_version(&self, ticket_id: Uuid) -> Option<String> {
322        self.with_key_settlement(ticket_id, async |keys| keys.system_version().cloned())
323            .await
324            .map(|v| serde_json::to_string(&v).expect("serialization should work"))
325    }
326
327    pub fn search<S: routex_api::Service>(
328        &self,
329        ticket: Authenticated<routex_api::Ticket<S>>,
330        filters: impl IntoIterator<Item = routex_api::info::SearchFilter>,
331    ) -> SearchRequest<S, C> {
332        SearchRequest {
333            inner: routex_api::info::Request::new(filters),
334            ticket,
335            client: self.clone(),
336        }
337    }
338
339    pub async fn info<S: routex_api::Service>(
340        &self,
341        ticket: &Authenticated<routex_api::Ticket<S>>,
342        connection_id: ConnectionId,
343    ) -> Result<ConnectionInfo> {
344        let response = self
345            .execute(
346                ticket,
347                self.get(
348                    &routex_api::info::fetch_path(&connection_id.to_string()).to_url(self.url()),
349                ),
350            )
351            .await
352            .map_err(|err| {
353                if let Error::ResponseError(response) = &err
354                    && response.status == StatusCode::NOT_FOUND
355                {
356                    Error::NotFound
357                } else {
358                    err
359                }
360            })?;
361
362        json_decode(&response)
363    }
364
365    pub async fn execute<S: routex_api::Service>(
366        &self,
367        ticket: &Authenticated<routex_api::Ticket<S>>,
368        request: RequestBuilder,
369    ) -> Result<Bytes> {
370        let ticket_id = ticket.to_data().id;
371
372        self.with_key_settlement(ticket_id, async |keys| {
373            let mut request = request.build(&ticket_id, self.user_agent.clone());
374
375            request.headers_mut().insert(
376                routex_api::headers::TICKET.clone(),
377                HeaderValue::from_str(
378                    &BASE64_STANDARD.encode(
379                        keys.seal(ticket.as_str().as_bytes(), &ticket_id)
380                            .await
381                            .map_err(Error::RequestError)?,
382                    ),
383                )
384                .expect("ASCII"),
385            );
386
387            if let Some(value) = self.redirect_uri.clone() {
388                request
389                    .headers_mut()
390                    .insert(&routex_api::headers::REDIRECT_URI, value);
391            }
392
393            if !request.body().is_empty() {
394                *request.body_mut() = keys
395                    .seal(request.body(), &ticket_id)
396                    .await
397                    .map_err(Error::RequestError)?;
398            }
399
400            request.headers_mut().insert(
401                &routex_api::headers::SESSION_ID,
402                keys.session_id(&ticket_id)
403                    .await
404                    .map_err(Error::RequestError)?
405                    .clone(),
406            );
407
408            request.headers_mut().remove(&CONTENT_LENGTH);
409
410            let response = self.http_client.execute(request).await?;
411
412            *self.trace_id.write().expect("poisoned") = response
413                .headers
414                .get(&routex_api::headers::TRACE_ID)
415                .and_then(|v| v.to_str().ok())
416                .and_then(|v| BASE64_STANDARD.decode(v).ok())
417                .and_then(|v| keys.unseal(&v).ok());
418
419            if response.status.is_client_error() || response.status.is_server_error() {
420                Err(Response {
421                    body: keys
422                        .unseal(&response.body)
423                        .map_or(response.body, Into::into),
424                    ..response
425                }
426                .into())
427            } else {
428                Ok(if response.body.is_empty() {
429                    response.body
430                } else {
431                    keys.unseal(&response.body)
432                        .map_err(|err| Error::RequestError(anyhow!(err)))?
433                        .into()
434                })
435            }
436        })
437        .await
438    }
439
440    pub fn get(&self, url: &Url) -> RequestBuilder {
441        RequestBuilder::get(url)
442    }
443
444    pub fn post(&self, url: &Url, json: &impl Serialize) -> RequestBuilder {
445        RequestBuilder::post(url, json)
446    }
447
448    pub fn url(&self) -> &Url {
449        &self.url
450    }
451
452    /// Trace identifier returned with the last request
453    ///
454    /// # Panics
455    ///
456    /// Panics of the internal lock is poisoned.
457    pub fn trace_id(&self) -> Option<Vec<u8>> {
458        self.trace_id.read().expect("poisoned").clone()
459    }
460
461    pub fn set_redirect_uri(
462        &mut self,
463        redirect_uri: &str,
464    ) -> std::result::Result<(), InvalidHeaderValue> {
465        self.redirect_uri = Some(redirect_uri.try_into()?);
466        Ok(())
467    }
468}
469
470#[must_use]
471pub fn handle_not_found(err: Error) -> Error {
472    if let Error::ResponseError(response) = &err
473        && response.status == StatusCode::NOT_FOUND
474    {
475        Error::NotFound
476    } else {
477        err
478    }
479}
480
481/// A ticket that contains only the service, but no other data.
482#[derive(serde::Deserialize, Clone)]
483pub struct ServiceOnlyTicket {
484    pub service: ServiceId,
485}
486
487#[macro_export]
488macro_rules! with_any_service {
489    ($ticket:expr, $authenticated:ident, $block:block) => {
490        match $ticket.parse::<routex_api::Authenticated<routex_client_common::ServiceOnlyTicket>>()
491        {
492            Ok($authenticated) => match $authenticated.to_data().service {
493                routex_api::ServiceId::Accounts { .. } => {
494                    with_any_service!(
495                        $ticket,
496                        $authenticated,
497                        $block,
498                        routex_api::accounts::Service
499                    )
500                }
501                routex_api::ServiceId::CollectPayment { .. } => {
502                    with_any_service!(
503                        $ticket,
504                        $authenticated,
505                        $block,
506                        routex_api::collect_payment::Service
507                    )
508                }
509                routex_api::ServiceId::Balances { .. } => {
510                    with_any_service!(
511                        $ticket,
512                        $authenticated,
513                        $block,
514                        routex_api::balances::Service
515                    )
516                }
517                routex_api::ServiceId::Transactions { .. } => {
518                    with_any_service!(
519                        $ticket,
520                        $authenticated,
521                        $block,
522                        routex_api::transactions::Service
523                    )
524                }
525                routex_api::ServiceId::Transfer { .. } => {
526                    with_any_service!(
527                        $ticket,
528                        $authenticated,
529                        $block,
530                        routex_api::transfer::Service
531                    )
532                }
533            },
534            Err(err) => Err(err.into()),
535        }
536    };
537    ($ticket:expr, $authenticated:ident, $block:block, $service:ty) => {
538        match $ticket.parse::<routex_api::Authenticated<routex_api::Ticket<$service>>>() {
539            Ok($authenticated) => $block.map_err(Into::into),
540            Err(err) => Err(err.into()),
541        }
542    };
543}
544
545#[cfg(feature = "uniffi")]
546::uniffi::setup_scaffolding!();
547
548#[cfg(feature = "error")]
549#[allow(clippy::enum_variant_names)]
550#[derive(thiserror::Error, Debug)]
551#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
552pub enum RoutexClientError {
553    #[error("Invalid redirect URI")]
554    InvalidRedirectUri,
555
556    #[error("Request error")]
557    RequestError { error: String },
558
559    #[error("Unexpected service error")]
560    UnexpectedError { user_message: Option<String> },
561
562    #[error("Canceled")]
563    Canceled,
564
565    #[error("Invalid credentials")]
566    InvalidCredentials { user_message: Option<String> },
567
568    #[error("Service blocked")]
569    ServiceBlocked {
570        user_message: Option<String>,
571        code: Option<ServiceBlockedCode>,
572    },
573
574    #[error("Unauthorized")]
575    Unauthorized { user_message: Option<String> },
576
577    #[error("Consent expired")]
578    ConsentExpired { user_message: Option<String> },
579
580    #[error("Access exceeded")]
581    AccessExceeded { user_message: Option<String> },
582
583    #[error("Period out of bounds")]
584    PeriodOutOfBounds { user_message: Option<String> },
585
586    #[error("Unsupported product")]
587    UnsupportedProduct {
588        reason: Option<UnsupportedProductReason>,
589        user_message: Option<String>,
590    },
591
592    #[error("Payment canceled or rejected")]
593    PaymentFailed {
594        code: Option<PaymentErrorCode>,
595        user_message: Option<String>,
596    },
597
598    #[error("Unexpected value")]
599    UnexpectedValue { error: String },
600
601    #[error("{error}")]
602    TicketError {
603        error: String,
604        code: TicketErrorCode,
605    },
606
607    #[error("The account-servicing provider indicated a technical error")]
608    ProviderError {
609        code: Option<ProviderErrorCode>,
610        user_message: Option<String>,
611    },
612
613    #[error("Error response")]
614    ResponseError { response: String },
615
616    #[error("Resource not found")]
617    NotFound,
618
619    #[error("The transaction is not possible without user interaction")]
620    InterruptError,
621}
622
623#[cfg(feature = "error")]
624impl From<Error> for RoutexClientError {
625    fn from(error: Error) -> Self {
626        match error {
627            Error::RequestError(error) => Self::RequestError {
628                error: error.to_string(),
629            },
630            Error::ServiceError(ServiceError::UnexpectedError { user_message, .. }) => {
631                Self::UnexpectedError { user_message }
632            }
633            Error::ServiceError(ServiceError::Canceled { .. }) => Self::Canceled,
634            Error::ServiceError(ServiceError::InvalidCredentials { user_message, .. }) => {
635                Self::InvalidCredentials { user_message }
636            }
637            Error::ServiceError(ServiceError::ServiceBlocked {
638                user_message, code, ..
639            }) => Self::ServiceBlocked { user_message, code },
640            Error::ServiceError(ServiceError::Unauthorized { user_message, .. }) => {
641                Self::Unauthorized { user_message }
642            }
643            Error::ServiceError(ServiceError::ConsentExpired { user_message, .. }) => {
644                Self::ConsentExpired { user_message }
645            }
646            Error::ServiceError(ServiceError::AccessExceeded { user_message, .. }) => {
647                Self::AccessExceeded { user_message }
648            }
649            Error::ServiceError(ServiceError::PeriodOutOfBounds { user_message, .. }) => {
650                Self::PeriodOutOfBounds { user_message }
651            }
652            Error::ServiceError(ServiceError::UnsupportedProduct {
653                reason,
654                user_message,
655                ..
656            }) => Self::UnsupportedProduct {
657                reason,
658                user_message,
659            },
660            Error::ServiceError(ServiceError::PaymentFailed {
661                code, user_message, ..
662            }) => Self::PaymentFailed { code, user_message },
663            Error::ServiceError(ServiceError::UnexpectedValue { error, .. }) => {
664                Self::UnexpectedValue { error }
665            }
666            Error::ServiceError(ServiceError::TicketError { error, code, .. }) => {
667                Self::TicketError { error, code }
668            }
669            Error::ServiceError(ServiceError::ProviderError {
670                code, user_message, ..
671            }) => Self::ProviderError { code, user_message },
672            Error::ServiceError(ServiceError::InterruptError { .. }) => Self::InterruptError,
673            Error::ResponseError(response) => Self::ResponseError {
674                response: format!("{response:?}"),
675            },
676            Error::NotFound => Self::NotFound,
677        }
678    }
679}
680
681#[cfg(feature = "error")]
682impl From<jsonwebtoken::errors::Error> for RoutexClientError {
683    fn from(err: jsonwebtoken::errors::Error) -> Self {
684        RoutexClientError::TicketError {
685            error: err.to_string(),
686            code: routex_api::TicketErrorCode::Invalid,
687        }
688    }
689}
690
691#[cfg(feature = "uniffi")]
692pub struct Ticket(String);
693
694#[cfg(feature = "uniffi")]
695impl Ticket {
696    #[must_use]
697    pub fn as_str(&self) -> &str {
698        &self.0
699    }
700}
701
702#[cfg(feature = "uniffi")]
703uniffi::custom_newtype!(Ticket, String);
704
705#[cfg(feature = "uniffi")]
706uniffi::custom_type!(Url, String, {
707    remote,
708    try_lift: |val| Ok(val.parse()?),
709    lower: |obj| obj.into(),
710});
711
712#[cfg(feature = "uniffi")]
713uniffi::use_remote_type!(routex_api::CountryCode);
714
715#[cfg(feature = "uniffi")]
716/// Filters for the connection lookup
717///
718/// String filters look for the given value anywhere in the related field, case-insensitive.
719#[derive(uniffi::Enum)]
720pub enum SearchFilter {
721    /// List of `ConnectionType`s to consider.
722    Types {
723        types: Vec<routex_api::info::ConnectionType>,
724    },
725    /// List of `CountryCode`s to consider.
726    Countries { countries: Vec<CountryCode> },
727    /// String filter for the provider / product name or any alias.
728    Name { name: String },
729    /// String filter for the BIC.
730    Bic { bic: String },
731    /// String filter for the (national) bank code.
732    BankCode { bank_code: String },
733    /// String filter for any of those fields.
734    Term { term: String },
735}
736
737#[cfg(feature = "uniffi")]
738impl From<SearchFilter> for routex_api::info::SearchFilter {
739    fn from(filter: SearchFilter) -> Self {
740        match filter {
741            SearchFilter::Types { types } => routex_api::info::SearchFilter::Types(types),
742            SearchFilter::Countries { countries } => {
743                routex_api::info::SearchFilter::Countries(countries)
744            }
745            SearchFilter::Name { name } => routex_api::info::SearchFilter::Name(name),
746            SearchFilter::Bic { bic } => routex_api::info::SearchFilter::Bic(bic),
747            SearchFilter::BankCode { bank_code } => {
748                routex_api::info::SearchFilter::BankCode(bank_code)
749            }
750            SearchFilter::Term { term } => routex_api::info::SearchFilter::Term(term),
751        }
752    }
753}
754
755#[cfg(feature = "uniffi")]
756macro_rules! account_filter {
757    {
758        $($field:ident $type:ty)+
759    } => {
760        paste::paste! {
761            #[derive(uniffi::Enum)]
762            pub enum AccountFilter {
763                $(
764                    [<$field Eq>] { value: $type },
765                    [<$field NotEq>] { value: $type },
766                )+
767                All {
768                    filters: Vec<AccountFilter>,
769                },
770                Any {
771                    filters: Vec<AccountFilter>,
772                },
773                Supports { service: routex_api::SupportedService },
774            }
775
776            impl From<AccountFilter> for Option<routex_api::Filter<routex_api::accounts::AccountField>> {
777                fn from(filter: AccountFilter) -> Self {
778                    match filter {
779                        $(
780                            AccountFilter::[<$field Eq>] { value } => Some(routex_api::accounts::AccountField::[<$field:snake:upper>].eq(value)),
781                            AccountFilter::[<$field NotEq>] { value } => Some(routex_api::accounts::AccountField::[<$field:snake:upper>].not_eq(value)),
782                        )+
783                        AccountFilter::All { filters } => filters
784                            .into_iter()
785                            .map(Option::<routex_api::Filter<_>>::from)
786                            .flatten()
787                            .reduce(routex_api::Filter::and),
788                        AccountFilter::Any { filters } => filters
789                            .into_iter()
790                            .map(Option::<routex_api::Filter<_>>::from)
791                            .flatten()
792                            .reduce(routex_api::Filter::or),
793                        AccountFilter::Supports { service } => {
794                            Some(routex_api::Account::supports(service))
795                        },
796                    }
797                }
798            }
799        }
800    }
801}
802
803#[cfg(feature = "uniffi")]
804account_filter! {
805    Iban Option<String>
806    Number Option<String>
807    Bic Option<String>
808    BankCode Option<String>
809    Currency String
810    Name Option<String>
811    DisplayName Option<String>
812    OwnerName Option<String>
813    ProductName Option<String>
814    Status Option<routex_api::AccountStatus>
815    Type Option<routex_api::AccountType>
816}