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..6f50dd1f 100644 --- a/crates/frontend/src/components/context_form/utils.rs +++ b/crates/frontend/src/components/context_form/utils.rs @@ -1,7 +1,8 @@ use crate::types::Dimension; -use crate::utils::{get_config_value, get_host, ConfigType}; +use crate::utils::{ + construct_request_headers, 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 +99,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), + construct_request_headers(&[("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..a7c3c56a 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::{construct_request_headers, 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), + construct_request_headers(&[("x-tenant", &tenant)])?, + ) + .await + .map_err(|err| err.to_string()) } diff --git a/crates/frontend/src/components/dimension_form/dimension_form.rs b/crates/frontend/src/components/dimension_form/dimension_form.rs index cd911418..28d5baf0 100644 --- a/crates/frontend/src/components/dimension_form/dimension_form.rs +++ b/crates/frontend/src/components/dimension_form/dimension_form.rs @@ -330,4 +330,4 @@ where } -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/dimension_form/utils.rs b/crates/frontend/src/components/dimension_form/utils.rs index c17cffe9..8affe702 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::{construct_request_headers, 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), + construct_request_headers(&[("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..1df7bca4 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::{construct_request_headers, 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), + construct_request_headers(&[("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), + construct_request_headers(&[("x-tenant", &tenant)])?, + ) + .await + .map_err(|err| err.to_string()) } diff --git a/crates/frontend/src/components/function_form/function_form.rs b/crates/frontend/src/components/function_form/function_form.rs index cfcb36f3..2ddedfd3 100644 --- a/crates/frontend/src/components/function_form/function_form.rs +++ b/crates/frontend/src/components/function_form/function_form.rs @@ -1,5 +1,5 @@ use super::utils::{create_function, test_function, update_function}; -use crate::components::button::button::Button; +use crate::{components::button::button::Button, types::FunctionTestResponse}; use leptos::*; use serde_json::{from_str, json, Value}; use web_sys::MouseEvent; @@ -176,7 +176,8 @@ where { let tenant_rs = use_context::>().unwrap(); let (error_message, set_error_message) = create_signal("".to_string()); - let (output_message, set_output_message) = create_signal("".to_string()); + let (output_message, set_output_message) = + create_signal::>(None); let (val, set_val) = create_signal(json!({})); let (key, set_key) = create_signal("".to_string()); @@ -201,11 +202,11 @@ where match result { Ok(resp) => { - set_error_message.set("".to_string()); - set_output_message.set(resp); + set_error_message.set(String::new()); + set_output_message.set(Some(resp)); } Err(e) => { - set_output_message.set("".to_string()); + set_output_message.set(None); set_error_message.set(e); } } @@ -255,13 +256,12 @@ where Ok(test_val) => { set_val.set(test_val); set_error_message.set("".to_string()); - set_output_message.set("".to_string()); + set_output_message.set(None); } Err(_) => { set_val.set(json!(value)); set_error_message.set("".to_string()); - set_output_message.set("".to_string()); - + set_output_message.set(None); } }; } @@ -281,11 +281,20 @@ where
-

{move || output_message.get()}

+

+ {move || { + output_message + .get() + .map_or( + String::new(), + |o| { format!("{}\n{}", o.message, o.stdout) }, + ) + }} +

} -} +} \ No newline at end of file diff --git a/crates/frontend/src/components/function_form/utils.rs b/crates/frontend/src/components/function_form/utils.rs index 5e006cc0..23613f58 100644 --- a/crates/frontend/src/components/function_form/utils.rs +++ b/crates/frontend/src/components/function_form/utils.rs @@ -1,7 +1,9 @@ use super::types::{FunctionCreateRequest, FunctionUpdateRequest}; -use crate::utils::get_host; -use reqwest::StatusCode; -use serde_json::{json, Value}; +use crate::{ + types::{FunctionResponse, FunctionTestResponse}, + utils::{construct_request_headers, get_host, request}, +}; +use serde_json::Value; pub async fn create_function( function_name: String, @@ -9,7 +11,7 @@ pub async fn create_function( runtime_version: String, description: String, tenant: String, -) -> Result { +) -> Result { let payload = FunctionCreateRequest { function_name, function, @@ -17,27 +19,16 @@ pub async fn create_function( description, }; - let client = reqwest::Client::new(); let host = get_host(); let url = format!("{host}/function"); - 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())?; - - let status = response.status(); - let resp_data = response - .text() - .await - .unwrap_or("Cannot decode response".to_string()); - match status { - StatusCode::OK => Ok(resp_data), - _ => Err(resp_data), - } + request( + url, + reqwest::Method::POST, + Some(payload), + construct_request_headers(&[("x-tenant", &tenant)])?, + ) + .await + .map_err(|err| err.to_string()) } pub async fn update_function( @@ -46,35 +37,24 @@ pub async fn update_function( runtime_version: String, description: String, tenant: String, -) -> Result { +) -> Result { let payload = FunctionUpdateRequest { function, runtime_version, description, }; - let client = reqwest::Client::new(); let host = get_host(); let url = format!("{host}/function/{function_name}"); - let request_payload = json!(payload); - - let response = client - .patch(url) - .header("x-tenant", tenant) - .json(&request_payload) - .send() - .await - .map_err(|e| e.to_string())?; - let status = response.status(); - let resp_data = response - .text() - .await - .unwrap_or("Cannot decode response".to_string()); - match status { - StatusCode::OK => Ok(resp_data), - _ => Err(resp_data), - } + request( + url, + reqwest::Method::PATCH, + Some(payload), + construct_request_headers(&[("x-tenant", &tenant)])?, + ) + .await + .map_err(|err| err.to_string()) } pub async fn test_function( @@ -82,26 +62,16 @@ pub async fn test_function( stage: String, val: Value, tenant: String, -) -> Result { - let client = reqwest::Client::new(); +) -> Result { let host = get_host(); let url = format!("{host}/function/{function_name}/{stage}/test"); - let response = client - .put(url) - .header("x-tenant", tenant) - .json(&val) - .send() - .await - .map_err(|e| e.to_string())?; - - let status = response.status(); - let resp_data = response - .text() - .await - .unwrap_or("Cannot decode response".to_string()); - match status { - StatusCode::OK => Ok(resp_data), - _ => Err(resp_data), - } -} + request( + url, + reqwest::Method::PUT, + Some(val), + construct_request_headers(&[("x-tenant", &tenant)])?, + ) + .await + .map_err(|err| err.to_string()) +} \ No newline at end of file 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..c3544094 100644 --- a/crates/frontend/src/types.rs +++ b/crates/frontend/src/types.rs @@ -63,6 +63,12 @@ pub struct FunctionResponse { pub draft_edited_by: String, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionTestResponse { + pub message: String, + pub stdout: String, +} + /*********************** Experimentation Types ****************************************/ #[derive( @@ -214,3 +220,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..8f55c046 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,66 @@ pub fn get_config_value( } } } + +/********* Request Utils **********/ + +use once_cell::sync::Lazy; +static HTTP_CLIENT: Lazy = Lazy::new(|| reqwest::Client::new()); + +pub fn construct_request_headers(entries: &[(&str, &str)]) -> Result { + entries + .into_iter() + .map(|(name, value)| { + let h_name = HeaderName::from_str(name); + let h_value = HeaderValue::from_str(value); + + match (h_name, h_value) { + (Ok(n), Ok(v)) => Some((n, v)), + _ => None, + } + }) + .collect::>>() + .map(HeaderMap::from_iter) + .ok_or(String::from("failed to parse headers")) +} + +pub async fn request<'a, T, R>( + url: String, + method: reqwest::Method, + body: Option, + headers: HeaderMap, +) -> anyhow::Result +where + T: serde::Serialize, + R: serde::de::DeserializeOwned, +{ + let mut request_builder = HTTP_CLIENT.request(method.clone(), url).headers(headers); + 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