Skip to content

Commit 8ae554e

Browse files
committed
Add API modules with the code to implement currency endpoints
1 parent 01d34d7 commit 8ae554e

8 files changed

Lines changed: 188 additions & 5 deletions

File tree

src/api.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
mod endpoints;
2+
mod payload;
3+
4+
use poem::{Endpoint, EndpointExt, Route};
5+
use poem_openapi::OpenApiService;
6+
7+
use crate::api::endpoints::CurrencyApi;
8+
use crate::repository::SharedRepository;
9+
10+
pub fn build_app(repository: SharedRepository) -> anyhow::Result<impl Endpoint> {
11+
let api_service =
12+
OpenApiService::new(CurrencyApi, "Currencies", "1.0").server("http://localhost:3000/api");
13+
let ui = api_service.swagger_ui();
14+
let app = Route::new()
15+
.nest("/api", api_service)
16+
.nest("/", ui)
17+
.data(repository);
18+
19+
Ok(app)
20+
}
21+
22+
#[cfg(test)]
23+
mod tests {
24+
use poem::http::StatusCode;
25+
use poem::test::TestClient;
26+
use poem::Endpoint;
27+
use std::sync::Arc;
28+
29+
use crate::build_app;
30+
use crate::repository::{Currency, InMemoryRepository};
31+
32+
fn setup_client() -> TestClient<impl Endpoint> {
33+
let repository = Arc::new(InMemoryRepository::default());
34+
let app = build_app(repository).expect("app to be created successfully");
35+
TestClient::new(app)
36+
}
37+
38+
#[tokio::test]
39+
async fn test_get_currency() {
40+
let client = setup_client();
41+
let ccy = Currency {
42+
code: "GBP".to_string(),
43+
name: "British Pound".to_string(),
44+
symbol: "£".to_string(),
45+
};
46+
let response = client.post("/api/currencies").body_json(&ccy).send().await;
47+
response.assert_status_is_ok();
48+
49+
let response = client.get("/api/currencies/gbp").send().await;
50+
response.assert_status_is_ok();
51+
response.assert_json(ccy).await;
52+
}
53+
54+
#[tokio::test]
55+
async fn test_get_non_existent() {
56+
let client = setup_client();
57+
let response = client.get("/api/currencies/eur").send().await;
58+
response.assert_status(StatusCode::NOT_FOUND);
59+
}
60+
61+
#[tokio::test]
62+
async fn test_create_then_delete() {
63+
let client = setup_client();
64+
let ccy = Currency {
65+
code: "GBP".to_string(),
66+
name: "British Pound".to_string(),
67+
symbol: "£".to_string(),
68+
};
69+
let response = client.post("/api/currencies").body_json(&ccy).send().await;
70+
response.assert_status_is_ok();
71+
72+
let response = client.delete("/api/currencies/gbp").send().await;
73+
response.assert_status_is_ok();
74+
}
75+
76+
#[tokio::test]
77+
async fn test_delete_non_existent() {
78+
let client = setup_client();
79+
let response = client.delete("/api/currencies/eur").send().await;
80+
response.assert_status(StatusCode::NOT_FOUND);
81+
}
82+
}

src/api/endpoints.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use poem::web::Data;
2+
use poem_openapi::param::Path;
3+
use poem_openapi::payload::Json;
4+
use poem_openapi::OpenApi;
5+
use tracing::info;
6+
7+
use crate::api::payload::{Currency, Response};
8+
use crate::repository;
9+
10+
pub struct CurrencyApi;
11+
12+
#[OpenApi]
13+
impl CurrencyApi {
14+
#[oai(path = "/currencies", method = "post")]
15+
async fn create(
16+
&self,
17+
Data(repository): Data<&repository::SharedRepository>,
18+
Json(payload): Json<Currency>,
19+
) -> Response {
20+
info!(code = payload.code, "creating currency");
21+
22+
let currency = repository::Currency {
23+
code: payload.code,
24+
name: payload.name,
25+
symbol: payload.symbol,
26+
};
27+
repository.add_currency(currency).await.into()
28+
}
29+
30+
#[oai(path = "/currencies/:code", method = "get")]
31+
async fn get(
32+
&self,
33+
Data(repository): Data<&repository::SharedRepository>,
34+
Path(code): Path<String>,
35+
) -> Response {
36+
info!(code, "getting currency");
37+
repository.get_currency(&code).await.into()
38+
}
39+
40+
#[oai(path = "/currencies/:code", method = "delete")]
41+
async fn delete(
42+
&self,
43+
Data(repository): Data<&repository::SharedRepository>,
44+
Path(code): Path<String>,
45+
) -> Response {
46+
info!(code, "deleting currency");
47+
repository.delete_currency(&code).await.into()
48+
}
49+
}

src/api/payload.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use poem_openapi::payload::{Json, PlainText};
2+
use poem_openapi::{ApiResponse, Object};
3+
4+
use crate::error::{Error, Result};
5+
6+
#[derive(Object)]
7+
pub struct Currency {
8+
pub code: String,
9+
pub name: String,
10+
pub symbol: String,
11+
}
12+
13+
impl From<crate::repository::Currency> for Currency {
14+
fn from(currency: crate::repository::Currency) -> Self {
15+
Self {
16+
code: currency.code,
17+
name: currency.name,
18+
symbol: currency.symbol,
19+
}
20+
}
21+
}
22+
23+
#[derive(ApiResponse)]
24+
pub(crate) enum Response {
25+
#[oai(status = 200)]
26+
Currency(Json<Currency>),
27+
28+
#[oai(status = 404)]
29+
NotFound(PlainText<String>),
30+
31+
#[oai(status = 500)]
32+
InternalServerError(PlainText<String>),
33+
}
34+
35+
impl From<Result<crate::repository::Currency>> for Response {
36+
fn from(result: Result<crate::repository::Currency>) -> Self {
37+
match result {
38+
Ok(currency) => Response::Currency(Json(currency.into())),
39+
Err(err) => match err {
40+
Error::NotFound(code) => {
41+
let msg = format!("{code} is not found");
42+
Response::NotFound(PlainText(msg))
43+
}
44+
Error::Other => {
45+
let msg = "internal server error".to_string();
46+
Response::InternalServerError(PlainText(msg))
47+
}
48+
},
49+
}
50+
}
51+
}

src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ pub enum Error {
88
Other,
99
}
1010

11-
pub type Result<T> = std::result::Result<T, Error>;
11+
pub type Result<T> = std::result::Result<T, Error>;

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod api;
12
pub mod repository;
23

3-
mod error;
4+
mod error;

src/repository.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ mod base;
22
mod memory;
33

44
pub use base::{Currency, Repository, SharedRepository};
5-
pub use memory::InMemoryRepository;
5+
pub use memory::InMemoryRepository;

src/repository/base.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ pub trait Repository: Sync + Send + 'static {
1515
async fn add_currency(&self, currency: Currency) -> crate::error::Result<Currency>;
1616
async fn get_currency(&self, code: &str) -> crate::error::Result<Currency>;
1717
async fn delete_currency(&self, code: &str) -> crate::error::Result<Currency>;
18-
}
18+
}

src/repository/memory.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ impl Repository for InMemoryRepository {
3939
.remove(&code.to_lowercase())
4040
.ok_or(Error::NotFound(code.to_string()))
4141
}
42-
}
42+
}

0 commit comments

Comments
 (0)