# Cerova LabelCore — Flutter / native integration guide

A practical guide for consuming the Cerova LabelCore API from a native Flutter
app. The machine-readable contract is [`openapi.yaml`](openapi.yaml) — you can
codegen a typed Dart client from it (see [Codegen](#codegen)).

- **Base URL:** `https://api.cerova.io/v1`
- **Auth:** every request (except `/health`, `/stats`) sends header
  `x-api-key: <your key>`.
- **Status:** Alpha. Spanish content is **unofficial** — see [Compliance](#compliance-non-negotiable).

## 1. Minimal client

```dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class CerovaClient {
  CerovaClient({required this.apiKey,
      this.base = 'https://api.cerova.io/v1', http.Client? client})
      : _http = client ?? http.Client();
  final String apiKey, base;
  final http.Client _http;

  Map<String, String> _headers([String? etag]) => {
        'x-api-key': apiKey,
        'accept': 'application/json',
        if (etag != null) 'if-none-match': etag,
      };

  /// Returns (statusCode, jsonBody?, etag?). 304 → body is null (use your cache).
  Future<(int, dynamic, String?)> _get(String path, {String? etag}) async {
    final r = await _http.get(Uri.parse('$base$path'), headers: _headers(etag));
    final tag = r.headers['etag'];
    if (r.statusCode == 304) return (304, null, tag);
    final body = r.body.isEmpty ? null : jsonDecode(utf8.decode(r.bodyBytes));
    return (r.statusCode, body, tag);
  }

  Future<List<dynamic>> search(String q, {String type = 'name'}) async {
    final (_, body, __) = await _get('/search?q=${Uri.encodeQueryComponent(q)}&type=$type');
    return (body?['results'] as List?) ?? const [];
  }

  Future<Map<String, dynamic>?> leaflet(String id, {String lang = 'es', String? etag}) async {
    final (code, body, _) = await _get('/products/$id/leaflet?lang=$lang', etag: etag);
    if (code == 404) return null; // try lang='en' (always available)
    return body as Map<String, dynamic>?;
  }

  Future<Map<String, dynamic>?> manifest(String id) async =>
      (await _get('/products/$id/manifest')).$2 as Map<String, dynamic>?;
}
```

## 2. Language negotiation

Pass `?lang=en|es`. Spanish may not be published for every product — a
`404 {"error":..,"availableLangs":["en"]}` means "fall back to English":

```dart
final es = await client.leaflet(setId, lang: 'es');
final shown = es ?? await client.leaflet(setId, lang: 'en'); // EN is always present
```

## 3. Offline delta sync (the mobile pattern)

The `manifest` endpoint exists for exactly this: cache leaflets offline, and on
reconnect re-download **only** the sections whose content hash changed.

```dart
// Persisted locally: setId -> { loinc -> {hash, html} } per lang
Future<void> sync(String setId, String lang, LocalStore store) async {
  final m = await client.manifest(setId);
  if (m == null) return;
  for (final s in (m['sections'] as List)) {
    final loinc = s['loinc'] as String;
    final remoteHash = (s['hashes'] as Map)[lang] as String?;
    if (remoteHash == null) continue;                 // not published in this lang
    if (store.hash(setId, lang, loinc) == remoteHash) continue; // unchanged → skip
    // changed → fetch just this section
    final sec = await client._get('/products/$setId/sections/$loinc?lang=$lang');
    final section = (sec.$2 as Map)['section'];
    store.put(setId, lang, loinc, remoteHash, section['html'], section['title']);
  }
}
```

Result: first sync downloads everything; subsequent syncs transfer only changed
sections. Combine with `ETag`/`If-None-Match` (pass the stored `etag` to `_get`)
so even the manifest fetch is a cheap `304` when nothing changed.

## 4. Rendering section HTML

Section bodies are **pre-sanitised** HTML limited to `p, ul, ol, li, table, thead,
tbody, tr, td, th, caption, strong, em, u, sub, sup, br, a`. Use a hardened
renderer (e.g. `flutter_widget_from_html` or `flutter_html`). For accessibility,
wrap Spanish content with the `es` locale so TalkBack/VoiceOver pronounce it
correctly:

```dart
Semantics(
  attributedLabel: null,
  child: Localizations.override(
    context: context, locale: const Locale('es'),
    child: HtmlWidget(section['html'] as String),
  ),
);
```

Never execute scripts or load remote subresources from the HTML; it contains none
by design, but render defensively.

## 5. Compliance (non-negotiable)

For **Spanish** leaflets you MUST, in the UI:

1. Display the returned `disclaimer` prominently
   (`"Traducción no oficial. Fuente oficial: FDA/DailyMed."`).
2. Show a tappable link to `officialSource` (the authoritative FDA/DailyMed page).
3. Keep the **English** (official) version reachable (e.g. a language toggle).

Also surface provenance where appropriate: `leaflet.version`, `translatedAt`,
`engine`, and `official: false`. English leaflets are FDA-verbatim (`official: true`).

## 6. Caching & etiquette

- Store the `ETag` from each response; send it as `If-None-Match` next time → `304`.
- Respect `Cache-Control` (content is immutable per `version`).
- Handle `429` (rate limit) with backoff. Treat `5xx` as transient + retry.
- Keep the API key out of source control; inject at build/runtime.

## 7. Codegen

```bash
# Dart + dio client from the contract
openapi-generator generate -i docs/api/openapi.yaml -g dart-dio -o packages/cerova_api
```

The generated models map 1:1 to the schemas in `openapi.yaml`
(`Leaflet`, `Section`, `Manifest`, `ProductSummary`, `SearchResult`, `Stats`).

## 8. Quick smoke (curl)

```bash
KEY=<your key>
curl -H "x-api-key: $KEY" "https://api.cerova.io/v1/search?q=omeprazole&type=name"
curl -H "x-api-key: $KEY" "https://api.cerova.io/v1/products/6919399e-f112-4274-b4de-df0b4c391e63/leaflet?lang=es"
curl -H "x-api-key: $KEY" "https://api.cerova.io/v1/products/6919399e-f112-4274-b4de-df0b4c391e63/manifest"
```
