Compare commits

...

4 Commits

Author SHA1 Message Date
25b699da28 docs: remove challenge file
All checks were successful
Rust / check (push) Successful in 32s
Rust / test (push) Successful in 32s
Docker / build (push) Successful in 37s
2026-06-16 14:21:31 +02:00
17dc59699e docs: improve docs
All checks were successful
Docker / build (push) Successful in 22s
Rust / check (push) Successful in 32s
Rust / test (push) Successful in 33s
2026-06-16 14:11:25 +02:00
f367692287 ci: avoid qemu on local runners
All checks were successful
Rust / test (push) Successful in 1m2s
Rust / check (push) Successful in 1m23s
Docker / build (push) Successful in 2m4s
2026-06-16 13:59:34 +02:00
fe6244f51c feat: add challenge file
Some checks failed
Docker / build (push) Failing after 28s
Rust / test (push) Successful in 53s
Rust / check (push) Successful in 56s
2026-06-16 13:53:39 +02:00
3 changed files with 49 additions and 53 deletions

View File

@@ -19,7 +19,18 @@ jobs:
steps: steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Select Docker platforms
id: docker-platforms
shell: bash
run: |
if [ "${ACT:-}" = "true" ] || [ "${GITEA_ACTIONS:-}" = "true" ]; then
echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT"
else
echo "platforms=linux/amd64,linux/arm64" >> "$GITHUB_OUTPUT"
fi
- name: Set up QEMU - name: Set up QEMU
if: ${{ steps.docker-platforms.outputs.platforms != 'linux/amd64' }}
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4 uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -31,7 +42,7 @@ jobs:
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: ${{ steps.docker-platforms.outputs.platforms }}
push: false push: false
build-args: | build-args: |
GIT_SHA=${{ github.sha }} GIT_SHA=${{ github.sha }}

View File

@@ -68,32 +68,50 @@ Example response:
`/pokemon/translated/{name}` applies Yoda when the Pokémon is legendary or its habitat is `cave`; otherwise it applies Shakespeare. If translation fails or returns an empty result, the API falls back to the standard PokéAPI description. `/pokemon/translated/{name}` applies Yoda when the Pokémon is legendary or its habitat is `cave`; otherwise it applies Shakespeare. If translation fails or returns an empty result, the API falls back to the standard PokéAPI description.
## Architecture # Architecture
This is a small Rust service, so the structure is intentionally a lightweight hexagonal/onion layout rather than a framework-heavy one.
```mermaid ```mermaid
flowchart LR flowchart LR
CLI[CLI helper] --> HTTP[HTTP layer] CLI[CLI helper] --> HTTP
HTTP --> APP[Application service] HTTP[HTTP layer: axum routes, handlers, middleware] --> Application
APP --> DOMAIN[Domain] Application[Application service: use cases and translation rules] --> Domain
APP --> CLIENTS[External clients] Application --> Clients
HTTP --> OTEL[Telemetry] Clients[External clients: PokéAPI and FunTranslations]
APP --> OTEL HTTP --> Telemetry
Application --> Telemetry
``` ```
See `docs/architecture.md` for the design notes. ## Layers
- `domain`: core data and business concepts (`PokemonInfo`, `TranslationKind`).
- `application`: orchestration and business rules, including Yoda vs Shakespeare selection and fallback behavior.
- `clients`: adapters for external providers (`PokéAPI`, `FunTranslations`).
- `http`: API transport concerns, routing, handlers, request IDs, metrics middleware, and rate limiting.
- `telemetry`: OpenTelemetry metrics and tracing setup.
- `bin`: executable entrypoints for the API and the local CLI helper.
## Deliberate trade-offs
The application service currently depends on concrete clients instead of trait-based ports. For this challenge that keeps the code easier to read and avoids abstractions that do not buy much yet. In a larger enterprise codebase, those clients would likely become traits owned by the application layer, with HTTP clients as adapters, so providers could be swapped or mocked without depending on concrete implementations.
Rate limiting is intentionally simple and in-memory. In production, this should usually live in an API gateway or shared rate-limiting service so limits are consistent across instances and can be keyed by API key, user, or client IP. PokéAPI species data also changes rarely, so production deployments would add a bounded LRU cache with TTL for successful species responses to avoid unnecessary random upstream hits, reduce latency, and make transient provider failures less visible to users.
External contract tests are separated from the normal deterministic test suite. Unit and integration tests use mocks; the nightly workflow calls real providers to detect contract drift early.
## Configuration ## Configuration
| Variable | Default | Description | | Variable | Default | Description |
| --- | --- | --- | | --------------------------- | ------------------------------------------- | -------------------------- |
| `BIND_ADDR` | `0.0.0.0:5000` | HTTP bind address | | `BIND_ADDR` | `0.0.0.0:5000` | HTTP bind address |
| `POKEAPI_BASE_URL` | `https://pokeapi.co/api/v2` | PokéAPI base URL | | `POKEAPI_BASE_URL` | `https://pokeapi.co/api/v2` | PokéAPI base URL |
| `FUN_TRANSLATIONS_BASE_URL` | `https://api.funtranslations.mercxry.me/v1` | FunTranslations base URL | | `FUN_TRANSLATIONS_BASE_URL` | `https://api.funtranslations.mercxry.me/v1` | FunTranslations base URL |
| `REQUEST_TIMEOUT_SECONDS` | `5` | Upstream HTTP timeout | | `REQUEST_TIMEOUT_SECONDS` | `5` | Upstream HTTP timeout |
| `RATE_LIMIT_PER_SECOND` | `20` | Global token refill rate | | `RATE_LIMIT_PER_SECOND` | `20` | Global token refill rate |
| `RATE_LIMIT_BURST` | `40` | Global burst capacity | | `RATE_LIMIT_BURST` | `40` | Global burst capacity |
| `OTEL_SERVICE_NAME` | `pokedex-api` | OpenTelemetry service name | | `OTEL_SERVICE_NAME` | `pokedex-api` | OpenTelemetry service name |
| `RUST_LOG` | `pokedex_api=info,tower_http=info` | JSON tracing log filter | | `RUST_LOG` | `pokedex_api=info,tower_http=info` | JSON tracing log filter |
## Instrumentation ## Instrumentation
@@ -132,4 +150,4 @@ The Dockerfile uses `cargo-chef` and BuildKit cache mounts for dependency and ta
## Production notes ## Production notes
For a production API I would add per-client or per-token rate limiting instead of one global bucket, retries with bounded exponential backoff, circuit breakers for upstream failures, and a bounded LRU/TTL cache for PokéAPI data. Species content changes rarely, so caching successful responses would avoid unnecessary random upstream hits, reduce latency, and make transient provider failures less visible to users. I would also add stronger health checks that distinguish readiness from liveness, dashboards and alerts from the OTEL metrics, and a dedicated OTEL collector pipeline. I would keep `/metrics` private, add dependency/container scanning, define SLOs, and add contract tests against recorded upstream fixtures. For a production API I would add retries with bounded exponential backoff, circuit breakers for upstream failures, stronger health checks that distinguish readiness from liveness, dashboards and alerts from the OTEL metrics, and a dedicated OTEL collector pipeline. I would keep `/metrics` private, add dependency/container scanning, define SLOs, and add contract tests against recorded upstream fixtures.

