# cruxi-api

API-facing data structures for query parsing and response envelopes.

## Purpose

`cruxi-api` provides framework-agnostic types for:

- response envelopes (`Response`, `ApiError`, `Meta`, `Includes`)
- query metadata (`QueryMeta`)
- filtering DSL (`Filter`, `FilterOp`, `FilterValue`)
- pagination/sorting/field selection (`Page`, `Sort`, `Fields`)

## Architecture Boundary

`cruxi-api` owns transport-shaped response envelopes (`Response`, `ApiError`, `ErrorCause`) and
query metadata types. Domain-to-transport conversion belongs at adapter/handler boundaries.

The crate includes optional conversions from `cruxi::CodedError` behind the `coded-error` feature:

- default behavior keeps `coded-error` disabled to avoid direct coupling.
- enable `coded-error` only when you want `CodedError -> ApiError` conversions.

Example dependency without core-coupling (default):

```toml
cruxi-api = { version = "0.1.0" }
```

Example dependency with explicit `CodedError` conversions:

```toml
cruxi-api = { version = "0.1.0", features = ["coded-error"] }
```

## Usage

Parse query concerns into typed metadata, pass it through handlers/services, and return consistent response envelopes to clients.

## Example

```rust
use cruxi_api::{Filter, FilterValue, Page, QueryMeta, Response, Sort};

let query = QueryMeta::new()
    .with_filter(Filter::eq("status", FilterValue::String("active".into())))
    .with_page(Page::new(1, 20))
    .with_sort(Sort::desc("created_at"));

assert!(query.has_filters());

let resp = Response::single("ok").with_meta(
    cruxi_api::Meta::new().with_entry("request_id", "req-123"),
);

assert_eq!(resp.data.len(), 1);
```

## Domain Error Transformation Pattern

Keep domain errors rich and typed, then transform them into `ApiError` at the handler boundary.

```rust
use cruxi_api::{ApiError, ErrorCause};
use uuid::Uuid;

#[derive(Debug)]
enum CreateUserError {
    ValidationFailed { issues: Vec<FieldIssue> },
    FanoutFailed { failures: Vec<ProviderFailure> },
    EmailTaken { email: String }, // useful for logs
}

#[derive(Debug)]
struct FieldIssue {
    code: &'static str,
    field: &'static str,
    reason: &'static str,
}

#[derive(Debug)]
struct ProviderFailure {
    code: &'static str,
    destination: &'static str,
    reason: String, // diagnostic detail
}

impl CreateUserError {
    fn to_api_error(&self, instance: Uuid) -> ApiError {
        match self {
            Self::ValidationFailed { issues } => ApiError::new(
                "USER-CREATE-000400",
                "Validation failed",
                "One or more fields are invalid",
            )
            .with_instance(instance)
            .with_causes(issues.iter().map(|issue| {
                ErrorCause::new(
                    issue.code,
                    format!("Invalid field '{}'", issue.field),
                    issue.reason,
                )
                .with_instance(instance)
            })),
            Self::FanoutFailed { failures } => ApiError::new(
                "USER-CREATE-000500",
                "Transaction failed",
                "One or more provider destinations failed",
            )
            .with_instance(instance)
            .with_causes(failures.iter().map(|failure| {
                ErrorCause::new(
                    failure.code,
                    format!("Provider '{}'", failure.destination),
                    failure.reason.clone(),
                )
                .with_instance(instance)
            })),
            Self::EmailTaken { .. } => ApiError::new(
                "USER-CREATE-000409",
                "Conflict",
                "A user with this email already exists",
            )
            .with_instance(instance),
        }
    }
}

// In your handler/adapter:
// let instance = Uuid::now_v7();
// tracing::error!(%instance, error = ?domain_error, "create_user failed");
// let api_error = domain_error.to_api_error(instance);
```
