use schematic::*; use serial_test::serial; use std::collections::HashMap; use std::env; fn test_list(_: &[String], _: &T, context: &Context, _: bool) -> ValidateResult { if context.fail { return Err(ValidateError::new("invalid")); } Ok(()) } fn test_map(_: &HashMap, _: &T, context: &Context, _: bool) -> ValidateResult { if context.fail { return Err(ValidateError::new("invalid")); } Ok(()) } fn test_cfg(_: &PartialProjectsConfig, _: &T, context: &Context, _: bool) -> ValidateResult { if context.fail { return Err(ValidateError::new("invalid")); } Ok(()) } fn assert_validation_error(error: ConfigError, count: usize) { if let ConfigError::Validator { error: inner, .. } = error { assert_eq!(inner.errors.len(), count); } else { panic!("expected validation error"); } } #[derive(Default)] pub struct Context { fail: bool, } #[derive(Debug, Config, Eq, PartialEq)] #[config(context = Context)] pub struct ProjectsConfig { #[setting(validate = test_list)] list: Vec, #[setting(validate = test_map)] map: HashMap, } #[derive(Debug, Config, Eq, PartialEq)] #[config(context = Context, serde(untagged))] pub enum Projects { #[setting(nested, validate = test_cfg)] Config(ProjectsConfig), #[setting(merge = merge::prepend_vec, validate = test_list)] List(Vec), #[setting(default, merge = merge::merge_hashmap, validate = test_map)] Map(HashMap), } #[derive(Debug, Config, Eq, PartialEq)] #[config(context = Context)] pub struct StandardSettings { #[setting(nested)] projects: Projects, } #[test] #[serial] fn returns_defaults() { let config = ConfigLoader::::new() .load() .unwrap() .config; assert_eq!(config.projects, Projects::Map(HashMap::new())); } mod loading { use super::*; #[test] fn loads_list() { let config = ConfigLoader::::new() .code( r#" { "projects": ["foo", "bar", "baz"] }"#, Format::Json, ) .unwrap() .load() .unwrap() .config; assert_eq!( config.projects, Projects::List(vec!["foo".into(), "bar".into(), "baz".into()]) ); } #[test] fn loads_map() { let config = ConfigLoader::::new() .code( r#" { "projects": { "foo": "bar" } }"#, Format::Json, ) .unwrap() .load() .unwrap() .config; assert_eq!( config.projects, Projects::Map(HashMap::from_iter([("foo".into(), "bar".into())])) ); } #[test] fn loads_config() { let config = ConfigLoader::::new() .code( r#" { "projects": { "list": ["baz"], "map": { "foo": "bar" } } }"#, Format::Json, ) .unwrap() .load() .unwrap() .config; assert_eq!( config.projects, Projects::Config(ProjectsConfig { list: vec!["baz".into()], map: HashMap::from_iter([("foo".into(), "bar".into())]) }) ); } } mod merging { use super::*; #[test] fn merges_list() { let config = ConfigLoader::::new() .code( r#" { "projects": ["foo", "bar"] }"#, Format::Json, ) .unwrap() .code( r#" { "projects": ["baz", "qux"] }"#, Format::Json, ) .unwrap() .load() .unwrap() .config; assert_eq!( config.projects, Projects::List(vec!["baz".into(), "qux".into(), "foo".into(), "bar".into()]) ); } #[test] fn merges_map() { let config = ConfigLoader::::new() .code( r#" { "projects": { "foo": "bar" } }"#, Format::Json, ) .unwrap() .code( r#" { "projects": { "baz": "qux" } }"#, Format::Json, ) .unwrap() .load() .unwrap() .config; assert_eq!( config.projects, Projects::Map(HashMap::from_iter([ ("foo".into(), "bar".into()), ("baz".into(), "qux".into()) ])) ); } #[test] fn replaces_config() { let config = ConfigLoader::::new() .code( r#" { "projects": { "list": ["qux", "bar"], "map": { "baz": "foo" } } }"#, Format::Json, ) .unwrap() .load() .unwrap() .config; assert_eq!( config.projects, Projects::Config(ProjectsConfig { list: vec!["qux".into(), "bar".into()], map: HashMap::from_iter([("baz".into(), "foo".into())]) }) ); } } mod validating { use super::*; #[test] fn validates_list() { let error = ConfigLoader::::new() .code( r#" { "projects": ["foo", "bar", "baz"] }"#, Format::Json, ) .unwrap() .load_with_context(&Context { fail: true }) .err() .unwrap(); assert_validation_error(error, 1); } #[test] fn validates_map() { let error = ConfigLoader::::new() .code( r#" { "projects": { "foo": "bar" } }"#, Format::Json, ) .unwrap() .load_with_context(&Context { fail: true }) .err() .unwrap(); assert_validation_error(error, 1); } #[test] fn validates_config() { let error = ConfigLoader::::new() .code( r#" { "projects": { "list": ["baz"], "map": { "foo": "bar" } } }"#, Format::Json, ) .unwrap() .load_with_context(&Context { fail: true }) .err() .unwrap(); // Also validates nested fields assert_validation_error(error, 3); } } #[cfg(feature = "renderer_json_schema")] #[test] fn generates_json_schema() { use starbase_sandbox::{assert_snapshot, create_empty_sandbox}; let sandbox = create_empty_sandbox(); let file = sandbox.path().join("schema.json"); let mut generator = schema::SchemaGenerator::default(); generator.add::(); generator .generate(&file, schema::json_schema::JsonSchemaRenderer::default()) .unwrap(); assert!(file.exists()); assert_snapshot!(std::fs::read_to_string(file).unwrap()); } #[cfg(feature = "renderer_typescript")] #[test] fn generates_typescript() { use starbase_sandbox::{assert_snapshot, create_empty_sandbox}; let sandbox = create_empty_sandbox(); let file = sandbox.path().join("config.ts"); let mut generator = schema::SchemaGenerator::default(); generator.add::(); generator .generate(&file, schema::typescript::TypeScriptRenderer::default()) .unwrap(); assert!(file.exists()); assert_snapshot!(std::fs::read_to_string(file).unwrap()); }