From 99dabd3f2f81ffcf0b6f2b59e13ebb4502b2ccac Mon Sep 17 00:00:00 2001 From: "Adam T. Carpenter" Date: Thu, 22 Oct 2020 17:44:38 -0400 Subject: Added product migration, better organization of DTOs, Entities, and Domain Models. Also made config loading/photo generation easier. --- .../migrations/2020-10-21-221149_products/down.sql | 1 + .../migrations/2020-10-21-221149_products/up.sql | 11 ++++ dichroism/src/config.rs | 8 +++ dichroism/src/constants.rs | 3 + dichroism/src/dtos/mod.rs | 70 +++++++++++++++++++++ dichroism/src/entities/mod.rs | 7 +++ dichroism/src/entities/photo.rs | 5 ++ dichroism/src/entities/photo_set.rs | 8 +++ dichroism/src/entities/product.rs | 11 ++++ dichroism/src/handlers.rs | 40 +++++------- dichroism/src/main.rs | 18 +++--- dichroism/src/models/photo.rs | 24 +++++++- dichroism/src/models/photo_set.rs | 72 ++++++++++++++++++++-- dichroism/src/models/product.rs | 26 ++------ dichroism/src/photo_repo.rs | 41 ------------ dichroism/src/product_repo.rs | 19 ++---- dichroism/src/schema.rs | 15 +++++ 17 files changed, 260 insertions(+), 119 deletions(-) create mode 100644 dichroism/migrations/2020-10-21-221149_products/down.sql create mode 100644 dichroism/migrations/2020-10-21-221149_products/up.sql create mode 100644 dichroism/src/dtos/mod.rs create mode 100644 dichroism/src/entities/mod.rs create mode 100644 dichroism/src/entities/photo.rs create mode 100644 dichroism/src/entities/photo_set.rs create mode 100644 dichroism/src/entities/product.rs delete mode 100644 dichroism/src/photo_repo.rs diff --git a/dichroism/migrations/2020-10-21-221149_products/down.sql b/dichroism/migrations/2020-10-21-221149_products/down.sql new file mode 100644 index 0000000..f800c5c --- /dev/null +++ b/dichroism/migrations/2020-10-21-221149_products/down.sql @@ -0,0 +1 @@ +DROP TABLE "products" diff --git a/dichroism/migrations/2020-10-21-221149_products/up.sql b/dichroism/migrations/2020-10-21-221149_products/up.sql new file mode 100644 index 0000000..9e9c696 --- /dev/null +++ b/dichroism/migrations/2020-10-21-221149_products/up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "products" ( + "id" INTEGER NOT NULL, + "photo_set" INTEGER NOT NULL, + "cents" INTEGER NOT NULL, + "quantity" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "featured" INTEGER NOT NULL, + PRIMARY KEY("id"), + FOREIGN KEY("photo_set") REFERENCES "photo_sets"("id") +) diff --git a/dichroism/src/config.rs b/dichroism/src/config.rs index c1c42f0..0dbc10e 100644 --- a/dichroism/src/config.rs +++ b/dichroism/src/config.rs @@ -1,5 +1,6 @@ use crate::constants::DEFAULT_CONFIG; use crate::result::Result; +use once_cell::sync::Lazy; use serde::Deserialize; use std::env::var; use std::fs::File; @@ -7,6 +8,13 @@ use std::io::prelude::*; use std::net::SocketAddr; use toml::from_str; +pub static CONFIG_INSTANCE: Lazy = Lazy::new(|| { + Config::from_toml().unwrap_or_else(|e| { + eprintln!("Error: {}", e.to_string()); + std::process::exit(1); + }) +}); + #[derive(Debug, Clone, Deserialize)] pub struct Config { pub db_url: String, diff --git a/dichroism/src/constants.rs b/dichroism/src/constants.rs index 87d0442..72a8de9 100644 --- a/dichroism/src/constants.rs +++ b/dichroism/src/constants.rs @@ -1 +1,4 @@ pub const DEFAULT_CONFIG: &str = "./Dichroism.toml"; +pub const PHOTO_FULLSIZE_XY: u32 = 1000; +pub const PHOTO_BASE_XY: u32 = 640; +pub const PHOTO_THUMBNAIL_XY: u32 = 300; diff --git a/dichroism/src/dtos/mod.rs b/dichroism/src/dtos/mod.rs new file mode 100644 index 0000000..76b04c7 --- /dev/null +++ b/dichroism/src/dtos/mod.rs @@ -0,0 +1,70 @@ +use crate::models; +use crate::result::Result; + +#[derive(Debug, Deserialize)] +pub struct NewProduct { + pub name: String, + pub quantity: u32, + pub cents: u32, + pub description: String, + pub featured: bool, + pub category_path: String, + pub photo_data: String, +} + +impl Into> for NewProduct { + fn into(self) -> Result { + Ok(models::Product { + id: 0, + name: self.name, + quantity: self.quantity, + cents: self.cents, + description: self.description, + featured: self.featured, + category: self.category_path, + photo_set: models::PhotoSet::from_data_uri(&self.photo_data)?, + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct ProductPatch { + pub name: Option, + pub quantity: Option, + pub cents: Option, + pub description: Option, + pub featured: Option, + pub category_path: Option, + pub photo_data: Option, +} + +#[derive(Debug, Serialize)] +pub struct Product { + pub id: u32, + pub name: String, + pub description: String, + pub cents: u32, + pub quantity: u32, + pub featured: bool, + pub photo_base: String, + pub photo_fullsize: String, + pub photo_thumbnail: String, + pub category: String, +} + +impl From for Product { + fn from(p: models::Product) -> Self { + Product { + id: p.id, + name: p.name, + description: p.description, + cents: p.cents, + quantity: p.quantity, + featured: p.featured, + category: p.category, + photo_fullsize: p.photo_set.fullsize.path, + photo_base: p.photo_set.base.path, + photo_thumbnail: p.photo_set.thumbnail.path, + } + } +} diff --git a/dichroism/src/entities/mod.rs b/dichroism/src/entities/mod.rs new file mode 100644 index 0000000..0b17cf7 --- /dev/null +++ b/dichroism/src/entities/mod.rs @@ -0,0 +1,7 @@ +mod photo; +mod photo_set; +mod product; + +pub use photo::Photo; +pub use photo_set::PhotoSet; +pub use product::Product; diff --git a/dichroism/src/entities/photo.rs b/dichroism/src/entities/photo.rs new file mode 100644 index 0000000..34e9c28 --- /dev/null +++ b/dichroism/src/entities/photo.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone, Serialize)] +pub struct Photo { + pub id: i32, + pub path: String, +} diff --git a/dichroism/src/entities/photo_set.rs b/dichroism/src/entities/photo_set.rs new file mode 100644 index 0000000..b9f7710 --- /dev/null +++ b/dichroism/src/entities/photo_set.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, Serialize)] +pub struct PhotoSet { + pub id: i32, + pub base: i32, + pub fullsize: i32, + pub thumbnail: i32, + pub original: i32, +} diff --git a/dichroism/src/entities/product.rs b/dichroism/src/entities/product.rs new file mode 100644 index 0000000..4f47c1d --- /dev/null +++ b/dichroism/src/entities/product.rs @@ -0,0 +1,11 @@ +#[derive(Debug, Clone, Queryable, Serialize)] +pub struct Product { + pub id: i32, + pub photo_set: i32, + pub cents: i32, + pub quantity: i32, + pub name: String, + pub description: String, + pub featured: i32, +} + diff --git a/dichroism/src/handlers.rs b/dichroism/src/handlers.rs index 1f657c2..beecb72 100644 --- a/dichroism/src/handlers.rs +++ b/dichroism/src/handlers.rs @@ -1,6 +1,6 @@ -//use super::product_repo; -use super::types::DbPool; -//use crate::config::Config; +use crate::dtos::*; +use crate::product_repo; +use crate::types::DbPool; use actix_web::{get, patch, post, web, Error, HttpResponse, Responder}; #[get("/")] @@ -9,24 +9,24 @@ async fn hello() -> impl Responder { } #[get("/products")] -async fn get_products(_pool: web::Data) -> Result { - dbg!("got products"); - Ok(HttpResponse::Ok().finish()) - //let conn = pool.get().expect("Couldn't get DB connection from pool."); - //let products = web::block(move || product_repo::read_products(&conn)) - // .await - // .map_err(|e| { - // eprintln!("{}", e); - // HttpResponse::InternalServerError().finish() - // })?; +async fn get_products(pool: web::Data) -> Result { + let conn = pool.get().expect("Couldn't get DB connection from pool."); + let products = web::block(move || product_repo::read_products(&conn)) + .await + .map_err(|e| { + eprintln!("{}", e); + HttpResponse::InternalServerError().finish() + })?; + //dbg!(&products); //Ok(HttpResponse::Ok().json(products)) + todo!() } #[patch("/products/{id}")] async fn update_product( _pool: web::Data, id: web::Path, - updated_product: web::Json, + updated_product: web::Json, ) -> Result { dbg!(id, updated_product); Ok(HttpResponse::Ok().finish()) @@ -40,15 +40,3 @@ async fn create_product( dbg!(new_product); Ok(HttpResponse::Ok().finish()) } - -#[derive(Debug, Deserialize)] -pub struct NewProduct { - pub name: String, - pub description: String, -} - -#[derive(Debug, Deserialize)] -pub struct UpdatedProduct { - pub name: Option, - pub description: Option, -} diff --git a/dichroism/src/main.rs b/dichroism/src/main.rs index fb5e2c8..5ecd9a4 100644 --- a/dichroism/src/main.rs +++ b/dichroism/src/main.rs @@ -4,7 +4,7 @@ extern crate serde; extern crate diesel; use actix_web::{App, HttpServer}; -use config::Config; +use config::CONFIG_INSTANCE as CONFIG; use diesel::prelude::SqliteConnection; use diesel::r2d2::ConnectionManager; use diesel::r2d2::Pool; @@ -13,10 +13,11 @@ use result::Result; mod config; mod constants; +mod dtos; +mod entities; mod error; mod handlers; mod models; -mod photo_repo; mod product_repo; mod result; mod schema; @@ -24,18 +25,13 @@ mod types; #[actix_web::main] async fn main() -> Result<()> { - // Gather config. - let config = Config::from_toml()?; - let bind_addr = config.bind_addr; - // Initialize DB connection pool. - let manager = ConnectionManager::::new(&config.db_url); + let manager = ConnectionManager::::new(&CONFIG.db_url); let pool = Pool::builder().build(manager)?; // Initialize application server. let mut server = HttpServer::new(move || { App::new() - .data(config.clone()) .data(pool.clone()) .service(handlers::hello) .service(handlers::get_products) @@ -43,16 +39,16 @@ async fn main() -> Result<()> { .service(handlers::create_product) }); + // If using listenfd, bind to it instead of the configured address to allow for cargo watch + // auto-reloading let mut listenfd = ListenFd::from_env(); server = if let Some(l) = listenfd .take_tcp_listener(0) .expect("Unable to grab TCP listener!") { - // If using listenfd, use it to allow for cargo watch auto-reloading. server.listen(l)? } else { - // Bind to config for release. - server.bind(bind_addr)? + server.bind(CONFIG.bind_addr)? }; Ok(server.run().await?) diff --git a/dichroism/src/models/photo.rs b/dichroism/src/models/photo.rs index 2e8b3bf..dee6288 100644 --- a/dichroism/src/models/photo.rs +++ b/dichroism/src/models/photo.rs @@ -1,5 +1,27 @@ +use crate::config::CONFIG_INSTANCE; +use crate::error::DichroismError; +use crate::result::Result; +use image::DynamicImage; +use std::path::PathBuf; +use uuid::Uuid; + #[derive(Debug, Queryable, Serialize, Clone)] pub struct Photo { - pub id: i32, pub path: String, } + +impl Photo { + pub fn from_image(image: &DynamicImage) -> Result { + let base_name = Uuid::new_v3(&Uuid::NAMESPACE_OID, &image.to_bytes()) + .to_hyphenated() + .to_string(); + let mut path = PathBuf::from(&CONFIG_INSTANCE.img_root); + path.push(base_name); + path.set_extension("jpg"); + image.save(&path)?; + + Ok(Self { + path: path.to_str().ok_or(DichroismError::ImageWrite)?.to_string(), + }) + } +} diff --git a/dichroism/src/models/photo_set.rs b/dichroism/src/models/photo_set.rs index 2f6c128..84fe0e1 100644 --- a/dichroism/src/models/photo_set.rs +++ b/dichroism/src/models/photo_set.rs @@ -1,8 +1,68 @@ -#[derive(Debug, Queryable, Clone)] +use super::photo::Photo; +use crate::constants::{PHOTO_BASE_XY, PHOTO_FULLSIZE_XY, PHOTO_THUMBNAIL_XY}; +use crate::error::DichroismError; +use crate::result::Result; +use base64::decode; +use image::imageops::FilterType; +use image::GenericImageView; +use once_cell::sync::Lazy; +use regex::Regex; + +static DATA_URI_RE: Lazy = Lazy::new(|| { + Regex::new("^data:image/(png|jpeg);base64,(?P.+)") + .expect("Couldn't parse data URI Regex.") +}); + pub struct PhotoSet { - pub id: i32, - pub base: i32, - pub fullsize: i32, - pub thumbnail: i32, - pub original: i32, + pub original: Photo, // original, just for safe-keeping + pub fullsize: Photo, // full-size, "zoomed" view + pub base: Photo, // basic viewing + pub thumbnail: Photo, // tiny, square thumbnail +} + +impl PhotoSet { + pub fn from_data_uri(uri: &str) -> Result { + let data = DATA_URI_RE + .captures(uri) + .ok_or(DichroismError::UriDataExtract)? + .name("data") + .ok_or(DichroismError::UriDataExtract)? + .as_str(); + let original = image::load_from_memory(&decode(data)?)?; + let fullsize = original.resize(PHOTO_FULLSIZE_XY, PHOTO_FULLSIZE_XY, FilterType::Lanczos3); + let base = original.resize(PHOTO_BASE_XY, PHOTO_BASE_XY, FilterType::Lanczos3); + + let (width, height) = original.dimensions(); + let thumbnail = if width > height { + let offset = (width - height) / 2; + original.crop_imm(offset, 0, width - offset * 2, height) + } else { + let offset = (height - width) / 2; + original.crop_imm(0, offset, width, height - offset * 2) + } + .resize(PHOTO_THUMBNAIL_XY, PHOTO_THUMBNAIL_XY, FilterType::Lanczos3); + + Ok(Self { + original: Photo::from_image(&original)?, + fullsize: Photo::from_image(&fullsize)?, + base: Photo::from_image(&base)?, + thumbnail: Photo::from_image(&thumbnail)?, + }) + } +} + +#[cfg(test)] +mod tests { + //use super::*; + + const TEST_DATA_URI: &str = include_str!("../unit_test_data/img_data_uri.txt"); + const TEST_DATA_BASE64: &str = include_str!("../unit_test_data/test_data_base64.txt"); + + #[test] + fn test_gen_product_images() { + // PhotoSet::from_data_uri(TEST_DATA_URI.trim()) + // .unwrap() + // .commit(".") + // .unwrap(); + } } diff --git a/dichroism/src/models/product.rs b/dichroism/src/models/product.rs index 9b81b8a..be0a5ec 100644 --- a/dichroism/src/models/product.rs +++ b/dichroism/src/models/product.rs @@ -1,26 +1,12 @@ -#[derive(Debug, Clone, Queryable, Serialize)] +use super::PhotoSet; + pub struct Product { - pub id: i32, + pub id: u32, pub name: String, - pub quantity: i32, - pub cents: i32, pub description: String, + pub cents: u32, + pub quantity: u32, pub featured: bool, - pub category_path: String, pub photo_set: PhotoSet, -} - -#[derive(Debug, Clone, Serialize)] -pub struct PhotoSet { - pub id: i32, - pub base: i32, - pub fullsize: i32, - pub thumbnail: i32, - pub original: i32, -} - -#[derive(Debug, Clone, Serialize)] -pub struct Photo { - pub id: i32, - pub path: String, + pub category: String, } diff --git a/dichroism/src/photo_repo.rs b/dichroism/src/photo_repo.rs deleted file mode 100644 index 64390c8..0000000 --- a/dichroism/src/photo_repo.rs +++ /dev/null @@ -1,41 +0,0 @@ -use super::models::*; -use diesel::prelude::*; -use diesel::result::Error; - -type DBConn = SqliteConnection; - -pub fn read_photos(conn: &DBConn) -> Result, Error> { - use crate::schema::photos::dsl::*; - let results = photos.load::(conn)?; - Ok(results) -} - -fn read_photos_by_path(conn: &DBConn, path: &str) -> Result, Error> { - use crate::schema::photos::dsl::*; - let results = photos.filter(path.eq(path)).load::(conn)?; - Ok(results) -} - -pub fn read_photo_by_path(conn: &DBConn, path: &str) -> Result, Error> { - use crate::schema::photos::dsl::*; - let results = photos.filter(path.eq(path)).limit(1).load::(conn)?; - Ok(results.first().cloned()) -} - -pub fn read_photo_by_id(conn: &DBConn, id: i32) -> Result, Error> { - use crate::schema::photos::dsl::*; - let results = photos.filter(id.eq(id)).limit(1).load::(conn)?; - Ok(results.first().cloned()) -} - -pub fn create_photo(conn: &DBConn, new_photo: NewPhoto) -> Result, Error> { - use super::schema::photos; - diesel::insert_into(photos::table) - .values(&new_photo) - .execute(conn)?; - read_photo_by_path(conn, &new_photo.path) -} - -pub fn update_photo() { - todo!() -} diff --git a/dichroism/src/product_repo.rs b/dichroism/src/product_repo.rs index 852b930..93176b2 100644 --- a/dichroism/src/product_repo.rs +++ b/dichroism/src/product_repo.rs @@ -1,21 +1,12 @@ -use super::models::Product; +use crate::entities; +use crate::models; use diesel::prelude::*; use diesel::result::Error; type DBConn = SqliteConnection; -pub fn create_product(conn: &DBConn) -> Result { - todo!() -} - -pub fn read_products(_conn: &DBConn) -> Result, Error> { - todo!() -} - -pub fn update_product(conn: &DBConn) -> Result { - todo!() -} - -pub fn delete_product(conn: &DBConn) -> Result<(), Error> { +pub fn read_products(conn: &DBConn) -> Result, Error> { + use crate::schema::products::dsl::*; + let results = products.load::(conn)?; todo!() } diff --git a/dichroism/src/schema.rs b/dichroism/src/schema.rs index 5b038e6..9eb92d8 100644 --- a/dichroism/src/schema.rs +++ b/dichroism/src/schema.rs @@ -15,7 +15,22 @@ table! { } } +table! { + products (id) { + id -> Integer, + photo_set -> Integer, + cents -> Integer, + quantity -> Integer, + name -> Text, + description -> Text, + featured -> Integer, + } +} + +joinable!(products -> photo_sets (photo_set)); + allow_tables_to_appear_in_same_query!( photo_sets, photos, + products, ); -- cgit v1.2.3