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 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) {
|
||||||
|
|||||||
@@ -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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"];
|
|
||||||
Reference in New Issue
Block a user