View File

@@ -1,33 +0,0 @@
# Architecture
This is a small Rust service, so the structure is intentionally a lightweight hexagonal/onion layout rather than a framework-heavy one.
```mermaid
flowchart LR
CLI[CLI helper] --> HTTP
HTTP[HTTP layer: axum routes, handlers, middleware] --> Application
Application[Application service: use cases and translation rules] --> Domain
Application --> Clients
Clients[External clients: PokéAPI and FunTranslations]
HTTP --> Telemetry
Application --> Telemetry
```
## Layers
- `domain`: core data and business concepts (`PokemonInfo`, `TranslationKind`).
- `application`: orchestration and business rules, including Yoda vs Shakespeare selection and fallback behavior.
- `clients`: adapters for external providers (`PokéAPI`, `FunTranslations`).
- `http`: API transport concerns, routing, handlers, request IDs, metrics middleware, and rate limiting.
- `telemetry`: OpenTelemetry metrics and tracing setup.
- `bin`: executable entrypoints for the API and the local CLI helper.
## Deliberate trade-offs
The application service currently depends on concrete clients instead of trait-based ports. For this challenge that keeps the code easier to read and avoids abstractions that do not buy much yet. In a larger enterprise codebase, those clients would likely become traits owned by the application layer, with HTTP clients as adapters, so providers could be swapped or mocked without depending on concrete implementations.
PokéAPI species data changes rarely, so production deployments would add a bounded LRU cache with TTL for successful species responses. That would avoid unnecessary random upstream hits, reduce latency, and make transient provider failures less visible to users.
Rate limiting is intentionally simple and in-memory. In production, this should usually live in an API gateway or shared rate-limiting service so limits are consistent across instances and can be keyed by API key, user, or client IP.
External contract tests are separated from the normal deterministic test suite. Unit and integration tests use mocks; the nightly workflow calls real providers to detect contract drift early.