serviceconf 0.2.1

Configuration library for microservices: load settings from environment variables with file-based secrets support
Documentation
# serviceconf

Environment variable configuration with file-based secrets support

`serviceconf` provides a declarative API for loading configuration from environment
variables with native support for **file-based secrets** (Kubernetes Secrets, Docker Secrets).

The primary feature that distinguishes this library from other environment variable
configuration libraries is the `#[conf(from_file)]` attribute, which allows reading
secrets from files while falling back to direct environment variables for local development.

## Features

- **File-based secrets**: Read secrets from Kubernetes/Docker mounted files
- **Declarative**: Automatic implementation with `#[derive(ServiceConf)]`
- **Type-safe**: Compile-time type checking
- **Default values**: Support for `Default` trait and explicit values
- **Custom deserializers**: Support for JSON, TOML, or custom parsing functions

## Quick Start

```rust
use serviceconf::ServiceConf;

#[derive(Debug, ServiceConf)]
struct Config {
    // File-based secret: reads from API_KEY or API_KEY_FILE
    #[conf(from_file)]
    pub api_key: String,

    // Default value if not set
    #[conf(default = 8080)]
    pub port: u16,
}

# fn main() -> anyhow::Result<()> {
#     std::env::set_var("API_KEY", "test-key");
let config = Config::from_env()?;
println!("Port: {}", config.port);
#     assert_eq!(config.api_key, "test-key");
#     assert_eq!(config.port, 8080);
#     Ok(())
# }
```

**Local development** (direct environment variable):
```bash
export API_KEY=dev-key-123
export PORT=3000
```

**Production** (Kubernetes/Docker with file-based secret):
```bash
export API_KEY_FILE=/run/secrets/api-key
export PORT=8080
```

## Key Feature: File-based Secrets

The `#[conf(from_file)]` attribute allows reading secrets from files mounted by
Kubernetes or Docker, avoiding the security risks of exposing secrets directly in
environment variables.

**Why file-based secrets?**
- **More secure**: Secrets stored in files, not environment variables (which can leak in logs, process lists, etc.)
-**Kubernetes native**: Works seamlessly with Kubernetes Secrets mounting
-**Docker Secrets**: Direct support for Docker Swarm secrets
-**Flexible**: Falls back to direct environment variables for local development

**Loading priority:**
1. Direct env var (`API_KEY`) - for local development
2. File path from env var (`API_KEY_FILE`) - for production

### Kubernetes Secret Example

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:
  api-key: "prod-api-key-123"
  db-password: "secure-password"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myservice
spec:
  template:
    spec:
      containers:
      - name: app
        image: myservice:latest
        env:
          - name: API_KEY_FILE
            value: /etc/secrets/api-key
          - name: DATABASE_PASSWORD_FILE
            value: /etc/secrets/db-password
        volumeMounts:
          - name: secrets
            mountPath: /etc/secrets
            readOnly: true
      volumes:
        - name: secrets
          secret:
            secretName: app-secrets
            items:
              - key: api-key
                path: api-key
              - key: db-password
                path: db-password
```

With this Kubernetes configuration, your Rust application can simply use:

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
struct Config {
    #[conf(from_file)]
    pub api_key: String,

    #[conf(from_file)]
    pub database_password: String,
}
```

## Value Parsing

**Default (using `FromStr`)**:
- Strings: `DATABASE_URL=postgres://localhost/db`
- Numbers: `MAX_CONNECTIONS=42`
- Booleans: `DEBUG=true`

**Custom deserializers** - specify with `#[conf(deserializer = "function")]`:
- JSON: `#[conf(deserializer = "serde_json::from_str")]`
- TOML: `#[conf(deserializer = "toml::from_str")]`
- Custom: Define your own deserializer function

## Attribute Reference

### Struct-level Attributes

#### `#[conf(prefix = "PREFIX_")]`

Add a prefix to all environment variable names in the struct.

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
#[conf(prefix = "MYAPP_")]
struct Config {
    pub database_url: String,  // Reads from MYAPP_DATABASE_URL
    pub api_key: String,       // Reads from MYAPP_API_KEY
}
```

Environment variables:
```bash
export MYAPP_DATABASE_URL=postgres://localhost/db
export MYAPP_API_KEY=secret123
```

### Field-level Attributes

#### `#[conf(from_file)]` - File-based Secrets

