Skip to main content

teamy_figue/
driver.rs

1//! Driver API for orchestrating layered configuration parsing, validation, and diagnostics.
2//!
3//! # Phases
4//! 1. **Parse layers**: CLI, env, file (defaults filled during deserialization)
5//! 2. **Check special fields**: If help/version/completions was requested, short-circuit
6//! 3. **Merge** layers by priority (CLI > env > file > defaults)
7//! 4. **Deserialize** merged ConfigValue into the target Facet type
8//!
9//! # TODO
10//! - [x] Wire override tracking from merge result into DriverReport
11//! - [x] Define DriverError enum (Failed, Help, Completions, Version)
12//! - [x] Implement unwrap() on DriverResult
13//! - [x] Add figue::help, figue::completions, figue::version attribute detection
14//! - [x] Handle special fields in Driver::run() before deserialization
15//! - [ ] Collect unused keys from layer parsers into LayerOutput
16//! - [ ] Add facet-validate pass after deserialization
17//! - [ ] Improve render_pretty() with Ariadne integration
18//! - [x] Migrate build_traced tests to driver API (removed - functionality covered by driver tests)
19#![allow(clippy::result_large_err)]
20
21use std::marker::PhantomData;
22use std::string::String;
23use std::vec::Vec;
24
25use crate::builder::Config;
26use crate::color::should_use_color;
27use crate::completions::{Shell, generate_completions_for_shape};
28use crate::config_value::ConfigValue;
29use crate::config_value_parser::{fill_defaults_from_schema, from_config_value};
30use crate::dump::dump_config_with_schema;
31use crate::enum_conflicts::detect_enum_conflicts;
32use crate::env_subst::{EnvSubstError, RealEnv, substitute_env_vars};
33use crate::help::generate_help_for_subcommand;
34use crate::help::generate_help_list_for_subcommand;
35use crate::help::implementation_source_for_subcommand_path;
36use crate::layers::{cli::parse_cli, env::parse_env, file::parse_file};
37use crate::merge::merge_layers;
38use crate::missing::{
39    build_corrected_command_diagnostics, collect_missing_fields, format_missing_fields_summary,
40};
41use crate::path::Path;
42use crate::provenance::{FileResolution, Override, Provenance};
43use crate::span::Span;
44use crate::span_registry::assign_virtual_spans;
45use facet_core::Facet;
46
47/// Diagnostics for a single layer.
48#[derive(Debug, Default)]
49pub struct LayerOutput {
50    /// Parsed value for this layer (if any).
51    pub value: Option<ConfigValue>,
52    /// Keys provided by this layer but unused by the schema.
53    pub unused_keys: Vec<UnusedKey>,
54    /// Layer-specific diagnostics collected while parsing.
55    pub diagnostics: Vec<Diagnostic>,
56    /// Virtual source text for this layer (for error reporting with Ariadne).
57    /// For env layers, this is a synthetic document like `VAR="value"\n`.
58    /// For file layers, this is the file contents.
59    /// For CLI, this is the concatenated args.
60    pub source_text: Option<String>,
61    /// Config file path captured from CLI (e.g., `--config path/to/file.json`).
62    /// Only set by the CLI layer when the user specifies a config file path.
63    pub config_file_path: Option<camino::Utf8PathBuf>,
64    /// Requested pseudo-help list mode (from `help list` shorthand), if any.
65    pub help_list_mode: Option<HelpListMode>,
66}
67
68/// Mode for pseudo-help listing (`help list`).
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum HelpListMode {
71    /// Show full help text for each immediate subcommand.
72    Full,
73    /// Show only immediate subcommand names.
74    Short,
75}
76
77/// A key that was unused by the schema, with provenance.
78#[derive(Debug)]
79pub struct UnusedKey {
80    /// The unused key path.
81    pub key: Path,
82    /// Provenance for where it came from (CLI/env/file/default).
83    pub provenance: Provenance,
84}
85
86/// Layered config values from CLI/env/file/defaults, with diagnostics.
87#[derive(Debug, Default)]
88pub struct ConfigLayers {
89    /// Default layer (lowest priority).
90    pub defaults: LayerOutput,
91    /// File layer.
92    pub file: LayerOutput,
93    /// Environment layer.
94    pub env: LayerOutput,
95    /// CLI layer (highest priority).
96    pub cli: LayerOutput,
97}
98
99/// Primary driver type that orchestrates parsing and validation.
100///
101/// The driver coordinates all phases of configuration parsing:
102/// 1. **Parse layers**: CLI, environment variables, config files
103/// 2. **Check special fields**: Handle `--help`, `--version`, `--completions`
104/// 3. **Merge layers**: Combine values by priority (CLI > env > file > defaults)
105/// 4. **Deserialize**: Convert merged config into the target type `T`
106///
107/// # Example
108///
109/// ```rust
110/// use facet::Facet;
111/// use figue::{self as args, builder, Driver};
112///
113/// #[derive(Facet)]
114/// struct Args {
115///     #[facet(args::positional)]
116///     file: String,
117/// }
118///
119/// let config = builder::<Args>()
120///     .unwrap()
121///     .cli(|cli| cli.args(["input.txt"]))
122///     .build();
123///
124/// // Create the driver and run parsing
125/// let output = Driver::new(config).run().into_result().unwrap();
126/// assert_eq!(output.value.file, "input.txt");
127/// ```
128pub struct Driver<T> {
129    config: Config<T>,
130    core: DriverCore,
131    _phantom: PhantomData<T>,
132}
133
134/// Non-generic driver core (placeholder for future monomorphization reduction).
135#[derive(Debug, Default)]
136pub struct DriverCore;
137
138impl DriverCore {
139    fn new() -> Self {
140        Self
141    }
142}
143
144impl<T: Facet<'static>> Driver<T> {
145    /// Create a driver from a fully built config.
146    ///
147    /// Use [`builder()`](crate::builder::builder) to create the config, then pass it here.
148    pub fn new(config: Config<T>) -> Self {
149        Self {
150            config,
151            core: DriverCore::new(),
152            _phantom: PhantomData,
153        }
154    }
155
156    /// Execute the driver and return an outcome.
157    ///
158    /// The returned `DriverOutcome` must be handled explicitly:
159    /// - Use `.unwrap()` for automatic exit handling (recommended)
160    /// - Use `.into_result()` if you need manual control
161    pub fn run(mut self) -> DriverOutcome<T> {
162        let _ = self.core;
163
164        let mut layers = ConfigLayers::default();
165        let mut all_diagnostics = Vec::new();
166        let mut file_resolution = None;
167
168        // Get CLI args source for Ariadne error display
169        // None means no arguments were provided (used for better error formatting)
170        let cli_args_source = self
171            .config
172            .cli_config
173            .as_ref()
174            .map(|c| {
175                let args = c.resolve_args().join(" ");
176                if args.is_empty() { None } else { Some(args) }
177            })
178            .unwrap_or(None);
179
180        // For Ariadne display, we need a string (use placeholder if empty)
181        let cli_args_display = cli_args_source.as_deref().unwrap_or("<no arguments>");
182
183        // Phase 1: Parse each layer
184        // Priority order (lowest to highest): defaults < file < env < cli
185        //
186        // Note: CLI is parsed first to capture the config file path (--config <path>),
187        // which is then used by the file layer. The priority ordering is enforced
188        // during the merge phase, not the parse phase.
189
190        // 1a. Defaults layer (TODO: extract defaults from schema)
191        // For now, defaults is empty - this will be filled in when we implement
192        // default value extraction from the schema
193
194        // 1b. CLI layer (parsed first to get config file path)
195        if let Some(ref cli_config) = self.config.cli_config {
196            layers.cli = parse_cli(&self.config.schema, cli_config);
197            tracing::debug!(cli_value = ?layers.cli.value, "driver: parsed CLI layer");
198            all_diagnostics.extend(layers.cli.diagnostics.iter().cloned());
199        }
200
201        // 1c. File layer (uses config file path from CLI if provided)
202        // If CLI provided a config file path, update the file config to use it
203        if let Some(ref cli_path) = layers.cli.config_file_path {
204            // Get mutable access to file_config, creating a default if none exists
205            let file_config = self.config.file_config.get_or_insert_with(Default::default);
206            file_config.explicit_path = Some(cli_path.clone());
207        }
208
209        if let Some(ref file_config) = self.config.file_config {
210            let result = parse_file(&self.config.schema, file_config);
211            layers.file = result.output;
212            file_resolution = Some(result.resolution);
213            all_diagnostics.extend(layers.file.diagnostics.iter().cloned());
214        }
215
216        // 1d. Environment layer
217        if let Some(ref env_config) = self.config.env_config {
218            layers.env = parse_env(&self.config.schema, env_config, env_config.source());
219            all_diagnostics.extend(layers.env.diagnostics.iter().cloned());
220        }
221
222        // Phase 1.5: Check special fields (help/version/completions)
223        // These short-circuit before merge/deserialization
224        if let Some(cli_value) = &layers.cli.value {
225            let special = self.config.schema.special();
226
227            // Check for --help
228            if let Some(ref help_path) = special.help
229                && let Some(ConfigValue::Bool(b)) = cli_value.get_by_path(help_path)
230                && b.value
231            {
232                let help_config = self
233                    .config
234                    .help_config
235                    .as_ref()
236                    .cloned()
237                    .unwrap_or_default();
238
239                // Extract subcommand path for subcommand-aware help
240                let subcommand_path = if let Some(subcommand_field) =
241                    self.config.schema.args().subcommand_field_name()
242                {
243                    cli_value.extract_subcommand_path(subcommand_field)
244                } else {
245                    Vec::new()
246                };
247
248                let mut text = if let Some(mode) = layers.cli.help_list_mode {
249                    generate_help_list_for_subcommand(
250                        &self.config.schema,
251                        &subcommand_path,
252                        &help_config,
253                        mode,
254                    )
255                } else {
256                    generate_help_for_subcommand(
257                        &self.config.schema,
258                        &subcommand_path,
259                        &help_config,
260                    )
261                };
262                maybe_append_implementation_source::<T>(&mut text, &help_config, &subcommand_path);
263                return DriverOutcome::err(DriverError::Help { text });
264            }
265
266            // Check for --version
267            if let Some(ref version_path) = special.version
268                && let Some(ConfigValue::Bool(b)) = cli_value.get_by_path(version_path)
269                && b.value
270            {
271                let version = self
272                    .config
273                    .help_config
274                    .as_ref()
275                    .and_then(|h| h.version.clone())
276                    .unwrap_or_else(|| "unknown".to_string());
277                let program_name = self
278                    .config
279                    .help_config
280                    .as_ref()
281                    .and_then(|h| h.program_name.clone())
282                    .or_else(|| std::env::args().next())
283                    .unwrap_or_else(|| "program".to_string());
284                let text = format!("{} {}", program_name, version);
285                return DriverOutcome::err(DriverError::Version { text });
286            }
287
288            // Check for --completions <shell>
289            if let Some(ref completions_path) = special.completions
290                && let Some(value) = cli_value.get_by_path(completions_path)
291            {
292                // The value should be a string representing the shell name
293                if let Some(shell) = extract_shell_from_value(value) {
294                    let program_name = self
295                        .config
296                        .help_config
297                        .as_ref()
298                        .and_then(|h| h.program_name.clone())
299                        .or_else(|| std::env::args().next())
300                        .unwrap_or_else(|| "program".to_string());
301                    let script = generate_completions_for_shape(T::SHAPE, shell, &program_name);
302                    return DriverOutcome::err(DriverError::Completions { script });
303                }
304            }
305        }
306
307        // Check for errors before proceeding
308        let has_errors = all_diagnostics
309            .iter()
310            .any(|d| d.severity == Severity::Error);
311        if has_errors {
312            return DriverOutcome::err(DriverError::Failed {
313                report: Box::new(DriverReport {
314                    diagnostics: all_diagnostics,
315                    layers,
316                    file_resolution,
317                    overrides: Vec::new(),
318                    cli_args_source: cli_args_display.to_string(),
319                    source_name: "<cli>".to_string(),
320                }),
321            });
322        }
323
324        // Phase 2: Merge layers by priority
325        let values_to_merge: Vec<ConfigValue> = [
326            layers.defaults.value.clone(),
327            layers.file.value.clone(),
328            layers.env.value.clone(),
329            layers.cli.value.clone(),
330        ]
331        .into_iter()
332        .flatten()
333        .collect();
334
335        let merged = merge_layers(values_to_merge);
336        tracing::debug!(merged_value = ?merged.value, "driver: merged layers");
337        let overrides = merged.overrides;
338
339        // Phase 2.5: Environment variable substitution
340        // Substitute ${VAR} patterns in string values where env_subst is enabled
341        let mut merged_value = merged.value;
342        if let Some(config_schema) = self.config.schema.config()
343            && let ConfigValue::Object(ref mut sourced_fields) = merged_value
344            && let Some(config_field_name) = config_schema.field_name()
345            && let Some(config_value) = sourced_fields.value.get_mut(&config_field_name.to_string())
346            && let Err(e) = substitute_env_vars(config_value, config_schema, &RealEnv)
347        {
348            return DriverOutcome::err(DriverError::EnvSubst { error: e });
349        }
350        tracing::debug!(merged_value = ?merged_value, "driver: after env_subst");
351
352        // Phase 2.75: Check for conflicting enum variants
353        // E.g., setting both storage.s3.bucket and storage.gcp.project when storage is an enum
354        let enum_conflicts = detect_enum_conflicts(&merged_value, &self.config.schema);
355        if !enum_conflicts.is_empty() {
356            let messages: Vec<String> = enum_conflicts.iter().map(|c| c.format()).collect();
357            let message = messages.join("\n\n");
358
359            return DriverOutcome::err(DriverError::Failed {
360                report: Box::new(DriverReport {
361                    diagnostics: vec![Diagnostic {
362                        message,
363                        label: None,
364                        path: None,
365                        span: None,
366                        severity: Severity::Error,
367                    }],
368                    layers,
369                    file_resolution,
370                    overrides,
371                    cli_args_source: cli_args_display.to_string(),
372                    source_name: "<cli>".to_string(),
373                }),
374            });
375        }
376
377        // Phase 3: Fill defaults and check for missing required fields
378        // This must happen BEFORE deserialization so we can show all missing fields at once
379        let value_with_defaults = fill_defaults_from_schema(&merged_value, &self.config.schema);
380        tracing::debug!(value_with_defaults = ?value_with_defaults, "driver: after fill_defaults_from_schema");
381
382        // Check for missing required fields by walking the schema
383        let mut missing_fields = Vec::new();
384        collect_missing_fields(
385            &value_with_defaults,
386            &self.config.schema,
387            &mut missing_fields,
388        );
389
390        // Collect unused keys from all layers (for strict mode reporting)
391        let cli_strict = self
392            .config
393            .cli_config
394            .as_ref()
395            .map(|c| c.strict())
396            .unwrap_or(false);
397        let env_strict = self
398            .config
399            .env_config
400            .as_ref()
401            .map(|c| c.strict)
402            .unwrap_or(false);
403        let file_strict = self
404            .config
405            .file_config
406            .as_ref()
407            .map(|c| c.strict)
408            .unwrap_or(false);
409
410        let mut unknown_keys: Vec<&UnusedKey> = Vec::new();
411        if cli_strict {
412            unknown_keys.extend(layers.cli.unused_keys.iter());
413        }
414        if env_strict {
415            unknown_keys.extend(layers.env.unused_keys.iter());
416        }
417        if file_strict {
418            unknown_keys.extend(layers.file.unused_keys.iter());
419        }
420
421        let has_missing = !missing_fields.is_empty();
422        let has_unknown = !unknown_keys.is_empty();
423
424        if has_missing || has_unknown {
425            // If the only missing field is the subcommand, show help instead of "missing fields"
426            let subcommand_field_name = self.config.schema.args().subcommand_field_name();
427            let only_missing_subcommand = !has_unknown
428                && subcommand_field_name.is_some()
429                && missing_fields.len() == 1
430                && missing_fields[0].field_name == subcommand_field_name.unwrap();
431
432            if only_missing_subcommand {
433                // Show help instead of "missing required fields"
434                let help_config = self
435                    .config
436                    .help_config
437                    .as_ref()
438                    .cloned()
439                    .unwrap_or_default();
440
441                let mut help = generate_help_for_subcommand(&self.config.schema, &[], &help_config);
442                maybe_append_implementation_source::<T>(&mut help, &help_config, &[]);
443                return DriverOutcome::err(DriverError::Help { text: help });
444            }
445
446            // Check if the only missing field is a subcommand with available variants
447            // (covers nested subcommands like `tracey query` missing a sub-subcommand)
448            let missing_subcommand_with_variants = !has_unknown
449                && missing_fields.len() == 1
450                && !missing_fields[0].available_subcommands.is_empty();
451
452            if missing_subcommand_with_variants {
453                let field = &missing_fields[0];
454
455                // Format the available subcommands list
456                let items: Vec<(String, Option<&str>)> = field
457                    .available_subcommands
458                    .iter()
459                    .map(|sub| (sub.name.clone(), sub.doc.as_deref()))
460                    .collect();
461
462                // Find max width for alignment
463                let max_width = items.iter().map(|(name, _)| name.len()).max().unwrap_or(0);
464                let mut cmds = String::new();
465                for (name, doc) in &items {
466                    use std::fmt::Write;
467                    write!(cmds, "  {name}").unwrap();
468                    let padding = max_width.saturating_sub(name.len());
469                    for _ in 0..padding {
470                        cmds.push(' ');
471                    }
472                    if let Some(doc) = doc {
473                        write!(cmds, "  {}", doc.trim()).unwrap();
474                    }
475                    cmds.push('\n');
476                }
477
478                let mut diagnostics = vec![Diagnostic {
479                    message: format!(
480                        "expected a subcommand\n\navailable subcommands:\n{}",
481                        cmds.trim_end()
482                    ),
483                    label: None,
484                    path: None,
485                    span: None,
486                    severity: Severity::Error,
487                }];
488
489                // Add help hint if the schema has a help field
490                if self.config.schema.special().help.is_some() {
491                    diagnostics.push(Diagnostic {
492                        message: "Run with --help for usage information.".to_string(),
493                        label: None,
494                        path: None,
495                        span: None,
496                        severity: Severity::Note,
497                    });
498                }
499
500                return DriverOutcome::err(DriverError::Failed {
501                    report: Box::new(DriverReport {
502                        diagnostics,
503                        layers,
504                        file_resolution,
505                        overrides,
506                        cli_args_source: cli_args_display.to_string(),
507                        source_name: "<cli>".to_string(),
508                    }),
509                });
510            }
511
512            // Check if all missing fields are simple CLI arguments (not config fields)
513            // Use the proper kind field to distinguish between CLI args and config fields
514            let all_cli_missing = has_missing
515                && !has_unknown
516                && missing_fields
517                    .iter()
518                    .all(|f| matches!(f.kind, crate::missing::MissingFieldKind::CliArg));
519
520            if all_cli_missing {
521                // Use corrected command as source with proper diagnostics
522                let mut corrected = build_corrected_command_diagnostics(
523                    &missing_fields,
524                    cli_args_source.as_deref(),
525                );
526
527                // Add help hint if the schema has a help field
528                if self.config.schema.special().help.is_some() {
529                    corrected.diagnostics.push(Diagnostic {
530                        message: "Run with --help for usage information.".to_string(),
531                        label: None,
532                        path: None,
533                        span: None,
534                        severity: Severity::Note,
535                    });
536                }
537
538                return DriverOutcome::err(DriverError::Failed {
539                    report: Box::new(DriverReport {
540                        diagnostics: corrected.diagnostics,
541                        layers,
542                        file_resolution,
543                        overrides,
544                        cli_args_source: corrected.corrected_source,
545                        source_name: "<suggestion>".to_string(),
546                    }),
547                });
548            }
549
550            let message = {
551                // Use detailed format with config dump
552                let mut dump_buf = Vec::new();
553                let resolution = file_resolution.as_ref().cloned().unwrap_or_default();
554                dump_config_with_schema(
555                    &mut dump_buf,
556                    &value_with_defaults,
557                    &resolution,
558                    &self.config.schema,
559                );
560                let dump =
561                    String::from_utf8(dump_buf).unwrap_or_else(|_| "error rendering dump".into());
562
563                // Build error message with both missing fields and unknown keys
564                let mut message_parts = Vec::new();
565
566                if has_unknown {
567                    message_parts.push("Unknown configuration keys:".to_string());
568                }
569                if has_missing {
570                    message_parts.push("Missing required fields:".to_string());
571                }
572
573                let header = message_parts.join(" / ");
574
575                // Format the summary of missing fields
576                let missing_summary = if has_missing {
577                    format!(
578                        "\nMissing:\n{}",
579                        format_missing_fields_summary(&missing_fields)
580                    )
581                } else {
582                    String::new()
583                };
584
585                // Format the summary of unknown keys with suggestions
586                let unknown_summary = if has_unknown {
587                    let config_schema = self.config.schema.config();
588                    let unknown_list: Vec<String> = unknown_keys
589                        .iter()
590                        .map(|uk| {
591                            let source = uk.provenance.source_description();
592                            let suggestion = config_schema
593                                .map(|cs| crate::suggest::suggest_config_path(cs, &uk.key))
594                                .unwrap_or_default();
595                            format!("  {} (from {}){}", uk.key.join("."), source, suggestion)
596                        })
597                        .collect();
598                    format!("\nUnknown keys:\n{}", unknown_list.join("\n"))
599                } else {
600                    String::new()
601                };
602
603                format!(
604                    "{}\n\n{}{}{}\nRun with --help for usage information.",
605                    header, dump, missing_summary, unknown_summary
606                )
607            };
608
609            return DriverOutcome::err(DriverError::Failed {
610                report: Box::new(DriverReport {
611                    diagnostics: vec![Diagnostic {
612                        message,
613                        label: None,
614                        path: None,
615                        span: None,
616                        severity: Severity::Error,
617                    }],
618                    layers,
619                    file_resolution,
620                    overrides,
621                    cli_args_source: cli_args_display.to_string(),
622                    source_name: "<cli>".to_string(),
623                }),
624            });
625        }
626
627        // Phase 4: Assign virtual spans and deserialize into T
628        // The span registry maps virtual spans back to real source locations
629        let mut value_with_virtual_spans = value_with_defaults;
630        let span_registry = assign_virtual_spans(&mut value_with_virtual_spans);
631
632        let value: T = match from_config_value(&value_with_virtual_spans) {
633            Ok(v) => v,
634            Err(e) => {
635                // Extract virtual span from the error, then look up real location
636                let (span, source_name, source_contents) = if let Some(virtual_span) = e.span() {
637                    if let Some(entry) =
638                        span_registry.lookup_by_offset(virtual_span.offset as usize)
639                    {
640                        let real_span = Span::new(
641                            entry.real_span.offset as usize,
642                            entry.real_span.len as usize,
643                        );
644                        let (name, contents) =
645                            get_source_for_provenance(&entry.provenance, cli_args_display, &layers);
646                        (Some(real_span), name, contents)
647                    } else {
648                        (None, "<unknown>".to_string(), cli_args_display.to_string())
649                    }
650                } else {
651                    (None, "<unknown>".to_string(), cli_args_display.to_string())
652                };
653
654                return DriverOutcome::err(DriverError::Failed {
655                    report: Box::new(DriverReport {
656                        diagnostics: vec![Diagnostic {
657                            message: e.to_string(),
658                            label: None,
659                            path: None,
660                            span,
661                            severity: Severity::Error,
662                        }],
663                        layers,
664                        file_resolution,
665                        overrides,
666                        cli_args_source: source_contents,
667                        source_name,
668                    }),
669                });
670            }
671        };
672
673        DriverOutcome::ok(DriverOutput {
674            value,
675            report: DriverReport {
676                diagnostics: all_diagnostics,
677                layers,
678                file_resolution,
679                overrides,
680                cli_args_source: cli_args_display.to_string(),
681                source_name: "<cli>".to_string(),
682            },
683            merged_config: value_with_virtual_spans,
684            schema: self.config.schema.clone(),
685        })
686    }
687}
688
689fn maybe_append_implementation_source<T: Facet<'static>>(
690    help_text: &mut String,
691    help_config: &crate::help::HelpConfig,
692    subcommand_path: &[String],
693) {
694    let Some(source_file) = implementation_source_for_subcommand_path(T::SHAPE, subcommand_path)
695    else {
696        return;
697    };
698
699    let implementation_url = help_config
700        .implementation_url
701        .as_ref()
702        .map(|render_url| render_url(source_file));
703
704    if !help_config.include_implementation_source_file && implementation_url.is_none() {
705        return;
706    }
707
708    if !help_text.ends_with('\n') {
709        help_text.push('\n');
710    }
711    help_text.push('\n');
712    help_text.push_str("Implementation:\n");
713    if help_config.include_implementation_source_file {
714        help_text.push_str("    ");
715        help_text.push_str(source_file);
716        help_text.push('\n');
717    }
718    if let Some(implementation_url) = implementation_url {
719        help_text.push_str("    ");
720        help_text.push_str(&implementation_url);
721        help_text.push('\n');
722    }
723}
724
725/// Get the source name and contents for a provenance.
726fn get_source_for_provenance(
727    provenance: &Provenance,
728    cli_args_display: &str,
729    layers: &ConfigLayers,
730) -> (String, String) {
731    match provenance {
732        Provenance::Cli { .. } => ("<cli>".to_string(), cli_args_display.to_string()),
733        Provenance::Env { .. } => {
734            // Use the virtual env document from the env layer
735            let source_text = layers.env.source_text.as_ref().cloned().unwrap_or_default();
736            ("<env>".to_string(), source_text)
737        }
738        Provenance::File { file, .. } => (file.path.to_string(), file.contents.clone()),
739        Provenance::Default => ("<default>".to_string(), String::new()),
740    }
741}
742
743/// The result of running the figue driver — either a parsed value or an early exit.
744///
745/// # Why this type exists
746///
747/// When a CLI user passes `--help` or `--version`, the program should print the
748/// relevant text and exit with code 0 (success). But these cases flow through the
749/// error path of the driver, since no config value `T` was produced. If `DriverOutcome`
750/// were just a `Result`, calling `.unwrap()` would panic on `--help`, and using `?`
751/// would propagate it as an error (exit code 1 instead of 0).
752///
753/// `DriverOutcome` solves this by providing an [`.unwrap()`](Self::unwrap) method that
754/// does the right thing for every case:
755///
756/// - Parsed successfully → returns `T`
757/// - `--help` / `--version` / `--completions` → prints to stdout, exits with code 0
758/// - Parse error → prints diagnostics to stderr, exits with code 1
759///
760/// This type intentionally does NOT implement `Try`, so you cannot use `?` on it
761/// accidentally.
762///
763/// # Usage
764///
765/// For most CLI programs, just call `.unwrap()`:
766///
767/// ```rust,no_run
768/// use facet::Facet;
769/// use figue::{self as args, FigueBuiltins};
770///
771/// #[derive(Facet)]
772/// struct Args {
773///     #[facet(args::positional)]
774///     file: String,
775///
776///     #[facet(flatten)]
777///     builtins: FigueBuiltins,
778/// }
779///
780/// // If the user passes --help, this prints help and exits with code 0.
781/// // If the user passes invalid args, this prints an error and exits with code 1.
782/// // Otherwise, it returns the parsed Args.
783/// let args: Args = figue::from_std_args().unwrap();
784/// println!("Processing: {}", args.file);
785/// ```
786///
787/// For tests or custom handling, use [`.into_result()`](Self::into_result) to get a
788/// `Result<DriverOutput<T>, DriverError>`:
789///
790/// ```rust
791/// use facet::Facet;
792/// use figue::{self as args, FigueBuiltins, DriverError};
793///
794/// #[derive(Facet)]
795/// struct Args {
796///     #[facet(args::positional, default)]
797///     file: Option<String>,
798///
799///     #[facet(flatten)]
800///     builtins: FigueBuiltins,
801/// }
802///
803/// // --help produces a DriverError::Help (exit code 0, not a "real" error)
804/// let outcome = figue::from_slice::<Args>(&["--help"]);
805/// let err = outcome.unwrap_err();
806/// assert!(err.is_help());
807/// assert_eq!(err.exit_code(), 0);
808///
809/// // Successful parse returns DriverOutput containing the value
810/// let outcome = figue::from_slice::<Args>(&["input.txt"]);
811/// let output = outcome.into_result().unwrap();
812/// assert_eq!(output.value.file.as_deref(), Some("input.txt"));
813/// ```
814#[must_use = "this `DriverOutcome` may contain a help/version request that should be handled"]
815pub struct DriverOutcome<T>(Result<DriverOutput<T>, DriverError>);
816
817impl<T: std::fmt::Debug> std::fmt::Debug for DriverOutcome<T> {
818    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819        match &self.0 {
820            Ok(output) => f
821                .debug_tuple("DriverOutcome::Ok")
822                .field(&output.value)
823                .finish(),
824            Err(e) => f.debug_tuple("DriverOutcome::Err").field(e).finish(),
825        }
826    }
827}
828
829impl<T> DriverOutcome<T> {
830    /// Create a successful outcome.
831    pub fn ok(output: DriverOutput<T>) -> Self {
832        Self(Ok(output))
833    }
834
835    /// Create an error outcome.
836    pub fn err(error: DriverError) -> Self {
837        Self(Err(error))
838    }
839
840    /// Convert to a standard `Result` for manual handling.
841    ///
842    /// Use this when you need to inspect the error yourself (e.g., in tests, or to
843    /// implement custom exit behavior). For most CLI programs, prefer
844    /// [`.unwrap()`](Self::unwrap) instead.
845    ///
846    /// **Warning**: Don't blindly use `?` on this result — early exits like `Help` and
847    /// `Version` will propagate as errors and cause exit code 1 instead of 0.
848    pub fn into_result(self) -> Result<DriverOutput<T>, DriverError> {
849        self.0
850    }
851
852    /// Returns `true` if this is a successful parse (not help/version/error).
853    pub fn is_ok(&self) -> bool {
854        self.0.is_ok()
855    }
856
857    /// Returns `true` if this is an error or early exit request.
858    pub fn is_err(&self) -> bool {
859        self.0.is_err()
860    }
861
862    /// Get the parsed value, handling all early-exit cases automatically.
863    ///
864    /// This is the primary way to use figue. It does exactly what a well-behaved
865    /// CLI should do:
866    ///
867    /// | Case | Behavior |
868    /// |------|----------|
869    /// | Parse succeeded | Prints warnings to stderr, returns `T` |
870    /// | `--help` passed | Prints help to stdout, exits with code 0 |
871    /// | `--version` passed | Prints version to stdout, exits with code 0 |
872    /// | `--completions` passed | Prints shell script to stdout, exits with code 0 |
873    /// | Parse failed | Prints diagnostics to stderr, exits with code 1 |
874    ///
875    /// # Example
876    ///
877    /// ```rust,no_run
878    /// use facet::Facet;
879    /// use figue::{self as args, FigueBuiltins};
880    ///
881    /// #[derive(Facet)]
882    /// struct Args {
883    ///     #[facet(args::positional)]
884    ///     file: String,
885    ///
886    ///     #[facet(flatten)]
887    ///     builtins: FigueBuiltins,
888    /// }
889    ///
890    /// // In your main():
891    /// let args: Args = figue::from_std_args().unwrap();
892    /// // If we get here, args were parsed successfully.
893    /// // --help and --version already exited before this line.
894    /// println!("Processing: {}", args.file);
895    /// ```
896    pub fn unwrap(self) -> T {
897        match self.0 {
898            Ok(output) => output.get(),
899            Err(DriverError::Help { text }) => {
900                println!("{}", text);
901                std::process::exit(0);
902            }
903            Err(DriverError::Completions { script }) => {
904                println!("{}", script);
905                std::process::exit(0);
906            }
907            Err(DriverError::Version { text }) => {
908                println!("{}", text);
909                std::process::exit(0);
910            }
911            Err(DriverError::Failed { report }) => {
912                eprintln!("{}", report.render_pretty());
913                std::process::exit(1);
914            }
915            Err(DriverError::Builder { error }) => {
916                eprintln!("{}", error);
917                std::process::exit(1);
918            }
919            Err(DriverError::EnvSubst { error }) => {
920                eprintln!("error: {}", error);
921                std::process::exit(1);
922            }
923        }
924    }
925
926    /// Unwrap the error, panicking if this is a success.
927    ///
928    /// Useful for testing error cases.
929    ///
930    /// # Panics
931    ///
932    /// Panics if this is a successful parse.
933    pub fn unwrap_err(self) -> DriverError {
934        match self.0 {
935            Ok(_) => panic!("called `DriverOutcome::unwrap_err()` on a success"),
936            Err(e) => e,
937        }
938    }
939}
940
941/// Successful driver output: a typed value plus an execution report.
942///
943/// This is returned when parsing succeeds. It contains:
944/// - The parsed value of type `T`
945/// - A [`DriverReport`] with diagnostics and metadata
946///
947/// # Example
948///
949/// ```rust
950/// use facet::Facet;
951/// use figue::{self as args, FigueBuiltins};
952///
953/// #[derive(Facet, Debug)]
954/// struct Args {
955///     #[facet(args::named, default)]
956///     verbose: bool,
957///
958///     #[facet(flatten)]
959///     builtins: FigueBuiltins,
960/// }
961///
962/// let result = figue::from_slice::<Args>(&["--verbose"]).into_result();
963/// let output = result.unwrap();
964///
965/// // Access the parsed value
966/// assert!(output.value.verbose);
967///
968/// // Or get the value with warnings printed
969/// // let args = output.get();
970/// ```
971pub struct DriverOutput<T> {
972    /// The fully-typed value produced by deserialization.
973    pub value: T,
974    /// Diagnostics and metadata produced by the driver.
975    pub report: DriverReport,
976    /// The merged configuration value (for extract()).
977    merged_config: ConfigValue,
978    /// The schema (for computing hints in extract()).
979    schema: crate::schema::Schema,
980}
981
982impl<T: std::fmt::Debug> std::fmt::Debug for DriverOutput<T> {
983    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
984        f.debug_struct("DriverOutput")
985            .field("value", &self.value)
986            .field("report", &self.report)
987            .finish_non_exhaustive()
988    }
989}
990
991impl<T> DriverOutput<T> {
992    /// Get the value, printing any warnings to stderr.
993    pub fn get(self) -> T {
994        self.print_warnings();
995        self.value
996    }
997
998    /// Get the value silently (no warning output).
999    pub fn get_silent(self) -> T {
1000        self.value
1001    }
1002
1003    /// Get value and report separately.
1004    pub fn into_parts(self) -> (T, DriverReport) {
1005        (self.value, self.report)
1006    }
1007
1008    /// Print any warnings to stderr.
1009    pub fn print_warnings(&self) {
1010        for diagnostic in &self.report.diagnostics {
1011            if diagnostic.severity == Severity::Warning {
1012                eprintln!("{}: {}", diagnostic.severity.as_str(), diagnostic.message);
1013            }
1014        }
1015    }
1016
1017    /// Extract a requirements struct from the parsed configuration.
1018    ///
1019    /// This allows subcommand-specific validation of required fields. The requirements
1020    /// struct should have fields annotated with `#[facet(args::origin = "path")]` to
1021    /// indicate which values from the config should be extracted.
1022    ///
1023    /// # Example
1024    ///
1025    /// ```ignore
1026    /// use figue as args;
1027    ///
1028    /// #[derive(Facet)]
1029    /// struct MigrateRequirements {
1030    ///     #[facet(args::origin = "config.database_url")]
1031    ///     database_url: String,  // Required for this context
1032    ///
1033    ///     #[facet(args::origin = "config.migrations_path")]
1034    ///     migrations_path: PathBuf,
1035    /// }
1036    ///
1037    /// let output = figue::builder::<Args>()?.cli(|c| c.from_std_args()).build().run()?;
1038    ///
1039    /// match output.value.command {
1040    ///     Command::Migrate => {
1041    ///         let req: MigrateRequirements = output.extract()?;
1042    ///         run_migrate(&req.database_url, &req.migrations_path);
1043    ///     }
1044    /// }
1045    /// ```
1046    ///
1047    /// # Errors
1048    ///
1049    /// Returns an error if any required field (non-Option) is missing in the config,
1050    /// or if a field lacks the `args::origin` attribute.
1051    pub fn extract<R: Facet<'static>>(&self) -> Result<R, crate::extract::ExtractError> {
1052        crate::extract::extract_requirements::<R>(&self.merged_config, &self.schema)
1053    }
1054}
1055
1056/// Full report of the driver execution.
1057///
1058/// The report should be pretty-renderable and capture all diagnostics,
1059/// plus optional supporting metadata (merge overrides, spans, etc).
1060#[derive(Default)]
1061pub struct DriverReport {
1062    /// Diagnostics emitted by the driver.
1063    pub diagnostics: Vec<Diagnostic>,
1064    /// Per-layer outputs, including unused keys and layer diagnostics.
1065    pub layers: ConfigLayers,
1066    /// File resolution metadata (paths tried, picked, etc).
1067    pub file_resolution: Option<FileResolution>,
1068    /// Records of values that were overridden during merge.
1069    pub overrides: Vec<Override>,
1070    /// Source contents for error display (CLI args, env var value, or file contents).
1071    pub cli_args_source: String,
1072    /// Name of the source for error display (e.g., `<cli>`, `$VAR`, `config.toml`).
1073    pub source_name: String,
1074}
1075
1076/// A simple cache that wraps a Source and provides a display name.
1077struct NamedSource {
1078    name: String,
1079    source: ariadne::Source<String>,
1080}
1081
1082impl ariadne::Cache<()> for NamedSource {
1083    type Storage = String;
1084
1085    fn fetch(&mut self, _: &()) -> Result<&ariadne::Source<Self::Storage>, impl std::fmt::Debug> {
1086        Ok::<_, std::convert::Infallible>(&self.source)
1087    }
1088
1089    fn display<'a>(&self, _: &'a ()) -> Option<impl std::fmt::Display + 'a> {
1090        Some(self.name.clone())
1091    }
1092}
1093
1094impl DriverReport {
1095    /// Render the report using Ariadne for pretty error display.
1096    pub fn render_pretty(&self) -> String {
1097        use ariadne::{Color, Config, Label, Report, ReportKind, Source};
1098
1099        if self.diagnostics.is_empty() {
1100            return String::new();
1101        }
1102
1103        let mut output = Vec::with_capacity(128);
1104        let mut cache = NamedSource {
1105            name: self.source_name.clone(),
1106            source: Source::from(self.cli_args_source.clone()),
1107        };
1108
1109        for diagnostic in &self.diagnostics {
1110            // For diagnostics without a span, just print the message directly
1111            // (e.g., missing required fields error doesn't point to a specific location)
1112            if diagnostic.span.is_none() {
1113                // If message starts with "Error:" it's already a formatted report (e.g., from Ariadne)
1114                // Just output it as-is without adding another prefix
1115                if diagnostic.message.starts_with("Error:")
1116                    || diagnostic.message.starts_with("Warning:")
1117                    || diagnostic.message.starts_with("Note:")
1118                {
1119                    output.extend_from_slice(diagnostic.message.as_bytes());
1120                    output.push(b'\n');
1121                    continue;
1122                }
1123
1124                // Otherwise add the appropriate prefix
1125                let prefix = match diagnostic.severity {
1126                    Severity::Error => "Error: ",
1127                    Severity::Warning => "Warning: ",
1128                    Severity::Note => "Note: ",
1129                };
1130                output.extend_from_slice(prefix.as_bytes());
1131                output.extend_from_slice(diagnostic.message.as_bytes());
1132                output.push(b'\n');
1133                continue;
1134            }
1135
1136            let span = diagnostic
1137                .span
1138                .map(|s| s.start..(s.start + s.len))
1139                .unwrap_or(0..0);
1140
1141            let report_kind = match diagnostic.severity {
1142                Severity::Error => ReportKind::Error,
1143                Severity::Warning => ReportKind::Warning,
1144                Severity::Note => ReportKind::Advice,
1145            };
1146
1147            let label_message = diagnostic.label.as_deref().unwrap_or(&diagnostic.message);
1148            let mut label = Label::new(span.clone()).with_message(label_message);
1149            if should_use_color() {
1150                let color = match diagnostic.severity {
1151                    Severity::Error => Color::Red,
1152                    Severity::Warning => Color::Yellow,
1153                    Severity::Note => Color::Cyan,
1154                };
1155                label = label.with_color(color);
1156            }
1157
1158            let report = Report::build(report_kind, span.clone())
1159                .with_config(Config::default().with_color(should_use_color()))
1160                .with_message(&diagnostic.message)
1161                .with_label(label)
1162                .finish();
1163
1164            report.write(&mut cache, &mut output).ok();
1165        }
1166
1167        String::from_utf8(output).unwrap_or_else(|_| "error rendering diagnostics".to_string())
1168    }
1169}
1170
1171impl core::fmt::Display for DriverReport {
1172    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1173        f.write_str(&self.render_pretty())
1174    }
1175}
1176
1177impl core::fmt::Debug for DriverReport {
1178    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1179        f.write_str(&self.render_pretty())
1180    }
1181}
1182
1183/// A diagnostic message produced by the driver.
1184///
1185/// This is intentionally minimal and will grow as we integrate facet-pretty
1186/// spans and Ariadne rendering.
1187#[derive(Debug, Clone)]
1188pub struct Diagnostic {
1189    /// Human-readable message.
1190    pub message: String,
1191    /// Optional label message for the span (if different from message).
1192    pub label: Option<String>,
1193    /// Optional path within the schema or config.
1194    pub path: Option<Path>,
1195    /// Optional byte span within a formatted shape or source file.
1196    pub span: Option<Span>,
1197    /// Diagnostic severity.
1198    pub severity: Severity,
1199}
1200
1201/// Severity for diagnostics.
1202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1203pub enum Severity {
1204    /// Error that prevents producing a value.
1205    Error,
1206    /// Warning that allows a value to be produced.
1207    Warning,
1208    /// Informational note.
1209    Note,
1210}
1211
1212impl Severity {
1213    fn as_str(self) -> &'static str {
1214        match self {
1215            Severity::Error => "error",
1216            Severity::Warning => "warning",
1217            Severity::Note => "note",
1218        }
1219    }
1220}
1221
1222/// Extract a Shell value from a ConfigValue.
1223///
1224/// The completions field is `Option<Shell>`, so after CLI parsing we get
1225/// either nothing (None) or a string like "bash", "zsh", "fish".
1226fn extract_shell_from_value(value: &ConfigValue) -> Option<Shell> {
1227    match value {
1228        ConfigValue::String(s) => match s.value.to_lowercase().as_str() {
1229            "bash" => Some(Shell::Bash),
1230            "zsh" => Some(Shell::Zsh),
1231            "fish" => Some(Shell::Fish),
1232            _ => None,
1233        },
1234        // Could also be an enum variant name directly
1235        ConfigValue::Enum(e) => match e.value.variant.to_lowercase().as_str() {
1236            "bash" => Some(Shell::Bash),
1237            "zsh" => Some(Shell::Zsh),
1238            "fish" => Some(Shell::Fish),
1239            _ => None,
1240        },
1241        _ => None,
1242    }
1243}
1244
1245/// Reason the driver did not produce a parsed value.
1246///
1247/// This enum covers two distinct cases:
1248///
1249/// - **Early exits** ([`Help`](Self::Help), [`Version`](Self::Version),
1250///   [`Completions`](Self::Completions)) — the user asked for something other than
1251///   running the program. These have exit code 0 and are "errors" only in the sense
1252///   that no `T` was produced.
1253///
1254/// - **Actual errors** ([`Failed`](Self::Failed), [`Builder`](Self::Builder),
1255///   [`EnvSubst`](Self::EnvSubst)) — something went wrong. These have exit code 1.
1256///
1257/// Most programs don't need to inspect this type at all — calling
1258/// [`DriverOutcome::unwrap()`] handles everything correctly. This type is useful
1259/// when you want to customize behavior, e.g. in tests or in programs that embed
1260/// figue's parsing in a larger framework.
1261///
1262/// # Exit Codes
1263///
1264/// | Variant | Exit Code | Kind |
1265/// |---------|-----------|------|
1266/// | `Help` | 0 | Early exit |
1267/// | `Version` | 0 | Early exit |
1268/// | `Completions` | 0 | Early exit |
1269/// | `Failed` | 1 | Error |
1270/// | `Builder` | 1 | Error |
1271/// | `EnvSubst` | 1 | Error |
1272///
1273/// # Example
1274///
1275/// ```rust
1276/// use figue::{self as args, FigueBuiltins, DriverError};
1277/// use facet::Facet;
1278///
1279/// #[derive(Facet)]
1280/// struct Args {
1281///     #[facet(args::positional, default)]
1282///     file: Option<String>,
1283///
1284///     #[facet(flatten)]
1285///     builtins: FigueBuiltins,
1286/// }
1287///
1288/// // --help is an early exit, not an error
1289/// let err = figue::from_slice::<Args>(&["--help"]).unwrap_err();
1290/// assert!(err.is_success());
1291/// assert_eq!(err.exit_code(), 0);
1292///
1293/// // Pattern matching for custom handling:
1294/// match figue::from_slice::<Args>(&["--help"]).into_result() {
1295///     Ok(output) => {
1296///         // use output.value
1297///     }
1298///     Err(DriverError::Help { text }) => {
1299///         assert!(text.contains("--help"));
1300///         // print text and exit(0)
1301///     }
1302///     Err(DriverError::Version { text }) => {
1303///         // print text and exit(0)
1304///     }
1305///     Err(DriverError::Failed { report }) => {
1306///         // print report.render_pretty() and exit(1)
1307///     }
1308///     Err(e) => {
1309///         // other error, exit(1)
1310///     }
1311/// }
1312/// ```
1313pub enum DriverError {
1314    /// Builder failed (e.g., schema validation, file not found).
1315    ///
1316    /// Exit code: 1
1317    Builder {
1318        /// The builder error.
1319        error: crate::builder::BuilderError,
1320    },
1321
1322    /// Parsing or validation failed.
1323    ///
1324    /// Exit code: 1
1325    ///
1326    /// The report contains detailed diagnostics with source locations
1327    /// when available.
1328    Failed {
1329        /// Report containing all diagnostics.
1330        report: Box<DriverReport>,
1331    },
1332
1333    /// Help was requested (via `#[facet(figue::help)]` field).
1334    ///
1335    /// Exit code: 0
1336    ///
1337    /// This is a "successful" exit - the user asked for help and got it.
1338    Help {
1339        /// Formatted help text ready to print to stdout.
1340        text: String,
1341    },
1342
1343    /// Shell completions were requested (via `#[facet(figue::completions)]` field).
1344    ///
1345    /// Exit code: 0
1346    ///
1347    /// This is a "successful" exit - the user asked for completions and got them.
1348    Completions {
1349        /// Generated completion script for the requested shell.
1350        script: String,
1351    },
1352
1353    /// Version was requested (via `#[facet(figue::version)]` field).
1354    ///
1355    /// Exit code: 0
1356    ///
1357    /// This is a "successful" exit - the user asked for version and got it.
1358    Version {
1359        /// Version string (e.g., "myapp 1.0.0").
1360        text: String,
1361    },
1362
1363    /// Environment variable substitution failed.
1364    ///
1365    /// Exit code: 1
1366    ///
1367    /// This happens when `${VAR}` substitution is enabled but the variable
1368    /// is not set and has no default.
1369    EnvSubst {
1370        /// The substitution error.
1371        error: EnvSubstError,
1372    },
1373}
1374
1375impl DriverError {
1376    /// Returns the appropriate exit code for this error.
1377    pub fn exit_code(&self) -> i32 {
1378        match self {
1379            DriverError::Builder { .. } => 1,
1380            DriverError::Failed { .. } => 1,
1381            DriverError::Help { .. } => 0,
1382            DriverError::Completions { .. } => 0,
1383            DriverError::Version { .. } => 0,
1384            DriverError::EnvSubst { .. } => 1,
1385        }
1386    }
1387
1388    /// Returns true if this is a "success" error (help, completions, version).
1389    pub fn is_success(&self) -> bool {
1390        self.exit_code() == 0
1391    }
1392
1393    /// Returns true if this is a help request.
1394    pub fn is_help(&self) -> bool {
1395        matches!(self, DriverError::Help { .. })
1396    }
1397
1398    /// Returns the help text if this is a help request.
1399    pub fn help_text(&self) -> Option<&str> {
1400        match self {
1401            DriverError::Help { text } => Some(text),
1402            _ => None,
1403        }
1404    }
1405}
1406
1407impl std::fmt::Display for DriverError {
1408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1409        match self {
1410            DriverError::Builder { error } => write!(f, "{}", error),
1411            DriverError::Failed { report } => write!(f, "{}", report),
1412            DriverError::Help { text } => write!(f, "{}", text),
1413            DriverError::Completions { script } => write!(f, "{}", script),
1414            DriverError::Version { text } => write!(f, "{}", text),
1415            DriverError::EnvSubst { error } => write!(f, "{}", error),
1416        }
1417    }
1418}
1419
1420impl std::fmt::Debug for DriverError {
1421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1422        std::fmt::Display::fmt(self, f)
1423    }
1424}
1425
1426impl std::error::Error for DriverError {}
1427
1428impl std::process::Termination for DriverError {
1429    fn report(self) -> std::process::ExitCode {
1430        // Print the appropriate output
1431        match &self {
1432            DriverError::Help { text } | DriverError::Version { text } => {
1433                println!("{}", text);
1434            }
1435            DriverError::Completions { script } => {
1436                println!("{}", script);
1437            }
1438            DriverError::Failed { report } => {
1439                eprintln!("{}", report.render_pretty());
1440            }
1441            DriverError::Builder { error } => {
1442                eprintln!("{}", error);
1443            }
1444            DriverError::EnvSubst { error } => {
1445                eprintln!("error: {}", error);
1446            }
1447        }
1448        std::process::ExitCode::from(self.exit_code() as u8)
1449    }
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454    use super::*;
1455    use crate as figue;
1456    use crate::FigueBuiltins;
1457    use crate::builder::builder;
1458    use facet::Facet;
1459    use facet_testhelpers::test;
1460
1461    /// Args struct with FigueBuiltins flattened in
1462    #[derive(Facet, Debug)]
1463    struct ArgsWithBuiltins {
1464        /// Input file
1465        #[facet(figue::positional)]
1466        input: Option<String>,
1467
1468        /// Standard CLI options
1469        #[facet(flatten)]
1470        builtins: FigueBuiltins,
1471    }
1472
1473    #[test]
1474    fn test_driver_help_flag() {
1475        let config = builder::<ArgsWithBuiltins>()
1476            .unwrap()
1477            .cli(|cli| cli.args(["--help"]))
1478            .help(|h| h.program_name("test-app").version("1.0.0"))
1479            .build();
1480
1481        let driver = Driver::new(config);
1482        let result = driver.run().into_result();
1483
1484        match result {
1485            Err(DriverError::Help { text }) => {
1486                assert!(
1487                    text.contains("test-app"),
1488                    "help should contain program name"
1489                );
1490                assert!(text.contains("--help"), "help should mention --help flag");
1491            }
1492            other => panic!("expected DriverError::Help, got {:?}", other),
1493        }
1494    }
1495
1496    #[test]
1497    fn test_driver_help_short_flag() {
1498        let config = builder::<ArgsWithBuiltins>()
1499            .unwrap()
1500            .cli(|cli| cli.args(["-h"]))
1501            .help(|h| h.program_name("test-app"))
1502            .build();
1503
1504        let driver = Driver::new(config);
1505        let result = driver.run().into_result();
1506
1507        assert!(
1508            matches!(result, Err(DriverError::Help { .. })),
1509            "expected DriverError::Help"
1510        );
1511    }
1512
1513    #[test]
1514    fn test_driver_version_flag() {
1515        let config = builder::<ArgsWithBuiltins>()
1516            .unwrap()
1517            .cli(|cli| cli.args(["--version"]))
1518            .help(|h| h.program_name("test-app").version("2.0.0"))
1519            .build();
1520
1521        let driver = Driver::new(config);
1522        let result = driver.run().into_result();
1523
1524        match result {
1525            Err(DriverError::Version { text }) => {
1526                assert!(
1527                    text.contains("test-app"),
1528                    "version should contain program name"
1529                );
1530                assert!(
1531                    text.contains("2.0.0"),
1532                    "version should contain version number"
1533                );
1534            }
1535            other => panic!("expected DriverError::Version, got {:?}", other),
1536        }
1537    }
1538
1539    #[test]
1540    fn test_driver_version_short_flag() {
1541        let config = builder::<ArgsWithBuiltins>()
1542            .unwrap()
1543            .cli(|cli| cli.args(["-V"]))
1544            .help(|h| h.program_name("test-app").version("3.0.0"))
1545            .build();
1546
1547        let driver = Driver::new(config);
1548        let result = driver.run().into_result();
1549
1550        match result {
1551            Err(DriverError::Version { text }) => {
1552                assert!(
1553                    text.contains("3.0.0"),
1554                    "version should contain version number"
1555                );
1556            }
1557            other => panic!("expected DriverError::Version, got {:?}", other),
1558        }
1559    }
1560
1561    #[test]
1562    fn test_driver_completions_bash() {
1563        let config = builder::<ArgsWithBuiltins>()
1564            .unwrap()
1565            .cli(|cli| cli.args(["--completions", "bash"]))
1566            .help(|h| h.program_name("test-app"))
1567            .build();
1568
1569        let driver = Driver::new(config);
1570        let result = driver.run().into_result();
1571
1572        match result {
1573            Err(DriverError::Completions { script }) => {
1574                assert!(
1575                    script.contains("_test-app"),
1576                    "bash completions should contain function name"
1577                );
1578                assert!(
1579                    script.contains("complete"),
1580                    "bash completions should contain 'complete'"
1581                );
1582            }
1583            other => panic!("expected DriverError::Completions, got {:?}", other),
1584        }
1585    }
1586
1587    #[test]
1588    fn test_driver_completions_zsh() {
1589        let config = builder::<ArgsWithBuiltins>()
1590            .unwrap()
1591            .cli(|cli| cli.args(["--completions", "zsh"]))
1592            .help(|h| h.program_name("myapp"))
1593            .build();
1594
1595        let driver = Driver::new(config);
1596        let result = driver.run().into_result();
1597
1598        match result {
1599            Err(DriverError::Completions { script }) => {
1600                assert!(
1601                    script.contains("#compdef myapp"),
1602                    "zsh completions should contain #compdef"
1603                );
1604            }
1605            other => panic!("expected DriverError::Completions, got {:?}", other),
1606        }
1607    }
1608
1609    #[test]
1610    fn test_driver_completions_fish() {
1611        let config = builder::<ArgsWithBuiltins>()
1612            .unwrap()
1613            .cli(|cli| cli.args(["--completions", "fish"]))
1614            .help(|h| h.program_name("myapp"))
1615            .build();
1616
1617        let driver = Driver::new(config);
1618        let result = driver.run().into_result();
1619
1620        match result {
1621            Err(DriverError::Completions { script }) => {
1622                assert!(
1623                    script.contains("complete -c myapp"),
1624                    "fish completions should contain 'complete -c myapp'"
1625                );
1626            }
1627            other => panic!("expected DriverError::Completions, got {:?}", other),
1628        }
1629    }
1630
1631    #[test]
1632    fn test_driver_normal_execution() {
1633        let config = builder::<ArgsWithBuiltins>()
1634            .unwrap()
1635            .cli(|cli| cli.args(["myfile.txt"]))
1636            .build();
1637
1638        let driver = Driver::new(config);
1639        let result = driver.run().into_result();
1640
1641        match result {
1642            Ok(output) => {
1643                assert_eq!(output.value.input, Some("myfile.txt".to_string()));
1644                assert!(!output.value.builtins.help);
1645                assert!(!output.value.builtins.version);
1646                assert!(output.value.builtins.completions.is_none());
1647            }
1648            Err(e) => panic!("expected success, got error: {:?}", e),
1649        }
1650    }
1651
1652    #[test]
1653    fn test_driver_error_exit_codes() {
1654        let help_err = DriverError::Help {
1655            text: "help".to_string(),
1656        };
1657        let version_err = DriverError::Version {
1658            text: "1.0".to_string(),
1659        };
1660        let completions_err = DriverError::Completions {
1661            script: "script".to_string(),
1662        };
1663        let failed_err = DriverError::Failed {
1664            report: Box::new(DriverReport::default()),
1665        };
1666
1667        assert_eq!(help_err.exit_code(), 0);
1668        assert_eq!(version_err.exit_code(), 0);
1669        assert_eq!(completions_err.exit_code(), 0);
1670        assert_eq!(failed_err.exit_code(), 1);
1671
1672        assert!(help_err.is_success());
1673        assert!(version_err.is_success());
1674        assert!(completions_err.is_success());
1675        assert!(!failed_err.is_success());
1676    }
1677
1678    // ========================================================================
1679    // Tests: Builder API with subcommands (regression test for issue #3)
1680    // ========================================================================
1681
1682    /// Subcommand enum for testing
1683    #[derive(Facet, Debug, PartialEq)]
1684    #[repr(u8)]
1685    enum TestCommand {
1686        /// Build the project
1687        Build {
1688            /// Build in release mode
1689            #[facet(figue::named, figue::short = 'r')]
1690            release: bool,
1691        },
1692        /// Run the project
1693        Run {
1694            /// Arguments to pass
1695            #[facet(figue::positional)]
1696            args: Vec<String>,
1697        },
1698    }
1699
1700    /// Args struct with subcommand only (minimal reproduction)
1701    #[derive(Facet, Debug)]
1702    struct ArgsWithSubcommandOnly {
1703        #[facet(figue::subcommand)]
1704        command: TestCommand,
1705    }
1706
1707    /// Args struct with subcommand AND FigueBuiltins (issue report pattern)
1708    #[derive(Facet, Debug)]
1709    struct ArgsWithSubcommandAndBuiltins {
1710        #[facet(figue::subcommand)]
1711        command: TestCommand,
1712
1713        #[facet(flatten)]
1714        builtins: FigueBuiltins,
1715    }
1716
1717    #[derive(Facet, Debug, PartialEq)]
1718    #[repr(u8)]
1719    enum TestCommandWithExplicitHelp {
1720        Help,
1721        Build {
1722            #[facet(figue::named, figue::short = 'r')]
1723            release: bool,
1724        },
1725    }
1726
1727    #[derive(Facet, Debug)]
1728    struct ArgsWithExplicitHelpSubcommand {
1729        #[facet(figue::subcommand)]
1730        command: TestCommandWithExplicitHelp,
1731
1732        #[facet(flatten)]
1733        builtins: FigueBuiltins,
1734    }
1735
1736    // Test the minimal case - subcommand only, no builtins
1737    #[test]
1738    fn test_builder_api_with_subcommand_minimal() {
1739        let config = builder::<ArgsWithSubcommandOnly>()
1740            .expect("failed to build args schema")
1741            .cli(|cli| cli.args(["build", "--release"]))
1742            .build();
1743
1744        let result = Driver::new(config).run().into_result();
1745
1746        match result {
1747            Ok(output) => match &output.value.command {
1748                TestCommand::Build { release } => {
1749                    assert!(*release, "release flag should be true");
1750                }
1751                TestCommand::Run { .. } => {
1752                    panic!("expected Build subcommand, got Run");
1753                }
1754            },
1755            Err(e) => panic!("expected success, got error: {:?}", e),
1756        }
1757    }
1758
1759    // Test with FigueBuiltins (the pattern from issue report)
1760    #[test]
1761    fn test_builder_api_with_subcommand_and_builtins() {
1762        let config = builder::<ArgsWithSubcommandAndBuiltins>()
1763            .expect("failed to build args schema")
1764            .cli(|cli| cli.args(["build", "--release"]))
1765            .help(|h| h.program_name("test-app").version("1.0.0"))
1766            .build();
1767
1768        let result = Driver::new(config).run().into_result();
1769
1770        match result {
1771            Ok(output) => match &output.value.command {
1772                TestCommand::Build { release } => {
1773                    assert!(*release, "release flag should be true");
1774                }
1775                TestCommand::Run { .. } => {
1776                    panic!("expected Build subcommand, got Run");
1777                }
1778            },
1779            Err(e) => panic!("expected success, got error: {:?}", e),
1780        }
1781    }
1782
1783    #[test]
1784    fn test_builder_help_word_alias_triggers_help_without_help_subcommand() {
1785        let config = builder::<ArgsWithSubcommandAndBuiltins>()
1786            .expect("failed to build args schema")
1787            .cli(|cli| cli.args(["help"]))
1788            .help(|h| h.program_name("test-app"))
1789            .build();
1790
1791        let result = Driver::new(config).run().into_result();
1792
1793        match result {
1794            Err(DriverError::Help { text }) => {
1795                assert!(
1796                    text.contains("test-app"),
1797                    "help should contain configured program name"
1798                );
1799                assert!(
1800                    text.contains("--help"),
1801                    "help output should still mention --help"
1802                );
1803            }
1804            other => panic!("expected DriverError::Help, got {:?}", other),
1805        }
1806    }
1807
1808    #[test]
1809    fn test_builder_help_word_prefers_explicit_help_subcommand() {
1810        let config = builder::<ArgsWithExplicitHelpSubcommand>()
1811            .expect("failed to build args schema")
1812            .cli(|cli| cli.args(["help"]))
1813            .build();
1814
1815        let result = Driver::new(config).run().into_result();
1816
1817        match result {
1818            Ok(output) => {
1819                assert_eq!(output.value.command, TestCommandWithExplicitHelp::Help);
1820                assert!(
1821                    !output.value.builtins.help,
1822                    "builtin help flag should remain false"
1823                );
1824            }
1825            Err(e) => panic!("expected success, got error: {:?}", e),
1826        }
1827    }
1828
1829    #[test]
1830    fn test_builder_api_with_subcommand_no_args() {
1831        let config = builder::<ArgsWithSubcommandOnly>()
1832            .expect("failed to build args schema")
1833            .cli(|cli| cli.args(["build"]))
1834            .build();
1835
1836        let result = Driver::new(config).run().into_result();
1837
1838        match result {
1839            Ok(output) => match &output.value.command {
1840                TestCommand::Build { release } => {
1841                    assert!(!*release, "release flag should be false by default");
1842                }
1843                TestCommand::Run { .. } => {
1844                    panic!("expected Build subcommand, got Run");
1845                }
1846            },
1847            Err(e) => panic!("expected success, got error: {:?}", e),
1848        }
1849    }
1850
1851    #[test]
1852    fn test_builder_api_with_run_subcommand() {
1853        let config = builder::<ArgsWithSubcommandOnly>()
1854            .expect("failed to build args schema")
1855            .cli(|cli| cli.args(["run", "arg1", "arg2"]))
1856            .build();
1857
1858        let result = Driver::new(config).run().into_result();
1859
1860        match result {
1861            Ok(output) => match &output.value.command {
1862                TestCommand::Run { args } => {
1863                    assert_eq!(args, &["arg1".to_string(), "arg2".to_string()]);
1864                }
1865                TestCommand::Build { .. } => {
1866                    panic!("expected Run subcommand, got Build");
1867                }
1868            },
1869            Err(e) => panic!("expected success, got error: {:?}", e),
1870        }
1871    }
1872
1873    #[test]
1874    fn test_from_slice_with_subcommand() {
1875        // Test that from_slice (which uses builder internally) works with subcommands
1876        let args: ArgsWithSubcommandOnly = crate::from_slice(&["build", "--release"]).unwrap();
1877
1878        match &args.command {
1879            TestCommand::Build { release } => {
1880                assert!(*release, "release flag should be true");
1881            }
1882            TestCommand::Run { .. } => {
1883                panic!("expected Build subcommand, got Run");
1884            }
1885        }
1886    }
1887
1888    #[test]
1889    fn test_from_slice_with_subcommand_and_builtins() {
1890        // Test that from_slice works with subcommands AND FigueBuiltins
1891        let args: ArgsWithSubcommandAndBuiltins =
1892            crate::from_slice(&["build", "--release"]).unwrap();
1893
1894        match &args.command {
1895            TestCommand::Build { release } => {
1896                assert!(*release, "release flag should be true");
1897            }
1898            TestCommand::Run { .. } => {
1899                panic!("expected Build subcommand, got Run");
1900            }
1901        }
1902    }
1903
1904    #[test]
1905    fn test_subcommand_with_builtins_parsing() {
1906        use crate::config_value_parser::fill_defaults_from_shape;
1907        use crate::layers::cli::CliConfigBuilder;
1908        use crate::layers::cli::parse_cli;
1909        use crate::missing::collect_missing_fields;
1910        use crate::schema::Schema;
1911
1912        // Build schema for the type with subcommand + builtins
1913        let schema =
1914            Schema::from_shape(ArgsWithSubcommandAndBuiltins::SHAPE).expect("schema should build");
1915
1916        // Parse CLI args
1917        let cli_config = CliConfigBuilder::new().args(["build", "--release"]).build();
1918        let output = parse_cli(&schema, &cli_config);
1919
1920        assert!(output.diagnostics.is_empty(), "should have no diagnostics");
1921
1922        // Now test fill_defaults_from_shape
1923        let cli_value = output.value.unwrap();
1924        let with_defaults =
1925            fill_defaults_from_shape(&cli_value, ArgsWithSubcommandAndBuiltins::SHAPE);
1926
1927        // Check for missing fields
1928        let mut missing_fields = Vec::new();
1929        collect_missing_fields(&with_defaults, &schema, &mut missing_fields);
1930
1931        assert!(
1932            missing_fields.is_empty(),
1933            "should have no missing fields, got: {:?}",
1934            missing_fields
1935        );
1936
1937        // Now try the full deserialization
1938        let result: Result<ArgsWithSubcommandAndBuiltins, _> =
1939            crate::config_value_parser::from_config_value(&with_defaults);
1940
1941        assert!(
1942            result.is_ok(),
1943            "deserialization should succeed: {:?}",
1944            result
1945        );
1946    }
1947
1948    // ========================================================================
1949    // More comprehensive builder API tests for issue #3
1950    // ========================================================================
1951
1952    /// Nested subcommand enum (inner level)
1953    #[derive(Facet, Debug, PartialEq)]
1954    #[repr(u8)]
1955    enum DatabaseAction {
1956        /// Create a new migration
1957        Create {
1958            /// Migration name
1959            #[facet(figue::positional)]
1960            name: String,
1961        },
1962        /// Run pending migrations
1963        Run {
1964            /// Run in dry-run mode
1965            #[facet(figue::named)]
1966            dry_run: bool,
1967        },
1968        /// Rollback last migration
1969        Rollback {
1970            /// Number of migrations to rollback
1971            #[facet(figue::named, default)]
1972            count: Option<u32>,
1973        },
1974    }
1975
1976    /// Top-level command with nested subcommand
1977    #[derive(Facet, Debug, PartialEq)]
1978    #[repr(u8)]
1979    enum TopLevelCommand {
1980        /// Database management commands
1981        Db {
1982            #[facet(figue::subcommand)]
1983            action: DatabaseAction,
1984        },
1985        /// Start the server
1986        Serve {
1987            /// Port to listen on
1988            #[facet(figue::named, default)]
1989            port: Option<u16>,
1990            /// Host to bind to
1991            #[facet(figue::named, default)]
1992            host: Option<String>,
1993        },
1994        /// Show version info (unit variant)
1995        Version,
1996    }
1997
1998    /// Args with nested subcommands
1999    #[derive(Facet, Debug)]
2000    struct ArgsWithNestedSubcommands {
2001        /// Global verbose flag
2002        #[facet(figue::named, figue::short = 'v')]
2003        verbose: bool,
2004
2005        #[facet(figue::subcommand)]
2006        command: TopLevelCommand,
2007
2008        #[facet(flatten)]
2009        builtins: FigueBuiltins,
2010    }
2011
2012    #[test]
2013    fn test_builder_nested_subcommand_db_create() {
2014        let config = builder::<ArgsWithNestedSubcommands>()
2015            .expect("failed to build schema")
2016            .cli(|cli| cli.args(["db", "create", "add_users_table"]))
2017            .help(|h| h.program_name("test-app"))
2018            .build();
2019
2020        let result = Driver::new(config).run().into_result();
2021        match result {
2022            Ok(output) => {
2023                assert!(!output.value.verbose);
2024                match &output.value.command {
2025                    TopLevelCommand::Db { action } => match action {
2026                        DatabaseAction::Create { name } => {
2027                            assert_eq!(name, "add_users_table");
2028                        }
2029                        _ => panic!("expected Create action"),
2030                    },
2031                    _ => panic!("expected Db command"),
2032                }
2033            }
2034            Err(e) => panic!("expected success: {:?}", e),
2035        }
2036    }
2037
2038    #[test]
2039    fn test_builder_nested_subcommand_db_run_with_flag() {
2040        let config = builder::<ArgsWithNestedSubcommands>()
2041            .expect("failed to build schema")
2042            .cli(|cli| cli.args(["-v", "db", "run", "--dry-run"]))
2043            .help(|h| h.program_name("test-app"))
2044            .build();
2045
2046        let result = Driver::new(config).run().into_result();
2047        match result {
2048            Ok(output) => {
2049                assert!(output.value.verbose, "verbose should be true");
2050                match &output.value.command {
2051                    TopLevelCommand::Db { action } => match action {
2052                        DatabaseAction::Run { dry_run } => {
2053                            assert!(*dry_run, "dry_run should be true");
2054                        }
2055                        _ => panic!("expected Run action"),
2056                    },
2057                    _ => panic!("expected Db command"),
2058                }
2059            }
2060            Err(e) => panic!("expected success: {:?}", e),
2061        }
2062    }
2063
2064    #[test]
2065    fn test_builder_nested_subcommand_db_rollback_default() {
2066        let config = builder::<ArgsWithNestedSubcommands>()
2067            .expect("failed to build schema")
2068            .cli(|cli| cli.args(["db", "rollback"]))
2069            .help(|h| h.program_name("test-app"))
2070            .build();
2071
2072        let result = Driver::new(config).run().into_result();
2073        match result {
2074            Ok(output) => match &output.value.command {
2075                TopLevelCommand::Db { action } => match action {
2076                    DatabaseAction::Rollback { count } => {
2077                        assert_eq!(*count, None, "count should default to None");
2078                    }
2079                    _ => panic!("expected Rollback action"),
2080                },
2081                _ => panic!("expected Db command"),
2082            },
2083            Err(e) => panic!("expected success: {:?}", e),
2084        }
2085    }
2086
2087    #[test]
2088    fn test_builder_nested_subcommand_db_rollback_with_count() {
2089        let config = builder::<ArgsWithNestedSubcommands>()
2090            .expect("failed to build schema")
2091            .cli(|cli| cli.args(["db", "rollback", "--count", "3"]))
2092            .help(|h| h.program_name("test-app"))
2093            .build();
2094
2095        let result = Driver::new(config).run().into_result();
2096        match result {
2097            Ok(output) => match &output.value.command {
2098                TopLevelCommand::Db { action } => match action {
2099                    DatabaseAction::Rollback { count } => {
2100                        assert_eq!(*count, Some(3));
2101                    }
2102                    _ => panic!("expected Rollback action"),
2103                },
2104                _ => panic!("expected Db command"),
2105            },
2106            Err(e) => panic!("expected success: {:?}", e),
2107        }
2108    }
2109
2110    #[test]
2111    fn test_builder_serve_with_defaults() {
2112        let config = builder::<ArgsWithNestedSubcommands>()
2113            .expect("failed to build schema")
2114            .cli(|cli| cli.args(["serve"]))
2115            .help(|h| h.program_name("test-app"))
2116            .build();
2117
2118        let result = Driver::new(config).run().into_result();
2119        match result {
2120            Ok(output) => match &output.value.command {
2121                TopLevelCommand::Serve { port, host } => {
2122                    assert_eq!(*port, None);
2123                    assert_eq!(*host, None);
2124                }
2125                _ => panic!("expected Serve command"),
2126            },
2127            Err(e) => panic!("expected success: {:?}", e),
2128        }
2129    }
2130
2131    #[test]
2132    fn test_builder_serve_with_options() {
2133        let config = builder::<ArgsWithNestedSubcommands>()
2134            .expect("failed to build schema")
2135            .cli(|cli| cli.args(["serve", "--port", "8080", "--host", "0.0.0.0"]))
2136            .help(|h| h.program_name("test-app"))
2137            .build();
2138
2139        let result = Driver::new(config).run().into_result();
2140        match result {
2141            Ok(output) => match &output.value.command {
2142                TopLevelCommand::Serve { port, host } => {
2143                    assert_eq!(*port, Some(8080));
2144                    assert_eq!(host.as_deref(), Some("0.0.0.0"));
2145                }
2146                _ => panic!("expected Serve command"),
2147            },
2148            Err(e) => panic!("expected success: {:?}", e),
2149        }
2150    }
2151
2152    #[test]
2153    fn test_builder_unit_variant_subcommand() {
2154        let config = builder::<ArgsWithNestedSubcommands>()
2155            .expect("failed to build schema")
2156            .cli(|cli| cli.args(["version"]))
2157            .help(|h| h.program_name("test-app"))
2158            .build();
2159
2160        let result = Driver::new(config).run().into_result();
2161        match result {
2162            Ok(output) => match &output.value.command {
2163                TopLevelCommand::Version => {
2164                    // Success - unit variant parsed correctly
2165                }
2166                _ => panic!("expected Version command"),
2167            },
2168            Err(e) => panic!("expected success: {:?}", e),
2169        }
2170    }
2171
2172    /// Test tuple variant subcommands with the builder API
2173    #[derive(Facet, Debug, PartialEq, Default)]
2174    struct InstallOptions {
2175        /// Install globally
2176        #[facet(figue::named)]
2177        global: bool,
2178        /// Force reinstall
2179        #[facet(figue::named)]
2180        force: bool,
2181    }
2182
2183    #[derive(Facet, Debug, PartialEq)]
2184    #[repr(u8)]
2185    enum PackageCommand {
2186        /// Install a package (tuple variant with flattened struct)
2187        Install(#[facet(flatten)] InstallOptions),
2188        /// Uninstall a package
2189        Uninstall {
2190            /// Package name
2191            #[facet(figue::positional)]
2192            name: String,
2193        },
2194    }
2195
2196    #[derive(Facet, Debug)]
2197    struct ArgsWithTupleVariant {
2198        #[facet(figue::subcommand)]
2199        command: PackageCommand,
2200
2201        #[facet(flatten)]
2202        builtins: FigueBuiltins,
2203    }
2204
2205    #[test]
2206    fn test_builder_tuple_variant_with_flatten() {
2207        let config = builder::<ArgsWithTupleVariant>()
2208            .expect("failed to build schema")
2209            .cli(|cli| cli.args(["install", "--global", "--force"]))
2210            .help(|h| h.program_name("pkg-manager"))
2211            .build();
2212
2213        let result = Driver::new(config).run().into_result();
2214        match result {
2215            Ok(output) => match &output.value.command {
2216                PackageCommand::Install(opts) => {
2217                    assert!(opts.global, "global should be true");
2218                    assert!(opts.force, "force should be true");
2219                }
2220                _ => panic!("expected Install command"),
2221            },
2222            Err(e) => panic!("expected success: {:?}", e),
2223        }
2224    }
2225
2226    #[test]
2227    fn test_builder_tuple_variant_defaults() {
2228        let config = builder::<ArgsWithTupleVariant>()
2229            .expect("failed to build schema")
2230            .cli(|cli| cli.args(["install"]))
2231            .help(|h| h.program_name("pkg-manager"))
2232            .build();
2233
2234        let result = Driver::new(config).run().into_result();
2235        match result {
2236            Ok(output) => match &output.value.command {
2237                PackageCommand::Install(opts) => {
2238                    assert!(!opts.global, "global should default to false");
2239                    assert!(!opts.force, "force should default to false");
2240                }
2241                _ => panic!("expected Install command"),
2242            },
2243            Err(e) => panic!("expected success: {:?}", e),
2244        }
2245    }
2246
2247    /// Test renamed subcommand variants
2248    #[derive(Facet, Debug, PartialEq)]
2249    #[repr(u8)]
2250    enum RenamedCommand {
2251        /// List items (renamed to 'ls')
2252        #[facet(rename = "ls")]
2253        List {
2254            /// Show all files
2255            #[facet(figue::named, figue::short = 'a')]
2256            all: bool,
2257        },
2258        /// Remove items (renamed to 'rm')
2259        #[facet(rename = "rm")]
2260        Remove {
2261            /// Force removal
2262            #[facet(figue::named, figue::short = 'f')]
2263            force: bool,
2264            /// Target path
2265            #[facet(figue::positional)]
2266            path: String,
2267        },
2268    }
2269
2270    #[derive(Facet, Debug)]
2271    struct ArgsWithRenamedSubcommands {
2272        #[facet(figue::subcommand)]
2273        command: RenamedCommand,
2274
2275        #[facet(flatten)]
2276        builtins: FigueBuiltins,
2277    }
2278
2279    #[test]
2280    fn test_builder_renamed_subcommand_ls() {
2281        let config = builder::<ArgsWithRenamedSubcommands>()
2282            .expect("failed to build schema")
2283            .cli(|cli| cli.args(["ls", "-a"]))
2284            .help(|h| h.program_name("file-tool"))
2285            .build();
2286
2287        let result = Driver::new(config).run().into_result();
2288        match result {
2289            Ok(output) => match &output.value.command {
2290                RenamedCommand::List { all } => {
2291                    assert!(*all, "all should be true");
2292                }
2293                _ => panic!("expected List command"),
2294            },
2295            Err(e) => panic!("expected success: {:?}", e),
2296        }
2297    }
2298
2299    #[test]
2300    fn test_builder_renamed_subcommand_rm() {
2301        let config = builder::<ArgsWithRenamedSubcommands>()
2302            .expect("failed to build schema")
2303            .cli(|cli| cli.args(["rm", "-f", "/tmp/file.txt"]))
2304            .help(|h| h.program_name("file-tool"))
2305            .build();
2306
2307        let result = Driver::new(config).run().into_result();
2308        match result {
2309            Ok(output) => match &output.value.command {
2310                RenamedCommand::Remove { force, path } => {
2311                    assert!(*force, "force should be true");
2312                    assert_eq!(path, "/tmp/file.txt");
2313                }
2314                _ => panic!("expected Remove command"),
2315            },
2316            Err(e) => panic!("expected success: {:?}", e),
2317        }
2318    }
2319
2320    /// Test deeply flattened structs within subcommands
2321    #[derive(Facet, Debug, PartialEq, Default)]
2322    struct LoggingOpts {
2323        /// Enable debug logging
2324        #[facet(figue::named)]
2325        debug: bool,
2326        /// Log to file
2327        #[facet(figue::named, default)]
2328        log_file: Option<String>,
2329    }
2330
2331    #[derive(Facet, Debug, PartialEq, Default)]
2332    struct CommonOpts {
2333        /// Verbose output
2334        #[facet(figue::named, figue::short = 'v')]
2335        verbose: bool,
2336        /// Quiet mode
2337        #[facet(figue::named, figue::short = 'q')]
2338        quiet: bool,
2339        #[facet(flatten)]
2340        logging: LoggingOpts,
2341    }
2342
2343    #[derive(Facet, Debug, PartialEq)]
2344    #[repr(u8)]
2345    enum DeepCommand {
2346        /// Run with common options
2347        Execute {
2348            #[facet(flatten)]
2349            common: CommonOpts,
2350            /// Target to execute
2351            #[facet(figue::positional)]
2352            target: String,
2353        },
2354    }
2355
2356    #[derive(Facet, Debug)]
2357    struct ArgsWithDeepFlatten {
2358        #[facet(figue::subcommand)]
2359        command: DeepCommand,
2360
2361        #[facet(flatten)]
2362        builtins: FigueBuiltins,
2363    }
2364
2365    #[test]
2366    fn test_builder_deep_flatten_all_flags() {
2367        let config = builder::<ArgsWithDeepFlatten>()
2368            .expect("failed to build schema")
2369            .cli(|cli| {
2370                cli.args([
2371                    "execute",
2372                    "-v",
2373                    "--debug",
2374                    "--log-file",
2375                    "/var/log/app.log",
2376                    "my-target",
2377                ])
2378            })
2379            .help(|h| h.program_name("deep-app"))
2380            .build();
2381
2382        let result = Driver::new(config).run().into_result();
2383        match result {
2384            Ok(output) => match &output.value.command {
2385                DeepCommand::Execute { common, target } => {
2386                    assert!(common.verbose, "verbose should be true");
2387                    assert!(!common.quiet, "quiet should be false");
2388                    assert!(common.logging.debug, "debug should be true");
2389                    assert_eq!(common.logging.log_file.as_deref(), Some("/var/log/app.log"));
2390                    assert_eq!(target, "my-target");
2391                }
2392            },
2393            Err(e) => panic!("expected success: {:?}", e),
2394        }
2395    }
2396
2397    #[test]
2398    fn test_builder_deep_flatten_defaults() {
2399        let config = builder::<ArgsWithDeepFlatten>()
2400            .expect("failed to build schema")
2401            .cli(|cli| cli.args(["execute", "simple-target"]))
2402            .help(|h| h.program_name("deep-app"))
2403            .build();
2404
2405        let result = Driver::new(config).run().into_result();
2406        match result {
2407            Ok(output) => match &output.value.command {
2408                DeepCommand::Execute { common, target } => {
2409                    assert!(!common.verbose);
2410                    assert!(!common.quiet);
2411                    assert!(!common.logging.debug);
2412                    assert_eq!(common.logging.log_file, None);
2413                    assert_eq!(target, "simple-target");
2414                }
2415            },
2416            Err(e) => panic!("expected success: {:?}", e),
2417        }
2418    }
2419
2420    // ========================================================================
2421    // Enum variant conflict tests
2422    // ========================================================================
2423
2424    #[derive(Facet, Debug)]
2425    #[facet(rename_all = "kebab-case")]
2426    #[repr(u8)]
2427    #[allow(dead_code)]
2428    enum StorageBackend {
2429        S3 { bucket: String, region: String },
2430        Gcp { project: String, zone: String },
2431        Local { path: String },
2432    }
2433
2434    #[derive(Facet, Debug)]
2435    struct StorageConfig {
2436        storage: StorageBackend,
2437    }
2438
2439    #[derive(Facet, Debug)]
2440    struct ArgsWithStorageConfig {
2441        #[facet(figue::config)]
2442        config: StorageConfig,
2443    }
2444
2445    #[test]
2446    #[ignore = "env parser doesn't support enum variant paths yet (issue #37)"]
2447    fn test_enum_variant_conflict_from_env() {
2448        use crate::layers::env::MockEnv;
2449
2450        // Set both s3 and gcp variants via env vars - should error
2451        let config = builder::<ArgsWithStorageConfig>()
2452            .expect("failed to build schema")
2453            .env(|env| {
2454                env.prefix("MYAPP").source(MockEnv::from_pairs([
2455                    ("MYAPP__STORAGE__S3__BUCKET", "my-bucket"),
2456                    ("MYAPP__STORAGE__S3__REGION", "us-east-1"),
2457                    ("MYAPP__STORAGE__GCP__PROJECT", "my-project"),
2458                ]))
2459            })
2460            .build();
2461
2462        let result = Driver::new(config).run().into_result();
2463        match result {
2464            Err(DriverError::Failed { report }) => {
2465                let msg = format!("{}", report);
2466                assert!(
2467                    msg.contains("Conflicting enum variants"),
2468                    "should report enum conflict: {msg}"
2469                );
2470                assert!(msg.contains("s3"), "should mention s3 variant: {msg}");
2471                assert!(msg.contains("gcp"), "should mention gcp variant: {msg}");
2472            }
2473            Ok(_) => panic!("expected conflict error, got success"),
2474            Err(e) => panic!("expected conflict error, got {:?}", e),
2475        }
2476    }
2477
2478    #[test]
2479    #[ignore = "env parser doesn't support enum variant paths yet (issue #37)"]
2480    fn test_enum_no_conflict_single_variant_from_env() {
2481        use crate::layers::env::MockEnv;
2482
2483        // Set only s3 variant via env vars - should succeed
2484        let config = builder::<ArgsWithStorageConfig>()
2485            .expect("failed to build schema")
2486            .env(|env| {
2487                env.prefix("MYAPP").source(MockEnv::from_pairs([
2488                    ("MYAPP__STORAGE__S3__BUCKET", "my-bucket"),
2489                    ("MYAPP__STORAGE__S3__REGION", "us-east-1"),
2490                ]))
2491            })
2492            .build();
2493
2494        let result = Driver::new(config).run().into_result();
2495        match result {
2496            Ok(output) => match &output.value.config.storage {
2497                StorageBackend::S3 { bucket, region } => {
2498                    assert_eq!(bucket, "my-bucket");
2499                    assert_eq!(region, "us-east-1");
2500                }
2501                other => panic!("expected S3 variant, got {:?}", other),
2502            },
2503            Err(e) => panic!("expected success, got {:?}", e),
2504        }
2505    }
2506
2507    #[test]
2508    #[ignore = "env parser doesn't support enum variant paths yet (issue #37)"]
2509    fn test_enum_variant_conflict_cross_source() {
2510        use crate::layers::env::MockEnv;
2511
2512        // Set s3 from env, gcp from CLI - should error
2513        let config = builder::<ArgsWithStorageConfig>()
2514            .expect("failed to build schema")
2515            .env(|env| {
2516                env.prefix("MYAPP").source(MockEnv::from_pairs([
2517                    ("MYAPP__STORAGE__S3__BUCKET", "my-bucket"),
2518                    ("MYAPP__STORAGE__S3__REGION", "us-east-1"),
2519                ]))
2520            })
2521            .cli(|cli| {
2522                cli.args([
2523                    "--config.storage.gcp.project",
2524                    "my-project",
2525                    "--config.storage.gcp.zone",
2526                    "us-central1-a",
2527                ])
2528            })
2529            .build();
2530
2531        let result = Driver::new(config).run().into_result();
2532        match result {
2533            Err(DriverError::Failed { report }) => {
2534                let msg = format!("{}", report);
2535                assert!(
2536                    msg.contains("Conflicting enum variants"),
2537                    "should report enum conflict: {msg}"
2538                );
2539                // Should mention both variants
2540                assert!(msg.contains("s3"), "should mention s3: {msg}");
2541                assert!(msg.contains("gcp"), "should mention gcp: {msg}");
2542            }
2543            Ok(_) => panic!("expected conflict error, got success"),
2544            Err(e) => panic!("expected conflict error, got {:?}", e),
2545        }
2546    }
2547
2548    #[test]
2549    fn test_enum_variant_conflict_from_cli() {
2550        // Set both s3 and gcp variants via CLI args - should error
2551        let config = builder::<ArgsWithStorageConfig>()
2552            .expect("failed to build schema")
2553            .cli(|cli| {
2554                cli.args([
2555                    "--config.storage.s3.bucket",
2556                    "my-bucket",
2557                    "--config.storage.gcp.project",
2558                    "my-project",
2559                ])
2560            })
2561            .build();
2562
2563        let result = Driver::new(config).run().into_result();
2564        match result {
2565            Err(DriverError::Failed { report }) => {
2566                let msg = format!("{}", report);
2567                assert!(
2568                    msg.contains("Conflicting enum variants"),
2569                    "should report enum conflict: {msg}"
2570                );
2571            }
2572            Ok(_) => panic!("expected conflict error, got success"),
2573            Err(e) => panic!("expected conflict error, got {:?}", e),
2574        }
2575    }
2576
2577    #[test]
2578    fn test_enum_no_conflict_single_variant_from_cli() {
2579        // Set only s3 variant via CLI args - should succeed
2580        let config = builder::<ArgsWithStorageConfig>()
2581            .expect("failed to build schema")
2582            .cli(|cli| {
2583                cli.args([
2584                    "--config.storage.s3.bucket",
2585                    "my-bucket",
2586                    "--config.storage.s3.region",
2587                    "us-east-1",
2588                ])
2589            })
2590            .build();
2591
2592        let result = Driver::new(config).run().into_result();
2593        match result {
2594            Ok(output) => match &output.value.config.storage {
2595                StorageBackend::S3 { bucket, region } => {
2596                    assert_eq!(bucket, "my-bucket");
2597                    assert_eq!(region, "us-east-1");
2598                }
2599                other => panic!("expected S3 variant, got {:?}", other),
2600            },
2601            Err(e) => panic!("expected success, got {:?}", e),
2602        }
2603    }
2604
2605    #[test]
2606    fn test_enum_variant_conflict_three_variants() {
2607        // Set all three variants via CLI args - should error
2608        let config = builder::<ArgsWithStorageConfig>()
2609            .expect("failed to build schema")
2610            .cli(|cli| {
2611                cli.args([
2612                    "--config.storage.s3.bucket",
2613                    "my-bucket",
2614                    "--config.storage.gcp.project",
2615                    "my-project",
2616                    "--config.storage.local.path",
2617                    "/data",
2618                ])
2619            })
2620            .build();
2621
2622        let result = Driver::new(config).run().into_result();
2623        match result {
2624            Err(DriverError::Failed { report }) => {
2625                let msg = format!("{}", report);
2626                assert!(
2627                    msg.contains("Conflicting enum variants"),
2628                    "should report enum conflict: {msg}"
2629                );
2630                // Should mention all three variants
2631                assert!(msg.contains("s3"), "should mention s3: {msg}");
2632                assert!(msg.contains("gcp"), "should mention gcp: {msg}");
2633                assert!(msg.contains("local"), "should mention local: {msg}");
2634            }
2635            Ok(_) => panic!("expected conflict error, got success"),
2636            Err(e) => panic!("expected conflict error, got {:?}", e),
2637        }
2638    }
2639
2640    /// Test tuple variant WITHOUT explicit #[facet(flatten)]
2641    /// This matches the dodeca pattern where Serve(ServeArgs) doesn't have flatten
2642    #[derive(Facet, Debug, PartialEq, Default)]
2643    struct SimpleOptions {
2644        /// Do the thing
2645        #[facet(figue::named)]
2646        do_thing: bool,
2647    }
2648
2649    #[derive(Facet, Debug, PartialEq)]
2650    #[repr(u8)]
2651    enum SimpleCommand {
2652        /// Do something (tuple variant WITHOUT explicit flatten)
2653        DoSomething(SimpleOptions),
2654    }
2655
2656    #[derive(Facet, Debug)]
2657    struct ArgsWithSimpleTupleVariant {
2658        #[facet(figue::subcommand)]
2659        command: SimpleCommand,
2660
2661        #[facet(flatten)]
2662        builtins: FigueBuiltins,
2663    }
2664
2665    /// Regression test: tuple variants without explicit #[facet(flatten)] should
2666    /// still get boolean defaults filled (previously the subcommand lookup was
2667    /// keyed by cli_name but ConfigValue uses effective_name)
2668    #[test]
2669    fn test_builder_tuple_variant_no_explicit_flatten() {
2670        let config = builder::<ArgsWithSimpleTupleVariant>()
2671            .expect("failed to build schema")
2672            .cli(|cli| cli.args(["do-something"]))
2673            .help(|h| h.program_name("test-app"))
2674            .build();
2675
2676        let result = Driver::new(config).run().into_result();
2677        match result {
2678            Ok(output) => match &output.value.command {
2679                SimpleCommand::DoSomething(opts) => {
2680                    assert!(!opts.do_thing, "do_thing should default to false");
2681                }
2682            },
2683            Err(e) => panic!("expected success: {:?}", e),
2684        }
2685    }
2686}