Skip to content

Commit

Permalink
Merge pull request #9 from MiSArch/product-variant-validation
Browse files Browse the repository at this point in the history
Validate product variants
  • Loading branch information
legendofa authored Jan 24, 2024
2 parents 73a1434 + 5966ab0 commit ed090ce
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 56 deletions.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-graphql = { version = "6.0.11", features = ["bson", "chrono", "uuid"] }
async-graphql = { version = "6.0.11", features = ["bson", "chrono", "uuid", "log"] }
async-graphql-axum = "6.0.11"
tokio = { version = "1.8", features = ["macros", "rt-multi-thread"] }
hyper = "1.0.1"
Expand All @@ -21,3 +21,6 @@ uuid = { version = "1.6.1", features = ["v4", "serde"] }
mongodb-cursor-pagination = "0.3.2"
dapr = "0.13.0"
tonic = "0.8"
json = "0.12.4"
log = "0.4.20"
simple_logger = "4.3.3"
37 changes: 17 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,22 @@
### What it can do

- CRUD wishlists:
```rust
pub struct Wishlist {
pub id: Uuid,
pub user_id: Uuid,
pub product_variants: HashSet<ProductVariant>,
pub name: String,
pub created_at: DateTime,
pub last_updated_at: DateTime,
}

/// Foreign ProductVariant
pub struct ProductVariant{
id: Uuid
}
```
- Validates all UUIDs input as strings
- Error prop to GraphQL

### Configuration
```rust
pub struct Wishlist {
pub id: Uuid,
pub user_id: Uuid,
pub product_variants: HashSet<ProductVariant>,
pub name: String,
pub created_at: DateTime,
pub last_updated_at: DateTime,
}

/// Foreign ProductVariant
pub struct ProductVariant{
id: Uuid
}
```

- The environment variables `${MONGODB_USERNAME}` and `${MONGODB_PASSWORD}` should be set in production to configure the database credentials.
- `${MONGODB_URL}` can be reconfigured in `.env` for experimentation purposes.
- Validates all UUIDs input as strings
- Error prop to GraphQL
4 changes: 3 additions & 1 deletion docker-compose-base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ services:
"--app-id",
"wishlist",
"--app-port",
"8080",
"50051",
"--app-protocol",
"grpc",
"--dapr-http-port",
"3500",
"-placement-host-address",
Expand Down
66 changes: 61 additions & 5 deletions src/app_callback_service.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
use json::JsonValue;
use log::info;
use mongodb::Collection;
use tonic::{Request, Response, Status};

use bson::Uuid;
use dapr::{appcallback::*, dapr::dapr::proto::runtime::v1::app_callback_server::AppCallback};

#[derive(Default)]
pub struct AppCallbackService {}
use crate::foreign_types::ProductVariant;

pub struct AppCallbackService {
pub collection: Collection<ProductVariant>,
}

impl AppCallbackService {
/// Add a newly created product variant to MongoDB.
pub async fn add_product_variant_to_mongodb(
&self,
product_variant_id: Uuid,
) -> Result<(), Status> {
let product_variant = ProductVariant {
_id: product_variant_id,
};
match self.collection.insert_one(product_variant, None).await {
Ok(_) => Ok(()),
Err(_) => Err(Status::internal(
"Adding product variant failed in MongoDB.",
)),
}
}
}

#[tonic::async_trait]
impl AppCallback for AppCallbackService {
Expand Down Expand Up @@ -39,11 +64,17 @@ impl AppCallback for AppCallbackService {
) -> Result<Response<TopicEventResponse>, Status> {
let r = request.into_inner();
let data = &r.data;
let data_content_type = &r.data_content_type;

let message = String::from_utf8_lossy(data);
println!("Message: {}", &message);
println!("Content-Type: {}", &data_content_type);
let error_message = format!("Expected message to be parsable JSON, got: {}", message);
let message_json = json::parse(&message).map_err(|_| Status::internal(error_message))?;
let product_variant_id_json_value = &message_json["id"];
let product_variant_id = parse_product_variant_id(product_variant_id_json_value)?;

info!("Event with message was received: {}", &message);

self.add_product_variant_to_mongodb(product_variant_id)
.await?;

Ok(Response::new(TopicEventResponse::default()))
}
Expand All @@ -64,3 +95,28 @@ impl AppCallback for AppCallbackService {
Ok(Response::new(BindingEventResponse::default()))
}
}

/// Parses Uuid from JsonValue containing a String.
fn parse_product_variant_id(product_variant_id_json_value: &JsonValue) -> Result<Uuid, Status> {
match product_variant_id_json_value {
json::JsonValue::String(product_variant_id_string) => {
match Uuid::parse_str(product_variant_id_string) {
Ok(product_variant_id_uuid) => Ok(product_variant_id_uuid),
Err(_) => {
let error_message = format!(
"String value in `id` field cannot be parsed as bson::Uuid, got: {}",
product_variant_id_string
);
Err(Status::internal(error_message))?
}
}
}
_ => {
let error_message = format!(
"`id` field does not exist or does not contain a String value, got: {}",
product_variant_id_json_value
);
Err(Status::internal(error_message))?
}
}
}
8 changes: 4 additions & 4 deletions src/foreign_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ use std::{cmp::Ordering, hash::Hash};
#[graphql(unresolvable)]
pub struct User {
/// UUID of the user.
pub id: Uuid,
pub _id: Uuid,
}

