Skip to main content

borderless_runtime/http/
ledger.rs

1pub use super::*;
2use crate::db::controller::Controller;
3use crate::log_shim::*;
4use borderless::http::queries::Pagination;
5use borderless::{BorderlessId, ContractId};
6use borderless_kv_store::{backend::lmdb::Lmdb, Db};
7use http::method::Method;
8use serde::Deserialize;
9use std::convert::Infallible;
10use std::future::Future;
11use std::{
12    pin::Pin,
13    task::{Context, Poll},
14    time::Instant,
15};
16
17/// Simple service around the runtime
18#[derive(Clone)]
19pub struct LedgerService<S = Lmdb>
20where
21    S: Db + 'static,
22{
23    db: S,
24}
25
26impl<S> LedgerService<S>
27where
28    S: Db + 'static,
29{
30    pub fn new(db: S) -> Self {
31        Self { db }
32    }
33
34    async fn process_rq(&self, req: Request) -> crate::Result<Response> {
35        let start = Instant::now();
36        let path = req.uri().path().to_string();
37        let result = match *req.method() {
38            Method::GET => self.process_get_rq(req).await,
39            Method::POST => self.process_post_rq(req).await,
40            _ => Ok(method_not_allowed()),
41        };
42        let elapsed = start.elapsed();
43        // TODO: I don't know if this should be logged every time
44        match &result {
45            Ok(res) => info!(
46                "Request success. path={path}. Time elapsed: {elapsed:?}, status={}",
47                res.status()
48            ),
49            Err(e) => warn!("Request failed. path={path}. Time elapsed: {elapsed:?}, error={e}"),
50        }
51        result
52    }
53
54    async fn process_get_rq(&self, req: Request) -> crate::Result<Response> {
55        // strip leading “/” and split, collecting all non-empty segments
56        let segs: Vec<&str> = req
57            .uri()
58            .path()
59            .trim_start_matches('/')
60            .split('/')
61            .filter(|s| !s.is_empty())
62            .collect();
63
64        let query = req.uri().query();
65
66        let controller = Controller::new(&self.db);
67        let pagination = Pagination::from_query(query).unwrap_or_default();
68        match segs.as_slice() {
69            // GET /
70            [] => {
71                let res = controller.ledger().all_paginated(pagination)?;
72                Ok(json_response(&res))
73            }
74            // GET /ids
75            ["ids"] => {
76                let ids = controller.ledger().all_ids_paginated(pagination)?;
77                Ok(json_response(&ids))
78            }
79            [id_str] => {
80                let ledger_id = match id_str.parse::<u64>() {
81                    Ok(id) => id,
82                    Err(e) => return Ok(bad_request(e.to_string())),
83                };
84                let ledger = controller.ledger().select(ledger_id);
85                Ok(json_response(&ledger.meta()?.map(|m| m.into_dto())))
86            }
87            [id_str, "entries"] => {
88                let ledger_id = match id_str.parse::<u64>() {
89                    Ok(id) => id,
90                    Err(e) => return Ok(bad_request(e.to_string())),
91                };
92                let ledger = controller.ledger().select(ledger_id);
93                let entries = ledger.get_entries_paginated(pagination)?;
94                Ok(json_response(&entries))
95            }
96            _ => Ok(reject_404()),
97        }
98    }
99
100    async fn process_post_rq(&self, req: Request) -> crate::Result<Response> {
101        // Check request header
102        let (parts, payload) = req.into_parts();
103        if !check_json_content(&parts) {
104            return Ok(unsupported_media_type());
105        }
106
107        // NOTE: We don't have any nested routes here, so we can get away with matching the path
108        let path = parts.uri.path();
109
110        // Parse pagination
111        let query = parts.uri.query();
112        let pagination = Pagination::from_query(query).unwrap_or_default();
113
114        let payload = match serde_json::from_slice::<LedgerQuery>(&payload) {
115            Ok(p) => p,
116            Err(e) => return Ok(bad_request(e.to_string())),
117        };
118
119        // Select ledger
120        let controller = Controller::new(&self.db);
121        let ledger_id = match payload.to_ledger_id() {
122            Some(id) => id,
123            None => {
124                return Ok(bad_request(
125                    "must specify either ledger-id or creditor / debitor pair".to_string(),
126                ))
127            }
128        };
129        let ledger = controller.ledger().select(ledger_id);
130
131        // NOTE: We haven't split the path, so the trailing '/' might be important depending on the
132        // web-framework that embeds this service !
133        match path {
134            "/" | "" => match payload.contract_id {
135                Some(cid) => Ok(json_response(&ledger.meta_for_contract(cid)?)),
136                None => Ok(json_response(&ledger.meta()?.map(|m| m.into_dto()))),
137            },
138            "/entries" | "entries" => match payload.contract_id {
139                Some(cid) => Ok(json_response(
140                    &ledger.get_contract_paginated(cid, pagination)?,
141                )),
142                None => Ok(json_response(&ledger.get_entries_paginated(pagination)?)),
143            },
144            _ => Ok(reject_404()),
145        }
146    }
147}
148
149/// Selects a ledger either by its id or creditor / debitor tuple
150#[derive(Deserialize)]
151pub struct LedgerQuery {
152    creditor: Option<BorderlessId>,
153    debitor: Option<BorderlessId>,
154    ledger_id: Option<u64>,
155    contract_id: Option<ContractId>,
156}
157
158impl LedgerQuery {
159    fn to_ledger_id(&self) -> Option<u64> {
160        if self.ledger_id.is_some() {
161            return self.ledger_id;
162        }
163        match (self.creditor, self.debitor) {
164            (Some(c), Some(d)) => Some(c.merge_compact(&d)),
165            _ => None,
166        }
167    }
168}
169
170impl<S> Service<Request> for LedgerService<S>
171where
172    S: Db + 'static,
173{
174    type Response = Response;
175    type Error = Infallible;
176    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
177
178    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
179        Poll::Ready(Ok(()))
180    }
181
182    fn call(&mut self, req: Request) -> Self::Future {
183        let this = self.clone();
184        let fut = async move {
185            let result: Response = match this.process_rq(req).await {
186                Ok(r) => r,
187                Err(e) => into_server_error(e),
188            };
189            Ok(result)
190        };
191        Box::pin(fut)
192    }
193}