README.md

<p align="center">
  <img src="https://raw.githubusercontent.com/lupodevelop/testcontainer_compose/main/assets/img/logo.png" alt="Pago, the paguro mascot, carrying a Docker container on his shell" width="220">
</p>

<h1 align="center">testcontainer_compose</h1>

<p align="center">
  <a href="https://hex.pm/packages/testcontainer_compose"><img src="https://img.shields.io/hexpm/v/testcontainer_compose?color=ffaff3" alt="Hex Package"></a>
  <a href="https://hexdocs.pm/testcontainer_compose/"><img src="https://img.shields.io/badge/hex-docs-ffaff3" alt="Hex Docs"></a>
  <a href="https://github.com/lupodevelop/testcontainer_compose/actions/workflows/test.yml"><img src="https://github.com/lupodevelop/testcontainer_compose/actions/workflows/test.yml/badge.svg" alt="CI"></a>
  <a href="https://github.com/lupodevelop/testcontainer_compose/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License: MIT"></a>
  <a href="https://gleam.run"><img src="https://img.shields.io/badge/made%20with-gleam-ffaff3?logo=gleam" alt="Made with Gleam"></a>
</p>

> Spin up `docker-compose.yml` stacks from your Gleam tests.
> Pago hands you typed services; the lifecycle (up + body + down) runs automatically.

Companion to [`testcontainer`](https://hex.pm/packages/testcontainer):
use it when you have an existing compose file and want it as the source
of truth at test time, with typed access to parsed services.

```gleam
let cfg =
  testcontainer_compose.from_file("./docker-compose.yml")
  |> testcontainer_compose.with_project_name("test_42")
  |> testcontainer_compose.with_env_override("PG_PASS", "secret")

use stack <- testcontainer_compose.with_compose(cfg)

// Inspect typed service info
case testcontainer_compose.service_by_name(stack, "postgres") {
  option.Some(svc) -> {
    let _image = testcontainer_compose.service_image(svc)
    let _ports = testcontainer_compose.service_ports(svc)
    Ok(Nil)
  }
  option.None -> Ok(Nil)
}
```

`with_compose/2` runs `docker compose up -d --wait`, calls your body,
then always tears the stack down with `docker compose down --volumes
--remove-orphans`, even if the body returns an error. If the body succeeds
but teardown fails, the teardown error is surfaced so leaked stacks are never
silent.

## Why use it

- 🐚 **Native multi-container**: uses Docker's own dependency
  resolution and health-wait
- 🔒 **Type-safe services**: parsed into `Service` records with
  `name`, `image`, `ports`
- 🩺 **Reliable YAML handling**: delegates parsing to
  `docker compose config --format json`, so anchors, includes and
  `extends:` all work
- ⏱ **Hard-cleanup guarantee**: `down --volumes --remove-orphans` runs
  even on body failure
- 🔐 **Secret-safe overrides**: `with_env_override` wraps values in
  `cowl.Secret`

## Install

```sh
gleam add testcontainer_compose@1
```

## API

### Builder

- `from_file(path)` → `ComposeConfig`
- `with_project_name(cfg, name)` → unique project per test run
- `with_env_override(cfg, key, value)` → values wrapped in
  `cowl.Secret` (no leak via `string.inspect`)

### Lifecycle

- `with_compose(cfg, body)` — up + body + down (always). Teardown failure
  surfaced when body succeeds.
- `formula(cfg)` — returns a `StandaloneFormula(ComposeServices, Error)`.
  Pass it to `testcontainer.with_standalone_formula/2` to get the same up +
  body + down guarantee with a typed output:

```gleam
let assert Ok(f) =
  testcontainer_compose.from_file("./docker-compose.yml")
  |> testcontainer_compose.with_project_name("test_42")
  |> testcontainer_compose.formula()

use stack <- testcontainer.with_standalone_formula(f)
```

### Service inspection

- `services(stack)` → `List(Service)`
- `service_by_name(stack, name)` → `Option(Service)`
- `service_name(svc)` / `service_image(svc)` / `service_ports(svc)`

### Parser (testable without Docker)

- `parse_services_json(path, json_str)`: feed it the output of
  `docker compose config --format json` and validate parsing.

### Errors (`testcontainer_compose/error`)

```gleam
pub type Error {
  ComposeFileNotFound(path: String)
  InvalidYaml(path: String, reason: String)
  ComposeFailed(path: String, reason: String)
}
```

`ComposeFailed.reason` carries the exit-code prefix (e.g.
`"docker daemon error: ..."`) plus full stderr/stdout, so diagnostics
never get silently swallowed.

## How it works

`testcontainer_compose` shells out to the Docker CLI for two reasons:

1. `docker compose config --format json` lets Docker handle YAML
   parsing, anchors, env-var interpolation, includes, and `extends:`
   inheritance. More reliable than re-implementing the spec.
2. `docker compose up/down` reuses Docker's own dependency resolution
   and health-wait logic.

The Erlang FFI uses `erlang:open_port` with a 60-second timeout and
explicit exit-code mapping (125 = daemon error, 126 = no permissions,
127 = not found).

## When to use this vs alternatives

| Scenario | Tool |
| --- | --- |
| Pre-built public image, single container | `testcontainer` core, `container.new` |
| Multi-service Gleam-defined stack | `testcontainer.with_network` + multiple `with_container` |
| Existing `docker-compose.yml`, source of truth | **this package** |
| Compose file rarely changes | [Formula Builder](https://github.com/lupodevelop/testcontainer_formulas_builder) → codegen native |
| Custom image built inline | [`testcontainer_dockerfile`](https://hex.pm/packages/testcontainer_dockerfile) |

## Development

```sh
gleam run -m testcontainer_compose_dev      # Dev runner (needs Docker)
gleam test                                   # Unit tests (no Docker)
TESTCONTAINERS_INTEGRATION=true gleam test   # Integration tests (Docker required)
```

## See also

- [`testcontainer`](https://github.com/lupodevelop/testcontainer): core container runner
- [`testcontainer_formulas`](https://github.com/lupodevelop/testcontainer_formulas): pre-built formulas for Postgres, Redis, MySQL, RabbitMQ, Mongo
- [`testcontainer_dockerfile`](https://github.com/lupodevelop/testcontainer_dockerfile): build custom images from a Dockerfile at test time
- [`testcontainer_formulas_builder`](https://github.com/lupodevelop/testcontainer_formulas_builder): visual builder + codegen, also accepts Dockerfile / compose uploads

Full API docs: <https://hexdocs.pm/testcontainer_compose>

## License

[MIT](LICENSE).