cat_gateway/db/event/config/
key.rs

1//! Configuration Key
2
3use std::{fmt::Display, net::IpAddr, sync::LazyLock};
4
5use jsonschema::{BasicOutput, Validator};
6use serde_json::{json, Value};
7use tracing::error;
8
9use crate::{
10    service::utilities::json::load_json,
11    utils::schema::{extract_json_schema_for, SCHEMA_VERSION},
12};
13
14/// Configuration key
15#[derive(Debug, Clone, PartialEq)]
16pub(crate) enum ConfigKey {
17    /// Frontend general configuration.
18    Frontend,
19    /// Frontend configuration for a specific IP address.
20    FrontendForIp(IpAddr),
21}
22
23impl Display for ConfigKey {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            ConfigKey::Frontend => write!(f, "config_key_frontend"),
27            ConfigKey::FrontendForIp(_) => write!(f, "config_key_frontend_ip"),
28        }
29    }
30}
31
32/// Frontend schema from API specification.
33pub(crate) static FRONTEND_SCHEMA: LazyLock<Value> =
34    LazyLock::new(|| extract_json_schema_for("FrontendConfig"));
35
36/// Frontend schema validator.
37static FRONTEND_SCHEMA_VALIDATOR: LazyLock<Validator> =
38    LazyLock::new(|| schema_validator(&FRONTEND_SCHEMA));
39
40/// Frontend default configuration.
41static FRONTEND_DEFAULT: LazyLock<Value> =
42    LazyLock::new(|| load_json(include_str!("default/frontend.json")));
43
44/// Frontend specific configuration.
45static FRONTEND_IP_DEFAULT: LazyLock<Value> =
46    LazyLock::new(|| load_json(include_str!("default/frontend_ip.json")));
47
48/// Helper function to create a JSON validator from a JSON schema.
49/// If the schema is invalid, a default JSON validator is created.
50fn schema_validator(schema: &Value) -> Validator {
51    Validator::options()
52        .with_draft(jsonschema::Draft::Draft202012)
53        .build(schema)
54        .unwrap_or_else(|err| {
55            error!(
56                id="schema_validator",
57                error=?err,
58                "Error creating JSON validator"
59            );
60
61            default_validator()
62        })
63}
64
65/// Create a default JSON validator as a fallback
66/// This should not fail since it is hard coded
67fn default_validator() -> Validator {
68    #[allow(clippy::expect_used)]
69    Validator::new(&json!({
70        "$schema": SCHEMA_VERSION,
71        "type": "object"
72    }))
73    .expect("Failed to create default JSON validator")
74}
75
76impl ConfigKey {
77    /// Convert a `ConfigKey` to its corresponding IDs.
78    pub(super) fn to_id(&self) -> (String, String, String) {
79        match self {
80            ConfigKey::Frontend => ("frontend".to_string(), String::new(), String::new()),
81            ConfigKey::FrontendForIp(ip) => {
82                ("frontend".to_string(), "ip".to_string(), ip.to_string())
83            },
84        }
85    }
86
87    /// Validate the provided value against the JSON schema.
88    pub(super) fn validate(&self, value: &Value) -> BasicOutput<'static> {
89        // Retrieve the validator based on ConfigKey
90        let validator = match self {
91            ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => &*FRONTEND_SCHEMA_VALIDATOR,
92        };
93
94        // Validate the value against the schema
95        validator.apply(value).basic()
96    }
97
98    /// Retrieve the default configuration value.
99    pub(super) fn default(&self) -> Value {
100        // Retrieve the default value based on the ConfigKey
101        match self {
102            ConfigKey::Frontend => FRONTEND_DEFAULT.clone(),
103            ConfigKey::FrontendForIp(_) => FRONTEND_IP_DEFAULT.clone(),
104        }
105    }
106
107    /// Retrieve the JSON schema.
108    pub(crate) fn schema(&self) -> &Value {
109        match self {
110            ConfigKey::Frontend | ConfigKey::FrontendForIp(_) => &FRONTEND_SCHEMA,
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use serde_json::json;
118
119    use super::*;
120
121    #[test]
122    fn test_schema_for_schema() {
123        // Invalid schema
124        let invalid_schema = json!({
125            "title": "Invalid Example Schema",
126            "type": "object",
127
128            "properties": {
129                "invalidProperty": {
130                    "type": "unknownType"
131                }
132            },
133
134        });
135        // This should not fail
136        schema_validator(&invalid_schema);
137    }
138
139    #[test]
140    fn test_valid_validate_1() {
141        let value = json!({
142            "sentry": {
143                "dsn": "https://test.com"
144            }
145        });
146        let result = ConfigKey::Frontend.validate(&value);
147        assert!(result.is_valid());
148        println!("{:?}", serde_json::to_value(result).unwrap());
149    }
150
151    #[test]
152    fn test_valid_validate_2() {
153        let value = json!({});
154        let result = ConfigKey::Frontend.validate(&value);
155        assert!(result.is_valid());
156        println!("{:?}", serde_json::to_value(result).unwrap());
157    }
158
159    #[test]
160    fn test_invalid_validate() {
161        let value = json!([]);
162        let result = ConfigKey::Frontend.validate(&value);
163        assert!(!result.is_valid());
164        println!("{:?}", serde_json::to_value(result).unwrap());
165    }
166
167    #[test]
168    fn test_default() {
169        let result = ConfigKey::Frontend.default();
170        assert!(result.is_object());
171    }
172
173    #[test]
174    fn test_default_validator() {
175        let result = std::panic::catch_unwind(|| {
176            default_validator();
177        });
178        // Assert that no panic occurred
179        assert!(result.is_ok(), "default_validator panicked");
180    }
181
182    // Since load_json return an empty object for invalid JSON,
183    // Add test for default file to ensure it is not empty object.
184    #[test]
185    fn test_default_files() {
186        assert!(!FRONTEND_DEFAULT.as_object().unwrap().is_empty());
187    }
188}