Skip to content

Commit

Permalink
fix: add new endpoint and fix validate json
Browse files Browse the repository at this point in the history
Signed-off-by: bkioshn <[email protected]>
  • Loading branch information
bkioshn committed Oct 16, 2024
1 parent fad3cfc commit 5fde921
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
42 changes: 31 additions & 11 deletions catalyst-gateway/bin/src/db/event/config/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,14 +15,21 @@ pub(crate) enum ConfigKey {
FrontendForIp(IpAddr),
}

static FRONTEND_SCHEMA: LazyLock<Value> =
LazyLock::new(|| load_json_lazy(include_str!("jsonschema/frontend.json")));

/// Frontend schema validator.
static FRONTEND_SCHEMA_VALIDATOR: LazyLock<Validator> =
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<Value> =
LazyLock::new(|| load_json_lazy(include_str!("default/frontend.json")));

/// Frontend specific configuration.
static FRONTEND_IP_DEFAULT: LazyLock<Value> =
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 {
Expand Down Expand Up @@ -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,
}
}
}
Expand All @@ -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]
Expand Down
31 changes: 24 additions & 7 deletions catalyst-gateway/bin/src/db/event/config/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Configuration query

use jsonschema::BasicOutput;
use key::ConfigKey;
use serde_json::Value;
use tracing::error;

use crate::db::event::EventDB;

Expand All @@ -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<Value> {
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<BasicOutput<'static>> {
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)
}
}
70 changes: 58 additions & 12 deletions catalyst-gateway/bin/src/service/api/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -20,13 +21,13 @@ pub(crate) struct ConfigApi;
enum Responses {
/// Configuration result
#[oai(status = 200)]
Ok(Json<String>),
/// Configuration not found
#[oai(status = 404)]
NotFound(Json<String>),
Ok(Json<Value>),
/// Bad request
#[oai(status = 400)]
BadRequest(Json<String>),
BadRequest(Json<Value>),
/// Internal server error
#[oai(status = 500)]
ServerError(Json<String>),
}

#[OpenApi(tag = "ApiTags::Config")]
Expand All @@ -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
};
Expand All @@ -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<Option<String>>,
) -> 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())),
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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}"))),
}
}

0 comments on commit 5fde921

Please sign in to comment.