nazara 0.2.1

A CLI application to create and update machines and VMs in NetBox.
# Nazara Error Handling Guide

## Philosophy

Nazara follows a **"fail gracefully"** philosophy. We prefer returning descriptive errors over panicking, reserving panics only for truly catastrophic and unfixable situations.

**Core Principles:**

- **Single Source of Truth**: Error messages are defined once in `NazaraError` variants and displayed consistently
- **Immediate Feedback**: Errors are logged immediately using the `failure!` macro for user visibility
- **Clean Propagation**: Most functions use the `?` operator without logging noise
- **Context When Needed**: Optional context prefixes indicate which module or operation triggered the error

## The NazaraError Enum

All errors in Nazara are represented by the `NazaraError` enum in `src/error.rs`:

```rust
pub enum NazaraError {
    /// Something went wrong trying to parse DMI tables.
    Dmi(dmidecode::InvalidEntryPointError),
    /// Used to indicate that the collection of system data failed.
    UnableToCollectData(String),
    /// Used for handling errors during file operations.
    FileOpError(std::io::Error),
    /// Indicates that a required config option is missing from the config file.
    MissingConfigOptionError(String),
    /// An error occurred while accessing data returned by NetBox.
    NetBoxApiError(String),
    /// Expects a `String` message. Used for edge cases and general purpose error cases.
    Other(String),
    // ... other variants
}
```

### Display Trait Implementation

Every `NazaraError` variant implements `std::fmt::Display` to provide user-friendly error messages:

```rust
impl std::fmt::Display for NazaraError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NazaraError::FileOpError(err) => {
                write!(f, "File operation failed: {err}")
            }
            NazaraError::NetBoxApiError(msg) => {
                write!(f, "NetBox API Error: {msg}")
            }
            NazaraError::Other(msg) => f.write_str(&msg),
            // ... other variants
        }
    }
}
```


~~~admonish important
The `Display` implementation should **never** call `failure!` or any other logging macro. It should only format the error message.
~~~

## Convenience Methods

The `NazaraError` enum provides two convenience methods for common error handling patterns:

### `fail()` - Log and Return

```rust
impl NazaraError {
    /// Log this error with failure! and return it wrapped in Err(...)
    pub fn fail<T>(self) -> NazaraResult<T> {
        failure!("{}", self);
        Err(self)
    }
}
```

**Purpose:** Combines logging and returning into a single operation.

~~~admonish example collapsible=true title="Example: Using the fail-function to log and return an error"
```rust
return NazaraError::Other("Config file contains invalid entries".to_owned()).fail();
```
~~~

### `log()` - Log with Context

```rust
impl NazaraError {
    /// Log this error with additional context prefix.
    /// Used when we need to log multiple errors but continue processing to fail in the end
    /// either with the calling function or some other higher instance.
    ///
    /// The optional context in this case refers to the module or program part that the error
    /// occurred in. For example: [DHCP-Mode].
    pub fn log(&self, context: Option<&str>) {
        if let Some(ctx) = context {
            failure!("[{}] {}", ctx, self);
            return;
        }
        failure!("{}", self);
    }
}
```

**Purpose:** Log errors with optional context when you need to accumulate multiple errors before failing.

~~~admonish example collapsible=true title="Example: Logging an error but continue processing"
```rust
// Accumulate errors, fail at the end
for validation in validations {
    if !validation.is_valid() {
        let err = NazaraError::Other(format!("Validation '{}' failed", validation.name()));
        err.log(None);
        error_count += 1;
    }
}

if error_count > 0 {
    return NazaraError::Other("Multiple validations failed".to_owned()).fail();
}
```
~~~

~~~admonish example collapsible=true title="Example: Logging an error with context"
`context` indicates where or why this error has occurred. For example when working with the `ip-mode` flags,
errors rooted in the changes of IP addresses by DHCP or something similar have the context "DHCP-Mode".
```rust
let err = NazaraError::NetBoxApiError("IPv4 not found".to_owned());
err.log(Some("DHCP-Mode"));
return Err(err);
```
~~~

## Examples

~~~admonish example collapsible=true title="Example: Logging with Return"
Use when an error is terminal and should be returned immediately.

```rust
return NazaraError::Other("Config file contains invalid entries".to_owned()).fail();
```
~~~

~~~admonish example collapsible=true title="Example: Log with Context, Then Return"

Use when the error needs additional module/operation context.

```rust
let err = NazaraError::NetBoxApiError(
    format!("IPv4 address \"{}\" was not registered in NetBox", ip)
);
err.log(Some("DHCP-Mode"));
return Err(err);
```

Or in a closure (for use with `ok_or_else`):

```rust
let ipv4_id = search_ip(client, &ip.to_string(), None)?.ok_or_else(|| {
    let err = NazaraError::NetBoxApiError(format!("IPv4 \"{}\" not found", ip));
    err.log(Some("DHCP-Mode"));
    err
})?;
```
~~~

