Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# FixerProvider
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`FixerProvider` is the [Fixer.io](https://fixer.io/) sibling provider for
|
||||
the slim `@suaveplan/currency-providers` core. It implements the
|
||||
`CurrencyProvider` contract from `@suaveplan/currency`, fetches the
|
||||
latest fiat exchange rates from the Fixer `/latest` REST endpoint, and
|
||||
maps the upstream JSON envelope into the canonical `RateFetchResult`
|
||||
shape the converter expects.
|
||||
|
||||
Fixer.io requires an API key on every request. Free-tier accounts are
|
||||
restricted to `base = "EUR"` and a fixed monthly request quota; paid plans
|
||||
unlock arbitrary base currencies and higher throughput. The provider
|
||||
surfaces the free-tier restriction as a typed `UNSUPPORTED_BASE` error so
|
||||
consumers can dispatch on the discriminator rather than parsing the
|
||||
upstream message string.
|
||||
|
||||
## Features
|
||||
|
||||
- Implements `CurrencyProvider` with stable identity flags
|
||||
(`name = "fixer"`, `supportsFiat = true`, `supportsCrypto = false`).
|
||||
- Constructor takes the slim core's shared `ApiKeyConfig` (`apiKey`,
|
||||
optional `baseUrl`, optional injectable `fetch`). An empty `apiKey`
|
||||
raises `CURRENCY_PROVIDER.MISSING_CONFIG` synchronously before any
|
||||
network call is issued, preventing wasted quota.
|
||||
- Delegates HTTP transport to the slim core's `httpRequest` helper —
|
||||
`AbortSignal` composition, non-2xx → typed `HTTP_ERROR` mapping, and
|
||||
`providerName` / `status` carry on the raised error.
|
||||
- Maps the upstream `{ success: false, error: { code, info } }` envelope
|
||||
to typed codes: `105` → `UNSUPPORTED_BASE`, `201` / `202` →
|
||||
`UNKNOWN_CURRENCY`, any other code → `UPSTREAM_ERROR`. The upstream
|
||||
`error.info` is preserved on `upstreamMessage` so diagnostics survive.
|
||||
- Distinguishes parse failures (non-JSON, non-object JSON, success
|
||||
envelope missing required fields) under a single `PARSE_ERROR` code.
|
||||
- Universal — no `node:` / `bun:` / DOM imports; runs unchanged
|
||||
under Node 18+, Bun, modern browsers, and Web Workers.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { FixerProvider } from "@suaveplan/currency-provider-fixer";
|
||||
|
||||
const provider = new FixerProvider({ apiKey: process.env.FIXER_KEY! });
|
||||
|
||||
// Free-tier: base is implicitly EUR.
|
||||
const result = await provider.fetchRates();
|
||||
console.log(result.base, result.rates.GBP, result.timestamp);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Wire the provider through the slim core's `CurrencyProviderRegistry` and
|
||||
use it as the primary source for a `CurrencyConverter`. The injected
|
||||
`FetchLike` makes the provider trivial to unit-test against a fixture
|
||||
fetch closure:
|
||||
|
||||
```ts
|
||||
import { CurrencyConverter } from "@suaveplan/currency";
|
||||
import { CurrencyProviderRegistry } from "@suaveplan/currency-providers";
|
||||
import { FixerProvider } from "@suaveplan/currency-provider-fixer";
|
||||
|
||||
const registry = new CurrencyProviderRegistry();
|
||||
registry.register(
|
||||
new FixerProvider({
|
||||
apiKey: process.env.FIXER_KEY!,
|
||||
// Optional: proxy through your own gateway for caching / auditing.
|
||||
baseUrl: "https://rates.internal/fixer/api",
|
||||
// Optional: inject a wrapped fetch for retries or logging.
|
||||
fetch: async (url, init) => {
|
||||
const response = await globalThis.fetch(url, init);
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const converter = new CurrencyConverter({
|
||||
provider: registry.get("fixer"),
|
||||
staleDurationMs: 3_600_000,
|
||||
});
|
||||
|
||||
// Free-tier accounts MUST keep base = "EUR" — supplying any other base
|
||||
// raises CurrencyProviderError(UNSUPPORTED_BASE) via the typed mapping
|
||||
// from upstream error code 105.
|
||||
const rate = await converter.getRate("EUR", "GBP");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class FixerProvider implements CurrencyProvider`
|
||||
|
||||
- `readonly name = "fixer"`
|
||||
- `readonly supportsCrypto = false`
|
||||
- `readonly supportsFiat = true`
|
||||
|
||||
#### `constructor(config: ApiKeyConfig)`
|
||||
|
||||
Validates `config.apiKey` is non-empty (throws
|
||||
`CurrencyProviderError(MISSING_CONFIG)` otherwise) and caches
|
||||
`config.baseUrl ?? "https://data.fixer.io/api"` and
|
||||
`config.fetch ?? globalThis.fetch`.
|
||||
|
||||
**Throws**: `CurrencyProviderError(MISSING_CONFIG)` when `apiKey === ""`.
|
||||
|
||||
#### `fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>`
|
||||
|
||||
Builds `?access_key=…&base=…[&symbols=…]`, delegates to the slim core's
|
||||
`httpRequest`, parses the JSON envelope, and returns the canonical
|
||||
`RateFetchResult` with `timestamp` converted from Unix seconds to
|
||||
milliseconds.
|
||||
|
||||
**Parameters** (all optional): `base` (default `"EUR"`), `targets`
|
||||
(array of currency codes — joined into `symbols`), `signal`
|
||||
(`AbortSignal` forwarded to the underlying fetch).
|
||||
|
||||
**Returns**: `{ base, rates, timestamp }` where `timestamp` is
|
||||
milliseconds since epoch.
|
||||
|
||||
**Throws** (all `CurrencyProviderError`):
|
||||
|
||||
- `MISSING_CONFIG` — at construction when `apiKey` is empty.
|
||||
- `HTTP_ERROR` — non-2xx upstream response (via `httpRequest`).
|
||||
- `PARSE_ERROR` — body is not JSON, parsed value is not an object, or
|
||||
a `success: true` envelope is missing `base` / `rates` / `timestamp`.
|
||||
- `UNSUPPORTED_BASE` — upstream code `105` (free-tier base restriction).
|
||||
- `UNKNOWN_CURRENCY` — upstream code `201` (`invalid_base_currency`) or
|
||||
`202` (`invalid_currency_codes`).
|
||||
- `UPSTREAM_ERROR` — any other `success: false` envelope, including the
|
||||
case where `error` is absent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Fixer.io returns HTTP 200 even for application-level errors; the
|
||||
provider must therefore parse every body and discriminate on
|
||||
`success`. The non-2xx path (`HTTP_ERROR`) is reserved for genuine
|
||||
network / gateway failures.
|
||||
- The upstream `error.code` integer is the discriminator, not
|
||||
`error.type` — the integer is one comparison and avoids string
|
||||
allocation. The human-readable `error.info` is preserved on
|
||||
`upstreamMessage` for diagnostics.
|
||||
- The `apiKey` non-empty guard at construction is suaveplan-native — the
|
||||
upstream skeleton allowed an empty string and let the API reject on the
|
||||
wire, wasting the request against the monthly quota.
|
||||
- `fetchRates` forwards `options.signal` through `httpRequest` so the
|
||||
caller can cancel an in-flight request from outside the provider; this
|
||||
too is suaveplan-native (upstream did not propagate the signal).
|
||||
- The package is universal and free of side effects (`sideEffects:
|
||||
false`). Importing the package never mutates any global registry — the
|
||||
caller decides whether to register the instance with
|
||||
`CurrencyProviderRegistry`.
|
||||
Reference in New Issue
Block a user