vika-cli 1.4.0

Generate TypeScript types, Zod schemas, and Fetch-based API clients from OpenAPI/Swagger specifications
Documentation
use thiserror::Error;

/// Main error type for vika-cli operations.
///
/// This enum represents all possible errors that can occur during
/// code generation, configuration loading, or file operations.
///
/// # Example
///
/// ```no_run
/// use vika_cli::error::VikaError;
///
/// fn example() -> Result<(), VikaError> {
///     // Some operation that might fail
///     let result: Result<String, VikaError> = Ok("success".to_string());
///     
///     match result {
///         Ok(value) => println!("Success: {:?}", value),
///         Err(VikaError::Schema(e)) => eprintln!("Schema error: {}", e),
///         Err(e) => eprintln!("Other error: {}", e),
///     }
///     
///     Ok(())
/// }
/// ```
#[derive(Debug, Error)]
pub enum VikaError {
    #[error("Schema error: {0}")]
    Schema(#[from] SchemaError),

    #[error("Config error: {0}")]
    Config(#[from] ConfigError),

    #[error("Network error: {0}")]
    Network(#[from] NetworkError),

    #[error("File system error: {0}")]
    FileSystem(#[from] FileSystemError),

    #[error("Generation error: {0}")]
    Generation(#[from] GenerationError),

    #[error("Validation error: {0}")]
    Validation(#[from] ValidationError),
}

#[derive(Debug, Error)]
pub enum SchemaError {
    #[error("Schema not found: {name}")]
    NotFound { name: String },

    #[error("Invalid schema reference: {ref_path}")]
    InvalidReference { ref_path: String },

    #[error("Circular reference detected: {cycle:?}")]
    CircularReference { cycle: Vec<String> },

    #[error("Unsupported schema type: {schema_type}")]
    UnsupportedType { schema_type: String },

    #[error("Parameter not found: {name}")]
    ParameterNotFound { name: String },

    #[error("Request body not found: {name}")]
    RequestBodyNotFound { name: String },

    #[error("Response not found: {name}")]
    ResponseNotFound { name: String },

    #[error("Unsupported reference path: {ref_path}")]
    UnsupportedReferencePath { ref_path: String },
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("Config file not found: {path}")]
    NotFound { path: String },

    #[error("Invalid config: {message}")]
    Invalid { message: String },

    #[error("Failed to read config: {0}")]
    ReadError(#[from] std::io::Error),

    #[error("Failed to parse config: {0}")]
    ParseError(#[from] serde_json::Error),

    #[error("Required field missing: {field}")]
    MissingField { field: String },

    #[error("Invalid output directory: {path}")]
    InvalidOutputDirectory { path: String },

    #[error("No specs are defined. At least one spec must be configured in the 'specs' array.")]
    NoSpecDefined,

    #[error("Duplicate spec name found: {name}")]
    DuplicateSpecName { name: String },

    #[error("Invalid spec name: {name}. Spec names must be non-empty and contain only alphanumeric characters, hyphens, and underscores.")]
    InvalidSpecName { name: String },
}

#[derive(Debug, Error)]
pub enum NetworkError {
    #[error("Failed to fetch spec from {url}: {source}")]
    FetchFailed {
        url: String,
        #[source]
        source: reqwest::Error,
    },

    #[error("Failed to read response from {url}: {source}")]
    ReadResponseFailed {
        url: String,
        #[source]
        source: reqwest::Error,
    },

    #[error("Invalid URL: {url}")]
    InvalidUrl { url: String },
}

#[derive(Debug, Error)]
pub enum FileSystemError {
    #[error("Failed to create directory: {path}")]
    CreateDirectoryFailed {
        path: String,
        #[source]
        source: std::io::Error,
    },

    #[error("Failed to read file: {path}")]
    ReadFileFailed {
        path: String,
        #[source]
        source: std::io::Error,
    },

    #[error("Failed to write file: {path}")]
    WriteFileFailed {
        path: String,
        #[source]
        source: std::io::Error,
    },

    #[error("File not found: {path}")]
    FileNotFound { path: String },

    #[error("Directory not found: {path}")]
    DirectoryNotFound { path: String },

    #[error("File was modified by user: {path}\n  This file was previously generated by vika-cli but has been modified.\n  Use --force to overwrite, or --backup to create a backup first.")]
    FileModifiedByUser { path: String },
}

#[derive(Debug, Error)]
pub enum GenerationError {
    #[error("No modules available after filtering")]
    NoModulesAvailable,

    #[error("Cannot use more than one hook generator at once. Please specify only one of --react-query or --swr")]
    InvalidHookFlags,

    #[error("No modules selected")]
    NoModulesSelected,

    #[error("Spec path or URL is required. Use --spec <path-or-url>")]
    SpecPathRequired,

    #[error("Spec not found: {name}. Available specs: {available:?}")]
    SpecNotFound {
        name: String,
        available: Vec<String>,
    },

    #[error("Failed to generate TypeScript types: {0}")]
    TypeScriptGenerationFailed(String),

    #[error("Failed to generate Zod schemas: {0}")]
    ZodGenerationFailed(String),

    #[error("Failed to generate API client: {0}")]
    ApiClientGenerationFailed(String),

    #[error("Invalid operation: {message}")]
    InvalidOperation { message: String },

    #[error("Template error: {0}")]
    Template(#[from] TemplateError),
}

#[derive(Debug, Error)]
pub enum ValidationError {
    #[error("Invalid TypeScript identifier: {name}")]
    InvalidTypeScriptIdentifier { name: String },

    #[error("Invalid module name: {name}")]
    InvalidModuleName { name: String },

    #[error("Validation failed: {message}")]
    Failed { message: String },
}

#[derive(Debug, Error)]
pub enum TemplateError {
    #[error("Template not found: {name}")]
    NotFound { name: String },

    #[error("Failed to render template {name}: {message}")]
    RenderFailed { name: String, message: String },

    #[error("Invalid template syntax in {name}: {message}")]
    InvalidSyntax { name: String, message: String },

    #[error("Missing required context field {field} in template {name}")]
    MissingContextField { name: String, field: String },

    #[error("Failed to load template {name}: {source}")]
    LoadFailed {
        name: String,
        #[source]
        source: FileSystemError,
    },
}

pub type Result<T> = std::result::Result<T, VikaError>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_schema_error_display() {
        let error = SchemaError::NotFound {
            name: "User".to_string(),
        };
        let error_msg = error.to_string();
        assert!(error_msg.contains("User"));
        assert!(error_msg.contains("not found"));
    }

    #[test]
    fn test_config_error_display() {
        let error = ConfigError::Invalid {
            message: "Test error".to_string(),
        };
        let error_msg = error.to_string();
        assert!(error_msg.contains("Test error"));
    }

    #[test]
    fn test_network_error_display() {
        let error = NetworkError::InvalidUrl {
            url: "invalid-url".to_string(),
        };
        let error_msg = error.to_string();
        assert!(error_msg.contains("invalid-url"));
    }

    #[test]
    fn test_file_system_error_display() {
        let error = FileSystemError::FileNotFound {
            path: "/test/path".to_string(),
        };
        let error_msg = error.to_string();
        assert!(error_msg.contains("/test/path"));
    }

    #[test]
    fn test_generation_error_display() {
        let error = GenerationError::NoModulesAvailable;
        let error_msg = error.to_string();
        assert!(error_msg.contains("No modules available"));
    }

    #[test]
    fn test_error_conversion() {
        let schema_error = SchemaError::NotFound {
            name: "Test".to_string(),
        };
        let vika_error: VikaError = schema_error.into();
        let error_msg = vika_error.to_string();
        assert!(error_msg.contains("Schema error"));
    }

    #[test]
    fn test_error_context_preservation() {
        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
        let config_error = ConfigError::ReadError(io_error);
        let vika_error: VikaError = config_error.into();
        let error_msg = vika_error.to_string();
        assert!(error_msg.contains("Config error"));
    }
}