datafake_rs/
config.rs

1//! Configuration parsing and validation for datafake-rs.
2//!
3//! This module provides [`ConfigParser`] for parsing and validating
4//! JSON configuration into [`DataFakeConfig`] structures.
5
6use crate::error::{DataFakeError, Result};
7use crate::types::{DataFakeConfig, GenerationContext};
8use serde_json::Value;
9use std::collections::HashMap;
10
11/// Parser and validator for datafake configuration.
12///
13/// `ConfigParser` provides static methods for parsing JSON strings or values
14/// into validated [`DataFakeConfig`] structures.
15pub struct ConfigParser;
16
17impl ConfigParser {
18    /// Parses a JSON string into a validated configuration.
19    ///
20    /// # Errors
21    ///
22    /// Returns an error if the JSON is invalid or fails validation.
23    pub fn parse(json_str: &str) -> Result<DataFakeConfig> {
24        let config: DataFakeConfig = serde_json::from_str(json_str)
25            .map_err(|e| DataFakeError::ConfigParse(format!("Failed to parse JSON: {e}")))?;
26
27        Self::validate_config(&config)?;
28        Ok(config)
29    }
30
31    /// Parses a `serde_json::Value` into a validated configuration.
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the value cannot be converted or fails validation.
36    pub fn parse_value(json_value: Value) -> Result<DataFakeConfig> {
37        let config: DataFakeConfig = serde_json::from_value(json_value)
38            .map_err(|e| DataFakeError::ConfigParse(format!("Failed to parse JSON value: {e}")))?;
39
40        Self::validate_config(&config)?;
41        Ok(config)
42    }
43
44    fn validate_config(config: &DataFakeConfig) -> Result<()> {
45        if config.schema.is_null() {
46            return Err(DataFakeError::InvalidConfig(
47                "Schema cannot be null".to_string(),
48            ));
49        }
50
51        Self::validate_variables(&config.variables)?;
52        Self::validate_schema(&config.schema)?;
53
54        Ok(())
55    }
56
57    fn validate_variables(variables: &HashMap<String, Value>) -> Result<()> {
58        for (name, value) in variables {
59            if name.is_empty() {
60                return Err(DataFakeError::InvalidConfig(
61                    "Variable name cannot be empty".to_string(),
62                ));
63            }
64
65            if value.is_null() {
66                return Err(DataFakeError::InvalidConfig(format!(
67                    "Variable '{name}' cannot be null"
68                )));
69            }
70
71            Self::validate_jsonlogic_expression(value)?;
72        }
73        Ok(())
74    }
75
76    fn validate_schema(schema: &Value) -> Result<()> {
77        match schema {
78            Value::Object(map) => Self::validate_schema_object(map, schema),
79            Value::Array(arr) => Self::validate_schema_array(arr),
80            Value::Null => Err(DataFakeError::InvalidConfig(
81                "Schema values cannot be null".to_string(),
82            )),
83            _ => Ok(()),
84        }
85    }
86
87    /// Validates an object within the schema.
88    fn validate_schema_object(map: &serde_json::Map<String, Value>, schema: &Value) -> Result<()> {
89        // Check if this is a JSONLogic expression
90        if map.contains_key("fake") || map.contains_key("var") {
91            return Self::validate_jsonlogic_expression(schema);
92        }
93
94        // Regular object: validate each property
95        for (key, value) in map {
96            if key.is_empty() {
97                return Err(DataFakeError::InvalidConfig(
98                    "Schema key cannot be empty".to_string(),
99                ));
100            }
101            Self::validate_schema(value)?;
102        }
103        Ok(())
104    }
105
106    /// Validates an array within the schema.
107    fn validate_schema_array(arr: &[Value]) -> Result<()> {
108        for item in arr {
109            Self::validate_schema(item)?;
110        }
111        Ok(())
112    }
113
114    fn validate_jsonlogic_expression(value: &Value) -> Result<()> {
115        if let Value::Object(map) = value {
116            if let Some(fake_args) = map.get("fake") {
117                Self::validate_fake_operator(fake_args)?;
118            } else if map.contains_key("var")
119                && let Some(Value::String(var_name)) = map.get("var")
120                && var_name.is_empty()
121            {
122                return Err(DataFakeError::InvalidConfig(
123                    "Variable reference cannot be empty".to_string(),
124                ));
125            }
126        }
127        Ok(())
128    }
129
130    fn validate_fake_operator(args: &Value) -> Result<()> {
131        match args {
132            Value::Array(arr) => {
133                if arr.is_empty() {
134                    return Err(DataFakeError::InvalidConfig(
135                        "Fake operator requires at least one argument".to_string(),
136                    ));
137                }
138
139                if let Some(Value::String(method)) = arr.first() {
140                    if method.is_empty() {
141                        return Err(DataFakeError::InvalidConfig(
142                            "Fake method name cannot be empty".to_string(),
143                        ));
144                    }
145
146                    match method.as_str() {
147                        "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "f32"
148                        | "f64" => {
149                            if arr.len() == 3 {
150                                let min = Self::extract_number(arr.get(1))?;
151                                let max = Self::extract_number(arr.get(2))?;
152                                if min > max {
153                                    return Err(DataFakeError::InvalidRange { min, max });
154                                }
155                            } else if arr.len() != 1 {
156                                return Err(DataFakeError::InvalidConfig(format!(
157                                    "Numeric type '{method}' requires either 0 or 2 arguments (min, max)"
158                                )));
159                            }
160                        }
161                        _ => {}
162                    }
163                } else {
164                    return Err(DataFakeError::InvalidConfig(
165                        "First argument of fake operator must be a string".to_string(),
166                    ));
167                }
168            }
169            _ => {
170                return Err(DataFakeError::InvalidConfig(
171                    "Fake operator arguments must be an array".to_string(),
172                ));
173            }
174        }
175        Ok(())
176    }
177
178    fn extract_number(value: Option<&Value>) -> Result<f64> {
179        match value {
180            Some(Value::Number(n)) => n
181                .as_f64()
182                .ok_or_else(|| DataFakeError::TypeConversion("Invalid number format".to_string())),
183            _ => Err(DataFakeError::TypeConversion(
184                "Expected a number".to_string(),
185            )),
186        }
187    }
188
189    /// Creates a generation context from the configuration's variables.
190    pub fn create_context(config: &DataFakeConfig) -> GenerationContext {
191        GenerationContext::with_variables(config.variables.clone())
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_parse_valid_config() {
201        let config_json = r#"{
202            "metadata": {
203                "name": "Test Config",
204                "version": "1.0.0"
205            },
206            "variables": {
207                "userId": {"fake": ["uuid"]}
208            },
209            "schema": {
210                "id": {"var": "userId"},
211                "name": {"fake": ["name", "en_US"]}
212            }
213        }"#;
214
215        let result = ConfigParser::parse(config_json);
216        assert!(result.is_ok());
217        let config = result.unwrap();
218        assert!(config.metadata.is_some());
219        assert_eq!(config.variables.len(), 1);
220    }
221
222    #[test]
223    fn test_parse_minimal_config() {
224        let config_json = r#"{
225            "schema": {
226                "name": {"fake": ["name"]}
227            }
228        }"#;
229
230        let result = ConfigParser::parse(config_json);
231        assert!(result.is_ok());
232    }
233
234    #[test]
235    fn test_invalid_empty_schema() {
236        let config_json = r#"{
237            "schema": null
238        }"#;
239
240        let result = ConfigParser::parse(config_json);
241        assert!(result.is_err());
242    }
243
244    #[test]
245    fn test_invalid_fake_operator_no_args() {
246        let config_json = r#"{
247            "schema": {
248                "field": {"fake": []}
249            }
250        }"#;
251
252        let result = ConfigParser::parse(config_json);
253        assert!(result.is_err());
254    }
255
256    #[test]
257    fn test_invalid_numeric_range() {
258        let config_json = r#"{
259            "schema": {
260                "age": {"fake": ["u8", 100, 0]}
261            }
262        }"#;
263
264        let result = ConfigParser::parse(config_json);
265        assert!(result.is_err());
266    }
267
268    #[test]
269    fn test_valid_numeric_range() {
270        let config_json = r#"{
271            "schema": {
272                "age": {"fake": ["u8", 0, 100]}
273            }
274        }"#;
275
276        let result = ConfigParser::parse(config_json);
277        assert!(result.is_ok());
278    }
279
280    #[test]
281    fn test_empty_variable_name() {
282        let config_json = r#"{
283            "variables": {
284                "": {"fake": ["uuid"]}
285            },
286            "schema": {}
287        }"#;
288
289        let result = ConfigParser::parse(config_json);
290        assert!(result.is_err());
291    }
292
293    #[test]
294    fn test_complex_nested_schema() {
295        let config_json = r#"{
296            "variables": {
297                "country": {"fake": ["country_code"]}
298            },
299            "schema": {
300                "users": [
301                    {
302                        "id": {"fake": ["uuid"]},
303                        "profile": {
304                            "name": {"fake": ["name", "en_US"]},
305                            "address": {
306                                "street": {"fake": ["street_address"]},
307                                "country": {"var": "country"}
308                            }
309                        }
310                    }
311                ]
312            }
313        }"#;
314
315        let result = ConfigParser::parse(config_json);
316        assert!(result.is_ok());
317    }
318}