From cd0ff473cd7fe411b45f3aa03bd242017900f39d Mon Sep 17 00:00:00 2001 From: Shubhranshu Sanjeev Date: Mon, 29 Apr 2024 20:08:08 +0530 Subject: [PATCH] refactor: refactored http calls to `request` fxn --- Cargo.lock | 1 + .../src/api/context/handlers.rs | 7 +- .../src/api/default_config/handlers.rs | 8 +- crates/context_aware_config/src/helpers.rs | 24 ++-- crates/frontend/Cargo.toml | 3 +- .../src/components/context_form/utils.rs | 26 ++-- .../components/default_config_form/utils.rs | 31 ++--- .../src/components/dimension_form/utils.rs | 30 ++-- .../src/components/experiment_form/utils.rs | 54 +++----- .../src/providers/alert_provider/mod.rs | 2 +- crates/frontend/src/types.rs | 7 + crates/frontend/src/utils.rs | 70 +++++++++- crates/service_utils/src/helpers.rs | 128 ++++++++++++++++++ 13 files changed, 282 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91294b02..70b0afe4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1494,6 +1494,7 @@ dependencies = [ "leptos_actix", "leptos_meta", "leptos_router", + "once_cell", "reqwest", "serde", "serde_json", diff --git a/crates/context_aware_config/src/api/context/handlers.rs b/crates/context_aware_config/src/api/context/handlers.rs index df7f0fe3..439339d2 100644 --- a/crates/context_aware_config/src/api/context/handlers.rs +++ b/crates/context_aware_config/src/api/context/handlers.rs @@ -32,6 +32,7 @@ use diesel::{ }; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{from_value, json, Map, Value}; +use service_utils::helpers::validation_err_to_str; use service_utils::service::types::DbConnection; use service_utils::{db_error, not_found, unexpected_error, validation_error}; use std::collections::HashMap; @@ -164,8 +165,8 @@ fn validate_override_with_default_configs( let verrors = e.collect::>(); log::error!("({key}) config key validation error: {:?}", verrors); return Err(validation_error!( - "schema validation failed for {key} with error {:?}", - verrors + "schema validation failed for {key}: {}", + validation_err_to_str(verrors).first().unwrap() )); }; } @@ -522,4 +523,4 @@ async fn bulk_operations( Ok(()) // Commit the transaction })?; Ok(Json(response)) -} +} \ No newline at end of file diff --git a/crates/context_aware_config/src/api/default_config/handlers.rs b/crates/context_aware_config/src/api/default_config/handlers.rs index 6f2755cb..c435f955 100644 --- a/crates/context_aware_config/src/api/default_config/handlers.rs +++ b/crates/context_aware_config/src/api/default_config/handlers.rs @@ -1,5 +1,6 @@ extern crate base64; use super::types::CreateReq; +use service_utils::helpers::validation_err_to_str; use service_utils::{bad_argument, unexpected_error, validation_error}; use superposition_types::{SuperpositionUser, User}; @@ -118,9 +119,8 @@ async fn create( verrors ); return Err(validation_error!( - "Schema validation failed for key {} with error {:?}", - default_config.key, - verrors + "Schema validation failed: {}", + &validation_err_to_str(verrors).first().unwrap() )); } @@ -181,4 +181,4 @@ async fn get(db_conn: DbConnection) -> superposition::Result = default_configs.get_results(&mut conn)?; Ok(Json(result)) -} +} \ No newline at end of file diff --git a/crates/context_aware_config/src/helpers.rs b/crates/context_aware_config/src/helpers.rs index 8b0d943c..9a0af804 100644 --- a/crates/context_aware_config/src/helpers.rs +++ b/crates/context_aware_config/src/helpers.rs @@ -2,7 +2,9 @@ use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue}; use itertools::{self, Itertools}; use jsonschema::{Draft, JSONSchema, ValidationError}; use serde_json::{json, Value}; -use service_utils::{result as superposition, validation_error}; +use service_utils::{ + helpers::validation_err_to_str, result as superposition, validation_error, +}; use std::collections::HashMap; pub fn get_default_config_validation_schema() -> JSONSchema { @@ -168,9 +170,9 @@ pub fn validate_context_jsonschema( verrors ); Err(validation_error!( - "failed to validate dimension value {:?} with error: {:?}", - dimension_value, - verrors + "failed to validate dimension value {}: {}", + dimension_value.to_string(), + validation_err_to_str(verrors).first().unwrap() )) } } @@ -179,14 +181,14 @@ pub fn validate_context_jsonschema( _ => dimension_schema.validate(dimension_value).map_err(|e| { let verrors = e.collect::>(); log::error!( - "failed to validate dimension value {:?} with error : {:?}", - dimension_value, + "failed to validate dimension value {}: {:?}", + dimension_value.to_string(), verrors ); validation_error!( - "failed to validate dimension value {:?} with error: {:?}", - dimension_value, - verrors + "failed to validate dimension value {}: {}", + dimension_value.to_string(), + validation_err_to_str(verrors).first().unwrap() ) }), } @@ -209,8 +211,8 @@ pub fn validate_jsonschema( //TODO: Try & render as json. let verrors = e.collect::>(); Err(validation_error!( - "schema validation failed: {:?}", - verrors.as_slice() + "schema validation failed: {}", + validation_err_to_str(verrors).first().unwrap() )) } }; diff --git a/crates/frontend/Cargo.toml b/crates/frontend/Cargo.toml index b94de46d..3477e4ff 100644 --- a/crates/frontend/Cargo.toml +++ b/crates/frontend/Cargo.toml @@ -30,6 +30,7 @@ strum_macros = { workspace = true } strum = { workspace = true } js-sys = "0.3.65" url = "2.5.0" +once_cell = { workspace = true } [features] @@ -42,4 +43,4 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", -] +] \ No newline at end of file diff --git a/crates/frontend/src/components/context_form/utils.rs b/crates/frontend/src/components/context_form/utils.rs index f68d30c5..7b51a0b5 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,7 +1,6 @@ use crate::types::Dimension; -use crate::utils::{get_config_value, get_host, ConfigType}; +use crate::utils::{get_config_value, get_host, request, ConfigType}; use anyhow::Result; -use reqwest::StatusCode; use serde_json::{json, Map, Value}; pub fn get_condition_schema( @@ -98,21 +97,16 @@ pub async fn create_context( overrides: Map, conditions: Vec<(String, String, String)>, dimensions: Vec, -) -> Result { - let client = reqwest::Client::new(); +) -> Result { let host = get_host(); let url = format!("{host}/context"); let request_payload = construct_request_payload(overrides, conditions, dimensions); - let response = client - .put(url) - .header("x-tenant", tenant) - .json(&request_payload) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), - _ => Err("Internal Server Error".to_string()), - } + request( + url, + reqwest::Method::PUT, + Some(request_payload), + &[("x-tenant", &tenant)], + ) + .await + .map_err(|err| err.to_string()) } diff --git a/crates/frontend/src/components/default_config_form/utils.rs b/crates/frontend/src/components/default_config_form/utils.rs index 32f49291..215cdcc1 100644 --- a/crates/frontend/src/components/default_config_form/utils.rs +++ b/crates/frontend/src/components/default_config_form/utils.rs @@ -1,31 +1,20 @@ use super::types::DefaultConfigCreateReq; -use crate::utils::get_host; -use reqwest::StatusCode; +use crate::utils::{get_host, request}; pub async fn create_default_config( key: String, tenant: String, payload: DefaultConfigCreateReq, -) -> Result { - let client = reqwest::Client::new(); +) -> Result { let host = get_host(); let url = format!("{host}/default-config/{key}"); - let response = client - .put(url) - .header("x-tenant", tenant) - .json(&payload) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK | StatusCode::CREATED => { - response.text().await.map_err(|e| e.to_string()) - } - StatusCode::BAD_REQUEST => Err(response - .text() - .await - .unwrap_or("Validation of configuration value failed, but the error could not be understood by the system. Contact an admin for help if this persists".to_string())), - _ => Err("Internal Server Error".to_string()), - } + request( + url, + reqwest::Method::PUT, + Some(payload), + &[("x-tenant", &tenant)], + ) + .await + .map_err(|err| err.to_string()) } diff --git a/crates/frontend/src/components/dimension_form/utils.rs b/crates/frontend/src/components/dimension_form/utils.rs index c17cffe9..2408bd11 100644 --- a/crates/frontend/src/components/dimension_form/utils.rs +++ b/crates/frontend/src/components/dimension_form/utils.rs @@ -1,26 +1,22 @@ use super::types::DimensionCreateReq; -use crate::utils::get_host; -use reqwest::StatusCode; +use crate::{ + types::Dimension, + utils::{get_host, request}, +}; pub async fn create_dimension( tenant: String, payload: DimensionCreateReq, -) -> Result { - let client = reqwest::Client::new(); +) -> Result { let host = get_host(); let url = format!("{host}/dimension"); - let response = client - .put(url) - .header("x-tenant", tenant) - .json(&payload) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::CREATED => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("Schema Validation Failed".to_string()), - _ => Err("Internal Server Error".to_string()), - } + request( + url, + reqwest::Method::PUT, + Some(payload), + &[("x-tenant", &tenant)], + ) + .await + .map_err(|err| err.to_string()) } diff --git a/crates/frontend/src/components/experiment_form/utils.rs b/crates/frontend/src/components/experiment_form/utils.rs index cc1f661e..7e301cba 100644 --- a/crates/frontend/src/components/experiment_form/utils.rs +++ b/crates/frontend/src/components/experiment_form/utils.rs @@ -3,9 +3,8 @@ use super::types::{ }; use crate::components::context_form::utils::construct_context; use crate::types::{Dimension, Variant}; -use crate::utils::get_host; -use reqwest::StatusCode; -use serde_json::json; +use crate::utils::{get_host, request}; +use serde_json::Value; pub fn validate_experiment(experiment: &ExperimentCreateRequest) -> Result { if experiment.name.is_empty() { @@ -20,7 +19,7 @@ pub async fn create_experiment( name: String, tenant: String, dimensions: Vec, -) -> Result { +) -> Result { let payload = ExperimentCreateRequest { name, variants, @@ -29,29 +28,23 @@ pub async fn create_experiment( let _ = validate_experiment(&payload)?; - let client = reqwest::Client::new(); let host = get_host(); let url = format!("{host}/experiments"); - let request_payload = json!(payload); - let response = client - .post(url) - .header("x-tenant", tenant) - .json(&request_payload) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), - _ => Err("Internal Server Error".to_string()), - } + request( + url, + reqwest::Method::POST, + Some(payload), + &[("x-tenant", &tenant)], + ) + .await + .map_err(|err| err.to_string()) } pub async fn update_experiment( experiment_id: String, variants: Vec, tenant: String, -) -> Result { +) -> Result { let payload = ExperimentUpdateRequest { variants: variants .into_iter() @@ -62,22 +55,15 @@ pub async fn update_experiment( .collect::>(), }; - let client = reqwest::Client::new(); let host = get_host(); let url = format!("{}/experiments/{}/overrides", host, experiment_id); - let request_payload = json!(payload); - let response = client - .put(url) - .header("x-tenant", tenant) - .header("Authorization", "Bearer 12345678") - .json(&request_payload) - .send() - .await - .map_err(|e| e.to_string())?; - match response.status() { - StatusCode::OK => response.text().await.map_err(|e| e.to_string()), - StatusCode::BAD_REQUEST => Err("epxeriment data corrupt".to_string()), - _ => Err("Internal Server Error".to_string()), - } + request( + url, + reqwest::Method::PUT, + Some(payload), + &[("x-tenant", &tenant)], + ) + .await + .map_err(|err| err.to_string()) } diff --git a/crates/frontend/src/providers/alert_provider/mod.rs b/crates/frontend/src/providers/alert_provider/mod.rs index 92fc7241..536bd328 100644 --- a/crates/frontend/src/providers/alert_provider/mod.rs +++ b/crates/frontend/src/providers/alert_provider/mod.rs @@ -2,7 +2,7 @@ use std::time::Duration; use leptos::*; -use crate::components::toast::{Alert, AlertType}; +use crate::components::alert::{Alert, AlertType}; #[derive(Clone, Debug)] pub struct AlertQueue { diff --git a/crates/frontend/src/types.rs b/crates/frontend/src/types.rs index a054e681..7194ed2d 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -214,3 +214,10 @@ pub struct BreadCrums { pub value: Option, pub is_link: bool, } + +/************************************************************************/ + +#[derive(Debug, Clone, Deserialize)] +pub struct ErrorResponse { + pub message: String, +} diff --git a/crates/frontend/src/utils.rs b/crates/frontend/src/utils.rs index fc4cb4fa..331ca7b5 100644 --- a/crates/frontend/src/utils.rs +++ b/crates/frontend/src/utils.rs @@ -1,7 +1,13 @@ use std::env; -use crate::types::{DefaultConfig, Dimension, Envs}; +use crate::{ + components::alert::AlertType, + providers::alert_provider::enqueue_alert, + types::{DefaultConfig, Dimension, Envs, ErrorResponse}, +}; +use anyhow::anyhow; use leptos::*; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use serde_json::{Number, Value}; use std::str::FromStr; use url::Url; @@ -343,3 +349,65 @@ pub fn get_config_value( } } } + +/********* Request Utils **********/ + +use once_cell::sync::Lazy; +static HTTP_CLIENT: Lazy = Lazy::new(|| reqwest::Client::new()); + +pub async fn request<'a, T, R>( + url: String, + method: reqwest::Method, + body: Option, + headers: &[(&str, &str)], +) -> anyhow::Result +where + T: serde::Serialize, + R: serde::de::DeserializeOwned, +{ + let parsed_headers = headers + .into_iter() + .map(|(name, value)| { + let header_name = HeaderName::from_str(name); + let header_value = HeaderValue::from_str(value); + match (header_name, header_value) { + (Ok(name), Ok(value)) => Some((name, value)), + _ => None, + } + }) + .collect::>>() + .ok_or(anyhow!("failed to parse headers"))?; + + let header_map = HeaderMap::from_iter(parsed_headers); + + let mut request_builder = + HTTP_CLIENT.request(method.clone(), url).headers(header_map); + request_builder = match (method, body) { + (reqwest::Method::GET | reqwest::Method::DELETE, _) => request_builder, + (_, Some(data)) => request_builder.json(&data), + _ => request_builder, + }; + + let response = request_builder + .send() + .await + .map_err(|err| anyhow!(err.to_string()))?; + + let status = response.status(); + if status.is_client_error() || status.is_server_error() { + let error_msg = response + .json::() + .await + .map_or(String::from("Something went wrong"), |error| error.message); + enqueue_alert(error_msg.clone(), AlertType::Error, 5000); + + return Err(anyhow!(error_msg)); + } + + response.json::().await.map_err(|err| { + enqueue_alert(err.to_string(), AlertType::Error, 5000); + anyhow!(err.to_string()) + }) +} + +/********* Reqyest Utils ends ****/ diff --git a/crates/service_utils/src/helpers.rs b/crates/service_utils/src/helpers.rs index 70e40d8c..c012e645 100644 --- a/crates/service_utils/src/helpers.rs +++ b/crates/service_utils/src/helpers.rs @@ -1,4 +1,5 @@ use actix_web::{error::ErrorInternalServerError, Error}; +use jsonschema::{error::ValidationErrorKind, ValidationError}; use log::info; use serde::de::{self, IntoDeserializer}; use std::{ @@ -198,3 +199,130 @@ pub fn get_variable_name_and_value( Ok((variable_name, variable_value)) } + +pub fn validation_err_to_str(errors: Vec) -> Vec { + errors.into_iter().map(|error| { + match error.kind { + ValidationErrorKind::AdditionalItems { limit } => { + format!("input array contain more items than expected, limit is {limit}") + } + ValidationErrorKind::AdditionalProperties { unexpected } => { + format!("unexpected properties `{}`", unexpected.join(", ")) + } + ValidationErrorKind::AnyOf => { + format!("not valid under any of the schemas listed in the 'anyOf' keyword") + } + ValidationErrorKind::BacktrackLimitExceeded { error: _ } => { + format!("backtrack limit exceeded while matching regex") + } + ValidationErrorKind::Constant { expected_value } => { + format!("value doesn't match expected constant `{expected_value}`") + } + ValidationErrorKind::Contains => { + format!("array doesn't contain items conforming to the specified schema") + } + ValidationErrorKind::ContentEncoding { content_encoding } => { + format!("value doesn't respect the defined contentEncoding `{content_encoding}`") + } + ValidationErrorKind::ContentMediaType { content_media_type } => { + format!("value doesn't respect the defined contentMediaType `{content_media_type}`") + } + ValidationErrorKind::Enum { options } => { + format!("value doesn't match any of specified options {}", options.to_string()) + } + ValidationErrorKind::ExclusiveMaximum { limit } => { + format!("value is too large, limit is {limit}") + } + ValidationErrorKind::ExclusiveMinimum { limit } => { + format!("value is too small, limit is {limit}") + } + ValidationErrorKind::FalseSchema => { + format!("everything is invalid for `false` schema") + } + ValidationErrorKind::FileNotFound { error: _ } => { + format!("referenced file not found") + } + ValidationErrorKind::Format { format } => { + format!("value doesn't match the specified format `{}`", format) + } + ValidationErrorKind::FromUtf8 { error: _ } => { + format!("invalid UTF-8 data") + } + ValidationErrorKind::InvalidReference { reference } => { + format!("`{}` is not a valid reference", reference) + } + ValidationErrorKind::InvalidURL { error } => { + format!("invalid URL: {}", error) + } + ValidationErrorKind::JSONParse { error } => { + format!("error parsing JSON: {}", error) + } + ValidationErrorKind::MaxItems { limit } => { + format!("too many items in array, limit is {}", limit) + } + ValidationErrorKind::Maximum { limit } => { + format!("value is too large, maximum is {}", limit) + } + ValidationErrorKind::MaxLength { limit } => { + format!("string is too long, maximum length is {}", limit) + } + ValidationErrorKind::MaxProperties { limit } => { + format!("too many properties in object, limit is {}", limit) + } + ValidationErrorKind::MinItems { limit } => { + format!("not enough items in array, minimum is {}", limit) + } + ValidationErrorKind::Minimum { limit } => { + format!("value is too small, minimum is {}", limit) + } + ValidationErrorKind::MinLength { limit } => { + format!("string is too short, minimum length is {}", limit) + } + ValidationErrorKind::MinProperties { limit } => { + format!("not enough properties in object, minimum is {}", limit) + } + ValidationErrorKind::MultipleOf { multiple_of } => { + format!("value is not a multiple of {}", multiple_of) + } + ValidationErrorKind::Not { schema } => { + format!("negated schema `{}` failed validation", schema) + } + ValidationErrorKind::OneOfMultipleValid => { + format!("value is valid under more than one schema listed in the 'oneOf' keyword") + } + ValidationErrorKind::OneOfNotValid => { + format!("value is not valid under any of the schemas listed in the 'oneOf' keyword") + } + ValidationErrorKind::Pattern { pattern } => { + format!("value doesn't match the pattern `{}`", pattern) + } + ValidationErrorKind::PropertyNames { error } => { + format!("object property names are invalid: {}", error) + } + ValidationErrorKind::Required { property } => { + format!("required property `{}` is missing", property) + } + ValidationErrorKind::Resolver { url, error } => { + format!("error resolving reference `{}`: {}", url, error) + } + ValidationErrorKind::Schema => { + format!("resolved schema failed to compile") + } + ValidationErrorKind::Type { kind } => { + format!("value doesn't match the required type(s) `{:?}`", kind) + } + ValidationErrorKind::UnevaluatedProperties { unexpected } => { + format!("unevaluated properties `{}`", unexpected.join(", ")) + } + ValidationErrorKind::UniqueItems => { + format!("array contains non-unique elements") + } + ValidationErrorKind::UnknownReferenceScheme { scheme } => { + format!("unknown reference scheme `{}`", scheme) + } + ValidationErrorKind::Utf8 { error } => { + format!("invalid UTF-8 string: {}", error) + } + } + }).collect() +} \ No newline at end of file