From 177003cc53e5f626e5f0d6d0d218470369c9fc0f Mon Sep 17 00:00:00 2001 From: Pratik Tripathy Date: Tue, 1 Dec 2020 20:56:19 +0530 Subject: [PATCH] Added readme --- readme.md | 75 +++++++++++++++++++ src/api/mod.rs | 3 +- src/api/order_controller.rs | 50 +++++++++++++ src/api/product_controller.rs | 14 ++-- src/config/db_pool.rs | 12 ++-- src/main.rs | 16 +++-- src/models/mod.rs | 3 +- src/models/order.rs | 132 ++++++++++++++++++++++++++++++++++ src/models/product.rs | 56 ++++++++------- src/utils/api_error.rs | 14 ++-- src/utils/constants.rs | 4 +- 11 files changed, 327 insertions(+), 52 deletions(-) create mode 100644 readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a5f4287 --- /dev/null +++ b/readme.md @@ -0,0 +1,75 @@ +# Shopping API + +Creates the Web API for a very basic e-commerce platform. The +server is backed by PostgreSQL database. + +## APIs + +### 1. GET /product + +- Returns a JSON response with all the events, ordered by date of creation in ascending order. + +- Output + + ```json + [ + { + "productid": 5, + "productname": "Hammer", + "created_at": "2020-11-30T21:46:41.359298" + }, + { + "productid": 4, + "productname": "Soap", + "created_at": "2020-11-30T21:46:41.359298" + } + ] + ``` + +### 2. POST /orders + +- Accepts a JSON request for an order that comprises one or more tickets. + +- Input + + ```json + { + "email": "mail@id.com", + "line_items": [ + { + "productid": 4, + "quantity": 10 + }, + { + "productid": 5, + "quantity": 10 + } + ] + } + ``` + +### 3. GET /orders (TODO) + +- Returns a JSON response with all the orders, ordered by date of creation. + +- Output + + ```json + { + “orders”: [ + { + “id”: 1, + “email”: “​test@example.com​”, + “line_items”: [ + { + “quantity”: 5, + “product”: { + “id”: 1, + “name”: “Product1” + } + }, + ], + } + ] + } + ``` diff --git a/src/api/mod.rs b/src/api/mod.rs index 33be58f..22e6e1b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1 +1,2 @@ -pub mod product_controller; \ No newline at end of file +pub mod product_controller; +pub mod order_controller; \ No newline at end of file diff --git a/src/api/order_controller.rs b/src/api/order_controller.rs index e69de29..58c01e1 100644 --- a/src/api/order_controller.rs +++ b/src/api/order_controller.rs @@ -0,0 +1,50 @@ +use actix_web::{get, HttpResponse, post, web}; + +use crate::models::order::*; +use crate::utils::api_error::ApiError; +use serde::{Deserialize, Serialize}; + +/// Model for user input for an __Order__ item +#[derive(Serialize, Deserialize, Debug)] +pub struct InputOrderDetail { + pub productid: i32, + pub quantity: i32, +} + +/// Model for user input for an __Order__ +#[derive(Serialize, Deserialize, Debug)] +pub struct InputOrder { + pub email: String, + pub line_items: Vec +} + +#[get("/orders")] +async fn find_all() -> core::result::Result { + Ok(HttpResponse::Ok().json(Order::find_all())) +} + +/// Accepts JSON input in the following format:- +/// ```json +/// { +/// "email": "mail@id.com", +/// "line_items": [ +/// { +/// "productid": 1, +/// "quantity": 10 +/// }, +/// { +/// "productid": 2, +/// "quantity": 30 +/// } +/// ] +///} +/// ``` +#[post("/orders")] +async fn insert_order(order: web::Json) -> core::result::Result { + Ok(HttpResponse::Ok().json(Order::insert(order.into_inner()))) +} + +pub fn routes(cfg: &mut web::ServiceConfig) { + cfg.service(find_all); + cfg.service(insert_order); +} \ No newline at end of file diff --git a/src/api/product_controller.rs b/src/api/product_controller.rs index f419c86..46d528b 100644 --- a/src/api/product_controller.rs +++ b/src/api/product_controller.rs @@ -1,16 +1,18 @@ +use actix_web::{get, HttpResponse, web}; + use crate::models::product::Product; -use actix_web::{get, web, HttpResponse, Responder}; +use crate::utils::api_error::ApiError; #[get("/products")] -async fn find_all() -> impl Responder { +async fn find_all() -> Result { let products = Product::find_all().expect("Error fetching all Products"); - HttpResponse::Ok().json(products) + Ok(HttpResponse::Ok().json(products)) } #[get("/product/{id}")] -async fn find(id: web::Path) -> impl Responder { - let product = Product::find(id.into_inner()).expect("Error fetching Product"); - HttpResponse::Ok().json(product) +async fn find(id: web::Path) -> Result { + let product = Product::find_by_id(id.into_inner()).expect("Error fetching Product"); + Ok(HttpResponse::Ok().json(product)) } pub fn routes(cfg: &mut web::ServiceConfig) { diff --git a/src/config/db_pool.rs b/src/config/db_pool.rs index 7c711ef..8078034 100644 --- a/src/config/db_pool.rs +++ b/src/config/db_pool.rs @@ -1,9 +1,11 @@ -use crate::utils::api_error::ApiError; -use diesel::pg::PgConnection; -use diesel::r2d2::ConnectionManager; +use std::env; + +use diesel::{pg::PgConnection, + r2d2::ConnectionManager}; use lazy_static::lazy_static; use r2d2; -use std::env; + +use crate::utils::api_error::ApiError; type Pool = r2d2::Pool>; pub type DbConnection = r2d2::PooledConnection>; @@ -14,7 +16,7 @@ lazy_static! { static ref POOL: Pool = { let db_url = env::var("DATABASE_URL").expect("Database url not set"); let manager = ConnectionManager::::new(db_url); - Pool::new(manager).expect("Failed to create config pool") + Pool::new(manager).expect("Failed to create DB Connection pool") }; } diff --git a/src/main.rs b/src/main.rs index e4b1a12..b75db13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,26 @@ #[macro_use] -extern crate log; -#[macro_use] extern crate diesel; #[macro_use] extern crate diesel_migrations; +#[macro_use] +extern crate log; + +use std::env; use actix_web::{App, HttpServer}; use dotenv::dotenv; -use std::env; -use crate::api::product_controller; + use config::db_pool; +use crate::api::{order_controller, product_controller}; + mod schema; mod utils; mod models; mod api; mod config; -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); env_logger::init(); @@ -30,7 +33,10 @@ async fn main() -> std::io::Result<()> { info!("Starting Server"); HttpServer::new(|| App::new() + .data(db_pool::get_connection()) + .wrap(actix_web::middleware::Logger::default()) .configure(product_controller::routes) + .configure(order_controller::routes) ).bind(bind_address)? .run().await } diff --git a/src/models/mod.rs b/src/models/mod.rs index 1b2fa7a..0df552a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1,2 @@ -pub mod product; \ No newline at end of file +pub mod product; +pub mod order; \ No newline at end of file diff --git a/src/models/order.rs b/src/models/order.rs index e69de29..b81f7f5 100644 --- a/src/models/order.rs +++ b/src/models/order.rs @@ -0,0 +1,132 @@ +use diesel::{insert_into, RunQueryDsl}; +use serde::{Deserialize, Serialize}; +use serde_json; + +use crate::{config::db_pool, + schema::order_details, schema::orders, + utils::api_error::ApiError}; +use crate::api::order_controller::InputOrder; + +/// Stores selected (queried) `orders` table row +#[derive(Serialize, Deserialize, Queryable)] +pub struct Order { + pub orderid: i32, + pub customer_email: String, + pub created_at: chrono::NaiveDateTime +} + +/// Store data that is to be inserted into `order_details` table +#[derive(Serialize, Deserialize, Queryable, Insertable, Debug)] +#[table_name = "orders"] +pub struct NewOrder { + pub customer_email: String, +} + +/// Stores selected (queried) `order_details` table row. +/// Can also be used to insert data into `order_details` table +#[derive(Serialize, Deserialize, Queryable, Insertable, Debug)] +#[table_name = "order_details"] +pub struct OrderDetail { + pub orderid: i32, + pub productid: i32, + pub quantity: i32, +} + +impl Order { + pub fn find_all() -> Result { + // let conn = db_pool::get_connection()?; + Ok(OrderDetail { quantity: 1, productid: 2, orderid: 3 }) + } + + + pub fn insert(new_order: InputOrder) -> Result { + use crate::schema::orders::dsl::*; + use crate::schema::order_details::dsl::*; + + // Order should have some items + if new_order.line_items.is_empty() { + error!("Invalid input. No 'item' in the received JSON"); + return Err(ApiError { + message: "Invalid input. No 'item' in the received JSON".to_string(), + status_code: 402 + }) + } + + // Order must have an non-empty email address + if new_order.email.is_empty() { + error!("Invalid input. Please provide a valid email"); + return Err(ApiError { + message: "Invalid input. Please provide a valid email".to_string(), + status_code: 403 + }) + } + + // Each item must have `productid` and `quantity` + for item in &new_order.line_items { + if item.quantity <= 0 { + error!("Invalid input. 'quantity' must be greater than 0"); + return Err(ApiError { + message: "Invalid input. 'quantity' must be greater than 0".to_string(), + status_code: 404 + }) + } + + if item.productid <= 0 { + error!("Invalid input. Invalid 'productid'"); + return Err(ApiError { + message: "Invalid input. Invalid 'productid'".to_string(), + status_code: 405 + }) + } + } + + // Insert customer_email to `orders` table + let conn = db_pool::get_connection()?; + let inserted_order = match insert_into(orders) + .values(NewOrder { customer_email: new_order.email }) + .get_result::(&conn) + { + Ok(o) => o, + // Suppress the DB error + Err(e) => { + error!("Error inserting to order table: {}", e); + return Err(ApiError { + message: "Error inserting the order".to_string(), + status_code: 409 + }) + } + }; + + info!("Order Inserted Successfully: {}", + serde_json::to_string(&inserted_order).unwrap()); + + // Insert each item to `orderdetails` table + for item in new_order.line_items { + let order_detail = OrderDetail { + orderid: inserted_order.orderid, + productid: item.productid, + quantity: item.quantity + }; + + let inserted_order_detail = match insert_into(order_details) + .values(order_detail) + .get_result::(&conn) + { + Ok(o) => o, + // Suppress the DB error + Err(e) => { + error!("Error inserting to orderdetails table: {}", e); + return Err(ApiError { + message: "Error inserting the order".to_string(), + status_code: 410 + }) + } + }; + + info!("Order Details inserted successfully: {}", + serde_json::to_string(&inserted_order_detail).unwrap()); + } + + Ok("Order recorded successfully".to_string()) + } +} \ No newline at end of file diff --git a/src/models/product.rs b/src/models/product.rs index 50db449..24fb37e 100644 --- a/src/models/product.rs +++ b/src/models/product.rs @@ -1,16 +1,9 @@ -use crate::config::db_pool; -use crate::schema::products; -use chrono::{NaiveDateTime, Utc}; +use chrono::NaiveDateTime; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -use crate::utils::api_error::ApiError; -#[derive(Serialize, Deserialize, AsChangeset)] -#[table_name = "products"] -pub struct ProductMessage { - pub productid: i32, - pub productname: String, -} +use crate::{config::db_pool, schema::products, + utils::api_error::ApiError}; #[derive(Serialize, Deserialize, Queryable, Insertable)] #[table_name = "products"] @@ -24,29 +17,42 @@ impl Product { pub fn find_all() -> Result, ApiError> { let conn = db_pool::get_connection()?; - let products = products::table - .load::(&conn)?; + let products = match products::table + .order(products::created_at.asc()) + .load::(&conn) + { + Ok(o) => o, + // Suppress the DB error + Err(e) => { + error!("Error fetching 'products' table: {}", e); + return Err(ApiError { + message: "Error fetching Product information".to_string(), + status_code: 411 + }) + } + }; Ok(products) } - pub fn find(id: i32) -> Result { + pub fn find_by_id(id: i32) -> Result { let conn = db_pool::get_connection()?; - let product = products::table + let product = match products::table .filter(products::productid.eq(id)) - .first(&conn)?; + .first(&conn) + { + Ok(o) => o, + // Suppress the DB error + Err(e) => { + error!("Error fetching 'products' information for productid ({}): {}", id, e); + return Err(ApiError { + message: "Error fetching Product information".to_string(), + status_code: 412 + }) + } + }; Ok(product) } } - -impl From for Product { - fn from(product: ProductMessage) -> Self { - Product { - productid: product.productid, - productname: product.productname, - created_at: Utc::now().naive_utc(), - } - } -} \ No newline at end of file diff --git a/src/utils/api_error.rs b/src/utils/api_error.rs index fb0e43b..a454938 100644 --- a/src/utils/api_error.rs +++ b/src/utils/api_error.rs @@ -1,11 +1,13 @@ -use actix_web::http::StatusCode; -use actix_web::{HttpResponse, ResponseError}; -use diesel::result::Error as DieselError; -use serde::Deserialize; -use serde_json::json; use std::fmt; -#[derive(Debug, Deserialize)] +use actix_web::{http::StatusCode, + HttpResponse, + ResponseError}; +use diesel::result::Error as DieselError; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Debug, Serialize, Deserialize)] pub struct ApiError { pub status_code: u16, pub message: String, diff --git a/src/utils/constants.rs b/src/utils/constants.rs index 89640f6..6e4e8d1 100644 --- a/src/utils/constants.rs +++ b/src/utils/constants.rs @@ -8,6 +8,4 @@ // // // Misc // pub const EMPTY: &str = ""; -// -// // ignore routes -// pub const IGNORE_ROUTES: [&str; 3] = ["/api/ping", "/api/auth/signup", "/api/auth/login"]; +// \ No newline at end of file