Skip to main content

prax_cli/
error.rs

1//! CLI error types and result alias.
2
3use miette::Diagnostic;
4use thiserror::Error;
5
6/// Result type alias for CLI operations
7pub type CliResult<T> = Result<T, CliError>;
8
9/// CLI error types
10#[derive(Error, Debug, Diagnostic)]
11pub enum CliError {
12    /// IO error
13    #[error("IO error: {0}")]
14    #[diagnostic(code(prax::io))]
15    Io(#[from] std::io::Error),
16
17    /// Configuration error
18    #[error("Configuration error: {0}")]
19    #[diagnostic(code(prax::config))]
20    Config(String),
21
22    /// Schema parsing error
23    #[error("Schema error: {0}")]
24    #[diagnostic(code(prax::schema))]
25    Schema(String),
26
27    /// Validation error
28    #[error("Validation error: {0}")]
29    #[diagnostic(code(prax::validation))]
30    Validation(String),
31
32    /// Migration error
33    #[error("Migration error: {0}")]
34    #[diagnostic(code(prax::migration))]
35    Migration(String),
36
37    /// Database error
38    #[error("Database error: {0}")]
39    #[diagnostic(code(prax::database))]
40    Database(String),
41
42    /// Database unreachable — the connection could not be established
43    /// (host down, refused, timeout, DNS). Distinct from [`CliError::Database`]
44    /// (connected, but a query failed) so callers such as `migrate dev` can
45    /// treat "no database reachable" as a greenfield source while still
46    /// surfacing genuine query/auth errors against a reachable server.
47    #[error("Database unreachable: {0}")]
48    #[diagnostic(code(prax::database_unreachable))]
49    Unreachable(String),
50
51    /// The requested backend's introspection support was not compiled in
52    /// (its cargo feature is off). Callers such as `migrate dev` treat this
53    /// like an unreachable database (greenfield source) rather than a hard
54    /// error, so it is a distinct variant from [`CliError::Config`] to avoid
55    /// routing on the human-readable message text.
56    #[error("{0}")]
57    #[diagnostic(code(prax::feature_unavailable))]
58    FeatureUnavailable(String),
59
60    /// Command error
61    #[error("Command error: {0}")]
62    #[diagnostic(code(prax::command))]
63    Command(String),
64
65    /// Format error
66    #[error("Format error: {0}")]
67    #[diagnostic(code(prax::format))]
68    Format(String),
69
70    /// Code generation error
71    #[error("Codegen error: {0}")]
72    #[diagnostic(code(prax::codegen))]
73    Codegen(String),
74}
75
76impl From<toml::de::Error> for CliError {
77    fn from(err: toml::de::Error) -> Self {
78        CliError::Config(format!("Failed to parse TOML: {}", err))
79    }
80}
81
82impl From<toml::ser::Error> for CliError {
83    fn from(err: toml::ser::Error) -> Self {
84        CliError::Config(format!("Failed to serialize TOML: {}", err))
85    }
86}
87
88impl From<prax_schema::LoadError> for CliError {
89    fn from(e: prax_schema::LoadError) -> Self {
90        use prax_schema::SchemaError;
91        // Resolve SourceId references to file paths for human-readable output.
92        let resolved = render_schema_error(&e.error, &e.sources);
93        match &e.error {
94            SchemaError::ValidationFailed { .. } => CliError::Validation(resolved),
95            _ => CliError::Schema(resolved),
96        }
97    }
98}
99
100impl From<prax_schema::SchemaError> for CliError {
101    fn from(e: prax_schema::SchemaError) -> Self {
102        use prax_schema::SchemaError;
103        match &e {
104            SchemaError::ValidationFailed { .. } => CliError::Validation(e.to_string()),
105            _ => CliError::Schema(e.to_string()),
106        }
107    }
108}
109
110/// Render a SchemaError with file paths resolved from the SourceMap.
111fn render_schema_error(err: &prax_schema::SchemaError, sources: &prax_schema::SourceMap) -> String {
112    use prax_schema::SchemaError;
113    use std::fmt::Write;
114
115    let mut out = err.to_string();
116    match err {
117        SchemaError::ParseInFile { source, inner } => {
118            if let Some(p) = sources.path_of(*source) {
119                let _ = write!(out, "\n  in: {}\n  detail: {}", p.display(), inner);
120            }
121        }
122        SchemaError::DuplicateAcrossFiles { first, second, .. }
123        | SchemaError::MultipleDatasource { first, second } => {
124            if let (Some(a), Some(b)) = (
125                sources.path_of(first.source),
126                sources.path_of(second.source),
127            ) {
128                let _ = write!(
129                    out,
130                    "\n  first:  {} (bytes {}..{})\n  second: {} (bytes {}..{})",
131                    a.display(),
132                    first.span.start,
133                    first.span.end,
134                    b.display(),
135                    second.span.start,
136                    second.span.end,
137                );
138            }
139        }
140        SchemaError::ValidationFailed { errors, .. } => {
141            for (i, e) in errors.iter().enumerate() {
142                let _ = write!(out, "\n  {}. {}", i + 1, render_schema_error(e, sources));
143            }
144        }
145        _ => {}
146    }
147    out
148}