/// Foreign type of a product variant.
#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq, Copy, Clone, SimpleObject)]
#[graphql(unresolvable)]
pub struct ProductVariant {
/// UUID of the product variant.
pub id: Uuid,
pub _id: Uuid,
}

impl PartialOrd for ProductVariant {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.id.partial_cmp(&other.id)
self._id.partial_cmp(&other._id)
}
}

impl From<ProductVariant> for Bson {
fn from(value: ProductVariant) -> Self {
Bson::Document(doc!("id": value.id))
Bson::Document(doc!("_id": value._id))
}
}
34 changes: 22 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use std::{collections::HashSet, env, fs::File, io::Write};

use async_graphql::{http::GraphiQLSource, EmptySubscription, SDLExportOptions, Schema};
use async_graphql::{
extensions::Logger, http::GraphiQLSource, EmptySubscription, SDLExportOptions, Schema,
};
use async_graphql_axum::GraphQL;
use axum::{
response::{self, IntoResponse},
routing::get,
Router, Server,
};
use clap::{arg, command, Parser};
use simple_logger::SimpleLogger;

use foreign_types::User;
use log::info;
use mongodb::{bson::DateTime, options::ClientOptions, Client, Collection, Database};

use dapr::dapr::dapr::proto::runtime::v1::app_callback_server::AppCallbackServer;
Expand All @@ -29,6 +33,8 @@ use mutation::Mutation;
mod app_callback_service;
use app_callback_service::AppCallbackService;

use crate::foreign_types::ProductVariant;

mod base_connection;
mod foreign_types;
mod mutation_input_structs;
Expand Down Expand Up @@ -61,13 +67,14 @@ async fn db_connection() -> Client {
/// Establishes connection to Dapr.
///
/// Adds AppCallbackService which defines pub/sub interaction with Dapr.
async fn dapr_connection() {
let addr = "[::]:50006".parse().unwrap();
async fn dapr_connection(db_client: Database) {
let addr = "[::]:50051".parse().unwrap();
let collection: mongodb::Collection<ProductVariant> =
db_client.collection::<ProductVariant>("product_variants");

let callback_service = AppCallbackService::default();

println!("AppCallback server listening on: {}", addr);
let callback_service = AppCallbackService { collection };

info!("AppCallback server listening on: {}", addr);
// Create a gRPC server with the callback_service.
TonicServer::builder()
.add_service(AppCallbackServer::new(callback_service))
Expand All @@ -81,7 +88,7 @@ async fn dapr_connection() {
async fn insert_dummy_data(collection: &Collection<Wishlist>) {
let wishlists: Vec<Wishlist> = vec![Wishlist {
_id: Uuid::new(),
user: User { id: Uuid::new() },
user: User { _id: Uuid::new() },
internal_product_variants: HashSet::new(),
name: String::from("test"),
created_at: DateTime::now(),
Expand All @@ -99,16 +106,19 @@ struct Args {
generate_schema: bool,
}

/// Activates logger and parses argument for optional schema generation. Otherwise starts gRPC and GraphQL server.
#[tokio::main]
async fn main() -> std::io::Result<()> {
SimpleLogger::new().init().unwrap();

let args = Args::parse();
if args.generate_schema {
let schema = Schema::build(Query, Mutation, EmptySubscription).finish();
let mut file = File::create("./schemas/wishlist.graphql")?;
let sdl_export_options = SDLExportOptions::new().federation();
let schema_sdl = schema.sdl_with_options(sdl_export_options);
file.write_all(schema_sdl.as_bytes())?;
println!("GraphQL schema: ./schemas/wishlist.graphql was successfully generated!");
info!("GraphQL schema: ./schemas/wishlist.graphql was successfully generated!");
} else {
start_service().await;
}
Expand All @@ -119,25 +129,25 @@ async fn main() -> std::io::Result<()> {
async fn start_service() {
let client = db_connection().await;
let db_client: Database = client.database("wishlist-database");
let collection: mongodb::Collection<Wishlist> = db_client.collection::<Wishlist>("wishlists");

let schema = Schema::build(Query, Mutation, EmptySubscription)
.data(collection)
.extension(Logger)
.data(db_client.clone())
.enable_federation()
.finish();

let app = Router::new().route("/", get(graphiql).post_service(GraphQL::new(schema)));
println!("GraphiQL IDE: http://0.0.0.0:8080");

let t1 = tokio::spawn(async {
info!("GraphiQL IDE: http://0.0.0.0:8080");
Server::bind(&"0.0.0.0:8080".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
});

let t2 = tokio::spawn(async {
dapr_connection().await;
dapr_connection(db_client).await;
});

t1.await.unwrap();
Expand Down
Loading

0 comments on commit ed090ce

Please sign in to comment.