dynamic-config-s3 0.0.1

Read dynamic-config configuration from an S3 object, on AWS or anything that speaks its API.
Documentation
# dynamic-config-s3

Read [`dynamic-config`] configuration from an S3 object — on AWS, or on anything
that speaks its API.

```toml
[dependencies]
dynamic-config = { version = "0.0.1", features = ["async"] }
dynamic-config-s3 = "0.0.1"
```

```rust
use dynamic_config_s3::S3;

DbConfig::set_remote_async(S3::new("myapp-config", "prod/db.json").await?);

DbConfig::refresh_remote_async().await?;
```

The AWS SDK is async throughout, so this implements the **async**
`AsyncRemoteSource` trait rather than the blocking one.

## What it reads

One object, whose body is **a whole configuration document** — the same bytes
that would be in a config file. The format comes from the key's extension, or
from `with_format`.

## Credentials

Through `aws-config`, which is the chain every AWS tool uses:
`AWS_ACCESS_KEY_ID`, the shared profile, the EC2 instance role, the ECS task
role, and IRSA on EKS. That is deliberately not re-implemented here — a second
credential chain in a program that already has one is a bug waiting for a
rotation.

## Not only AWS

`with_config` takes an `SdkConfig` the program already built, which is how a
non-AWS endpoint is reached. Path-style addressing is forced, because
`http://bucket.host/key` needs DNS entries only AWS has:

```rust
let config = aws_config::from_env()
    .endpoint_url("http://minio.internal:9000")
    .load()
    .await;

let s3 = S3::with_config(&config, "myapp-config", "prod/db.json");
```

MinIO, Ceph, Cloudflare R2 and Backblaze B2 all work this way — and the test
suite runs against MinIO, so that is checked rather than claimed.

## Watching

S3 cannot say when an object changes without a notification pipeline — SNS, SQS,
EventBridge — and that is a deployment's decision rather than a library's. So
`watch` polls, and says so.

What it does *not* do is download the object every tick. `HEAD` returns the
ETag, which changes when the body does, so an unchanged configuration costs one
small request and no transfer — which matters on a bucket that charges per
gigabyte.

```rust
s3.watch(&watching, Duration::from_secs(30), DbConfig::apply_remote).await
```

- The current object is **not** delivered at startup.
- The ETag is taken from the *read*, not from the check that preceded it, so a
  write landing between the two is not delivered twice.
- A failed check does not end the watch. Stopping is noticed within a quarter
  second whatever the interval is.

## Builders

| Method | Default |
|---|---|
| `with_format(..)` | from the key's extension |
| `with_config(..)` | credentials from the environment |
| `from_client(..)` | builds its own client |

## Testing

The test suite drives **MinIO in a container** — the same API, offline and free.
That the crate works against MinIO at all is itself the assertion that matters
for everyone on Ceph, R2 or B2.

```sh
cargo test -p dynamic-config-s3    # needs a working Docker daemon
```

## MSRV

1.88 — higher than [`dynamic-config`]'s own 1.71, because the AWS SDK moves
faster than that crate wants to.

## License

MIT

[`dynamic-config`]: https://docs.rs/dynamic-config