diff --git a/Cargo.lock b/Cargo.lock index a393bec..854fb6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1371,8 +1371,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1647,6 +1649,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 0.26.1", ] [[package]] @@ -1792,6 +1795,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1836,6 +1848,19 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "ring", + "serde", + "serde_json", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1990,6 +2015,46 @@ dependencies = [ "digest", ] +[[package]] +name = "meilisearch-index-setting-macro" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54abc57ee746d0f8c2a40ca900af67cbf022b0e4be3d2ff806939322b5f3bc39" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "structmeta", + "syn 2.0.63", +] + +[[package]] +name = "meilisearch-sdk" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d5ded866ed900150a707bd923f2d178ff64a0981d2306c823e8d8003d95ef2" +dependencies = [ + "async-trait", + "bytes", + "either", + "futures", + "futures-io", + "iso8601", + "jsonwebtoken", + "log", + "meilisearch-index-setting-macro", + "pin-project-lite", + "reqwest", + "serde", + "serde_json", + "thiserror", + "time", + "uuid", + "wasm-bindgen-futures", + "web-sys", + "yaup", +] + [[package]] name = "memchr" version = "2.7.2" @@ -2592,6 +2657,53 @@ dependencies = [ "cc", ] +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.6", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls 0.23.6", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -2717,7 +2829,10 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls 0.23.6", "rustls-pemfile 2.1.2", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", @@ -2725,11 +2840,15 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-rustls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", + "webpki-roots 0.26.1", "winreg", ] @@ -2842,6 +2961,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -3442,6 +3567,29 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.63", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "subtle" version = "2.5.0" @@ -3559,18 +3707,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", @@ -3962,6 +4110,7 @@ checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "serde", + "wasm-bindgen", ] [[package]] @@ -4053,6 +4202,7 @@ dependencies = [ "derive_more", "lettre", "log", + "meilisearch-sdk", "mockall", "postgres-es", "pretty_env_logger", @@ -4180,6 +4330,19 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.69" @@ -4422,6 +4585,17 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yaup" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0144f1a16a199846cb21024da74edd930b43443463292f536b7110b4855b5c6" +dependencies = [ + "form_urlencoded", + "serde", + "thiserror", +] + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/Cargo.toml b/Cargo.toml index bbab54e..762f031 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ derive_builder = "0.20.0" derive_more = "0.99.17" lettre = { version = "0.11.7", features = ["tokio1-rustls-tls", "tracing", "dkim", "tokio1-native-tls", "smtp-transport", "pool", "builder"], default-features = false } log = "0.4.21" +meilisearch-sdk = "0.27.0" mockall = { version = "0.12.1", features = ["nightly"] } postgres-es = "0.4.11" pretty_env_logger = "0.5.0" diff --git a/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs b/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs new file mode 100644 index 0000000..9c097b8 --- /dev/null +++ b/src/inventory/adapters/output/full_text_search/meili/add_product_to_store.rs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::InventoryFTSMeili; +use crate::inventory::application::port::output::full_text_search::{ + add_product_to_store::*, errors::*, +}; +use crate::inventory::domain::{category_aggregate::*, product_aggregate::*}; +//use super::errors::*; + +#[derive( + Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct MeiliProduct { + name: String, + description: Option, + image: Option, // string = file_name + price: Price, + category_name: String, + product_id: Uuid, +} + +impl MeiliProduct { + pub fn new(product: &Product, category: &Category) -> Self { + Self { + name: product.name().into(), + description: product.description().clone(), + image: product.image().clone(), + price: product.price().clone(), + category_name: category.name().into(), + product_id: *product.product_id(), + } + } +} + +#[async_trait::async_trait] +impl AddProductToStoreFTSPort for InventoryFTSMeili { + async fn add_product_to_store( + &self, + product: &Product, + category: &Category, + ) -> InventoryFTSResult<()> { + let store_index = self.client.index(category.store_id().to_string()); + let meili_product = MeiliProduct::new(product, category); + store_index + .add_documents(&[meili_product], Some("product_id")) + .await + .unwrap(); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use uuid::Uuid; + + use crate::types::quantity::Quantity; + + use super::*; + + #[actix_rt::test] + async fn test_meili() { + let settings = crate::settings::tests::get_settings().await; + let fts = InventoryFTSMeili::new(&settings.meili.url, &settings.meili.api_key); + + let category = Category::default(); + let product = ProductBuilder::default() + .name("test_meili_product".into()) + .description(Some("this is a test product".into())) + .image(None) + .price( + PriceBuilder::default() + .major(100) + .minor(0) + .currency(Currency::INR) + .build() + .unwrap(), + ) + .quantity(Quantity::default()) + .sku_able(false) + .deleted(false) + .category_id(*category.category_id()) + .product_id(Uuid::new_v4()) + .build() + .unwrap(); + + fts.add_product_to_store(&product, &category).await.unwrap(); + } +} diff --git a/src/inventory/adapters/output/full_text_search/meili/mod.rs b/src/inventory/adapters/output/full_text_search/meili/mod.rs new file mode 100644 index 0000000..009fbcd --- /dev/null +++ b/src/inventory/adapters/output/full_text_search/meili/mod.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use meilisearch_sdk::client::*; + +mod add_product_to_store; + +#[derive(Clone)] +pub struct InventoryFTSMeili { + client: Client, +} + +impl InventoryFTSMeili { + pub fn new(meili_url: &str, api_key: &str) -> Self { + let client = Client::new(meili_url, Some(api_key)).unwrap(); + Self { client } + } +} diff --git a/src/inventory/adapters/output/full_text_search/mod.rs b/src/inventory/adapters/output/full_text_search/mod.rs new file mode 100644 index 0000000..f7b764c --- /dev/null +++ b/src/inventory/adapters/output/full_text_search/mod.rs @@ -0,0 +1,5 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +mod meili; diff --git a/src/inventory/adapters/output/meili/add_product_to_store.rs b/src/inventory/adapters/output/meili/add_product_to_store.rs new file mode 100644 index 0000000..9c097b8 --- /dev/null +++ b/src/inventory/adapters/output/meili/add_product_to_store.rs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::InventoryFTSMeili; +use crate::inventory::application::port::output::full_text_search::{ + add_product_to_store::*, errors::*, +}; +use crate::inventory::domain::{category_aggregate::*, product_aggregate::*}; +//use super::errors::*; + +#[derive( + Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, Getters, Builder, +)] +pub struct MeiliProduct { + name: String, + description: Option, + image: Option, // string = file_name + price: Price, + category_name: String, + product_id: Uuid, +} + +impl MeiliProduct { + pub fn new(product: &Product, category: &Category) -> Self { + Self { + name: product.name().into(), + description: product.description().clone(), + image: product.image().clone(), + price: product.price().clone(), + category_name: category.name().into(), + product_id: *product.product_id(), + } + } +} + +#[async_trait::async_trait] +impl AddProductToStoreFTSPort for InventoryFTSMeili { + async fn add_product_to_store( + &self, + product: &Product, + category: &Category, + ) -> InventoryFTSResult<()> { + let store_index = self.client.index(category.store_id().to_string()); + let meili_product = MeiliProduct::new(product, category); + store_index + .add_documents(&[meili_product], Some("product_id")) + .await + .unwrap(); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use uuid::Uuid; + + use crate::types::quantity::Quantity; + + use super::*; + + #[actix_rt::test] + async fn test_meili() { + let settings = crate::settings::tests::get_settings().await; + let fts = InventoryFTSMeili::new(&settings.meili.url, &settings.meili.api_key); + + let category = Category::default(); + let product = ProductBuilder::default() + .name("test_meili_product".into()) + .description(Some("this is a test product".into())) + .image(None) + .price( + PriceBuilder::default() + .major(100) + .minor(0) + .currency(Currency::INR) + .build() + .unwrap(), + ) + .quantity(Quantity::default()) + .sku_able(false) + .deleted(false) + .category_id(*category.category_id()) + .product_id(Uuid::new_v4()) + .build() + .unwrap(); + + fts.add_product_to_store(&product, &category).await.unwrap(); + } +} diff --git a/src/inventory/adapters/output/meili/mod.rs b/src/inventory/adapters/output/meili/mod.rs new file mode 100644 index 0000000..009fbcd --- /dev/null +++ b/src/inventory/adapters/output/meili/mod.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use meilisearch_sdk::client::*; + +mod add_product_to_store; + +#[derive(Clone)] +pub struct InventoryFTSMeili { + client: Client, +} + +impl InventoryFTSMeili { + pub fn new(meili_url: &str, api_key: &str) -> Self { + let client = Client::new(meili_url, Some(api_key)).unwrap(); + Self { client } + } +} diff --git a/src/inventory/application/port/output/full_text_search/add_product_to_store.rs b/src/inventory/application/port/output/full_text_search/add_product_to_store.rs new file mode 100644 index 0000000..265e0ed --- /dev/null +++ b/src/inventory/application/port/output/full_text_search/add_product_to_store.rs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use mockall::predicate::*; +use mockall::*; + +use super::errors::*; +#[cfg(test)] +#[allow(unused_imports)] +pub use tests::*; + +use crate::inventory::domain::{category_aggregate::*, product_aggregate::*}; + +#[automock] +#[async_trait::async_trait] +pub trait AddProductToStoreFTSPort: Send + Sync { + async fn add_product_to_store( + &self, + product: &Product, + cateogry: &Category, + ) -> InventoryFTSResult<()>; +} + +pub type AddProductToStoreFTSPortObj = std::sync::Arc; + +#[cfg(test)] +pub mod tests { + use super::*; + + use std::sync::Arc; + + pub fn mock_add_product_to_store_fts_port(times: Option) -> AddProductToStoreFTSPortObj { + let mut m = MockAddProductToStoreFTSPort::new(); + if let Some(times) = times { + m.expect_add_product_to_store() + .times(times) + .returning(|_, _| Ok(())); + } else { + m.expect_add_product_to_store().returning(|_, _| Ok(())); + } + + Arc::new(m) + } +} diff --git a/src/inventory/application/port/output/full_text_search/errors.rs b/src/inventory/application/port/output/full_text_search/errors.rs new file mode 100644 index 0000000..7a28f77 --- /dev/null +++ b/src/inventory/application/port/output/full_text_search/errors.rs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use derive_more::Display; +use serde::{Deserialize, Serialize}; + +pub type InventoryFTSResult = Result; + +#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub enum InventoryFTSError {} diff --git a/src/inventory/application/port/output/full_text_search/mod.rs b/src/inventory/application/port/output/full_text_search/mod.rs new file mode 100644 index 0000000..422bcd3 --- /dev/null +++ b/src/inventory/application/port/output/full_text_search/mod.rs @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub mod add_product_to_store; +pub mod errors; +pub mod update_product; diff --git a/src/inventory/application/port/output/full_text_search/update_product.rs b/src/inventory/application/port/output/full_text_search/update_product.rs new file mode 100644 index 0000000..56f60de --- /dev/null +++ b/src/inventory/application/port/output/full_text_search/update_product.rs @@ -0,0 +1,3 @@ +// SPDX-FileCopyrightText: 2024 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/src/inventory/application/port/output/mod.rs b/src/inventory/application/port/output/mod.rs index 1589173..8a85158 100644 --- a/src/inventory/application/port/output/mod.rs +++ b/src/inventory/application/port/output/mod.rs @@ -3,3 +3,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod db; +pub mod full_text_search;