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}