From 5fde921cf58440962f8222cc8fe04010eeb99aff Mon Sep 17 00:00:00 2001 From: bkioshn Date: Wed, 16 Oct 2024 14:54:54 +0700 Subject: [PATCH] fix: add new endpoint and fix validate json Signed-off-by: bkioshn --- .../db/event/config/default/frontend_ip.json | 1 + .../db/event/config/jsonschema/frontend.json | 1 + .../bin/src/db/event/config/key.rs | 42 ++++++++--- .../bin/src/db/event/config/mod.rs | 31 ++++++-- .../bin/src/service/api/config/mod.rs | 70 +++++++++++++++---- 5 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 catalyst-gateway/bin/src/db/event/config/default/frontend_ip.json diff --git a/catalyst-gateway/bin/src/db/event/config/default/frontend_ip.json b/catalyst-gateway/bin/src/db/event/config/default/frontend_ip.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/catalyst-gateway/bin/src/db/event/config/default/frontend_ip.json @@ -0,0 +1 @@ +{} diff --git a/catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json b/catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json index e8233bb95a..cc521e747e 100644 --- a/catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json +++ b/catalyst-gateway/bin/src/db/event/config/jsonschema/frontend.json @@ -1,6 +1,7 @@ { "$id": "https://www.stephenlewis.me/sentry-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Frontend JSON schema", "type": "object", "properties": { "sentry_dsn": { diff --git a/catalyst-gateway/bin/src/db/event/config/key.rs b/catalyst-gateway/bin/src/db/event/config/key.rs index ad84bd70ca..8c2e0a526f 100644 --- a/catalyst-gateway/bin/src/db/event/config/key.rs +++ b/catalyst-gateway/bin/src/db/event/config/key.rs @@ -2,7 +2,7 @@ use std::{net::IpAddr, sync::LazyLock}; -use jsonschema::Validator; +use jsonschema::{BasicOutput, Validator}; use serde_json::{json, Value}; use tracing::error; @@ -15,14 +15,21 @@ pub(crate) enum ConfigKey { FrontendForIp(IpAddr), } +static FRONTEND_SCHEMA: LazyLock = + LazyLock::new(|| load_json_lazy(include_str!("jsonschema/frontend.json"))); + /// Frontend schema validator. static FRONTEND_SCHEMA_VALIDATOR: LazyLock = - LazyLock::new(|| schema_validator(&load_json_lazy(include_str!("jsonschema/frontend.json")))); + LazyLock::new(|| schema_validator(&FRONTEND_SCHEMA)); /// Frontend default configuration. static FRONTEND_DEFAULT: LazyLock = LazyLock::new(|| load_json_lazy(include_str!("default/frontend.json"))); +/// Frontend specific configuration. +static FRONTEND_IP_DEFAULT: LazyLock = + LazyLock::new(|| load_json_lazy(include_str!("default/frontend_ip.json"))); + /// Helper function to create a JSON validator from a JSON schema. /// If the schema is invalid, a default JSON validator is created. fn schema_validator(schema: &Value) -> Validator { @@ -60,25 +67,29 @@ impl ConfigKey { } /// Validate the provided value against the JSON schema. - pub(super) fn validate(&self, value: &Value) -> anyhow::Result<()> { + pub(super) fn validate(&self, value: &Value) -> BasicOutput<'static> { // Retrieve the validator based on ConfigKey let validator = match self { ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => &*FRONTEND_SCHEMA_VALIDATOR, }; // Validate the value against the schema - anyhow::ensure!( - validator.is_valid(value), - "Invalid JSON, Failed schema validation" - ); - Ok(()) + validator.apply(value).basic() } /// Retrieve the default configuration value. pub(super) fn default(&self) -> Value { // Retrieve the default value based on the ConfigKey match self { - ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => FRONTEND_DEFAULT.clone(), + ConfigKey::Frontend => FRONTEND_DEFAULT.clone(), + ConfigKey::FrontendForIp(_) => FRONTEND_IP_DEFAULT.clone(), + } + } + + /// Retrieve the JSON schema. + pub(crate) fn schema(&self) -> &Value { + match self { + ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => &FRONTEND_SCHEMA, } } } @@ -90,12 +101,21 @@ mod tests { use super::*; #[test] - fn test_validate() { + fn test_valid_validate() { let value = json!({ "test": "test" }); let result = ConfigKey::Frontend.validate(&value); - assert!(result.is_ok()); + assert!(result.is_valid()); + println!("{:?}", serde_json::to_value(result).unwrap()); + } + + #[test] + fn test_invalid_validate() { + let value = json!([]); + let result = ConfigKey::Frontend.validate(&value); + assert!(!result.is_valid()); + println!("{:?}", serde_json::to_value(result).unwrap()); } #[test] diff --git a/catalyst-gateway/bin/src/db/event/config/mod.rs b/catalyst-gateway/bin/src/db/event/config/mod.rs index c931b4596d..f7cda6cbab 100644 --- a/catalyst-gateway/bin/src/db/event/config/mod.rs +++ b/catalyst-gateway/bin/src/db/event/config/mod.rs @@ -1,7 +1,9 @@ //! Configuration query +use jsonschema::BasicOutput; use key::ConfigKey; use serde_json::Value; +use tracing::error; use crate::db::event::EventDB; @@ -21,28 +23,43 @@ impl Config { /// # Returns /// /// - A JSON value of the configuration, if not found, returns the default value. + /// - Error if the query fails. pub(crate) async fn get(id: ConfigKey) -> anyhow::Result { let (id1, id2, id3) = id.to_id(); let rows = EventDB::query(GET_CONFIG, &[&id1, &id2, &id3]).await?; if let Some(row) = rows.first() { let value: Value = row.get(0); - id.validate(&value).map_err(|e| anyhow::anyhow!(e))?; - Ok(value) + match id.validate(&value) { + BasicOutput::Valid(_) => Ok(value), + BasicOutput::Invalid(errors) => { + // This should not happen; expecting the schema to be valid + error!("Validate schema failed: {:?}", errors); + Err(anyhow::anyhow!("Validate schema failed")) + }, + } } else { - // If data not found return default config value + // If data is not found, return the default config value Ok(id.default()) } } /// Set the configuration for the given `ConfigKey`. - pub(crate) async fn set(id: ConfigKey, value: Value) -> anyhow::Result<()> { - // Validate the value - id.validate(&value)?; + /// + /// # Returns + /// + /// - A `BasicOutput` of the validation result, which can be valid or invalid. + /// - Error if the query fails. + pub(crate) async fn set(id: ConfigKey, value: Value) -> anyhow::Result> { + let validate = id.validate(&value); + // Validate schema failed, return immediately with JSON schema error + if !validate.is_valid() { + return Ok(validate); + } let (id1, id2, id3) = id.to_id(); EventDB::query(UPSERT_CONFIG, &[&id1, &id2, &id3, &value]).await?; - Ok(()) + Ok(validate) } } diff --git a/catalyst-gateway/bin/src/service/api/config/mod.rs b/catalyst-gateway/bin/src/service/api/config/mod.rs index 46df7a58c3..0cc7f4e295 100644 --- a/catalyst-gateway/bin/src/service/api/config/mod.rs +++ b/catalyst-gateway/bin/src/service/api/config/mod.rs @@ -2,9 +2,10 @@ use std::{net::IpAddr, str::FromStr}; +use jsonschema::BasicOutput; use poem::web::RealIp; use poem_openapi::{param::Query, payload::Json, ApiResponse, OpenApi}; -use serde_json::Value; +use serde_json::{json, Value}; use tracing::info; use crate::{ @@ -20,13 +21,13 @@ pub(crate) struct ConfigApi; enum Responses { /// Configuration result #[oai(status = 200)] - Ok(Json), - /// Configuration not found - #[oai(status = 404)] - NotFound(Json), + Ok(Json), /// Bad request #[oai(status = 400)] - BadRequest(Json), + BadRequest(Json), + /// Internal server error + #[oai(status = 500)] + ServerError(Json), } #[OpenApi(tag = "ApiTags::Config")] @@ -47,7 +48,12 @@ impl ConfigApi { // Attempt to fetch the IP configuration let ip_config = if let Some(ip) = ip_address.0 { - Config::get(ConfigKey::FrontendForIp(ip)).await.ok() + match Config::get(ConfigKey::FrontendForIp(ip)).await { + Ok(value) => Some(value), + Err(_) => { + return Responses::ServerError(Json("Failed to get configuration".to_string())) + }, + } } else { None }; @@ -63,9 +69,34 @@ impl ConfigApi { general }; - Responses::Ok(Json(response_config.to_string())) + Responses::Ok(Json(response_config)) + }, + Err(err) => Responses::ServerError(Json(err.to_string())), + } + } + + /// Get the frontend JSON schema. + #[oai( + path = "/draft/config/frontend/schema", + method = "get", + operation_id = "get_config_frontend_schema" + )] + #[allow(clippy::unused_async)] + async fn get_frontend_schema( + &self, #[oai(name = "IP")] ip_query: Query>, + ) -> Responses { + match ip_query.0 { + Some(ip) => { + match IpAddr::from_str(&ip) { + Ok(parsed_ip) => { + Responses::Ok(Json(ConfigKey::FrontendForIp(parsed_ip).schema().clone())) + }, + Err(err) => { + Responses::BadRequest(Json(json!(format!("Invalid IP address: {err}")))) + }, + } }, - Err(err) => Responses::NotFound(Json(err.to_string())), + None => Responses::Ok(Json(ConfigKey::Frontend.schema().clone())), } } @@ -84,7 +115,9 @@ impl ConfigApi { Some(ip) => { match IpAddr::from_str(&ip) { Ok(parsed_ip) => set(ConfigKey::FrontendForIp(parsed_ip), body_value).await, - Err(err) => Responses::BadRequest(Json(format!("Invalid IP address: {err}"))), + Err(err) => { + Responses::BadRequest(Json(json!(format!("Invalid IP address: {err}")))) + }, } }, None => set(ConfigKey::Frontend, body_value).await, @@ -114,7 +147,20 @@ fn merge_configs(general: &Value, ip_specific: &Value) -> Value { /// Helper function to handle set. async fn set(key: ConfigKey, value: Value) -> Responses { match Config::set(key, value).await { - Ok(()) => Responses::Ok(Json("Configuration successfully set.".to_string())), - Err(err) => Responses::BadRequest(Json(format!("Failed to set configuration: {err}"))), + Ok(validate) => { + match validate { + BasicOutput::Valid(_) => { + Responses::Ok(Json(json!("Configuration successfully set."))) + }, + BasicOutput::Invalid(errors) => { + let mut e = vec![]; + for error in errors { + e.push(error.error_description().to_string()); + } + Responses::BadRequest(Json(json!({"errors": e}))) + }, + } + }, + Err(err) => Responses::ServerError(Json(format!("Failed to set configuration: {err}"))), } }