feat: initial API and docs
This commit is contained in:
parent
79be4eb543
commit
5d709d658b
16 changed files with 3169 additions and 342 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1 +1,4 @@
|
|||
/target
|
||||
|
||||
*.db
|
||||
.env
|
||||
|
|
|
|||
2954
Cargo.lock
generated
2954
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
|
@ -4,5 +4,15 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4"
|
||||
axum = { version = "0.8.4", features = [ "macros" ] }
|
||||
dotenvy = "0.15.7"
|
||||
reqwest = "0.12.22"
|
||||
sea-orm = { version = "1.1.13", features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
|
||||
serde = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.46.1", features = [ "full" ] }
|
||||
utoipa = "5.4.0"
|
||||
utoipa-axum = "0.2.0"
|
||||
utoipa-swagger-ui = { version = "9", features = ["axum", "reqwest"] }
|
||||
utoipa-redoc = { version = "6", features = ["axum"] }
|
||||
|
||||
|
|
|
|||
38
src/entities/book.rs
Normal file
38
src/entities/book.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[sea_orm(table_name = "Book")]
|
||||
#[schema(title="Book", as=entities::Book)]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = true)]
|
||||
pub id: u32,
|
||||
pub ean: String,
|
||||
pub title: String,
|
||||
pub author: String
|
||||
}
|
||||
|
||||
impl Default for Model {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
ean: "".to_string(),
|
||||
title: "".to_string(),
|
||||
author: "".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::book_instance::Entity")]
|
||||
BookInstance,
|
||||
}
|
||||
|
||||
impl Related<super::book_instance::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::BookInstance.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
57
src/entities/book_instance.rs
Normal file
57
src/entities/book_instance.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, utoipa::ToSchema)]
|
||||
#[sea_orm(table_name = "BookInstances")]
|
||||
#[schema(title="BookInstance", as=entities::BookInstance)]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = true)]
|
||||
pub id: u32,
|
||||
pub price: i32,
|
||||
pub sold_price: Option<i32>,
|
||||
pub status: BookStatus,
|
||||
pub book_id: u32,
|
||||
pub owner_id: u32
|
||||
}
|
||||
// Missing: Bal
|
||||
|
||||
#[derive(EnumIter, DeriveActiveEnum, PartialEq, Eq ,Deserialize, Serialize, Clone, Copy, Debug, utoipa::ToSchema)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(1))")]
|
||||
pub enum BookStatus {
|
||||
#[sea_orm(string_value = "A")]
|
||||
Available,
|
||||
#[sea_orm(string_value = "S")]
|
||||
Sold,
|
||||
#[sea_orm(string_value = "D")]
|
||||
Damaged
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::book::Entity",
|
||||
from = "Column::BookId",
|
||||
to = "super::book::Column::Id"
|
||||
)]
|
||||
Book,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::owner::Entity",
|
||||
from = "Column::OwnerId",
|
||||
to = "super::owner::Column::Id"
|
||||
)]
|
||||
Owner,
|
||||
}
|
||||
|
||||
impl Related<super::book::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Book.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::owner::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Owner.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
6
src/entities/mod.rs
Normal file
6
src/entities/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod prelude;
|
||||
|
||||
pub mod book;
|
||||
pub mod book_instance;
|
||||
pub mod owner;
|
||||
|
||||
27
src/entities/owner.rs
Normal file
27
src/entities/owner.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize, Serialize, utoipa::ToSchema)]
|
||||
#[sea_orm(table_name = "Owners")]
|
||||
#[schema(title="Owner", as=entities::Owner)]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = true)]
|
||||
pub id: u32,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub contact: String
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::book_instance::Entity")]
|
||||
BookInstance
|
||||
}
|
||||
|
||||
impl Related<super::book_instance::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::BookInstance.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
4
src/entities/prelude.rs
Normal file
4
src/entities/prelude.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
pub use super::book::Entity as Book;
|
||||
pub use super::book_instance::Entity as BookInstance;
|
||||
pub use super::owner::Entity as Owner;
|
||||
101
src/main.rs
101
src/main.rs
|
|
@ -1,3 +1,100 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::State, http::HeaderMap, routing::get};
|
||||
use reqwest::{header::USER_AGENT};
|
||||
use sea_orm::{ConnectionTrait, Database, DatabaseConnection, EntityTrait, PaginatorTrait, Schema};
|
||||
use utoipa::openapi::{ContactBuilder, InfoBuilder, LicenseBuilder};
|
||||
use utoipa_axum::router::OpenApiRouter;
|
||||
use utoipa_redoc::{Redoc, Servable};
|
||||
use utoipa_swagger_ui::{Config, SwaggerUi};
|
||||
use utoipa_axum::routes;
|
||||
|
||||
use crate::entities::prelude::BookInstance;
|
||||
|
||||
pub mod entities;
|
||||
pub mod utils;
|
||||
pub mod routes;
|
||||
|
||||
pub struct AppState {
|
||||
app_name: String,
|
||||
db_conn: Arc<DatabaseConnection>,
|
||||
web_client: reqwest::Client
|
||||
}
|
||||
|
||||
async fn index(
|
||||
State(state): State<Arc<AppState>>
|
||||
) ->String {
|
||||
let app_name = &state.app_name;
|
||||
let db_conn = &state.db_conn;
|
||||
let status: &str = match db_conn.ping().await {
|
||||
Ok(_) => "working",
|
||||
Err(_) => "erroring"
|
||||
};
|
||||
let book_count = BookInstance::find().count(db_conn.as_ref()).await.unwrap();
|
||||
format!("Hello from {app_name}! Database is {status}. We currently have {book_count} books in stock !")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let db: Arc<DatabaseConnection> = Arc::new(Database::connect(String::from("sqlite://./alexandria.db?mode=rwc")).await.unwrap());
|
||||
|
||||
let builder = db.get_database_backend();
|
||||
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 {
|
||||
println!("Error while creating book table: {err:?}");
|
||||
return;
|
||||
}
|
||||
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:?}");
|
||||
return;
|
||||
}
|
||||
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:?}");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut default_headers = HeaderMap::new();
|
||||
default_headers.append(USER_AGENT, "Alexandria/1.0 (unionetudianteauvergne@gmail.com)".parse().unwrap());
|
||||
let shared_state = Arc::new(AppState {
|
||||
app_name: "Alexandria".to_string(),
|
||||
db_conn: db,
|
||||
web_client: reqwest::Client::builder().default_headers(default_headers).build().expect("creating the reqwest client failed")
|
||||
});
|
||||
|
||||
let (router, mut api) = OpenApiRouter::new()
|
||||
.routes(routes!(routes::book::get_book_by_ean))
|
||||
.routes(routes!(routes::book::get_book_by_id))
|
||||
.routes(routes!(routes::book_instance::get_book_instance_by_id))
|
||||
.routes(routes!(routes::book_instance::create_book_instance))
|
||||
.routes(routes!(routes::owner::get_owner_by_id))
|
||||
.routes(routes!(routes::owner::create_owner))
|
||||
.route("/", get(index))
|
||||
.with_state(shared_state)
|
||||
.split_for_parts();
|
||||
|
||||
api.info = InfoBuilder::new()
|
||||
.title("Alexandria")
|
||||
.description(Some("Alexandria is a server that manages books and users for Union Étudiante's book exchange"))
|
||||
.contact(Some(ContactBuilder::new()
|
||||
.url(Some("https://ueauvergne.fr"))
|
||||
.name(Some("Union Étudiante Auvergne"))
|
||||
.email(Some("unionetudianteauvergne@gmail.com"))
|
||||
.build()))
|
||||
.license(Some(LicenseBuilder::new().name("MIT").url(Some("https://spdx.org/licenses/MIT.html")).build()))
|
||||
.version("1.0.0")
|
||||
.build();
|
||||
|
||||
let redoc = Redoc::with_url("/docs/", api.clone());
|
||||
let swagger = SwaggerUi::new("/docs2/")
|
||||
.url("/docs2/openapi.json", api)
|
||||
.config(Config::default()
|
||||
.try_it_out_enabled(true)
|
||||
);
|
||||
|
||||
let router = router.merge(redoc);
|
||||
let router = router.merge(swagger);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, router).await.unwrap();
|
||||
}
|
||||
|
||||
|
|
|
|||
83
src/routes/book.rs
Normal file
83
src/routes/book.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::{Path, State}, Json};
|
||||
use reqwest::{StatusCode};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::{entities::{book, prelude::{Book}}, utils::open_library};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
|
||||
#[derive(IntoParams)]
|
||||
#[into_params(names("id"), parameter_in = Path)]
|
||||
#[allow(dead_code)]
|
||||
struct BookByIdParams(u32);
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/book/id/{id}",
|
||||
params(BookByIdParams),
|
||||
responses(
|
||||
(status = OK, body = book::Model, description = "Found book with corresponding ID in the database"),
|
||||
(status = NOT_FOUND, description = "No book with this id exists in the database")
|
||||
),
|
||||
summary = "Get a book by its internal ID",
|
||||
description = "Get a book from its ID in Alexandria's internal database",
|
||||
tag = "book-api",
|
||||
)]
|
||||
pub async fn get_book_by_id(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<u32>,
|
||||
) -> (StatusCode, Json<Option<book::Model>>) {
|
||||
if let Ok(Some(res)) = Book::find_by_id(id).one(state.db_conn.as_ref()).await {
|
||||
(StatusCode::OK, Json(Some(res)))
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, Json(None))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoParams)]
|
||||
#[into_params(names("ean"), parameter_in = Path)]
|
||||
#[allow(dead_code)]
|
||||
struct BookByEanParams(String);
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/book/ean/{ean}",
|
||||
params(BookByEanParams),
|
||||
responses(
|
||||
(status = OK, body = book::Model, description = "Found book with corresponding EAN", examples(
|
||||
("Found regular book" = (value = json!({"author": "Pierre Bottero", "ean": "9782700234015", "id": 5642, "title": "Ellana l'envol"}))),
|
||||
("Book doesn't have an EAN" = (value = json!({"author": "Author B. Ook", "ean": "", "id": 1465312, "title": "Itsabook"})))
|
||||
)),
|
||||
(status = NOT_FOUND, description = "No book with this EAN found in the database or on openlibrary")
|
||||
),
|
||||
summary = "Get a book by its EAN",
|
||||
description = "Get a book from its EAN. If it doesn't exist in its database, Alexandria will try to find it using openlibrary.org's API",
|
||||
tag = "book-api"
|
||||
)]
|
||||
pub async fn get_book_by_ean(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(ean): Path<String>,
|
||||
) -> (StatusCode, Json<Option<book::Model>>) {
|
||||
if let Ok(Some(res)) = Book::find().filter(book::Column::Ean.eq(&ean)).one(state.db_conn.as_ref()).await {
|
||||
(StatusCode::OK, Json(Some(res)))
|
||||
} else {
|
||||
let fetched_book = open_library::fetch_book_by_ean(&state.web_client, &ean).await;
|
||||
if let Some(book) = fetched_book {
|
||||
let res = Book::insert(book.to_active_model()).exec(state.db_conn.as_ref()).await.unwrap();
|
||||
(StatusCode::OK, Json(Some(book::Model {
|
||||
id: res.last_insert_id,
|
||||
ean: ean,
|
||||
title: book.title,
|
||||
author: book.author
|
||||
})))
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, Json(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/routes/book_instance.rs
Normal file
81
src/routes/book_instance.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::{Path, State}, Json};
|
||||
use reqwest::{StatusCode};
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::Set, EntityTrait, TryIntoModel};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::{entities::{book_instance, prelude::BookInstance}, AppState};
|
||||
|
||||
|
||||
#[derive(IntoParams)]
|
||||
#[into_params(names("id"), parameter_in = Path)]
|
||||
#[allow(dead_code)]
|
||||
struct BookInstanceByIdParams(u32);
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/book_instance/id/{id}",
|
||||
params(BookInstanceByIdParams),
|
||||
responses(
|
||||
(status = OK, body = book_instance::Model, description = "Found book instance with corresponding ID in the database"),
|
||||
(status = NOT_FOUND, description = "No book instance with this id exists in the database")
|
||||
),
|
||||
summary = "Get a book instance by its ID",
|
||||
description = "Get a book instance from its ID",
|
||||
tag = "book-instance-api",
|
||||
)]
|
||||
pub async fn get_book_instance_by_id(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<u32>,
|
||||
) -> (StatusCode, Json<Option<book_instance::Model>>) {
|
||||
if let Ok(Some(res)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await {
|
||||
(StatusCode::OK, Json(Some(res)))
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, Json(None))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct BookInstanceCreateParams {
|
||||
book_id: u32,
|
||||
owner_id: u32,
|
||||
price: i32,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/book_instance",
|
||||
request_body = BookInstanceCreateParams,
|
||||
responses(
|
||||
(status = OK, body = book_instance::Model, description = "Successfully created book instance"),
|
||||
),
|
||||
summary = "Create a new book instance",
|
||||
description = "Create a new book instance",
|
||||
tag = "book-instance-api",
|
||||
)]
|
||||
pub async fn create_book_instance(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(instance_payload): Json<BookInstanceCreateParams>,
|
||||
) -> (StatusCode, Json<Option<book_instance::Model>>) {
|
||||
|
||||
let book_instance = book_instance::ActiveModel {
|
||||
book_id: Set(instance_payload.book_id),
|
||||
owner_id: Set(instance_payload.owner_id),
|
||||
price: Set(instance_payload.price),
|
||||
status: Set(book_instance::BookStatus::Available),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let b = book_instance.save(state.db_conn.as_ref()).await;
|
||||
match b {
|
||||
Err(e) => {
|
||||
println!("Error while inserting new book instance: {:#?}", e);
|
||||
(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"))))
|
||||
}
|
||||
}
|
||||
4
src/routes/mod.rs
Normal file
4
src/routes/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod book;
|
||||
pub mod book_instance;
|
||||
pub mod owner;
|
||||
|
||||
80
src/routes/owner.rs
Normal file
80
src/routes/owner.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::{Path, State}, Json};
|
||||
use reqwest::{StatusCode};
|
||||
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, EntityTrait, TryIntoModel};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::{entities::{owner, prelude::Owner}, AppState};
|
||||
|
||||
|
||||
#[derive(IntoParams)]
|
||||
#[into_params(names("id"), parameter_in = Path)]
|
||||
#[allow(dead_code)]
|
||||
struct OwnerByIdParams(u32);
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/owner/{id}",
|
||||
params(OwnerByIdParams),
|
||||
responses(
|
||||
(status = OK, body = owner::Model, description = "Found owner with corresponding ID in the database"),
|
||||
(status = NOT_FOUND, description = "No owner with this id exists in the database")
|
||||
),
|
||||
summary = "Get an owner by its ID",
|
||||
description = "Get an owner from its ID",
|
||||
tag = "owner-api",
|
||||
)]
|
||||
pub async fn get_owner_by_id(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(id): Path<u32>,
|
||||
) -> (StatusCode, Json<Option<owner::Model>>) {
|
||||
if let Ok(Some(res)) = Owner::find_by_id(id).one(state.db_conn.as_ref()).await {
|
||||
(StatusCode::OK, Json(Some(res)))
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, Json(None))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct OwnerCreateParams {
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
contact: String
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/owner",
|
||||
request_body = OwnerCreateParams,
|
||||
responses(
|
||||
(status = OK, body = owner::Model, description = "Successfully created owner"),
|
||||
),
|
||||
summary = "Create a new owner",
|
||||
description = "Create a new owner",
|
||||
tag = "owner-api",
|
||||
)]
|
||||
pub async fn create_owner(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(instance_payload): Json<OwnerCreateParams>,
|
||||
) -> (StatusCode, Json<Option<owner::Model>>) {
|
||||
|
||||
let book_instance = owner::ActiveModel {
|
||||
first_name: Set(instance_payload.first_name),
|
||||
last_name: Set(instance_payload.last_name),
|
||||
contact: Set(instance_payload.contact),
|
||||
id: NotSet
|
||||
};
|
||||
|
||||
let b = book_instance.save(state.db_conn.as_ref()).await;
|
||||
match b {
|
||||
Err(e) => {
|
||||
println!("Error while inserting new owner: {:#?}", e);
|
||||
(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"))))
|
||||
}
|
||||
}
|
||||
2
src/utils/mod.rs
Normal file
2
src/utils/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod open_library;
|
||||
pub mod serde;
|
||||
43
src/utils/open_library.rs
Normal file
43
src/utils/open_library.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use reqwest::StatusCode;
|
||||
use sea_orm::ActiveValue::{NotSet, Set};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::entities::book;
|
||||
|
||||
pub struct FetchedBook {
|
||||
pub ean: String,
|
||||
pub title: String,
|
||||
pub author: String
|
||||
}
|
||||
|
||||
impl FetchedBook {
|
||||
pub fn to_active_model(&self) -> book::ActiveModel {
|
||||
book::ActiveModel {
|
||||
id: NotSet,
|
||||
ean: Set(self.ean.clone()),
|
||||
title: Set(self.title.clone()),
|
||||
author: Set(self.author.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_book_by_ean(web_client: &reqwest::Client, ean: &String) -> Option<FetchedBook> {
|
||||
let body = web_client.execute(
|
||||
web_client.get(format!("https://openlibrary.org/isbn/{ean}.json"))
|
||||
.build()
|
||||
.expect("get request creation failed")
|
||||
).await.unwrap();
|
||||
match body.status() {
|
||||
StatusCode::OK => {
|
||||
let res = body.text().await.unwrap();
|
||||
println!("Res: {res:#?}");
|
||||
let v: Value = serde_json::from_str(&res).unwrap();
|
||||
Some(FetchedBook {
|
||||
ean: ean.to_string(),
|
||||
title: v.get("title").unwrap().to_string(),
|
||||
author: "temp".to_owned()
|
||||
})
|
||||
},
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
16
src/utils/serde.rs
Normal file
16
src/utils/serde.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use serde::{de, Deserialize, Deserializer};
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
|
||||
pub fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: FromStr,
|
||||
T::Err: fmt::Display,
|
||||
{
|
||||
let opt = Option::<String>::deserialize(de)?;
|
||||
match opt.as_deref() {
|
||||
None | Some("") => Ok(None),
|
||||
Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some),
|
||||
}
|
||||
}
|
||||
Reference in a new issue