pub const GENERATE_CONFIG_FILENAME: &str = "autumn.generate.toml";
use std::collections::HashMap;
use std::path::Path;
use serde::Deserialize;
use super::GenerateError;
use super::dsl::IdType;
use super::model::ModelOptions;
use super::naming::pascal;
use super::scaffold::ScaffoldOptions;
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[allow(clippy::struct_excessive_bools)]
pub struct ScaffoldConfigEntry {
#[serde(default)]
pub fields: Vec<String>,
#[serde(default)]
pub indexes: Vec<String>,
#[serde(default)]
pub uniques: Vec<String>,
#[serde(default)]
pub validations: Vec<String>,
#[serde(default)]
pub defaults: Vec<String>,
#[serde(default)]
pub queries: Vec<String>,
#[serde(default)]
pub soft_delete: bool,
#[serde(default)]
pub api: bool,
#[serde(default)]
pub sharded: bool,
#[serde(default)]
pub shard_key: Option<String>,
#[serde(default)]
pub live: bool,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub live_validation: bool,
#[serde(default)]
pub no_policy: bool,
#[serde(default)]
pub belongs_to: Option<String>,
#[serde(default)]
pub counter_cache: Option<bool>,
#[serde(default)]
pub searchable: Vec<String>,
#[serde(default)]
pub i18n: bool,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct GenerateDefaults {
#[serde(default)]
id: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct GeneratorConfig {
#[serde(default)]
scaffold: HashMap<String, ScaffoldConfigEntry>,
#[serde(default)]
generate: GenerateDefaults,
}
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct GenerateDefaultsConfig {
#[serde(default)]
generate: GenerateDefaults,
#[serde(default)]
#[allow(dead_code)]
scaffold: HashMap<String, toml::Value>,
}
fn parse_config(config_path: &Path) -> Result<GeneratorConfig, GenerateError> {
let content = std::fs::read_to_string(config_path)
.map_err(|e| std::io::Error::new(e.kind(), format!("{}: {e}", config_path.display())))?;
toml::from_str(&content).map_err(|e| {
GenerateError::Config(format!("invalid TOML in {}: {e}", config_path.display()))
})
}
pub fn read_scaffold_config(
config_path: &Path,
resource_name: &str,
) -> Result<Option<ScaffoldConfigEntry>, GenerateError> {
let mut config = parse_config(config_path)?;
let mut entry = config.scaffold.remove(&pascal(resource_name));
if let Some(e) = entry.as_mut()
&& e.id.is_none()
{
e.id = config.generate.id;
}
Ok(entry)
}
pub fn read_generate_defaults_entry(
config_path: &Path,
) -> Result<ScaffoldConfigEntry, GenerateError> {
Ok(ScaffoldConfigEntry {
id: read_generate_id(config_path)?,
..ScaffoldConfigEntry::default()
})
}
pub fn read_explicit_scaffold_config(
config_path: &Path,
resource_name: &str,
cli_has_fields: bool,
) -> Result<ScaffoldConfigEntry, GenerateError> {
if let Some(entry) = read_scaffold_config(config_path, resource_name)? {
return Ok(entry);
}
let config = parse_config(config_path)?;
if config.scaffold.is_empty() || cli_has_fields {
return Ok(ScaffoldConfigEntry {
id: config.generate.id,
..ScaffoldConfigEntry::default()
});
}
Err(GenerateError::Config(format!(
"no [scaffold.{}] section found in {}",
pascal(resource_name),
config_path.display()
)))
}
fn read_generate_id(config_path: &Path) -> Result<Option<String>, GenerateError> {
let content = std::fs::read_to_string(config_path)
.map_err(|e| std::io::Error::new(e.kind(), format!("{}: {e}", config_path.display())))?;
let config: GenerateDefaultsConfig = toml::from_str(&content).map_err(|e| {
GenerateError::Config(format!("invalid TOML in {}: {e}", config_path.display()))
})?;
Ok(config.generate.id)
}
pub fn read_generate_defaults(config_path: &Path) -> Result<IdType, GenerateError> {
read_generate_id(config_path)?.map_or_else(|| Ok(IdType::default()), |s| IdType::parse(&s))
}
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub fn merge_config_with_cli(
config: ScaffoldConfigEntry,
cli_fields: &[String],
cli_indexes: &[String],
cli_uniques: &[String],
cli_validations: &[String],
cli_defaults: &[String],
cli_queries: &[String],
cli_soft_delete: bool,
cli_api: bool,
cli_sharded: bool,
cli_shard_key: Option<&str>,
cli_live: bool,
cli_id: Option<&str>,
cli_live_validation: bool,
cli_no_policy: bool,
cli_belongs_to: Option<&str>,
cli_counter_cache: bool,
cli_searchable: &[String],
cli_i18n: bool,
) -> Result<(Vec<String>, ScaffoldOptions), GenerateError> {
let pick = |cli: &[String], toml: Vec<String>| -> Vec<String> {
if cli.is_empty() { toml } else { cli.to_vec() }
};
let fields = pick(cli_fields, config.fields);
let indexes = pick(cli_indexes, config.indexes);
let uniques = pick(cli_uniques, config.uniques);
let validations = pick(cli_validations, config.validations);
let defaults = pick(cli_defaults, config.defaults);
let queries = pick(cli_queries, config.queries);
let searchable = pick(cli_searchable, config.searchable);
let soft_delete = cli_soft_delete || config.soft_delete;
let api = cli_api || config.api;
let sharded = cli_sharded || config.sharded;
let shard_key = cli_shard_key.map(str::to_owned).or(config.shard_key);
let live = cli_live || config.live;
let live_validation = cli_live_validation || config.live_validation;
let no_policy = cli_no_policy || config.no_policy;
let belongs_to = cli_belongs_to.map(str::to_owned).or(config.belongs_to);
let counter_cache = cli_counter_cache || config.counter_cache.unwrap_or(false);
let i18n = cli_i18n || config.i18n;
let id_type = if let Some(s) = cli_id {
IdType::parse(s)?
} else if let Some(ref s) = config.id {
IdType::parse(s)?
} else {
IdType::default()
};
Ok((
fields,
ScaffoldOptions {
model: ModelOptions {
indexes,
uniques,
validations,
defaults,
soft_delete,
sharded,
shard_key,
id_type,
searchable,
},
queries,
api,
live,
live_validation,
no_policy,
belongs_to,
counter_cache,
i18n,
},
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_config(tmp: &TempDir, content: &str) -> std::path::PathBuf {
let path = tmp.path().join("autumn.generate.toml");
fs::write(&path, content).unwrap();
path
}
#[test]
fn parse_valid_scaffold_config() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
r#"
[scaffold.Bookmark]
fields = ["url:String", "title:String", "tag:String", "alive:bool"]
indexes = ["url", "tag"]
validations = ["url=url", "title=length:min=1,max=200"]
defaults = ["alive=true"]
queries = ["find_by_tag:tag", "find_by_alive:alive"]
"#,
);
let entry = read_scaffold_config(&path, "Bookmark")
.unwrap()
.expect("Bookmark section must be present");
assert_eq!(
entry.fields,
vec!["url:String", "title:String", "tag:String", "alive:bool"]
);
assert_eq!(entry.indexes, vec!["url", "tag"]);
assert_eq!(
entry.validations,
vec!["url=url", "title=length:min=1,max=200"]
);
assert_eq!(entry.defaults, vec!["alive=true"]);
assert_eq!(
entry.queries,
vec!["find_by_tag:tag", "find_by_alive:alive"]
);
}
#[test]
fn parse_minimal_config_with_fields_only() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[scaffold.Post]\nfields = [\"title:String\"]\n");
let entry = read_scaffold_config(&path, "Post").unwrap().unwrap();
assert_eq!(entry.fields, vec!["title:String"]);
assert!(entry.indexes.is_empty());
assert!(entry.validations.is_empty());
assert!(entry.defaults.is_empty());
assert!(entry.queries.is_empty());
}
#[test]
fn snake_case_name_matches_pascal_case_toml_section() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[scaffold.Bookmark]\nfields = [\"url:String\"]\n");
let entry = read_scaffold_config(&path, "bookmark")
.unwrap()
.expect("snake_case name should resolve to PascalCase section");
assert_eq!(entry.fields, vec!["url:String"]);
}
#[test]
fn unknown_key_returns_config_error() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[scaffold.Post]\nfields = [\"title:String\"]\nindex = [\"title\"]\n",
);
let err = read_scaffold_config(&path, "Post").unwrap_err();
assert!(
matches!(err, GenerateError::Config(_)),
"misspelled key should return Config error, got: {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("index"),
"error should mention the unknown key; got: {msg}"
);
}
#[test]
fn missing_section_returns_none() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[scaffold.Post]\nfields = [\"title:String\"]\n");
let result = read_scaffold_config(&path, "Bookmark").unwrap();
assert!(result.is_none(), "unknown resource should return None");
}
#[test]
fn invalid_toml_returns_config_error() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "not valid toml {{{{");
let err = read_scaffold_config(&path, "Bookmark").unwrap_err();
assert!(
matches!(err, GenerateError::Config(_)),
"expected Config error, got: {err:?}"
);
}
#[test]
fn missing_file_returns_io_error() {
let tmp = TempDir::new().unwrap();
let path = tmp.path().join("nonexistent.toml");
let err = read_scaffold_config(&path, "Bookmark").unwrap_err();
assert!(
matches!(err, GenerateError::Io(_)),
"expected Io error for missing file, got: {err:?}"
);
}
#[test]
fn explicit_config_uses_matching_section() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[scaffold.Post]\nfields = [\"title:String\"]\n");
let entry = read_explicit_scaffold_config(&path, "Post", false).unwrap();
assert_eq!(entry.fields, vec!["title:String"]);
}
#[test]
fn explicit_config_missing_section_among_others_errors_without_cli_fields() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[scaffold.Other]\nfields = [\"name:String\"]\n");
let err = read_explicit_scaffold_config(&path, "Post", false).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("no [scaffold.Post] section"),
"missing section with other sections present must error: {msg}"
);
}
#[test]
fn explicit_config_missing_section_ok_with_cli_fields() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[generate]\nid = \"uuid\"\n\n[scaffold.Other]\nfields = [\"name:String\"]\n",
);
let entry = read_explicit_scaffold_config(&path, "Post", true).unwrap();
assert_eq!(entry.id.as_deref(), Some("uuid"));
assert!(entry.fields.is_empty());
}
#[test]
fn unknown_key_inside_generate_table_errors() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[generate]\nide = \"uuid\"\n");
let err = read_generate_defaults(&path).unwrap_err();
assert!(
matches!(err, GenerateError::Config(_)),
"unknown [generate] key must error, got: {err:?}"
);
assert!(read_generate_defaults_entry(&path).is_err());
assert!(read_scaffold_config(&path, "Post").is_err());
}
#[test]
fn defaults_only_read_rejects_misspelled_top_level_table() {
for bad in [
"[genrate]\nid = \"uuid\"\n",
"id = \"uuid\"\n",
"[generate]\nid = \"uuid\"\n\n[scafold.Post]\nfields = [\"x:String\"]\n",
] {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, bad);
assert!(
read_generate_defaults(&path).is_err(),
"defaults-only read must reject misspelled top-level config: {bad:?}"
);
}
}
#[test]
fn defaults_only_read_tolerates_malformed_scaffold_recipe() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[generate]\nid = \"uuid\"\n\n\
[scaffold.Post]\nfields = [\"title:String\"]\nindex = [\"title\"]\n",
);
assert_eq!(read_generate_defaults(&path).unwrap(), IdType::Uuid);
assert_eq!(
read_generate_defaults_entry(&path).unwrap().id.as_deref(),
Some("uuid")
);
assert!(read_scaffold_config(&path, "Post").is_err());
}
#[test]
fn generate_defaults_entry_ignores_per_resource_recipe() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[generate]\nid = \"uuid\"\n\n\
[scaffold.Post]\nfields = [\"name:String\"]\napi = true\nsharded = true\n",
);
let entry = read_generate_defaults_entry(&path).unwrap();
assert_eq!(
entry.id.as_deref(),
Some("uuid"),
"[generate] id must carry over"
);
assert!(
entry.fields.is_empty(),
"per-resource fields must be ignored"
);
assert!(!entry.api, "per-resource api must be ignored");
assert!(!entry.sharded, "per-resource sharded must be ignored");
}
#[test]
fn explicit_config_misspelled_scaffold_table_errors() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[scafold.Post]\nfields = [\"title:String\"]\n");
let err = read_explicit_scaffold_config(&path, "Post", false).unwrap_err();
assert!(
matches!(err, GenerateError::Config(_)),
"misspelled top-level table must error, got: {err:?}"
);
assert!(read_generate_defaults(&path).is_err());
}
#[test]
fn explicit_config_pure_generate_file_is_lenient() {
let tmp = TempDir::new().unwrap();
let path = write_config(&tmp, "[generate]\nid = \"uuid\"\n");
let entry = read_explicit_scaffold_config(&path, "Post", false).unwrap();
assert_eq!(entry.id.as_deref(), Some("uuid"));
}
fn bookmark_entry() -> ScaffoldConfigEntry {
ScaffoldConfigEntry {
fields: vec!["url:String".into(), "tag:String".into()],
indexes: vec!["url".into()],
uniques: vec![],
validations: vec!["url=url".into()],
defaults: vec![],
queries: vec!["find_by_url:url".into()],
soft_delete: false,
api: false,
sharded: false,
shard_key: None,
live: false,
id: None,
live_validation: false,
no_policy: false,
belongs_to: None,
counter_cache: None,
searchable: vec![],
i18n: false,
}
}
fn merge(entry: ScaffoldConfigEntry) -> (Vec<String>, ScaffoldOptions) {
merge_config_with_cli(
entry,
&[],
&[],
&[],
&[],
&[],
&[],
false,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap()
}
#[test]
fn merge_uses_toml_when_all_cli_empty() {
let (fields, opts) = merge(bookmark_entry());
assert_eq!(fields, vec!["url:String", "tag:String"]);
assert_eq!(opts.model.indexes, vec!["url"]);
assert_eq!(opts.model.validations, vec!["url=url"]);
assert!(opts.model.defaults.is_empty());
assert_eq!(opts.queries, vec!["find_by_url:url"]);
}
#[test]
fn merge_cli_fields_override_toml_fields() {
let (fields, _) = merge_config_with_cli(
bookmark_entry(),
&["title:String".into(), "body:Text".into()],
&[],
&[],
&[],
&[],
&[],
false,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(fields, vec!["title:String", "body:Text"]);
}
#[test]
fn merge_cli_indexes_override_toml_indexes() {
let (_, opts) = merge_config_with_cli(
bookmark_entry(),
&[],
&["tag".into()],
&[],
&[],
&[],
&[],
false,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(opts.model.indexes, vec!["tag"]);
}
#[test]
fn merge_cli_validations_override_toml_validations() {
let (_, opts) = merge_config_with_cli(
bookmark_entry(),
&[],
&[],
&[],
&["url=email".into()],
&[],
&[],
false,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(opts.model.validations, vec!["url=email"]);
}
#[test]
fn merge_cli_defaults_override_toml_defaults() {
let mut entry = bookmark_entry();
entry.defaults = vec!["url=example.com".into()];
let (_, opts) = merge_config_with_cli(
entry,
&[],
&[],
&[],
&[],
&["tag=general".into()],
&[],
false,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(opts.model.defaults, vec!["tag=general"]);
}
#[test]
fn merge_cli_queries_override_toml_queries() {
let (_, opts) = merge_config_with_cli(
bookmark_entry(),
&[],
&[],
&[],
&[],
&[],
&["find_by_tag:tag".into()],
false,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(opts.queries, vec!["find_by_tag:tag"]);
}
#[test]
fn merge_empty_cli_keeps_empty_toml() {
let entry = ScaffoldConfigEntry::default();
let (fields, opts) = merge_config_with_cli(
entry,
&[],
&[],
&[],
&[],
&[],
&[],
false,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert!(fields.is_empty());
assert!(opts.model.indexes.is_empty());
assert!(opts.model.validations.is_empty());
assert!(opts.model.defaults.is_empty());
assert!(opts.queries.is_empty());
}
#[test]
fn merge_cli_soft_delete_flag_wins() {
let (_, opts) = merge_config_with_cli(
bookmark_entry(),
&[],
&[],
&[],
&[],
&[],
&[],
true,
false,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert!(
opts.model.soft_delete,
"cli_soft_delete=true must set soft_delete on the merged options"
);
}
#[test]
fn merge_toml_soft_delete_propagates() {
let mut entry = bookmark_entry();
entry.soft_delete = true;
let (_, opts) = merge(entry);
assert!(
opts.model.soft_delete,
"soft_delete=true in TOML config must propagate when CLI flag is false"
);
}
#[test]
fn merge_soft_delete_false_when_both_unset() {
let (_, opts) = merge(bookmark_entry());
assert!(
!opts.model.soft_delete,
"soft_delete must be false when neither CLI nor TOML sets it"
);
}
#[test]
fn parse_scaffold_config_with_soft_delete() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[scaffold.Article]\nfields = [\"title:String\"]\nsoft_delete = true\n",
);
let entry = read_scaffold_config(&path, "Article")
.unwrap()
.expect("Article section must be present");
assert!(
entry.soft_delete,
"soft_delete = true in TOML must be parsed into ScaffoldConfigEntry"
);
}
#[test]
fn merge_cli_api_flag_wins() {
let (_, opts) = merge_config_with_cli(
bookmark_entry(),
&[],
&[],
&[],
&[],
&[],
&[],
false,
true,
false,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert!(opts.api);
}
#[test]
fn merge_toml_api_propagates() {
let mut entry = bookmark_entry();
entry.api = true;
let (_, opts) = merge(entry);
assert!(opts.api);
}
#[test]
fn parse_scaffold_config_with_api() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[scaffold.Article]\nfields = [\"title:String\"]\napi = true\n",
);
let entry = read_scaffold_config(&path, "Article")
.unwrap()
.expect("Article section must be present");
assert!(
entry.api,
"api = true in TOML must be parsed into ScaffoldConfigEntry"
);
}
#[test]
fn merge_cli_sharded_flag_wins() {
let (_, opts) = merge_config_with_cli(
bookmark_entry(),
&[],
&[],
&[],
&[],
&[],
&[],
false,
false,
true,
None,
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert!(
opts.model.sharded,
"cli_sharded=true must set sharded on the merged options"
);
}
#[test]
fn merge_toml_sharded_propagates() {
let mut entry = bookmark_entry();
entry.sharded = true;
let (_, opts) = merge(entry);
assert!(
opts.model.sharded,
"sharded=true in TOML config must propagate when CLI flag is false"
);
}
#[test]
fn merge_shard_key_cli_overrides_toml() {
let mut entry = bookmark_entry();
entry.shard_key = Some("tenant_id".into());
let (_, opts) = merge_config_with_cli(
entry,
&[],
&[],
&[],
&[],
&[],
&[],
false,
false,
false,
Some("user_id"),
false,
None,
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(
opts.model.shard_key.as_deref(),
Some("user_id"),
"CLI shard_key must override TOML shard_key"
);
}
#[test]
fn merge_shard_key_toml_used_when_no_cli() {
let mut entry = bookmark_entry();
entry.shard_key = Some("org_id".into());
let (_, opts) = merge(entry);
assert_eq!(
opts.model.shard_key.as_deref(),
Some("org_id"),
"TOML shard_key must propagate when no CLI shard_key is given"
);
}
#[test]
fn parse_scaffold_config_with_sharded() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[scaffold.Account]\nfields = [\"tenant_id:i64\"]\nsharded = true\nshard_key = \"tenant_id\"\n",
);
let entry = read_scaffold_config(&path, "Account")
.unwrap()
.expect("Account section must be present");
assert!(
entry.sharded,
"sharded = true in TOML must be parsed into ScaffoldConfigEntry"
);
assert_eq!(
entry.shard_key.as_deref(),
Some("tenant_id"),
"shard_key in TOML must be parsed into ScaffoldConfigEntry"
);
}
#[test]
fn merge_cli_id_uuid_sets_uuid_id_type() {
let (_, opts) = merge_config_with_cli(
bookmark_entry(),
&[],
&[],
&[],
&[],
&[],
&[],
false,
false,
false,
None,
false,
Some("uuid"),
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(
opts.model.id_type,
IdType::Uuid,
"cli --id uuid must set Uuid id_type"
);
}
#[test]
fn merge_toml_id_uuid_propagates() {
let mut entry = bookmark_entry();
entry.id = Some("uuid".into());
let (_, opts) = merge(entry);
assert_eq!(
opts.model.id_type,
IdType::Uuid,
"[scaffold.X] id = 'uuid' must propagate"
);
}
#[test]
fn merge_cli_id_overrides_toml_id() {
let mut entry = bookmark_entry();
entry.id = Some("uuid".into());
let (_, opts) = merge_config_with_cli(
entry,
&[],
&[],
&[],
&[],
&[],
&[],
false,
false,
false,
None,
false,
Some("bigint"),
false,
false,
None,
false,
&[],
false,
)
.unwrap();
assert_eq!(
opts.model.id_type,
IdType::BigSerial,
"CLI --id bigint must override TOML id = 'uuid'"
);
}
#[test]
fn merge_default_id_type_is_bigserial() {
let (_, opts) = merge(bookmark_entry());
assert_eq!(
opts.model.id_type,
IdType::BigSerial,
"default id_type must be BigSerial (AC4)"
);
}
#[test]
fn merge_cli_bad_id_returns_error() {
let err = merge_config_with_cli(
bookmark_entry(),
&[],
&[],
&[],
&[],
&[],
&[],
false,
false,
false,
None,
false,
Some("guid"),
false,
false,
None,
false,
&[],
false,
)
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("guid"),
"error must mention the bad value: {msg}"
);
assert!(
msg.contains("uuid"),
"error must list accepted values: {msg}"
);
}
#[test]
fn project_default_id_propagates_via_generate_section() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[generate]\nid = \"uuid\"\n\n[scaffold.Post]\nfields = [\"title:String\"]\n",
);
let defaults_id = read_generate_defaults(&path).unwrap();
assert_eq!(
defaults_id,
IdType::Uuid,
"[generate] id = 'uuid' must be read as Uuid"
);
}
#[test]
fn project_default_propagates_into_scaffold_entry() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[generate]\nid = \"uuid\"\n\n[scaffold.Post]\nfields = [\"title:String\"]\n",
);
let entry = read_scaffold_config(&path, "Post").unwrap().unwrap();
assert_eq!(
entry.id.as_deref(),
Some("uuid"),
"[generate] id must propagate into scaffold entry id when unset"
);
}
#[test]
fn per_resource_id_overrides_project_default() {
let tmp = TempDir::new().unwrap();
let path = write_config(
&tmp,
"[generate]\nid = \"uuid\"\n\n[scaffold.Post]\nfields = [\"title:String\"]\nid = \"bigint\"\n",
);
let entry = read_scaffold_config(&path, "Post").unwrap().unwrap();
assert_eq!(
entry.id.as_deref(),
Some("bigint"),
"per-resource id must not be overridden by [generate] default"
);
}
}