1#[allow(dead_code)]
7#[derive(Debug, Clone)]
8pub enum SchemaType {
9 Bool,
10 Int {
11 min: i64,
12 max: i64,
13 },
14 Float {
15 min: f64,
16 max: f64,
17 },
18 String {
19 max_len: usize,
20 },
21 Enum {
22 variants: Vec<std::string::String>,
23 },
24 Array {
25 item_type: Box<SchemaType>,
26 max_items: usize,
27 },
28}
29
30#[allow(dead_code)]
31#[derive(Debug, Clone)]
32pub struct SchemaField {
33 pub name: std::string::String,
34 pub schema_type: SchemaType,
35 pub required: bool,
36 pub default_value: Option<std::string::String>,
37 pub description: std::string::String,
38}
39
40#[allow(dead_code)]
41#[derive(Debug, Clone)]
42pub struct ConfigSchema {
43 pub name: std::string::String,
44 pub version: std::string::String,
45 pub fields: Vec<SchemaField>,
46}
47
48#[allow(dead_code)]
49#[derive(Debug, Clone)]
50pub struct ConfigValue {
51 pub fields: std::collections::HashMap<std::string::String, std::string::String>,
52}
53
54#[allow(dead_code)]
56pub fn new_config_schema(name: &str, version: &str) -> ConfigSchema {
57 ConfigSchema {
58 name: name.to_string(),
59 version: version.to_string(),
60 fields: Vec::new(),
61 }
62}
63
64#[allow(dead_code)]
66pub fn add_field(schema: &mut ConfigSchema, field: SchemaField) {
67 schema.fields.push(field);
68}
69
70#[allow(dead_code)]
72pub fn validate_value(schema: &ConfigSchema, value: &ConfigValue) -> Vec<std::string::String> {
73 let mut errors = Vec::new();
74
75 for field in &schema.fields {
76 match value.fields.get(&field.name) {
77 Some(v) => {
78 if let Some(err) = validate_field_value(field, v) {
79 errors.push(err);
80 }
81 }
82 None => {
83 if field.required && field.default_value.is_none() {
84 errors.push(format!("required field '{}' is missing", field.name));
85 }
86 }
87 }
88 }
89
90 errors
91}
92
93#[allow(dead_code)]
95pub fn validate_field_value(field: &SchemaField, value: &str) -> Option<std::string::String> {
96 match &field.schema_type {
97 SchemaType::Bool => {
98 if value != "true" && value != "false" {
99 return Some(format!(
100 "field '{}': expected bool (true/false), got '{value}'",
101 field.name
102 ));
103 }
104 }
105 SchemaType::Int { min, max } => match value.parse::<i64>() {
106 Ok(n) => {
107 if n < *min || n > *max {
108 return Some(format!(
109 "field '{}': int {n} out of range [{min}, {max}]",
110 field.name
111 ));
112 }
113 }
114 Err(_) => {
115 return Some(format!(
116 "field '{}': cannot parse '{value}' as int",
117 field.name
118 ));
119 }
120 },
121 SchemaType::Float { min, max } => match value.parse::<f64>() {
122 Ok(f) => {
123 if f < *min || f > *max {
124 return Some(format!(
125 "field '{}': float {f} out of range [{min}, {max}]",
126 field.name
127 ));
128 }
129 }
130 Err(_) => {
131 return Some(format!(
132 "field '{}': cannot parse '{value}' as float",
133 field.name
134 ));
135 }
136 },
137 SchemaType::String { max_len } => {
138 let s = strip_json_string(value);
140 if s.len() > *max_len {
141 return Some(format!(
142 "field '{}': string length {} exceeds max {max_len}",
143 field.name,
144 s.len()
145 ));
146 }
147 }
148 SchemaType::Enum { variants } => {
149 let s = strip_json_string(value);
150 if !variants.iter().any(|v| v == &s) {
151 return Some(format!(
152 "field '{}': '{}' is not a valid variant (expected one of: {})",
153 field.name,
154 s,
155 variants.join(", ")
156 ));
157 }
158 }
159 SchemaType::Array {
160 item_type: _,
161 max_items,
162 } => {
163 let trimmed = value.trim();
165 if trimmed == "[]" {
166 } else if trimmed.starts_with('[') && trimmed.ends_with(']') {
168 let inner = &trimmed[1..trimmed.len() - 1];
169 let count = if inner.trim().is_empty() {
170 0
171 } else {
172 inner.split(',').count()
173 };
174 if count > *max_items {
175 return Some(format!(
176 "field '{}': array has {count} items, max is {max_items}",
177 field.name
178 ));
179 }
180 } else {
181 return Some(format!(
182 "field '{}': expected JSON array, got '{value}'",
183 field.name
184 ));
185 }
186 }
187 }
188 None
189}
190
191#[allow(dead_code)]
193pub fn apply_defaults(schema: &ConfigSchema, value: &mut ConfigValue) {
194 for field in &schema.fields {
195 if !value.fields.contains_key(&field.name) {
196 if let Some(default) = &field.default_value {
197 value.fields.insert(field.name.clone(), default.clone());
198 }
199 }
200 }
201}
202
203#[allow(dead_code)]
205pub fn config_value_get_bool(value: &ConfigValue, key: &str) -> Option<bool> {
206 value.fields.get(key).and_then(|v| match v.as_str() {
207 "true" => Some(true),
208 "false" => Some(false),
209 _ => None,
210 })
211}
212
213#[allow(dead_code)]
215pub fn config_value_get_int(value: &ConfigValue, key: &str) -> Option<i64> {
216 value.fields.get(key)?.parse::<i64>().ok()
217}
218
219#[allow(dead_code)]
221pub fn config_value_get_float(value: &ConfigValue, key: &str) -> Option<f64> {
222 value.fields.get(key)?.parse::<f64>().ok()
223}
224
225#[allow(dead_code)]
227pub fn config_value_get_str<'a>(value: &'a ConfigValue, key: &str) -> Option<&'a str> {
228 value.fields.get(key).map(|s| s.as_str())
229}
230
231#[allow(dead_code)]
233pub fn schema_to_json(schema: &ConfigSchema) -> std::string::String {
234 let fields_json: Vec<std::string::String> = schema
235 .fields
236 .iter()
237 .map(|f| {
238 let type_str = schema_type_to_json(&f.schema_type);
239 let required = if f.required { "true" } else { "false" };
240 let default = match &f.default_value {
241 Some(d) => format!("{d:?}"),
242 None => "null".to_string(),
243 };
244 format!(
245 r#"{{"name":{:?},"type":{},"required":{},"default":{},"description":{:?}}}"#,
246 f.name, type_str, required, default, f.description
247 )
248 })
249 .collect();
250
251 format!(
252 r#"{{"name":{:?},"version":{:?},"fields":[{}]}}"#,
253 schema.name,
254 schema.version,
255 fields_json.join(",")
256 )
257}
258
259#[allow(dead_code)]
261pub fn default_render_schema() -> ConfigSchema {
262 let mut schema = new_config_schema("render", "1.0");
263
264 add_field(
265 &mut schema,
266 SchemaField {
267 name: "width".to_string(),
268 schema_type: SchemaType::Int { min: 1, max: 16384 },
269 required: true,
270 default_value: Some("1920".to_string()),
271 description: "Output image width in pixels".to_string(),
272 },
273 );
274 add_field(
275 &mut schema,
276 SchemaField {
277 name: "height".to_string(),
278 schema_type: SchemaType::Int { min: 1, max: 16384 },
279 required: true,
280 default_value: Some("1080".to_string()),
281 description: "Output image height in pixels".to_string(),
282 },
283 );
284 add_field(
285 &mut schema,
286 SchemaField {
287 name: "quality".to_string(),
288 schema_type: SchemaType::Float { min: 0.0, max: 1.0 },
289 required: false,
290 default_value: Some("0.9".to_string()),
291 description: "Render quality 0..1".to_string(),
292 },
293 );
294 add_field(
295 &mut schema,
296 SchemaField {
297 name: "format".to_string(),
298 schema_type: SchemaType::Enum {
299 variants: vec!["png".to_string(), "jpg".to_string(), "webp".to_string()],
300 },
301 required: false,
302 default_value: Some("png".to_string()),
303 description: "Output image format".to_string(),
304 },
305 );
306 add_field(
307 &mut schema,
308 SchemaField {
309 name: "antialiasing".to_string(),
310 schema_type: SchemaType::Bool,
311 required: false,
312 default_value: Some("true".to_string()),
313 description: "Enable antialiasing".to_string(),
314 },
315 );
316 add_field(
317 &mut schema,
318 SchemaField {
319 name: "output_path".to_string(),
320 schema_type: SchemaType::String { max_len: 512 },
321 required: false,
322 default_value: Some("\"output.png\"".to_string()),
323 description: "Output file path".to_string(),
324 },
325 );
326
327 schema
328}
329
330#[allow(dead_code)]
332pub fn merge_configs(base: &ConfigValue, override_: &ConfigValue) -> ConfigValue {
333 let mut merged = base.fields.clone();
334 for (k, v) in &override_.fields {
335 merged.insert(k.clone(), v.clone());
336 }
337 ConfigValue { fields: merged }
338}
339
340fn strip_json_string(s: &str) -> std::string::String {
342 let trimmed = s.trim();
343 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
344 trimmed[1..trimmed.len() - 1].to_string()
345 } else {
346 trimmed.to_string()
347 }
348}
349
350fn schema_type_to_json(t: &SchemaType) -> std::string::String {
351 match t {
352 SchemaType::Bool => r#""bool""#.to_string(),
353 SchemaType::Int { min, max } => format!(r#"{{"int":{{"min":{min},"max":{max}}}}}"#),
354 SchemaType::Float { min, max } => format!(r#"{{"float":{{"min":{min},"max":{max}}}}}"#),
355 SchemaType::String { max_len } => format!(r#"{{"string":{{"max_len":{max_len}}}}}"#),
356 SchemaType::Enum { variants } => {
357 let vs: Vec<std::string::String> = variants.iter().map(|v| format!("{v:?}")).collect();
358 format!(r#"{{"enum":{{"variants":[{}]}}}}"#, vs.join(","))
359 }
360 SchemaType::Array {
361 item_type,
362 max_items,
363 } => {
364 format!(
365 r#"{{"array":{{"item_type":{},"max_items":{max_items}}}}}"#,
366 schema_type_to_json(item_type)
367 )
368 }
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 fn make_simple_schema() -> ConfigSchema {
377 let mut schema = new_config_schema("test", "1.0");
378 add_field(
379 &mut schema,
380 SchemaField {
381 name: "count".to_string(),
382 schema_type: SchemaType::Int { min: 0, max: 100 },
383 required: true,
384 default_value: None,
385 description: "A count".to_string(),
386 },
387 );
388 schema
389 }
390
391 fn make_value(pairs: &[(&str, &str)]) -> ConfigValue {
392 ConfigValue {
393 fields: pairs
394 .iter()
395 .map(|(k, v)| (k.to_string(), v.to_string()))
396 .collect(),
397 }
398 }
399
400 #[test]
401 fn test_new_config_schema() {
402 let schema = new_config_schema("test", "2.0");
403 assert_eq!(schema.name, "test");
404 assert_eq!(schema.version, "2.0");
405 assert!(schema.fields.is_empty());
406 }
407
408 #[test]
409 fn test_add_field() {
410 let mut schema = new_config_schema("s", "1");
411 add_field(
412 &mut schema,
413 SchemaField {
414 name: "x".to_string(),
415 schema_type: SchemaType::Bool,
416 required: false,
417 default_value: None,
418 description: "".to_string(),
419 },
420 );
421 assert_eq!(schema.fields.len(), 1);
422 }
423
424 #[test]
425 fn test_validate_valid_int() {
426 let schema = make_simple_schema();
427 let val = make_value(&[("count", "42")]);
428 let errs = validate_value(&schema, &val);
429 assert!(errs.is_empty(), "expected no errors, got: {errs:?}");
430 }
431
432 #[test]
433 fn test_validate_int_out_of_range() {
434 let schema = make_simple_schema();
435 let val = make_value(&[("count", "200")]);
436 let errs = validate_value(&schema, &val);
437 assert!(!errs.is_empty());
438 assert!(errs[0].contains("out of range"));
439 }
440
441 #[test]
442 fn test_validate_required_missing() {
443 let schema = make_simple_schema();
444 let val = ConfigValue {
445 fields: std::collections::HashMap::new(),
446 };
447 let errs = validate_value(&schema, &val);
448 assert!(!errs.is_empty());
449 assert!(errs[0].contains("missing"));
450 }
451
452 #[test]
453 fn test_validate_bool_field() {
454 let mut schema = new_config_schema("s", "1");
455 add_field(
456 &mut schema,
457 SchemaField {
458 name: "flag".to_string(),
459 schema_type: SchemaType::Bool,
460 required: true,
461 default_value: None,
462 description: "".to_string(),
463 },
464 );
465 let valid = make_value(&[("flag", "true")]);
466 assert!(validate_value(&schema, &valid).is_empty());
467
468 let invalid = make_value(&[("flag", "yes")]);
469 assert!(!validate_value(&schema, &invalid).is_empty());
470 }
471
472 #[test]
473 fn test_validate_float_field() {
474 let mut schema = new_config_schema("s", "1");
475 add_field(
476 &mut schema,
477 SchemaField {
478 name: "ratio".to_string(),
479 schema_type: SchemaType::Float { min: 0.0, max: 1.0 },
480 required: true,
481 default_value: None,
482 description: "".to_string(),
483 },
484 );
485 let valid = make_value(&[("ratio", "0.5")]);
486 assert!(validate_value(&schema, &valid).is_empty());
487
488 let invalid = make_value(&[("ratio", "2.0")]);
489 assert!(!validate_value(&schema, &invalid).is_empty());
490 }
491
492 #[test]
493 fn test_validate_enum_field() {
494 let mut schema = new_config_schema("s", "1");
495 add_field(
496 &mut schema,
497 SchemaField {
498 name: "mode".to_string(),
499 schema_type: SchemaType::Enum {
500 variants: vec!["a".to_string(), "b".to_string()],
501 },
502 required: true,
503 default_value: None,
504 description: "".to_string(),
505 },
506 );
507 let valid = make_value(&[("mode", "a")]);
508 assert!(validate_value(&schema, &valid).is_empty());
509
510 let invalid = make_value(&[("mode", "c")]);
511 assert!(!validate_value(&schema, &invalid).is_empty());
512 }
513
514 #[test]
515 fn test_apply_defaults() {
516 let mut schema = new_config_schema("s", "1");
517 add_field(
518 &mut schema,
519 SchemaField {
520 name: "x".to_string(),
521 schema_type: SchemaType::Int { min: 0, max: 100 },
522 required: false,
523 default_value: Some("42".to_string()),
524 description: "".to_string(),
525 },
526 );
527 let mut val = ConfigValue {
528 fields: std::collections::HashMap::new(),
529 };
530 apply_defaults(&schema, &mut val);
531 assert_eq!(val.fields.get("x").map(|s| s.as_str()), Some("42"));
532 }
533
534 #[test]
535 fn test_config_value_get_bool() {
536 let val = make_value(&[("flag", "true")]);
537 assert_eq!(config_value_get_bool(&val, "flag"), Some(true));
538 assert_eq!(config_value_get_bool(&val, "missing"), None);
539 }
540
541 #[test]
542 fn test_config_value_get_int() {
543 let val = make_value(&[("n", "77")]);
544 assert_eq!(config_value_get_int(&val, "n"), Some(77));
545 }
546
547 #[test]
548 fn test_config_value_get_float() {
549 let val = make_value(&[("f", "2.71")]);
550 let result = config_value_get_float(&val, "f").expect("should succeed");
551 assert!((result - 2.71).abs() < 1e-4);
552 }
553
554 #[test]
555 fn test_config_value_get_str() {
556 let val = make_value(&[("key", "hello")]);
557 assert_eq!(config_value_get_str(&val, "key"), Some("hello"));
558 assert_eq!(config_value_get_str(&val, "missing"), None);
559 }
560
561 #[test]
562 fn test_schema_to_json_contains_name() {
563 let schema = default_render_schema();
564 let json = schema_to_json(&schema);
565 assert!(json.contains("render"));
566 assert!(json.contains("width"));
567 assert!(json.contains("quality"));
568 }
569
570 #[test]
571 fn test_default_render_schema_validates() {
572 let schema = default_render_schema();
573 let val = make_value(&[("width", "1920"), ("height", "1080")]);
574 let errs = validate_value(&schema, &val);
575 assert!(errs.is_empty(), "errors: {errs:?}");
576 }
577
578 #[test]
579 fn test_merge_configs_override_wins() {
580 let base = make_value(&[("a", "1"), ("b", "2")]);
581 let over = make_value(&[("b", "99"), ("c", "3")]);
582 let merged = merge_configs(&base, &over);
583 assert_eq!(merged.fields.get("a").map(|s| s.as_str()), Some("1"));
584 assert_eq!(merged.fields.get("b").map(|s| s.as_str()), Some("99"));
585 assert_eq!(merged.fields.get("c").map(|s| s.as_str()), Some("3"));
586 }
587}