use crate::error::ConfigError;
use serde::Serialize;
use std::fs;
#[cfg(feature = "schema")]
use schemars::{schema_for, JsonSchema};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GenerateLevel {
Minimal,
Full,
Documentation,
}
impl GenerateLevel {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"minimal" | "min" => GenerateLevel::Minimal,
"doc" | "documentation" => GenerateLevel::Documentation,
"full" => GenerateLevel::Full,
_ => GenerateLevel::Full,
}
}
}
impl std::str::FromStr for GenerateLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parse(s))
}
}
pub struct GenerateCommand;
impl GenerateCommand {
#[cfg(feature = "schema")]
pub fn execute<T>(output: Option<&String>, level: &str) -> Result<(), ConfigError>
where
T: Serialize + Default + JsonSchema,
{
let defaults = T::default();
let generate_level = GenerateLevel::parse(level);
let content = match generate_level {
GenerateLevel::Minimal => toml::to_string(&defaults)
.map_err(|e| ConfigError::FormatDetectionFailed(e.to_string()))?,
GenerateLevel::Documentation => generate_documentation_template::<T>(),
GenerateLevel::Full => toml::to_string_pretty(&defaults)
.map_err(|e| ConfigError::FormatDetectionFailed(e.to_string()))?,
};
Self::write_content(output, &content)
}
#[cfg(not(feature = "schema"))]
pub fn execute<T>(output: Option<&String>, level: &str) -> Result<(), ConfigError>
where
T: Serialize + Default,
{
let defaults = T::default();
let generate_level = GenerateLevel::parse(level);
let content = match generate_level {
GenerateLevel::Minimal => toml::to_string(&defaults)
.map_err(|e| ConfigError::FormatDetectionFailed(e.to_string()))?,
GenerateLevel::Documentation => generate_documentation_template::<T>(),
GenerateLevel::Full => toml::to_string_pretty(&defaults)
.map_err(|e| ConfigError::FormatDetectionFailed(e.to_string()))?,
};
Self::write_content(output, &content)
}
pub fn execute_placeholder(
output: Option<&String>,
level: &str,
struct_name: Option<&String>,
format: &str,
) -> Result<String, ConfigError> {
let generate_level = GenerateLevel::parse(level);
let mut toml_content = match generate_level {
GenerateLevel::Minimal => Self::minimal_template(),
GenerateLevel::Documentation => Self::documentation_template(),
GenerateLevel::Full => Self::full_template(),
};
if let Some(name) = struct_name {
toml_content =
toml_content.replace("name = \"example\"", &format!("name = \"{}\"", name));
toml_content = toml_content.replace(
"# Minimal Config Template",
&format!("# {} Config Template", name),
);
toml_content = toml_content.replace(
"# Full Config Template",
&format!("# {} Config Template", name),
);
toml_content = toml_content.replace(
"Configuration Template",
&format!("{} Configuration Template", name),
);
}
let content = if format.eq_ignore_ascii_case("toml") {
toml_content
} else {
let value: toml::Value = toml::from_str(&toml_content)
.map_err(|e| ConfigError::ParseError(format!("Failed to parse template: {}", e)))?;
match format.to_lowercase().as_str() {
"json" => serde_json::to_string_pretty(&value)
.map_err(|e| ConfigError::SerializationError(e.to_string()))?,
"yaml" | "yml" => serde_yaml::to_string(&value)
.map_err(|e| ConfigError::SerializationError(e.to_string()))?,
"ini" => {
serde_ini::to_string(&value).map_err(|e| {
ConfigError::SerializationError(format!(
"INI serialization failed (structure might be too deep): {}",
e
))
})?
}
_ => {
return Err(ConfigError::FormatDetectionFailed(format!(
"Unsupported format: {}",
format
)))
}
}
};
Self::write_content(output, &content)?;
Ok(content)
}
fn minimal_template() -> String {
"# Minimal Config Template\n# Generated by confers\n\n[app]\nname = \"example\"\nversion = \"1.0.0\"\n".to_string()
}
fn full_template() -> String {
"# Full Config Template\n# Generated by confers\n\n# Basic configuration\n[app]\nname = \"example\"\nversion = \"1.0.0\"\n\n# Server settings\n[server]\nhost = \"localhost\"\nport = 8080\n\n# Database configuration\n[database]\nurl = \"postgres://localhost/mydb\"\npool_size = 10\n\n# Logging configuration\n[logging]\nlevel = \"info\"\nformat = \"json\"\n".to_string()
}
#[cfg(feature = "schema")]
fn documentation_template() -> String {
#[derive(Default, Serialize, JsonSchema)]
struct GenericPlaceholder {
name: String,
version: String,
description: String,
}
generate_documentation_template::<GenericPlaceholder>()
}
#[cfg(not(feature = "schema"))]
fn documentation_template() -> String {
generate_documentation_template::<()>()
}
fn write_content(output: Option<&String>, content: &str) -> Result<(), ConfigError> {
if let Some(path) = output {
fs::write(path, content)
.map_err(|e| ConfigError::FormatDetectionFailed(e.to_string()))?;
} else {
println!("{}", content);
}
Ok(())
}
}
#[cfg(feature = "schema")]
fn generate_documentation_template<T: Default + JsonSchema + serde::Serialize>() -> String {
let defaults = T::default();
let defaults_str = toml::to_string_pretty(&defaults)
.map_err(|_| ConfigError::FormatDetectionFailed("Failed to serialize defaults".to_string()))
.unwrap_or_default();
let schema = schema_for!(T);
let schema_value = serde_json::to_value(&schema)
.map_err(|_| ConfigError::FormatDetectionFailed("Failed to serialize schema".to_string()))
.unwrap_or_default();
let struct_name = schema_value
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("Config");
let mut doc = format!(
r#"# Documentation Configuration Template for {}
# Generated by confers - Configuration Management Tool
# ============================================================
# This template includes detailed comments to help you understand
# each configuration option and its purpose.
# -----------------------------------------------------------
"#,
struct_name
);
if let Some(properties) = schema_value.get("properties").and_then(|p| p.as_object()) {
generate_property_documentation(properties, "", &mut doc);
}
doc.push_str(&format!(
"\n# ============================================================\n# Default Values Reference\n# -----------------------------------------------------------\n{}\n# -----------------------------------------------------------\n",
defaults_str
));
doc
}
#[cfg(not(feature = "schema"))]
fn generate_documentation_template<T: Default + serde::Serialize>() -> String {
let defaults = T::default();
let defaults_str = toml::to_string_pretty(&defaults)
.map_err(|_| ConfigError::FormatDetectionFailed("Failed to serialize defaults".to_string()))
.unwrap_or_default();
let mut doc = r#"# Configuration Template (Documentation Mode)
# Generated by confers - Configuration Management Tool
# ============================================================
# This template includes detailed comments to help you understand
# each configuration option and its purpose.
"#
.to_string();
doc.push_str(&format!(
"# Default Values\n# -----------------------------------------------------------\n{}\n# -----------------------------------------------------------\n",
defaults_str
));
doc
}
#[cfg(feature = "schema")]
fn generate_property_documentation(
properties: &serde_json::Map<String, serde_json::Value>,
prefix: &str,
doc: &mut String,
) {
for (prop_name, prop_schema) in properties {
let full_name = if prefix.is_empty() {
prop_name.clone()
} else {
format!("{}.{}", prefix, prop_name)
};
if let Some(obj_props) = prop_schema.get("properties").and_then(|p| p.as_object()) {
doc.push_str(&format!("# [{}]\n", full_name));
if let Some(desc) = prop_schema.get("description").and_then(|d| d.as_str()) {
doc.push_str(&format!("# {}\n", desc));
}
doc.push_str("#\n");
generate_property_documentation(obj_props, &full_name, doc);
doc.push('\n');
} else {
let type_str = prop_schema
.get("type")
.and_then(|t| t.as_str())
.unwrap_or("unknown");
if let Some(desc) = prop_schema.get("description").and_then(|d| d.as_str()) {
doc.push_str(&format!("# {}\n", desc));
}
let default_val = prop_schema
.get("default")
.map(|v| format!(" (default: {})", v));
let enum_vals = prop_schema
.get("enum")
.and_then(|e| e.as_array())
.map(|vals| {
let vals_str: Vec<String> = vals
.iter()
.filter_map(|v| v.as_str().map(|s| format!("'{}'", s)))
.collect();
format!(" (options: {})", vals_str.join(", "))
});
let example = match type_str {
"string" | "integer" | "number" | "boolean" => {
format!("{} = \"{}\"", prop_name, "<value>")
}
_ => format!("{} = <{}>", prop_name, type_str),
};
if let Some(dv) = default_val {
doc.push_str(&format!("{}{}\n", example, dv));
} else if let Some(ev) = enum_vals {
doc.push_str(&format!("{}{}\n", example, ev));
} else {
doc.push_str(&format!("{}\n", example));
}
doc.push('\n');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "schema")]
mod with_schema {
use super::*;
use schemars::JsonSchema;
#[derive(Debug, Default, Serialize, JsonSchema)]
struct TestConfig {
name: String,
port: u16,
#[schemars(skip)]
secret: String,
}
#[test]
fn test_documentation_with_schema() {
let result = GenerateCommand::execute::<TestConfig>(None, "documentation");
assert!(result.is_ok());
let result = GenerateCommand::execute::<TestConfig>(None, "doc");
assert!(result.is_ok());
}
}
#[test]
fn test_generate_level_parsing() {
assert_eq!(GenerateLevel::parse("minimal"), GenerateLevel::Minimal);
assert_eq!(GenerateLevel::parse("min"), GenerateLevel::Minimal);
assert_eq!(GenerateLevel::parse("doc"), GenerateLevel::Documentation);
assert_eq!(
GenerateLevel::parse("documentation"),
GenerateLevel::Documentation
);
assert_eq!(GenerateLevel::parse("full"), GenerateLevel::Full);
assert_eq!(GenerateLevel::parse("unknown"), GenerateLevel::Full);
assert_eq!(GenerateLevel::parse("FULL"), GenerateLevel::Full);
assert_eq!(GenerateLevel::parse("DOC"), GenerateLevel::Documentation);
}
#[test]
fn test_generate_command_minimal() {
let result = GenerateCommand::execute_placeholder(None, "minimal", None, "toml");
assert!(result.is_ok());
let content = result.unwrap();
assert!(content.contains("name = \"example\""));
assert!(content.contains("# Minimal Config Template"));
}
#[test]
fn test_generate_command_full() {
let result = GenerateCommand::execute_placeholder(None, "full", None, "toml");
assert!(result.is_ok());
let content = result.unwrap();
assert!(content.contains("[server]"));
assert!(content.contains("[database]"));
assert!(content.contains("# Full Config Template"));
}
#[test]
fn test_generate_command_documentation_fallback() {
let result = GenerateCommand::execute_placeholder(None, "doc", None, "toml");
assert!(result.is_ok());
let content = result.unwrap();
assert!(content.contains("Configuration Template"));
assert!(content.contains("Generated by confers"));
}
#[test]
fn test_generate_level_affects_output() {
let minimal = GenerateCommand::execute_placeholder(None, "minimal", None, "toml").unwrap();
let full = GenerateCommand::execute_placeholder(None, "full", None, "toml").unwrap();
let doc = GenerateCommand::execute_placeholder(None, "doc", None, "toml").unwrap();
assert_ne!(minimal, full);
assert_ne!(full, doc);
assert_ne!(minimal, doc);
assert!(minimal.contains("Minimal"));
assert!(full.contains("Full"));
assert!(doc.contains("Documentation"));
}
}