Load from `{VAR_NAME}_FILE` in addition to the environment variable.
This is the primary feature of `serviceconf` for handling file-based secrets
in Kubernetes and Docker environments.

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
pub struct Config {
    // Reads from API_KEY or API_KEY_FILE
    #[conf(from_file)]
    pub api_key: String,
}
```

#### `#[conf(name = "CUSTOM_NAME")]`

Specify an environment variable name different from the field name.

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
pub struct Config {
    // Load from REDIS_URL environment variable
    #[conf(name = "REDIS_URL")]
    pub redis_connection_string: String,
}
```

#### `#[conf(default)]`

Use `Default::default()` if the environment variable is not set.

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
struct Config {
    #[conf(default)]
    pub port: u16,  // Uses 0 (u16::default()) if PORT not set

    #[conf(default)]
    pub host: String,  // Uses "" (String::default()) if HOST not set
}
```

#### `#[conf(default = value)]`

Specify an explicit default value when the environment variable is not set.

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
struct Config {
    #[conf(default = "127.0.0.1:8080".to_string())]
    pub server_addr: String,

    #[conf(default = 10)]
    pub max_connections: u32,

    #[conf(default = false)]
    pub enable_tls: bool,
}
```

#### `#[conf(deserializer = "function")]`

Use a custom deserializer function for complex types or custom parsing logic.

The deserializer function must have the signature:
```rust,ignore
fn deserialize(s: &str) -> Result<T, impl std::fmt::Display>
```

**Example with JSON:**
```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
struct Config {
    // Parse JSON array
    #[conf(deserializer = "serde_json::from_str")]
    pub tags: Vec<String>,
}
```

Environment variable:
```bash
export TAGS='["prod","api","v2"]'
```

**Example with custom function:**
```rust
use serviceconf::ServiceConf;

// Custom comma-separated parser
fn comma_separated(s: &str) -> Result<Vec<String>, String> {
    Ok(s.split(',').map(|s| s.trim().to_string()).collect())
}

#[derive(ServiceConf)]
struct Config {
    #[conf(deserializer = "comma_separated")]
    pub features: Vec<String>,
}
```

Environment variable:
```bash
export FEATURES=feature1,feature2,feature3
```

## Optional Fields

Use `Option<T>` for optional fields. Returns `None` if the environment variable is not set.

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
struct Config {
    pub api_key: Option<String>,  // None if API_KEY not set
    pub max_retries: Option<u32>, // None if MAX_RETRIES not set
}
```

## Type Behavior

| Type | When Env Var Missing | When Env Var Set |
|------|---------------------|------------------|
| `T` (no attribute) | Error | Parsed with `FromStr` |
| `T` + `#[conf(default)]` | `Default::default()` | Parsed with `FromStr` |
| `T` + `#[conf(default = value)]` | Uses `value` | Parsed with `FromStr` |
| `Option<T>` | `None` | `Some(parsed_value)` |
| `T` + `#[conf(deserializer = "fn")]` | Error | Parsed with custom function |

## Combining Attributes

Multiple attributes can be combined to create powerful configurations:

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
#[conf(prefix = "APP_")]
struct Config {
    // Combines: prefix + custom name + from_file + Option
    #[conf(name = "DB_URL")]
    #[conf(from_file)]
    pub database_url: Option<String>,  // Reads from APP_DB_URL or APP_DB_URL_FILE

    // Combines: prefix + default
    #[conf(default = 8080)]
    pub port: u16,  // Reads from APP_PORT, defaults to 8080
}
```

**Invalid combinations** (compile errors):
- `Option<T>` + `#[conf(default)]` or `#[conf(default = value)]` → Option already defaults to None

## Error Handling

The `from_env()` method returns a `Result` that can be handled appropriately:

```rust
use serviceconf::ServiceConf;

#[derive(ServiceConf)]
struct Config {
    pub api_key: String,
}

match Config::from_env() {
    Ok(config) => println!("Config loaded successfully"),
    Err(e) => eprintln!("Failed to load config: {}", e),
}
```

Example error messages:
- `Environment variable 'DATABASE_URL' is required but not set`
- `Failed to parse environment variable 'PORT' as u16: invalid digit found in string`
- `Failed to read file '/etc/secrets/key' for environment variable 'API_KEY_FILE': No such file or directory`