~~~admonish example collapsible=true title="Example: Accumulate Errors, Fail Later"
Use when processing multiple items and collecting all errors before failing.

```rust
let mut config_errors = Vec::new();

for field in required_fields {
    if config.get(field).is_none() {
        let err = NazaraError::MissingConfigOptionError(field.to_string());
        err.log(None);
        config_errors.push(err);
    }
}

if !config_errors.is_empty() {
    return NazaraError::Other("Missing required config fields".to_owned()).fail();
}
```
~~~

~~~admonish example collapsible=true title="Example: Custom User-Facing Messages"
When you need to provide specific user guidance, use `failure!` directly.

```rust
failure!(
    "Tag '{}' does not exist. Use --prepare-environment to create it.",
    tag_name
);
```

This is clearer than trying to fit the guidance into a `NazaraError` variant's display message.
~~~

## When to Use What

### Use `.fail()` when:

- The error is terminal (nothing else can be done)
- The error message is sufficient (no additional context needed)
- You want clean, single-line error handling
- This is the 90% common case

### Use `.log()` when:

- You need to accumulate multiple errors before failing
- You want to add module/operation context
- The error should be logged but processing should continue
- You need to track multiple issues

### Use `failure!` directly when:

- You need a custom message with user guidance
- The message is temporary/debugging (will be replaced later)
- You're providing feedback before returning a different error

### Don't use either when:

- You can simply return the error with `?` (no logging needed)
- The caller will handle the error (low-level utilities)
- The error will be logged by the caller anyway

## Best Practices

### 1. Choose the Right Error Variant

Prefer specific variants over `Other` when possible:

```rust
// GOOD
return NazaraError::MissingConfigOptionError("netbox_uri".to_owned()).fail();

// OK (when no specific variant fits)
return NazaraError::Other("Custom error message".to_owned()).fail();
```

### 2. Include Context in Error Messages

When using `Other`, make the message descriptive:

```rust
// GOOD
return NazaraError::Other("Config file contains invalid entries".to_owned()).fail();

// BAD
return NazaraError::Other("Error".to_owned()).fail();
```

### 3. Use Context for Module Identification

When logging with context, use module or operation names:

```rust
err.log(Some("DHCP-Mode"));
err.log(Some("Config-Parser"));
```

### 4. Don't Duplicate Error Messages

The error message should be defined once—in the `Display` implementation or the error construction:

```rust
// GOOD
let msg = "Config file contains invalid entries".to_owned();
failure!("{}", msg);
return Err(NazaraError::Other(msg));

// REDUNDANT (duplicates the message)
failure!("Config file contains invalid entries");
return NazaraError::Other("Config file contains invalid entries".to_owned()).fail();
```

### 5. Let Errors Propagate

For low-level utility functions, just return errors. Let the caller decide whether to log:

```rust
// In a low-level utility
pub fn read_config_file() -> NazaraResult<ConfigData> {
    let mut file = File::open(get_config_path(true))?;  // Just propagate
    // ...
}

// In the calling function
let config = read_config_file()
    .map_err(|e| e.log(None))?;  // Log here
```

## Error Flow

```
[Deep Function] → Creates/Returns NazaraError
        ↓ (via ? operator)
[Middle Function] → Propagates with ?
        ↓ (via ? operator)
[Application Logic] → Logs with .fail() or .log()
[main.rs] → Catches error, logs with failure!
[stderr] → User sees formatted error message
```

## Example: Complete Error Handling Flow

Here's how error handling works in a real scenario:

```rust
// 1. Low-level function (no logging, just propagation)
pub fn read_config_file() -> NazaraResult<ConfigData> {
    let mut file = File::open(get_config_path(true))?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    toml::from_str(&contents).map_err(NazaraError::DeserializationError)
}

// 2. Mid-level function (propagates errors)
pub fn validate_config(config: &ConfigData) -> NazaraResult<()> {
    if config.netbox_uri.is_empty() {
        return Err(NazaraError::MissingConfigOptionError("netbox_uri".to_owned()));
    }
    Ok(())
}

// 3. Application logic (logs and returns)
pub fn setup_configuration() -> NazaraResult<ConfigData> {
    let config = read_config_file()?;
    validate_config(&config)?;
    
    if config.is_invalid() {
        return NazaraError::Other("Config validation failed".to_owned()).fail();
    }
    
    Ok(config)
}

// 4. Main entry point (logs final error)
fn main() {
    match nazara::run() {
        Ok(_) => success!("All done!"),
        Err(e) => failure!("{}", e),  // Uses failure! for consistency
    }
}
```

Remember: **Errors should be logged once, at the appropriate level, with sufficient context.**