Added readme

This commit is contained in:
Pratik Tripathy
2020-12-01 20:56:19 +05:30
parent 4278b458ce
commit 177003cc53
11 changed files with 327 additions and 52 deletions

75
readme.md Normal file
View File

@@ -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”
}
},
],
}
]
}
```

View File

@@ -1 +1,2 @@
pub mod product_controller; pub mod product_controller;
pub mod order_controller;

View File

@@ -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<InputOrderDetail>
}
#[get("/orders")]
async fn find_all() -> core::result::Result<HttpResponse, ApiError> {
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<InputOrder>) -> core::result::Result<HttpResponse, ApiError> {
Ok(HttpResponse::Ok().json(Order::insert(order.into_inner())))
}
pub fn routes(cfg: &mut web::ServiceConfig) {
cfg.service(find_all);
cfg.service(insert_order);
}

View File

@@ -1,16 +1,18 @@
use actix_web::{get, HttpResponse, web};
use crate::models::product::Product; use crate::models::product::Product;
use actix_web::{get, web, HttpResponse, Responder}; use crate::utils::api_error::ApiError;
#[get("/products")] #[get("/products")]
async fn find_all() -> impl Responder { async fn find_all() -> Result<HttpResponse, ApiError> {
let products = Product::find_all().expect("Error fetching all Products"); let products = Product::find_all().expect("Error fetching all Products");
HttpResponse::Ok().json(products) Ok(HttpResponse::Ok().json(products))
} }
#[get("/product/{id}")] #[get("/product/{id}")]
async fn find(id: web::Path<i32>) -> impl Responder { async fn find(id: web::Path<i32>) -> Result<HttpResponse, ApiError> {
let product = Product::find(id.into_inner()).expect("Error fetching Product"); let product = Product::find_by_id(id.into_inner()).expect("Error fetching Product");
HttpResponse::Ok().json(product) Ok(HttpResponse::Ok().json(product))
} }
pub fn routes(cfg: &mut web::ServiceConfig) { pub fn routes(cfg: &mut web::ServiceConfig) {

View File

@@ -1,9 +1,11 @@
use crate::utils::api_error::ApiError; use std::env;
use diesel::pg::PgConnection;
use diesel::r2d2::ConnectionManager; use diesel::{pg::PgConnection,
r2d2::ConnectionManager};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use r2d2; use r2d2;
use std::env;
use crate::utils::api_error::ApiError;
type Pool = r2d2::Pool<ConnectionManager<PgConnection>>; type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
pub type DbConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>; pub type DbConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
@@ -14,7 +16,7 @@ lazy_static! {
static ref POOL: Pool = { static ref POOL: Pool = {
let db_url = env::var("DATABASE_URL").expect("Database url not set"); let db_url = env::var("DATABASE_URL").expect("Database url not set");
let manager = ConnectionManager::<PgConnection>::new(db_url); let manager = ConnectionManager::<PgConnection>::new(db_url);
Pool::new(manager).expect("Failed to create config pool") Pool::new(manager).expect("Failed to create DB Connection pool")
}; };
} }

View File

@@ -1,23 +1,26 @@
#[macro_use] #[macro_use]
extern crate log;
#[macro_use]
extern crate diesel; extern crate diesel;
#[macro_use] #[macro_use]
extern crate diesel_migrations; extern crate diesel_migrations;
#[macro_use]
extern crate log;
use std::env;
use actix_web::{App, HttpServer}; use actix_web::{App, HttpServer};
use dotenv::dotenv; use dotenv::dotenv;
use std::env;
use crate::api::product_controller;
use config::db_pool; use config::db_pool;
use crate::api::{order_controller, product_controller};
mod schema; mod schema;
mod utils; mod utils;
mod models; mod models;
mod api; mod api;
mod config; mod config;
#[actix_rt::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenv().ok(); dotenv().ok();
env_logger::init(); env_logger::init();
@@ -30,7 +33,10 @@ async fn main() -> std::io::Result<()> {
info!("Starting Server"); info!("Starting Server");
HttpServer::new(|| HttpServer::new(||
App::new() App::new()
.data(db_pool::get_connection())
.wrap(actix_web::middleware::Logger::default())
.configure(product_controller::routes) .configure(product_controller::routes)
.configure(order_controller::routes)
).bind(bind_address)? ).bind(bind_address)?
.run().await .run().await
} }

View File

@@ -1 +1,2 @@
pub mod product; pub mod product;
pub mod order;

View File

@@ -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<OrderDetail, ApiError> {
// let conn = db_pool::get_connection()?;
Ok(OrderDetail { quantity: 1, productid: 2, orderid: 3 })
}
pub fn insert(new_order: InputOrder) -> Result<String, ApiError> {
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::<Order>(&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::<OrderDetail>(&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())
}
}

View File

@@ -1,16 +1,9 @@
use crate::config::db_pool; use chrono::NaiveDateTime;
use crate::schema::products;
use chrono::{NaiveDateTime, Utc};
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::utils::api_error::ApiError;
#[derive(Serialize, Deserialize, AsChangeset)] use crate::{config::db_pool, schema::products,
#[table_name = "products"] utils::api_error::ApiError};
pub struct ProductMessage {
pub productid: i32,
pub productname: String,
}
#[derive(Serialize, Deserialize, Queryable, Insertable)] #[derive(Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "products"] #[table_name = "products"]
@@ -24,29 +17,42 @@ impl Product {
pub fn find_all() -> Result<Vec<Self>, ApiError> { pub fn find_all() -> Result<Vec<Self>, ApiError> {
let conn = db_pool::get_connection()?; let conn = db_pool::get_connection()?;
let products = products::table let products = match products::table
.load::<Product>(&conn)?; .order(products::created_at.asc())
.load::<Product>(&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) Ok(products)
} }
pub fn find(id: i32) -> Result<Self, ApiError> { pub fn find_by_id(id: i32) -> Result<Self, ApiError> {
let conn = db_pool::get_connection()?; let conn = db_pool::get_connection()?;
let product = products::table let product = match products::table
.filter(products::productid.eq(id)) .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) Ok(product)
} }
} }
impl From<ProductMessage> for Product {
fn from(product: ProductMessage) -> Self {
Product {
productid: product.productid,
productname: product.productname,
created_at: Utc::now().naive_utc(),
}
}
}

View File

@@ -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; 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 struct ApiError {
pub status_code: u16, pub status_code: u16,
pub message: String, pub message: String,

View File

@@ -8,6 +8,4 @@
// //
// // Misc // // Misc
// pub const EMPTY: &str = ""; // pub const EMPTY: &str = "";
// //
// // ignore routes
// pub const IGNORE_ROUTES: [&str; 3] = ["/api/ping", "/api/auth/signup", "/api/auth/login"];