Compare commits
2 commits
3e1c744db1
...
24afa3407e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24afa3407e | ||
|
|
c016fc915b |
7 changed files with 99 additions and 55 deletions
61
Cargo.lock
generated
61
Cargo.lock
generated
|
|
@ -44,6 +44,8 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"pretty_env_logger",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -562,6 +564,19 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||||
|
dependencies = [
|
||||||
|
"humantime",
|
||||||
|
"is-terminal",
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
"termcolor",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -857,6 +872,12 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
@ -936,6 +957,12 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
@ -1196,6 +1223,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
|
|
@ -1639,6 +1677,16 @@ dependencies = [
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_env_logger"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
|
||||||
|
dependencies = [
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.3.0"
|
version = "3.3.0"
|
||||||
|
|
@ -2288,9 +2336,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.141"
|
version = "1.0.142"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
|
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -2759,6 +2807,15 @@ dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.12"
|
version = "2.0.12"
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,6 @@ utoipa-axum = "0.2.0"
|
||||||
utoipa-swagger-ui = { version = "9", features = ["axum", "reqwest"] }
|
utoipa-swagger-ui = { version = "9", features = ["axum", "reqwest"] }
|
||||||
utoipa-redoc = { version = "6", features = ["axum"] }
|
utoipa-redoc = { version = "6", features = ["axum"] }
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
|
log = "0.4.27"
|
||||||
|
pretty_env_logger = "0.5.0"
|
||||||
|
|
||||||
|
|
|
||||||
33
src/main.rs
33
src/main.rs
|
|
@ -1,16 +1,16 @@
|
||||||
use std::{net::SocketAddr, sync::Arc, time::Duration};
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use axum::{extract::State, http::HeaderMap, routing::get};
|
use axum::{extract::State, http::HeaderMap, routing::get};
|
||||||
use reqwest::{header::USER_AGENT};
|
use reqwest::{header::USER_AGENT};
|
||||||
use sea_orm::{ConnectionTrait, Database, DatabaseConnection, EntityTrait, PaginatorTrait, Schema};
|
use sea_orm::{ConnectionTrait, Database, DatabaseConnection, EntityTrait, PaginatorTrait, Schema};
|
||||||
use tokio::{sync::broadcast::{self, Sender}, task, time};
|
use tokio::{sync::broadcast::{self, Sender}};
|
||||||
use utoipa::openapi::{ContactBuilder, InfoBuilder, LicenseBuilder};
|
use utoipa::openapi::{ContactBuilder, InfoBuilder, LicenseBuilder};
|
||||||
use utoipa_axum::router::OpenApiRouter;
|
use utoipa_axum::router::OpenApiRouter;
|
||||||
use utoipa_redoc::{Redoc, Servable};
|
use utoipa_redoc::{Redoc, Servable};
|
||||||
use utoipa_swagger_ui::{Config, SwaggerUi};
|
use utoipa_swagger_ui::{Config, SwaggerUi};
|
||||||
use utoipa_axum::routes;
|
use utoipa_axum::routes;
|
||||||
|
|
||||||
use crate::{entities::{owner, prelude::BookInstance}, utils::events::Event};
|
use crate::{entities::prelude::BookInstance, utils::events::Event};
|
||||||
|
|
||||||
pub mod entities;
|
pub mod entities;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
@ -38,42 +38,27 @@ async fn index(
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let db: Arc<DatabaseConnection> = Arc::new(Database::connect(String::from("sqlite://./alexandria.db?mode=rwc")).await.unwrap());
|
let db: Arc<DatabaseConnection> = Arc::new(Database::connect(String::from("sqlite://./alexandria.db?mode=rwc")).await.unwrap());
|
||||||
|
|
||||||
let builder = db.get_database_backend();
|
let builder = db.get_database_backend();
|
||||||
let schema = Schema::new(builder);
|
let schema = Schema::new(builder);
|
||||||
if let Err(err) = db.execute(builder.build(schema.create_table_from_entity(crate::entities::prelude::Book).if_not_exists())).await {
|
if let Err(err) = db.execute(builder.build(schema.create_table_from_entity(crate::entities::prelude::Book).if_not_exists())).await {
|
||||||
println!("Error while creating book table: {err:?}");
|
log::error!(target: "database", "Error while creating book table: {err:?}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Err(err) = db.execute(builder.build(schema.create_table_from_entity(crate::entities::prelude::BookInstance).if_not_exists())).await {
|
if let Err(err) = db.execute(builder.build(schema.create_table_from_entity(crate::entities::prelude::BookInstance).if_not_exists())).await {
|
||||||
println!("Error while creating book_instance table: {err:?}");
|
log::error!(target: "database", "Error while creating book_instance table: {err:?}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Err(err) = db.execute(builder.build(schema.create_table_from_entity(crate::entities::prelude::Owner).if_not_exists())).await {
|
if let Err(err) = db.execute(builder.build(schema.create_table_from_entity(crate::entities::prelude::Owner).if_not_exists())).await {
|
||||||
println!("Error while creating owner table: {err:?}");
|
log::error!(target: "database", "Error while creating owner table: {err:?}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (event_bus, _) = broadcast::channel(16);
|
let (event_bus, _) = broadcast::channel(16);
|
||||||
|
|
||||||
let ntx = event_bus.clone();
|
|
||||||
let _forever = task::spawn(async move {
|
|
||||||
let mut interval = time::interval(Duration::from_secs(5));
|
|
||||||
|
|
||||||
let mut id = 1;
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
let _ = ntx.send(Event::WebsocketBroadcast(utils::events::WebsocketMessage::NewOwner(Arc::new(owner::Model {
|
|
||||||
id,
|
|
||||||
first_name: "Avril".to_string(),
|
|
||||||
last_name: "Papillon".to_string(),
|
|
||||||
contact: "avril.papillon@proton.me".to_string()
|
|
||||||
}))));
|
|
||||||
id += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut default_headers = HeaderMap::new();
|
let mut default_headers = HeaderMap::new();
|
||||||
default_headers.append(USER_AGENT, "Alexandria/1.0 (unionetudianteauvergne@gmail.com)".parse().unwrap());
|
default_headers.append(USER_AGENT, "Alexandria/1.0 (unionetudianteauvergne@gmail.com)".parse().unwrap());
|
||||||
let shared_state = Arc::new(AppState {
|
let shared_state = Arc::new(AppState {
|
||||||
|
|
@ -118,7 +103,7 @@ async fn main() {
|
||||||
let router = router.merge(swagger);
|
let router = router.merge(swagger);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||||
println!("Running on http://{}", listener.local_addr().unwrap());
|
log::info!("Running on http://{}", listener.local_addr().unwrap());
|
||||||
axum::serve(
|
axum::serve(
|
||||||
listener,
|
listener,
|
||||||
router.into_make_service_with_connect_info::<SocketAddr>()
|
router.into_make_service_with_connect_info::<SocketAddr>()
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ pub async fn create_book_instance(
|
||||||
let b = book_instance.save(state.db_conn.as_ref()).await;
|
let b = book_instance.save(state.db_conn.as_ref()).await;
|
||||||
match b {
|
match b {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while inserting new book instance: {:#?}", e);
|
log::error!(target: "api", "Error while inserting new book instance: {:#?}", e);
|
||||||
(StatusCode::BAD_REQUEST, Json(None))
|
(StatusCode::BAD_REQUEST, Json(None))
|
||||||
},
|
},
|
||||||
Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved"))))
|
Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved"))))
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, EntityTrait, TryInto
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
use crate::{entities::{owner, prelude::Owner}, AppState};
|
use crate::{entities::{owner, prelude::Owner}, utils::events::{Event, WebsocketMessage}, AppState};
|
||||||
|
|
||||||
|
|
||||||
#[derive(IntoParams)]
|
#[derive(IntoParams)]
|
||||||
|
|
@ -62,19 +62,23 @@ pub async fn create_owner(
|
||||||
Json(instance_payload): Json<OwnerCreateParams>,
|
Json(instance_payload): Json<OwnerCreateParams>,
|
||||||
) -> (StatusCode, Json<Option<owner::Model>>) {
|
) -> (StatusCode, Json<Option<owner::Model>>) {
|
||||||
|
|
||||||
let book_instance = owner::ActiveModel {
|
let owner = owner::ActiveModel {
|
||||||
first_name: Set(instance_payload.first_name),
|
first_name: Set(instance_payload.first_name),
|
||||||
last_name: Set(instance_payload.last_name),
|
last_name: Set(instance_payload.last_name),
|
||||||
contact: Set(instance_payload.contact),
|
contact: Set(instance_payload.contact),
|
||||||
id: NotSet
|
id: NotSet
|
||||||
};
|
};
|
||||||
|
|
||||||
let b = book_instance.save(state.db_conn.as_ref()).await;
|
let o = owner.insert(state.db_conn.as_ref()).await;
|
||||||
match b {
|
match o {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while inserting new owner: {:#?}", e);
|
log::error!(target: "api", "Error while inserting new owner from api: {:#?}", e);
|
||||||
(StatusCode::BAD_REQUEST, Json(None))
|
(StatusCode::BAD_REQUEST, Json(None))
|
||||||
},
|
},
|
||||||
Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved"))))
|
Ok(res) => {
|
||||||
|
let model = res.try_into_model().expect("All fields should be set once the owner is saved");
|
||||||
|
let _ = state.event_bus.send(Event::WebsocketBroadcast(WebsocketMessage::NewOwner(Arc::new(model.clone()))));
|
||||||
|
(StatusCode::OK, Json(Some(model)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ use axum::{
|
||||||
},
|
},
|
||||||
response::IntoResponse
|
response::IntoResponse
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::{utils::events, AppState};
|
use crate::{utils::events, AppState};
|
||||||
|
|
||||||
|
|
@ -21,9 +20,7 @@ pub async fn ws_handler(
|
||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
State(state): State<Arc<AppState>>
|
State(state): State<Arc<AppState>>
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
println!("`{addr} connected.");
|
log::debug!(target: "websocket", "{addr} connected.");
|
||||||
// finalize the upgrade process by returning upgrade callback.
|
|
||||||
// we can customize the callback by sending additional info such as address.
|
|
||||||
ws.on_upgrade(move |socket| handle_socket(socket, addr, state))
|
ws.on_upgrade(move |socket| handle_socket(socket, addr, state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,9 +31,9 @@ async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc<AppSta
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
println!("WS >>> Pinged {who}...");
|
log::debug!(target: "websocket", "Pinged {who}...");
|
||||||
} else {
|
} else {
|
||||||
println!("WS >>> Could not send ping {who}!");
|
log::debug!(target: "websocket", "Could not send ping to {who}!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +43,7 @@ async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc<AppSta
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("WS >>> Client {who} abruptly disconnected");
|
log::debug!(target: "websocket", "Client {who} abruptly disconnected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,15 +75,15 @@ async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc<AppSta
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
rv_a = (&mut send_task) => {
|
rv_a = (&mut send_task) => {
|
||||||
match rv_a {
|
match rv_a {
|
||||||
Ok(()) => println!("WS >>> Sender connection with {who} gracefully shut down"),
|
Ok(()) => log::debug!(target: "websocket", "Sender connection with {who} gracefully shut down"),
|
||||||
Err(a) => println!("WS >>> Error sending messages {a:?}")
|
Err(a) => log::debug!(target: "websocket", "Error sending messages {a:?}")
|
||||||
}
|
}
|
||||||
recv_task.abort();
|
recv_task.abort();
|
||||||
},
|
},
|
||||||
rv_b = (&mut recv_task) => {
|
rv_b = (&mut recv_task) => {
|
||||||
match rv_b {
|
match rv_b {
|
||||||
Ok(()) => println!("WS >>> Receiver connection with {who} gracefully shut down"),
|
Ok(()) => log::debug!(target: "websocket", "Receiver connection with {who} gracefully shut down"),
|
||||||
Err(b) => println!("WS >>> Error receiving messages {b:?}")
|
Err(b) => log::debug!(target: "websocket", "Error receiving messages {b:?}")
|
||||||
}
|
}
|
||||||
send_task.abort();
|
send_task.abort();
|
||||||
}
|
}
|
||||||
|
|
@ -96,28 +93,27 @@ async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc<AppSta
|
||||||
fn process_message(msg: Message, who: SocketAddr) -> ControlFlow<(), ()> {
|
fn process_message(msg: Message, who: SocketAddr) -> ControlFlow<(), ()> {
|
||||||
match msg {
|
match msg {
|
||||||
Message::Text(t) => {
|
Message::Text(t) => {
|
||||||
println!("WS >>> {who} sent str: {t:?}");
|
log::debug!(target: "websocket", "{who} sent str: {t:?}");
|
||||||
}
|
}
|
||||||
Message::Binary(d) => {
|
Message::Binary(d) => {
|
||||||
println!("WS >>> {who} sent {} bytes: {d:?}", d.len());
|
log::debug!(target: "websocket", "{who} sent {} bytes: {d:?}", d.len());
|
||||||
}
|
}
|
||||||
Message::Close(c) => {
|
Message::Close(c) => {
|
||||||
if let Some(cf) = c {
|
if let Some(cf) = c {
|
||||||
println!(
|
log::debug!(target: "websocket",
|
||||||
"WS >>> {who} sent close with code {} and reason `{}`",
|
"{who} sent close with code {} and reason `{}`",
|
||||||
cf.code, cf.reason
|
cf.code, cf.reason
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("WS >>> {who} somehow sent close message without CloseFrame");
|
log::debug!(target: "websocket", "{who} somehow sent close message without CloseFrame");
|
||||||
}
|
}
|
||||||
return ControlFlow::Break(());
|
return ControlFlow::Break(());
|
||||||
}
|
}
|
||||||
|
Message::Pong(_v) => {
|
||||||
Message::Pong(v) => {
|
//log::debug!(target: "websocket", "{who} sent pong with {v:?}");
|
||||||
println!("WS >>> {who} sent pong with {v:?}");
|
|
||||||
}
|
}
|
||||||
Message::Ping(v) => {
|
Message::Ping(_v) => {
|
||||||
println!("WS >>> {who} sent ping with {v:?}");
|
//log::debug!(target: "websocket", "{who} sent ping with {v:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlFlow::Continue(())
|
ControlFlow::Continue(())
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ pub async fn fetch_book_by_ean(web_client: &reqwest::Client, ean: &String) -> Op
|
||||||
match body.status() {
|
match body.status() {
|
||||||
StatusCode::OK => {
|
StatusCode::OK => {
|
||||||
let res = body.text().await.unwrap();
|
let res = body.text().await.unwrap();
|
||||||
println!("Res: {res:#?}");
|
log::trace!(target: "api", "OpenLibrary book fetch result: {res:#?}");
|
||||||
let v: Value = serde_json::from_str(&res).unwrap();
|
let v: Value = serde_json::from_str(&res).unwrap();
|
||||||
Some(FetchedBook {
|
Some(FetchedBook {
|
||||||
ean: ean.to_string(),
|
ean: ean.to_string(),
|
||||||
|
|
|
||||||
Reference in a new issue