Added readme
This commit is contained in:
75
readme.md
Normal file
75
readme.md
Normal 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”
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -1 +1,2 @@
|
||||
pub mod product_controller;
|
||||
pub mod product_controller;
|
||||
pub mod order_controller;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<HttpResponse, ApiError> {
|
||||
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<i32>) -> impl Responder {
|
||||
let product = Product::find(id.into_inner()).expect("Error fetching Product");
|
||||
HttpResponse::Ok().json(product)
|
||||
async fn find(id: web::Path<i32>) -> Result<HttpResponse, ApiError> {
|
||||
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) {
|
||||
|
||||
@@ -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<ConnectionManager<PgConnection>>;
|
||||
pub type DbConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
|
||||
@@ -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::<PgConnection>::new(db_url);
|
||||
Pool::new(manager).expect("Failed to create config pool")
|
||||
Pool::new(manager).expect("Failed to create DB Connection pool")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
16
src/main.rs
16
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
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod product;
|
||||
pub mod product;
|
||||
pub mod order;
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<Self>, ApiError> {
|
||||
let conn = db_pool::get_connection()?;
|
||||
|
||||
let products = products::table
|
||||
.load::<Product>(&conn)?;
|
||||
let products = match products::table
|
||||
.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)
|
||||
}
|
||||
|
||||
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 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<ProductMessage> for Product {
|
||||
fn from(product: ProductMessage) -> Self {
|
||||
Product {
|
||||
productid: product.productid,
|
||||
productname: product.productname,
|
||||
created_at: Utc::now().naive_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"];
|
||||
//
|
||||
Reference in New Issue
Block a user