Skip to main content

lino_arguments/
lib.rs

1//! lino-arguments - A unified configuration library
2//!
3//! Combines environment variables and CLI arguments into a single
4//! easy-to-use configuration system with clear priority ordering.
5//!
6//! Works like a combination of [clap](https://docs.rs/clap) and
7//! [dotenvy](https://docs.rs/dotenvy), but also with support for `.lenv` files
8//! via [lino-env](https://docs.rs/lino-env).
9//!
10//! Priority (highest to lowest):
11//! 1. CLI arguments (manually entered options)
12//! 2. Environment variables (from process env)
13//! 3. `.lenv` file (local environment overrides)
14//! 4. `.env` file (standard dotenv, for compatibility)
15//! 5. Default values
16//!
17//! # Drop-in Replacement for clap
18//!
19//! Just replace `use clap::Parser` with `use lino_arguments::Parser` —
20//! everything else stays exactly the same. `.lenv` and `.env` files are
21//! loaded automatically at startup before `main()` runs:
22//!
23//! ```rust,ignore
24//! // Only change: import from lino_arguments instead of clap
25//! use lino_arguments::Parser;
26//!
27//! #[derive(Parser, Debug)]
28//! #[command(name = "my-app")]
29//! struct Args {
30//!     #[arg(long, env = "PORT", default_value = "3000")]
31//!     port: u16,
32//!
33//!     #[arg(long, env = "API_KEY")]
34//!     api_key: Option<String>,
35//!
36//!     #[arg(long, env = "VERBOSE")]
37//!     verbose: bool,
38//! }
39//!
40//! fn main() {
41//!     let args = Args::parse(); // .lenv + .env already loaded
42//!     println!("port = {}", args.port);
43//! }
44//! ```
45//!
46//! # Functional Usage (like the JavaScript API)
47//!
48//! ```rust,ignore
49//! use lino_arguments::make_config;
50//!
51//! let config = make_config(|c| {
52//!     c.lenv(".lenv")
53//!      .env(".env")
54//!      .option("port", "Server port", "3000")
55//!      .option("api-key", "API key", "")
56//!      .flag("verbose", "Enable verbose logging")
57//! });
58//!
59//! let port: u16 = config.get("port").parse().unwrap();
60//! let verbose: bool = config.get_bool("verbose");
61//! ```
62//!
63//! # .lenv File Format
64//!
65//! The `.lenv` file format uses `: ` (colon-space) as the separator:
66//!
67//! ```text
68//! PORT: 8080
69//! API_KEY: my-secret-key
70//! DEBUG: true
71//! ```
72
73use std::collections::HashMap;
74use std::env;
75use thiserror::Error;
76
77// Re-export clap's Parser (derive macro + trait) so that `#[derive(Parser)]`
78// and `Args::parse()` work as a true drop-in replacement for clap.
79// The .lenv/.env files are loaded automatically at startup via the `ctor` crate,
80// so `Args::parse()` sees the environment variables from these files without
81// any extra `init()` call.
82pub use clap::Parser;
83pub use clap::{Args, Subcommand, ValueEnum};
84
85// Re-export the arg attribute macro
86pub use clap::arg;
87
88// Re-export the command macro for #[command(...)] attribute
89pub use clap::command;
90
91// Re-export lino-env for direct file operations
92pub use lino_env::{read_lino_env, write_lino_env, LinoEnv};
93
94// ============================================================================
95// Error Types
96// ============================================================================
97
98/// Errors that can occur during configuration
99#[derive(Error, Debug)]
100pub enum ConfigError {
101    #[error("Environment variable error: {0}")]
102    EnvError(String),
103
104    #[error("Parse error: {0}")]
105    ParseError(String),
106
107    #[error("Configuration file error: {0}")]
108    FileError(String),
109
110    #[error("IO error: {0}")]
111    IoError(#[from] std::io::Error),
112}
113
114// ============================================================================
115// Auto-initialization via ctor
116// ============================================================================
117
118/// Automatically load `.lenv` and `.env` files at program startup.
119///
120/// This runs before `main()`, so by the time `Args::parse()` is called,
121/// all values from `.lenv` and `.env` are already in the process environment.
122/// This is what makes the drop-in replacement work: just change the import
123/// from `use clap::Parser` to `use lino_arguments::Parser` and everything
124/// else stays the same.
125#[ctor::ctor]
126fn auto_init() {
127    init();
128}
129
130// ============================================================================
131// init() — Load .lenv and .env files into the process environment
132// ============================================================================
133
134/// Load `.lenv` and `.env` files into the process environment.
135///
136/// Loads `.lenv` first (higher priority), then `.env` (lower priority).
137/// Neither overwrites existing environment variables.
138///
139/// This is called automatically at program startup. You only need to call
140/// it manually if you want to reload files after the program has started,
141/// or if you're using [`init_with()`] with custom paths.
142pub fn init() {
143    load_lenv_file(".lenv").ok();
144    load_env_file(".env").ok();
145}
146
147/// Load specified `.lenv` and `.env` files into the process environment.
148///
149/// Like [`init()`], but with custom file paths.
150///
151/// ```rust,ignore
152/// lino_arguments::init_with(Some("config/app.lenv"), Some(".env.local"));
153/// let args = Args::parse();
154/// ```
155pub fn init_with(lenv_path: Option<&str>, env_path: Option<&str>) {
156    if let Some(path) = lenv_path {
157        load_lenv_file(path).ok();
158    }
159    if let Some(path) = env_path {
160        load_env_file(path).ok();
161    }
162}
163
164// ============================================================================
165// LinoParser Trait — convenience extension for custom file paths
166// ============================================================================
167
168/// Extension trait for `clap::Parser` that provides methods for parsing
169/// with custom `.lenv`/`.env` file paths.
170///
171/// Automatically implemented for any type that derives `Parser`.
172///
173/// For standard usage, you don't need this trait at all — just use
174/// `Args::parse()` directly and `.lenv`/`.env` files are loaded
175/// automatically at startup.
176///
177/// Use `LinoParser` methods only when you need custom file paths:
178///
179/// ```rust,ignore
180/// use lino_arguments::{Parser, LinoParser};
181///
182/// #[derive(Parser, Debug)]
183/// struct Args {
184///     #[arg(long, env = "PORT", default_value = "3000")]
185///     port: u16,
186/// }
187///
188/// // Standard usage — just parse(), .lenv/.env already loaded:
189/// let args = Args::parse();
190///
191/// // Custom file paths:
192/// let args = Args::lino_parse_from_with(
193///     ["app"], Some("custom.lenv"), Some("custom.env")
194/// );
195/// ```
196pub trait LinoParser: Parser {
197    /// Parse CLI arguments after loading `.lenv` and `.env` files.
198    /// Equivalent to `Args::parse()` (since auto-init already loads files).
199    fn lino_parse() -> Self {
200        init();
201        <Self as Parser>::parse()
202    }
203
204    /// Parse CLI arguments after loading specified `.lenv` and `.env` files.
205    fn lino_parse_with(lenv_path: Option<&str>, env_path: Option<&str>) -> Self {
206        init_with(lenv_path, env_path);
207        <Self as Parser>::parse()
208    }
209
210    /// Parse from custom arguments after loading `.lenv` and `.env` files.
211    /// Useful for testing.
212    fn lino_parse_from<I, T>(args: I) -> Self
213    where
214        I: IntoIterator<Item = T>,
215        T: Into<std::ffi::OsString> + Clone,
216    {
217        init();
218        <Self as Parser>::parse_from(args)
219    }
220
221    /// Parse from custom arguments after loading specified config files.
222    /// Useful for testing.
223    fn lino_parse_from_with<I, T>(args: I, lenv_path: Option<&str>, env_path: Option<&str>) -> Self
224    where
225        I: IntoIterator<Item = T>,
226        T: Into<std::ffi::OsString> + Clone,
227    {
228        init_with(lenv_path, env_path);
229        <Self as Parser>::parse_from(args)
230    }
231}
232
233/// Blanket implementation: any type that derives `clap::Parser`
234/// automatically gets `LinoParser` methods.
235impl<T: Parser> LinoParser for T {}
236
237// ============================================================================
238// .lenv File Loading
239// ============================================================================
240
241/// Load environment variables from a `.lenv` file.
242///
243/// This function reads a `.lenv` configuration file and sets the values
244/// as environment variables. If the file doesn't exist, this function
245/// returns Ok without setting any variables.
246///
247/// Note: Existing environment variables are NOT overwritten. This follows
248/// the principle that environment variables have higher priority than
249/// configuration files.
250///
251/// # Arguments
252///
253/// * `file_path` - Path to the `.lenv` file
254///
255/// # Examples
256///
257/// ```rust,ignore
258/// use lino_arguments::load_lenv_file;
259///
260/// // Load from default .lenv file
261/// load_lenv_file(".lenv").ok();
262///
263/// // Load from custom path
264/// load_lenv_file("config/production.lenv")?;
265/// ```
266pub fn load_lenv_file(file_path: &str) -> Result<usize, ConfigError> {
267    let lenv = read_lino_env(file_path)?;
268    let mut loaded_count = 0;
269
270    for key in lenv.keys() {
271        // Only set if not already present in environment
272        if env::var(&key).is_err() {
273            if let Some(value) = lenv.get(&key) {
274                env::set_var(&key, &value);
275                loaded_count += 1;
276            }
277        }
278    }
279
280    Ok(loaded_count)
281}
282
283/// Load environment variables from a `.lenv` file, overwriting existing values.
284///
285/// Unlike `load_lenv_file`, this function will overwrite any existing
286/// environment variables with the values from the file.
287///
288/// # Arguments
289///
290/// * `file_path` - Path to the `.lenv` file
291///
292/// # Examples
293///
294/// ```rust,ignore
295/// use lino_arguments::load_lenv_file_override;
296///
297/// // Force load values, overwriting any existing env vars
298/// load_lenv_file_override("config/override.lenv")?;
299/// ```
300pub fn load_lenv_file_override(file_path: &str) -> Result<usize, ConfigError> {
301    let lenv = read_lino_env(file_path)?;
302    let mut loaded_count = 0;
303
304    for key in lenv.keys() {
305        if let Some(value) = lenv.get(&key) {
306            env::set_var(&key, &value);
307            loaded_count += 1;
308        }
309    }
310
311    Ok(loaded_count)
312}
313
314// ============================================================================
315// .env File Loading (standard dotenv format, for compatibility)
316// ============================================================================
317
318/// Load environment variables from a `.env` file (standard `KEY=VALUE` format).
319///
320/// Uses the [dotenvy](https://docs.rs/dotenvy) crate under the hood.
321/// Existing environment variables are NOT overwritten.
322///
323/// # Arguments
324///
325/// * `file_path` - Path to the `.env` file
326///
327/// # Examples
328///
329/// ```rust,ignore
330/// use lino_arguments::load_env_file;
331///
332/// // Load from default .env file
333/// load_env_file(".env").ok();
334/// ```
335pub fn load_env_file(file_path: &str) -> Result<usize, ConfigError> {
336    let path = std::path::Path::new(file_path);
337    if !path.exists() {
338        return Ok(0);
339    }
340
341    let iter = dotenvy::from_path_iter(path)
342        .map_err(|e| ConfigError::FileError(format!("Failed to read {}: {}", file_path, e)))?;
343
344    let mut loaded_count = 0;
345    for item in iter {
346        match item {
347            Ok((key, value)) => {
348                // Only set if not already present in environment
349                if env::var(&key).is_err() {
350                    env::set_var(&key, &value);
351                    loaded_count += 1;
352                }
353            }
354            Err(_) => continue,
355        }
356    }
357
358    Ok(loaded_count)
359}
360
361/// Load environment variables from a `.env` file, overwriting existing values.
362///
363/// # Arguments
364///
365/// * `file_path` - Path to the `.env` file
366pub fn load_env_file_override(file_path: &str) -> Result<usize, ConfigError> {
367    let path = std::path::Path::new(file_path);
368    if !path.exists() {
369        return Ok(0);
370    }
371
372    let iter = dotenvy::from_path_iter(path)
373        .map_err(|e| ConfigError::FileError(format!("Failed to read {}: {}", file_path, e)))?;
374
375    let mut loaded_count = 0;
376    for item in iter {
377        match item {
378            Ok((key, value)) => {
379                env::set_var(&key, &value);
380                loaded_count += 1;
381            }
382            Err(_) => continue,
383        }
384    }
385
386    Ok(loaded_count)
387}
388
389// ============================================================================
390// Case Conversion Utilities
391// ============================================================================
392
393/// Convert string to UPPER_CASE (for environment variables)
394///
395/// # Examples
396///
397/// ```
398/// use lino_arguments::to_upper_case;
399///
400/// assert_eq!(to_upper_case("apiKey"), "API_KEY");
401/// assert_eq!(to_upper_case("my-variable-name"), "MY_VARIABLE_NAME");
402/// ```
403pub fn to_upper_case(s: &str) -> String {
404    // If already all uppercase, just replace separators
405    if s.chars().all(|c| c.is_uppercase() || c == '_' || c == '-') {
406        return s.replace('-', "_");
407    }
408
409    let mut result = String::new();
410    let chars: Vec<char> = s.chars().collect();
411
412    for (i, c) in chars.iter().enumerate() {
413        if c.is_uppercase() && i > 0 {
414            result.push('_');
415        }
416        if *c == '-' || *c == ' ' {
417            result.push('_');
418        } else {
419            result.push(c.to_ascii_uppercase());
420        }
421    }
422
423    // Remove leading underscore and double underscores
424    result = result.trim_start_matches('_').to_string();
425    while result.contains("__") {
426        result = result.replace("__", "_");
427    }
428
429    result
430}
431
432/// Convert string to camelCase (for config object keys)
433///
434/// # Examples
435///
436/// ```
437/// use lino_arguments::to_camel_case;
438///
439/// assert_eq!(to_camel_case("api-key"), "apiKey");
440/// assert_eq!(to_camel_case("API_KEY"), "apiKey");
441/// ```
442pub fn to_camel_case(s: &str) -> String {
443    let lower = s.to_lowercase();
444    let mut result = String::new();
445    let mut capitalize_next = false;
446
447    for c in lower.chars() {
448        if c == '-' || c == '_' || c == ' ' {
449            capitalize_next = true;
450        } else if capitalize_next {
451            result.push(c.to_ascii_uppercase());
452            capitalize_next = false;
453        } else {
454            result.push(c);
455        }
456    }
457
458    // Ensure first character is lowercase
459    if let Some(first) = result.chars().next() {
460        if first.is_uppercase() {
461            result = first.to_lowercase().to_string() + &result[1..];
462        }
463    }
464
465    result
466}
467
468/// Convert string to kebab-case (for CLI options)
469///
470/// # Examples
471///
472/// ```
473/// use lino_arguments::to_kebab_case;
474///
475/// assert_eq!(to_kebab_case("apiKey"), "api-key");
476/// assert_eq!(to_kebab_case("API_KEY"), "api-key");
477/// ```
478pub fn to_kebab_case(s: &str) -> String {
479    // If already all uppercase with underscores, convert directly
480    if s.chars().all(|c| c.is_uppercase() || c == '_') && s.contains('_') {
481        return s.replace('_', "-").to_lowercase();
482    }
483
484    let mut result = String::new();
485    let chars: Vec<char> = s.chars().collect();
486
487    for (i, c) in chars.iter().enumerate() {
488        if c.is_uppercase() && i > 0 {
489            result.push('-');
490        }
491        if *c == '_' || *c == ' ' {
492            result.push('-');
493        } else {
494            result.push(c.to_ascii_lowercase());
495        }
496    }
497
498    // Remove leading dash and double dashes
499    result = result.trim_start_matches('-').to_string();
500    while result.contains("--") {
501        result = result.replace("--", "-");
502    }
503
504    result
505}
506
507/// Convert string to snake_case
508///
509/// # Examples
510///
511/// ```
512/// use lino_arguments::to_snake_case;
513///
514/// assert_eq!(to_snake_case("apiKey"), "api_key");
515/// assert_eq!(to_snake_case("API_KEY"), "api_key");
516/// ```
517pub fn to_snake_case(s: &str) -> String {
518    // If already all uppercase with underscores, just lowercase
519    if s.chars().all(|c| c.is_uppercase() || c == '_') && s.contains('_') {
520        return s.to_lowercase();
521    }
522
523    let mut result = String::new();
524    let chars: Vec<char> = s.chars().collect();
525
526    for (i, c) in chars.iter().enumerate() {
527        if c.is_uppercase() && i > 0 {
528            result.push('_');
529        }
530        if *c == '-' || *c == ' ' {
531            result.push('_');
532        } else {
533            result.push(c.to_ascii_lowercase());
534        }
535    }
536
537    // Remove leading underscore and double underscores
538    result = result.trim_start_matches('_').to_string();
539    while result.contains("__") {
540        result = result.replace("__", "_");
541    }
542
543    result
544}
545
546/// Convert string to PascalCase
547///
548/// # Examples
549///
550/// ```
551/// use lino_arguments::to_pascal_case;
552///
553/// assert_eq!(to_pascal_case("api-key"), "ApiKey");
554/// assert_eq!(to_pascal_case("api_key"), "ApiKey");
555/// ```
556pub fn to_pascal_case(s: &str) -> String {
557    let lower = s.to_lowercase();
558    let mut result = String::new();
559    let mut capitalize_next = true;
560
561    for c in lower.chars() {
562        if c == '-' || c == '_' || c == ' ' {
563            capitalize_next = true;
564        } else if capitalize_next {
565            result.push(c.to_ascii_uppercase());
566            capitalize_next = false;
567        } else {
568            result.push(c);
569        }
570    }
571
572    result
573}
574
575// ============================================================================
576// Environment Variable Helper
577// ============================================================================
578
579/// Get environment variable with default value and case conversion.
580/// Tries multiple case formats to find the variable.
581///
582/// # Examples
583///
584/// ```
585/// use lino_arguments::getenv;
586///
587/// // Try to get API_KEY, apiKey, api-key, etc.
588/// let api_key = getenv("apiKey", "default-key");
589/// let port = getenv("PORT", "3000");
590/// ```
591pub fn getenv(key: &str, default: &str) -> String {
592    // Try different case formats
593    let variants = [
594        key.to_string(),
595        to_upper_case(key),
596        to_camel_case(key),
597        to_kebab_case(key),
598        to_snake_case(key),
599        to_pascal_case(key),
600    ];
601
602    for variant in variants.iter() {
603        if let Ok(value) = env::var(variant) {
604            return value;
605        }
606    }
607
608    default.to_string()
609}
610
611/// Get environment variable as integer with default value.
612/// Tries multiple case formats to find the variable.
613///
614/// # Examples
615///
616/// ```
617/// use lino_arguments::getenv_int;
618///
619/// let port = getenv_int("PORT", 3000);
620/// ```
621pub fn getenv_int(key: &str, default: i64) -> i64 {
622    let value = getenv(key, "");
623    if value.is_empty() {
624        return default;
625    }
626    value.parse().unwrap_or(default)
627}
628
629/// Get environment variable as boolean with default value.
630/// Tries multiple case formats to find the variable.
631/// Accepts: "true", "false", "1", "0", "yes", "no" (case-insensitive)
632///
633/// # Examples
634///
635/// ```
636/// use lino_arguments::getenv_bool;
637///
638/// let debug = getenv_bool("DEBUG", false);
639/// ```
640pub fn getenv_bool(key: &str, default: bool) -> bool {
641    let value = getenv(key, "");
642    if value.is_empty() {
643        return default;
644    }
645    match value.to_lowercase().as_str() {
646        "true" | "1" | "yes" | "on" => true,
647        "false" | "0" | "no" | "off" => false,
648        _ => default,
649    }
650}
651
652// ============================================================================
653// Functional Configuration API (like JavaScript's makeConfig)
654// ============================================================================
655
656/// Resolved configuration values from the functional API.
657///
658/// Contains all parsed configuration values accessible by key name.
659/// Values are stored as strings and can be retrieved with type conversion.
660#[derive(Debug, Clone)]
661pub struct Config {
662    values: HashMap<String, String>,
663}
664
665impl Config {
666    /// Get a configuration value as a string.
667    /// Returns empty string if the key is not found.
668    pub fn get(&self, key: &str) -> String {
669        let camel = to_camel_case(key);
670        self.values
671            .get(&camel)
672            .or_else(|| self.values.get(key))
673            .cloned()
674            .unwrap_or_default()
675    }
676
677    /// Get a configuration value as an integer.
678    /// Returns the default if the key is not found or cannot be parsed.
679    pub fn get_int(&self, key: &str, default: i64) -> i64 {
680        let val = self.get(key);
681        if val.is_empty() {
682            return default;
683        }
684        val.parse().unwrap_or(default)
685    }
686
687    /// Get a configuration value as a boolean.
688    /// Accepts: "true", "false", "1", "0", "yes", "no", "on", "off" (case-insensitive).
689    /// Returns false if the key is not found.
690    pub fn get_bool(&self, key: &str) -> bool {
691        let val = self.get(key);
692        matches!(val.to_lowercase().as_str(), "true" | "1" | "yes" | "on")
693    }
694
695    /// Check if a configuration key exists.
696    pub fn has(&self, key: &str) -> bool {
697        let camel = to_camel_case(key);
698        self.values.contains_key(&camel) || self.values.contains_key(key)
699    }
700}
701
702/// Option definition for the functional configuration API.
703#[derive(Debug, Clone)]
704struct OptionDef {
705    name: String,
706    description: String,
707    default: String,
708    is_flag: bool,
709    short: Option<char>,
710}
711
712/// Builder for functional-style configuration.
713///
714/// Provides a chainable API for defining configuration options, similar to
715/// the JavaScript `makeConfig` API.
716///
717/// # Example
718///
719/// ```rust,ignore
720/// use lino_arguments::make_config;
721///
722/// let config = make_config(|c| {
723///     c.lenv(".lenv")
724///      .option("port", "Server port", "3000")
725///      .option_short("api-key", 'k', "API key", "")
726///      .flag("verbose", "Enable verbose logging")
727/// });
728/// ```
729pub struct ConfigBuilder {
730    options: Vec<OptionDef>,
731    lenv_path: Option<String>,
732    lenv_override: bool,
733    env_path: Option<String>,
734    env_override: bool,
735    app_name: Option<String>,
736    app_about: Option<String>,
737    app_version: Option<String>,
738}
739
740impl ConfigBuilder {
741    fn new() -> Self {
742        ConfigBuilder {
743            options: Vec::new(),
744            lenv_path: None,
745            lenv_override: false,
746            env_path: None,
747            env_override: false,
748            app_name: None,
749            app_about: None,
750            app_version: None,
751        }
752    }
753
754    /// Set the application name for help text.
755    pub fn name(&mut self, name: &str) -> &mut Self {
756        self.app_name = Some(name.to_string());
757        self
758    }
759
760    /// Set the application description for help text.
761    pub fn about(&mut self, about: &str) -> &mut Self {
762        self.app_about = Some(about.to_string());
763        self
764    }
765
766    /// Set the application version for --version flag.
767    pub fn version(&mut self, version: &str) -> &mut Self {
768        self.app_version = Some(version.to_string());
769        self
770    }
771
772    /// Load a .lenv configuration file (without overriding existing env vars).
773    pub fn lenv(&mut self, path: &str) -> &mut Self {
774        self.lenv_path = Some(path.to_string());
775        self.lenv_override = false;
776        self
777    }
778
779    /// Load a .lenv configuration file, overriding existing env vars.
780    pub fn lenv_override(&mut self, path: &str) -> &mut Self {
781        self.lenv_path = Some(path.to_string());
782        self.lenv_override = true;
783        self
784    }
785
786    /// Load a .env configuration file (without overriding existing env vars).
787    pub fn env(&mut self, path: &str) -> &mut Self {
788        self.env_path = Some(path.to_string());
789        self.env_override = false;
790        self
791    }
792
793    /// Load a .env configuration file, overriding existing env vars.
794    pub fn env_override(&mut self, path: &str) -> &mut Self {
795        self.env_path = Some(path.to_string());
796        self.env_override = true;
797        self
798    }
799
800    /// Define a string/number option with a long name, description, and default value.
801    pub fn option(&mut self, name: &str, description: &str, default: &str) -> &mut Self {
802        self.options.push(OptionDef {
803            name: name.to_string(),
804            description: description.to_string(),
805            default: default.to_string(),
806            is_flag: false,
807            short: None,
808        });
809        self
810    }
811
812    /// Define a string/number option with both short and long names.
813    pub fn option_short(
814        &mut self,
815        name: &str,
816        short: char,
817        description: &str,
818        default: &str,
819    ) -> &mut Self {
820        self.options.push(OptionDef {
821            name: name.to_string(),
822            description: description.to_string(),
823            default: default.to_string(),
824            is_flag: false,
825            short: Some(short),
826        });
827        self
828    }
829
830    /// Define a boolean flag (defaults to false).
831    pub fn flag(&mut self, name: &str, description: &str) -> &mut Self {
832        self.options.push(OptionDef {
833            name: name.to_string(),
834            description: description.to_string(),
835            default: String::new(),
836            is_flag: true,
837            short: None,
838        });
839        self
840    }
841
842    /// Define a boolean flag with a short name.
843    pub fn flag_short(&mut self, name: &str, short: char, description: &str) -> &mut Self {
844        self.options.push(OptionDef {
845            name: name.to_string(),
846            description: description.to_string(),
847            default: String::new(),
848            is_flag: true,
849            short: Some(short),
850        });
851        self
852    }
853
854    /// Build the configuration from the defined options.
855    ///
856    /// This parses CLI arguments using clap and resolves values from:
857    /// 1. CLI arguments (highest priority)
858    /// 2. Environment variables
859    /// 3. .lenv file
860    /// 4. .env file
861    /// 5. Default values (lowest priority)
862    fn build(&self) -> Config {
863        self.build_from(env::args_os().collect())
864    }
865
866    /// Build the configuration from custom arguments (for testing).
867    fn build_from(&self, args: Vec<std::ffi::OsString>) -> Config {
868        // Step 1: Load .lenv file if configured (higher priority than .env)
869        if let Some(ref path) = self.lenv_path {
870            if self.lenv_override {
871                let _ = load_lenv_file_override(path);
872            } else {
873                let _ = load_lenv_file(path);
874            }
875        }
876
877        // Step 2: Load .env file if configured (lower priority than .lenv)
878        if let Some(ref path) = self.env_path {
879            if self.env_override {
880                let _ = load_env_file_override(path);
881            } else {
882                let _ = load_env_file(path);
883            }
884        }
885
886        // Step 3: Build clap command dynamically
887        let mut cmd =
888            clap::Command::new(self.app_name.clone().unwrap_or_else(|| "app".to_string()));
889
890        if let Some(ref about) = self.app_about {
891            cmd = cmd.about(about.clone());
892        }
893
894        if let Some(ref version) = self.app_version {
895            cmd = cmd.version(version.clone());
896        }
897
898        // Add --configuration option for dynamic .lenv loading
899        cmd = cmd.arg(
900            clap::Arg::new("configuration")
901                .long("configuration")
902                .short('c')
903                .help("Path to configuration .lenv file")
904                .value_name("PATH"),
905        );
906
907        // Add user-defined options
908        for opt in &self.options {
909            let kebab_name = to_kebab_case(&opt.name);
910            let env_name = to_upper_case(&opt.name);
911
912            let mut arg = clap::Arg::new(kebab_name.clone()).long(kebab_name.clone());
913
914            // Set help text
915            arg = arg.help(opt.description.clone());
916
917            if let Some(short) = opt.short {
918                arg = arg.short(short);
919            }
920
921            if opt.is_flag {
922                arg = arg.action(clap::ArgAction::SetTrue);
923            } else {
924                // Use clap's env feature so it picks up values from env vars
925                // (which now include .lenv and .env values we loaded above)
926                arg = arg.env(env_name);
927                if !opt.default.is_empty() {
928                    arg = arg.default_value(opt.default.clone());
929                }
930            }
931
932            cmd = cmd.arg(arg);
933        }
934
935        // Step 4: Parse arguments
936        let matches = cmd.get_matches_from(args);
937
938        // Step 5: Load --configuration file if provided
939        if let Some(config_path) = matches.get_one::<String>("configuration") {
940            let _ = load_lenv_file_override(config_path);
941        }
942
943        // Step 6: Collect values into Config
944        let mut values = HashMap::new();
945
946        for opt in &self.options {
947            let kebab_name = to_kebab_case(&opt.name);
948            let camel_name = to_camel_case(&opt.name);
949
950            if opt.is_flag {
951                let val = matches.get_flag(&kebab_name);
952                values.insert(camel_name, val.to_string());
953            } else if let Some(val) = matches.get_one::<String>(&kebab_name) {
954                values.insert(camel_name, val.clone());
955            }
956        }
957
958        Config { values }
959    }
960}
961
962/// Create a unified configuration using a functional builder API.
963///
964/// This is the Rust equivalent of the JavaScript `makeConfig` function.
965/// It combines .lenv file loading, .env file loading, environment variables,
966/// and CLI argument parsing into a single configuration step.
967///
968/// Priority (highest to lowest):
969/// 1. CLI arguments
970/// 2. Environment variables
971/// 3. .lenv file (via `--configuration` flag or builder `.lenv()`)
972/// 4. .env file (via builder `.env()`)
973/// 5. Default values
974///
975/// # Example
976///
977/// ```rust,ignore
978/// use lino_arguments::make_config;
979///
980/// let config = make_config(|c| {
981///     c.lenv(".lenv")
982///      .env(".env")
983///      .option("port", "Server port", "3000")
984///      .option_short("api-key", 'k', "API key", "")
985///      .flag("verbose", "Enable verbose logging")
986/// });
987///
988/// let port: u16 = config.get("port").parse().unwrap();
989/// let api_key = config.get("api-key");
990/// let verbose = config.get_bool("verbose");
991/// ```
992pub fn make_config<F>(configure: F) -> Config
993where
994    F: FnOnce(&mut ConfigBuilder) -> &mut ConfigBuilder,
995{
996    let mut builder = ConfigBuilder::new();
997    configure(&mut builder);
998    builder.build()
999}
1000
1001/// Create a unified configuration using a functional builder API with custom arguments.
1002///
1003/// Same as `make_config` but accepts custom arguments for testing purposes.
1004///
1005/// # Example
1006///
1007/// ```rust,ignore
1008/// use lino_arguments::make_config_from;
1009///
1010/// let args = vec!["my-app", "--port", "9090", "--verbose"];
1011/// let config = make_config_from(args, |c| {
1012///     c.option("port", "Server port", "3000")
1013///      .flag("verbose", "Enable verbose logging")
1014/// });
1015///
1016/// assert_eq!(config.get("port"), "9090");
1017/// assert!(config.get_bool("verbose"));
1018/// ```
1019pub fn make_config_from<I, T, F>(args: I, configure: F) -> Config
1020where
1021    I: IntoIterator<Item = T>,
1022    T: Into<std::ffi::OsString>,
1023    F: FnOnce(&mut ConfigBuilder) -> &mut ConfigBuilder,
1024{
1025    let mut builder = ConfigBuilder::new();
1026    configure(&mut builder);
1027    builder.build_from(args.into_iter().map(|a| a.into()).collect())
1028}
1029
1030// ============================================================================
1031// Tests
1032// ============================================================================
1033
1034#[cfg(test)]
1035mod tests {
1036    use super::*;
1037
1038    mod case_conversion {
1039        use super::*;
1040
1041        #[test]
1042        fn test_to_upper_case() {
1043            assert_eq!(to_upper_case("apiKey"), "API_KEY");
1044            assert_eq!(to_upper_case("myVariableName"), "MY_VARIABLE_NAME");
1045            assert_eq!(to_upper_case("api-key"), "API_KEY");
1046            assert_eq!(to_upper_case("API_KEY"), "API_KEY");
1047        }
1048
1049        #[test]
1050        fn test_to_camel_case() {
1051            assert_eq!(to_camel_case("api-key"), "apiKey");
1052            assert_eq!(to_camel_case("API_KEY"), "apiKey");
1053            assert_eq!(to_camel_case("my_variable_name"), "myVariableName");
1054        }
1055
1056        #[test]
1057        fn test_to_kebab_case() {
1058            assert_eq!(to_kebab_case("apiKey"), "api-key");
1059            assert_eq!(to_kebab_case("API_KEY"), "api-key");
1060            assert_eq!(to_kebab_case("MyVariableName"), "my-variable-name");
1061        }
1062
1063        #[test]
1064        fn test_to_snake_case() {
1065            assert_eq!(to_snake_case("apiKey"), "api_key");
1066            assert_eq!(to_snake_case("api-key"), "api_key");
1067            assert_eq!(to_snake_case("API_KEY"), "api_key");
1068        }
1069
1070        #[test]
1071        fn test_to_pascal_case() {
1072            assert_eq!(to_pascal_case("api-key"), "ApiKey");
1073            assert_eq!(to_pascal_case("api_key"), "ApiKey");
1074            assert_eq!(to_pascal_case("my-variable-name"), "MyVariableName");
1075        }
1076    }
1077
1078    mod getenv_tests {
1079        use super::*;
1080        use std::env;
1081
1082        #[test]
1083        fn test_getenv_with_default() {
1084            let result = getenv("NON_EXISTENT_VAR_12345", "default");
1085            assert_eq!(result, "default");
1086        }
1087
1088        #[test]
1089        fn test_getenv_finds_var() {
1090            env::set_var("TEST_LINO_VAR", "test_value");
1091            let result = getenv("TEST_LINO_VAR", "default");
1092            assert_eq!(result, "test_value");
1093            env::remove_var("TEST_LINO_VAR");
1094        }
1095
1096        #[test]
1097        fn test_getenv_int() {
1098            env::set_var("TEST_PORT", "8080");
1099            let result = getenv_int("TEST_PORT", 3000);
1100            assert_eq!(result, 8080);
1101            env::remove_var("TEST_PORT");
1102        }
1103
1104        #[test]
1105        fn test_getenv_bool() {
1106            env::set_var("TEST_DEBUG", "true");
1107            let result = getenv_bool("TEST_DEBUG", false);
1108            assert!(result);
1109            env::remove_var("TEST_DEBUG");
1110
1111            env::set_var("TEST_DEBUG", "1");
1112            let result = getenv_bool("TEST_DEBUG", false);
1113            assert!(result);
1114            env::remove_var("TEST_DEBUG");
1115        }
1116    }
1117}