#![allow(dead_code)]
#![allow(unused_variables)]
use server_less::program;
use server_less::Config;
struct BasicApp;
#[program]
impl BasicApp {
pub fn create_user(&self, name: String) {
println!("Created {}", name);
}
pub fn list_users(&self) {
println!("Listing users...");
}
}
#[test]
fn test_program_basic_cli_command() {
let cmd = BasicApp::cli_command();
let subcommands: Vec<_> = cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect();
assert!(subcommands.contains(&"create-user".to_string()));
assert!(subcommands.contains(&"list-users".to_string()));
}
#[test]
fn test_program_basic_markdown_docs() {
let docs = BasicApp::markdown_docs();
assert!(
docs.contains("create_user"),
"Docs should contain create_user: {}",
docs
);
}
struct NamedApp;
#[program(name = "myctl", version = "2.0.0", description = "My cool CLI")]
impl NamedApp {
pub fn do_thing(&self, input: String) {
println!("{}", input);
}
}
#[test]
fn test_program_named_cli_command() {
let cmd = NamedApp::cli_command();
assert_eq!(cmd.get_name(), "myctl");
}
struct NoDocsApp;
#[program(markdown = false)]
impl NoDocsApp {
pub fn run(&self) {
println!("Running...");
}
}
#[test]
fn test_program_no_markdown() {
let cmd = NoDocsApp::cli_command();
let subcommands: Vec<_> = cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect();
assert!(subcommands.contains(&"run".to_string()));
}
struct FullApp;
#[program(
name = "fullctl",
version = "1.0.0",
description = "Full app",
markdown = true
)]
impl FullApp {
pub fn create(&self, name: String) {
println!("Created {}", name);
}
}
#[test]
fn test_program_full_options() {
let cmd = FullApp::cli_command();
assert_eq!(cmd.get_name(), "fullctl");
let docs = FullApp::markdown_docs();
assert!(!docs.is_empty());
}
#[derive(Config)]
struct AppConfig {
#[param(default = "localhost", help = "Hostname to bind")]
host: String,
#[param(default = 8080, help = "Port to listen on", env = "APP_PORT")]
port: u16,
database_url: Option<String>,
}
struct ConfiguredApp;
#[program(config = AppConfig, name = "myapp")]
impl ConfiguredApp {
pub fn greet(&self, name: String) -> String {
format!("Hello, {name}!")
}
}
#[test]
fn test_config_subcommand_appears_in_cli() {
let cmd = ConfiguredApp::cli_command();
let subcommands: Vec<_> = cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect();
assert!(
subcommands.contains(&"config".to_string()),
"Expected 'config' subcommand; got: {subcommands:?}"
);
assert!(
subcommands.contains(&"greet".to_string()),
"Expected 'greet' subcommand; got: {subcommands:?}"
);
}
#[test]
fn test_config_subcommand_has_children() {
let cmd = ConfiguredApp::cli_command();
let config_cmd = cmd
.get_subcommands()
.find(|s| s.get_name() == "config")
.expect("config subcommand missing");
let children: Vec<_> = config_cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect();
assert!(children.contains(&"show".to_string()), "Missing 'show'");
assert!(children.contains(&"schema".to_string()), "Missing 'schema'");
assert!(children.contains(&"validate".to_string()), "Missing 'validate'");
assert!(children.contains(&"set".to_string()), "Missing 'set'");
}
#[test]
fn test_config_cmd_custom_name() {
struct AltApp;
#[program(config = AppConfig, config_cmd = "settings", name = "altapp")]
impl AltApp {
pub fn ping(&self) -> String { "pong".into() }
}
let cmd = AltApp::cli_command();
let names: Vec<_> = cmd
.get_subcommands()
.map(|s| s.get_name().to_string())
.collect();
assert!(names.contains(&"settings".to_string()), "Expected 'settings'; got: {names:?}");
assert!(!names.contains(&"config".to_string()), "Unexpected 'config'");
}
#[test]
fn test_derive_config_load_defaults() {
use server_less::{ConfigSource, ConfigLoad};
let cfg = AppConfig::load(&[ConfigSource::Defaults]).unwrap();
assert_eq!(cfg.host, "localhost");
assert_eq!(cfg.port, 8080);
assert_eq!(cfg.database_url, None);
}
#[test]
fn test_derive_config_field_meta() {
use server_less::ConfigLoad;
let meta = AppConfig::field_meta();
assert_eq!(meta.len(), 3);
let host_meta = meta.iter().find(|f| f.name == "host").expect("host field");
assert_eq!(host_meta.default, Some("localhost"));
assert_eq!(host_meta.help, Some("Hostname to bind"));
assert!(!host_meta.required);
let port_meta = meta.iter().find(|f| f.name == "port").expect("port field");
assert_eq!(port_meta.env_var, Some("APP_PORT"));
assert_eq!(port_meta.default, Some("8080"));
let db_meta = meta.iter().find(|f| f.name == "database_url").expect("database_url field");
assert!(!db_meta.required, "Option<T> should not be required");
}
#[derive(Config)]
struct DaemonConfig {
#[param(default = "true", help = "Enable daemon mode")]
enabled: bool,
#[param(default = "30", help = "Heartbeat interval in seconds")]
heartbeat_secs: u64,
}
#[derive(Config)]
struct SearchConfig {
#[param(default = "100")]
max_results: u32,
index_path: Option<String>,
}
#[derive(Config)]
struct FullNestedConfig {
#[param(default = "myapp", help = "Application name")]
app_name: String,
#[param(nested)]
daemon: DaemonConfig,
#[param(nested, file_key = "text-search")]
search: SearchConfig,
}
#[test]
fn test_nested_config_load_defaults() {
use server_less::{ConfigSource, ConfigLoad};
let cfg = FullNestedConfig::load(&[ConfigSource::Defaults]).unwrap();
assert_eq!(cfg.app_name, "myapp");
assert!(cfg.daemon.enabled);
assert_eq!(cfg.daemon.heartbeat_secs, 30);
assert_eq!(cfg.search.max_results, 100);
assert_eq!(cfg.search.index_path, None);
}
#[test]
fn test_nested_config_from_toml_file() {
use server_less::{ConfigSource, ConfigLoad};
use std::io::Write;
let mut f = tempfile::NamedTempFile::new().unwrap();
write!(
f,
r#"
app_name = "testapp"
[daemon]
enabled = false
heartbeat_secs = 60
[text-search]
max_results = 50
index_path = "/var/search"
"#
)
.unwrap();
let cfg = FullNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::File(f.path().to_path_buf()),
])
.unwrap();
assert_eq!(cfg.app_name, "testapp");
assert!(!cfg.daemon.enabled);
assert_eq!(cfg.daemon.heartbeat_secs, 60);
assert_eq!(cfg.search.max_results, 50);
assert_eq!(cfg.search.index_path, Some("/var/search".to_string()));
}
#[test]
fn test_nested_config_env_prefix_inheritance() {
use server_less::{ConfigSource, ConfigLoad};
unsafe {
std::env::set_var("APP_DAEMON_ENABLED", "false");
std::env::set_var("APP_DAEMON_HEARTBEAT_SECS", "120");
}
let cfg = FullNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::Env { prefix: Some("APP".into()) },
])
.unwrap();
unsafe {
std::env::remove_var("APP_DAEMON_ENABLED");
std::env::remove_var("APP_DAEMON_HEARTBEAT_SECS");
}
assert!(!cfg.daemon.enabled);
assert_eq!(cfg.daemon.heartbeat_secs, 120);
assert_eq!(cfg.search.max_results, 100);
}
#[test]
fn test_nested_config_env_prefix_override() {
use server_less::{ConfigSource, ConfigLoad};
#[derive(Config)]
struct OverriddenPrefixConfig {
#[param(default = "main")]
app_name: String,
#[param(nested, env_prefix = "SEARCH")]
search: SearchConfig,
}
unsafe {
std::env::set_var("SEARCH_MAX_RESULTS", "42");
}
let cfg = OverriddenPrefixConfig::load(&[
ConfigSource::Defaults,
ConfigSource::Env { prefix: Some("APP".into()) },
])
.unwrap();
unsafe {
std::env::remove_var("SEARCH_MAX_RESULTS");
}
assert_eq!(cfg.search.max_results, 42);
}
#[test]
fn test_nested_config_field_meta_populated() {
use server_less::ConfigLoad;
let meta = FullNestedConfig::field_meta();
let app_name_meta = meta.iter().find(|f| f.name == "app_name").expect("app_name field");
assert!(app_name_meta.nested.is_none(), "app_name should not be nested");
assert!(app_name_meta.env_prefix.is_none());
let daemon_meta = meta.iter().find(|f| f.name == "daemon").expect("daemon field");
assert!(daemon_meta.nested.is_some(), "daemon should have nested meta");
let child_meta = daemon_meta.nested.unwrap();
assert_eq!(child_meta.len(), 2);
assert!(child_meta.iter().any(|f| f.name == "enabled"));
assert!(child_meta.iter().any(|f| f.name == "heartbeat_secs"));
let search_meta = meta.iter().find(|f| f.name == "search").expect("search field");
assert!(search_meta.nested.is_some(), "search should have nested meta");
assert_eq!(search_meta.file_key, Some("text-search"));
}
#[test]
fn test_nested_config_merge_file() {
use server_less::{ConfigSource, ConfigLoad};
use std::io::Write;
let mut global = tempfile::NamedTempFile::new().unwrap();
write!(
global,
r#"
app_name = "global"
[daemon]
enabled = true
heartbeat_secs = 10
[text-search]
max_results = 200
"#
)
.unwrap();
let mut local = tempfile::NamedTempFile::new().unwrap();
write!(
local,
r#"
[daemon]
heartbeat_secs = 99
"#
)
.unwrap();
let cfg = FullNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::File(global.path().to_path_buf()),
ConfigSource::MergeFile(local.path().to_path_buf()),
])
.unwrap();
assert_eq!(cfg.app_name, "global");
assert!(cfg.daemon.enabled);
assert_eq!(cfg.daemon.heartbeat_secs, 10);
assert_eq!(cfg.search.max_results, 200);
}
#[derive(serde::Deserialize, Debug, Default, PartialEq)]
struct RulesConfig {
#[serde(default)]
strict: bool,
#[serde(default = "default_max_rules")]
max_rules: u32,
}
fn default_max_rules() -> u32 {
50
}
#[derive(serde::Deserialize, Debug, Default, PartialEq)]
struct AliasMap {
#[serde(flatten)]
entries: std::collections::HashMap<String, String>,
}
#[derive(serde::Deserialize, Debug, PartialEq)]
struct ExtrasConfig {
#[serde(default)]
notes: String,
}
#[derive(Config)]
struct SerdeNestedConfig {
#[param(default = "main", help = "App name")]
app_name: String,
#[param(nested, serde)]
rules: RulesConfig,
#[param(nested, serde, file_key = "aliases")]
alias_map: AliasMap,
#[param(nested, serde)]
extras: Option<ExtrasConfig>,
}
#[test]
fn test_serde_nested_load_from_toml_file() {
use server_less::{ConfigSource, ConfigLoad};
use std::io::Write;
let mut f = tempfile::NamedTempFile::new().unwrap();
write!(
f,
r#"
app_name = "serde-test"
[rules]
strict = true
max_rules = 100
[aliases]
foo = "bar"
baz = "qux"
[extras]
notes = "hello"
"#
)
.unwrap();
let cfg = SerdeNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::File(f.path().to_path_buf()),
])
.unwrap();
assert_eq!(cfg.app_name, "serde-test");
assert!(cfg.rules.strict);
assert_eq!(cfg.rules.max_rules, 100);
assert_eq!(cfg.alias_map.entries.get("foo").map(String::as_str), Some("bar"));
assert_eq!(cfg.alias_map.entries.get("baz").map(String::as_str), Some("qux"));
assert_eq!(cfg.extras, Some(ExtrasConfig { notes: "hello".to_string() }));
}
#[test]
fn test_serde_nested_serde_defaults_via_serde_default() {
use server_less::{ConfigSource, ConfigLoad};
use std::io::Write;
let mut f = tempfile::NamedTempFile::new().unwrap();
write!(
f,
r#"
[rules]
# strict defaults to false via #[serde(default)]
max_rules = 10
[aliases]
"#
)
.unwrap();
let cfg = SerdeNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::File(f.path().to_path_buf()),
])
.unwrap();
assert!(!cfg.rules.strict);
assert_eq!(cfg.rules.max_rules, 10);
assert_eq!(cfg.extras, None);
}
#[test]
fn test_serde_nested_file_key_override() {
use server_less::{ConfigSource, ConfigLoad};
use std::io::Write;
let mut f = tempfile::NamedTempFile::new().unwrap();
write!(
f,
r#"
[rules]
strict = false
[aliases]
mykey = "myval"
"#
)
.unwrap();
let cfg = SerdeNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::File(f.path().to_path_buf()),
])
.unwrap();
assert_eq!(cfg.alias_map.entries.get("mykey").map(String::as_str), Some("myval"));
}
#[test]
fn test_serde_nested_env_vars_ignored() {
use server_less::{ConfigSource, ConfigLoad};
use std::io::Write;
unsafe {
std::env::set_var("APP_RULES_STRICT", "true");
std::env::set_var("APP_ALIAS_MAP_FOO", "fromenv");
}
let mut f = tempfile::NamedTempFile::new().unwrap();
write!(
f,
r#"
[rules]
strict = false
max_rules = 5
[aliases]
"#
)
.unwrap();
let cfg = SerdeNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::File(f.path().to_path_buf()),
ConfigSource::Env { prefix: Some("APP".into()) },
])
.unwrap();
unsafe {
std::env::remove_var("APP_RULES_STRICT");
std::env::remove_var("APP_ALIAS_MAP_FOO");
}
assert!(!cfg.rules.strict, "env var should not override serde-nested field");
assert!(!cfg.alias_map.entries.contains_key("foo"), "env var should not inject into serde-nested field");
}
#[test]
fn test_serde_nested_merge_file_semantics() {
use server_less::{ConfigSource, ConfigLoad};
use std::io::Write;
let mut primary = tempfile::NamedTempFile::new().unwrap();
write!(
primary,
r#"
[rules]
strict = true
max_rules = 20
[aliases]
a = "1"
"#
)
.unwrap();
let mut merge = tempfile::NamedTempFile::new().unwrap();
write!(
merge,
r#"
[rules]
strict = false
max_rules = 99
[aliases]
b = "2"
"#
)
.unwrap();
let cfg = SerdeNestedConfig::load(&[
ConfigSource::Defaults,
ConfigSource::File(primary.path().to_path_buf()),
ConfigSource::MergeFile(merge.path().to_path_buf()),
])
.unwrap();
assert!(cfg.rules.strict, "MergeFile should not overwrite rules set by File");
assert_eq!(cfg.rules.max_rules, 20, "MergeFile should not overwrite rules set by File");
assert!(cfg.alias_map.entries.contains_key("a"));
assert!(!cfg.alias_map.entries.contains_key("b"), "MergeFile should not overwrite aliases");
}