diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..e7a2ffb --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,52 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate JSON syntax + run: find . -name "*.json" -type f -print0 | xargs -0 -I{} jq empty {} + + - name: Generate manifest.json + run: bash scripts/generate-manifest.sh + + - name: Show generated manifest + run: cat v1/manifest.json + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: . + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/json-validator.yml b/.github/workflows/json-validator.yml new file mode 100644 index 0000000..3c49da2 --- /dev/null +++ b/.github/workflows/json-validator.yml @@ -0,0 +1,14 @@ +name: JSON Lint +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Validate JSON syntax + run: | + echo "Vérification des fichiers JSON..." + find . -name "*.json" -type f -print0 | xargs -0 -I{} jq empty {} + echo "Tous les fichiers JSON sont valides !" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31dd5d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +v1/manifest.json diff --git a/README.md b/README.md index 07f4b64..3086771 100644 --- a/README.md +++ b/README.md @@ -1 +1,240 @@ # HelpAbroadAPI + +[![JSON Validation](https://github.com/HeavyStudio/HelpAbroadAPI/actions/workflows/json-validator.yml/badge.svg)](https://github.com/HeavyStudio/HelpAbroadAPI/actions) +[![API Version](https://img.shields.io/badge/API-v1-blue.svg)](./v1/) +[![Pages](https://img.shields.io/badge/hosted%20on-GitHub%20Pages-181717.svg)](https://heavystudio.github.io/HelpAbroadAPI/) + +This repository serves as the single source of truth (SSOT) for the **HelpAbroad** application ecosystem. It contains curated, high-availability, offline-first datasets encompassing international emergency dispatch numbers and UI localization packages. + +The primary objective of this architecture is to provide **zero-network overhead dependency structures** for native mobile clients, allowing seamless deserialization and full offline reliability in critical situations. + +## Base URL + +All endpoints are served as static JSON via GitHub Pages: + +``` +https://heavystudio.github.io/HelpAbroadAPI/ +``` + +The `v1/` namespace is **immutable**: any breaking change will be released under a new version prefix (`v2/`, `v3/`, ...) so existing clients never break. + +## Endpoints + +| Resource | Path | Description | +| --------------------- | ----------------------------- | ------------------------------------------------ | +| Countries registry | `v1/global_countries.json` | Emergency contacts and metadata for every country | +| Locale (English) | `v1/locales/en.json` | Source language / fallback | +| Locale (French) | `v1/locales/fr.json` | French translation matrix | +| Locale (German) | `v1/locales/de.json` | German translation matrix | +| Locale (Spanish) | `v1/locales/es.json` | Spanish translation matrix | +| Locale (Italian) | `v1/locales/it.json` | Italian translation matrix | + +## Manifest & Caching + +`v1/manifest.json` is the entry point for clients that want to avoid re-downloading unchanged data. Fetch it first, compare against your local cache, and only download resources whose `sha256` (or `updated_at`) has changed. + +```json +{ + "api_version": "1.0.0", + "generated_at": "2026-05-23T13:27:00Z", + "resources": { + "global_countries.json": { + "updated_at": "2026-05-23T12:26:00Z", + "sha256": "" + }, + "locales/en.json": { "updated_at": "…", "sha256": "…" }, + "locales/fr.json": { "updated_at": "…", "sha256": "…" } + } +} +``` + +**Recommended client flow:** + +1. `GET v1/manifest.json` (small, fast). +2. For each entry in `resources`, compare `sha256` against the cached value. +3. Only `GET` resources whose checksum changed. +4. Persist the new manifest atomically once all downloads succeed. + +`api_version` follows semver: a major bump (`2.0.0`) means breaking schema changes and will only ever ship under a new `vN/` prefix. Minor / patch bumps remain backward-compatible within `v1/`. + +> **Tip:** GitHub Pages also serves `ETag` and `Last-Modified` headers, so you can additionally send `If-None-Match` on each resource request to receive a `304 Not Modified` and skip the body entirely. + +## Usage + +### Kotlin Multiplatform (Ktor + kotlinx.serialization) + +The primary consumer of this API is the HelpAbroad KMP client. Add the dependencies to your shared module: + +```kotlin +// build.gradle.kts (commonMain) +dependencies { + implementation("io.ktor:ktor-client-core:") + implementation("io.ktor:ktor-client-content-negotiation:") + implementation("io.ktor:ktor-serialization-kotlinx-json:") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:") +} +``` + +Define the data models: + +```kotlin +@Serializable +data class Country( + val id: String, + @SerialName("iso_3") val iso3: String, + val prefix: String, + val continent: String, + @SerialName("member_112") val member112: Boolean, + val emergency: Emergency, + val geo: Geo, +) + +@Serializable +data class Emergency( + val police: List, + val medical: List, + val firefighters: List = http.get("$BASE/global_countries.json").body() + + suspend fun locale(lang: String): Locale = http.get("$BASE/locales/$lang.json").body() +} +``` + +> **Tip:** because the dataset is offline-first, cache the JSON locally on first fetch and refresh in the background. The `v1/` namespace is immutable, so cached payloads stay schema-compatible. + +### cURL + +```sh +curl -s https://heavystudio.github.io/HelpAbroadAPI/v1/global_countries.json | jq '.FR' +curl -s https://heavystudio.github.io/HelpAbroadAPI/v1/locales/fr.json +``` + +### JavaScript / TypeScript + +```ts +const BASE = "https://heavystudio.github.io/HelpAbroadAPI/v1"; + +const [countries, locale] = await Promise.all([ + fetch(`${BASE}/global_countries.json`).then(r => r.json()), + fetch(`${BASE}/locales/fr.json`).then(r => r.json()), +]); + +console.log(locale.police, countries.FR.emergency.police); +``` + +### Swift (iOS) + +```swift +let url = URL(string: "https://heavystudio.github.io/HelpAbroadAPI/v1/global_countries.json")! +let (data, _) = try await URLSession.shared.data(from: url) +let countries = try JSONDecoder().decode([String: Country].self, from: data) +``` + +## Data Schemas + +### `global_countries.json` + +Keyed by ISO 3166-1 alpha-2 country code: + +```json +{ + "FR": { + "id": "FR", + "iso_3": "FRA", + "prefix": "+33", + "continent": "europe", + "member_112": true, + "emergency": { + "police": ["17"], + "medical": ["15"], + "firefighters": ["18"] + }, + "geo": { + "latitude": 46.2276, + "longitude": 2.2137 + } + } +} +``` + +| Field | Type | Notes | +| -------------- | ---------------- | ---------------------------------------------------- | +| `id` | string | ISO 3166-1 alpha-2 | +| `iso_3` | string | ISO 3166-1 alpha-3 | +| `prefix` | string | International dialing prefix | +| `continent` | string | `africa` \| `americas` \| `asia` \| `europe` \| `oceania` | +| `member_112` | boolean | Whether 112 is a recognized emergency number | +| `emergency.*` | array of strings | Empty array = data not yet curated | +| `geo` | object | Approximate country centroid | + +### `locales/{lang}.json` + +Flat key/value dictionary of UI strings. `en.json` is the source of truth — every other locale mirrors its keys. + +```json +{ + "police": "Police", + "medical": "Medical Emergencies", + "firefighters": "Firefighters", + "dispatch": "General Emergency" +} +``` + +## Repository Architecture + +```text +.github/ +└── workflows/ + └── json-validator.yml # GitHub Actions CI syntax linter +v1/ # API Namespace Version 1 (Immutable) +├── locales/ # Client-side UI string localizations +│ ├── de.json +│ ├── en.json # Source / Fallback language +│ ├── es.json +│ ├── fr.json +│ └── it.json +└── global_countries.json # Global emergency contacts registry +README.md +``` + +## Versioning Policy + +- `v1/` is **frozen** in terms of structure. Additive, backward-compatible changes (new countries, new locale keys, filled-in `emergency` arrays) are allowed. +- Any breaking change (field rename, type change, removed key) ships under a new `vN/` prefix. + +## Contributing + +PRs are welcome — especially to fill in missing emergency numbers. All JSON files must pass the CI syntax check (`jq empty`). For locale changes, mirror the full key set of `v1/locales/en.json`. + +## License + +TBD. \ No newline at end of file diff --git a/scripts/generate-manifest.sh b/scripts/generate-manifest.sh new file mode 100755 index 0000000..643f24d --- /dev/null +++ b/scripts/generate-manifest.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +API_VERSION="1.0.0" +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +MANIFEST_PATH="$ROOT_DIR/v1/manifest.json" +GENERATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + +cd "$ROOT_DIR" + +resources='{}' +while IFS= read -r -d '' file; do + rel="${file#v1/}" + [[ "$rel" == "manifest.json" ]] && continue + + sha="$(sha256sum "$file" | cut -d' ' -f1)" + + updated_at="$(git log -1 --format=%cI -- "$file" 2>/dev/null || true)" + [[ -z "$updated_at" ]] && updated_at="$GENERATED_AT" + + resources="$(jq \ + --arg path "$rel" \ + --arg sha "$sha" \ + --arg ts "$updated_at" \ + '. + {($path): {updated_at: $ts, sha256: $sha}}' <<<"$resources")" +done < <(find v1 -name "*.json" -type f -print0 | sort -z) + +jq -n \ + --arg version "$API_VERSION" \ + --arg generated "$GENERATED_AT" \ + --argjson resources "$resources" \ + '{api_version: $version, generated_at: $generated, resources: $resources}' \ + >"$MANIFEST_PATH" + +echo "Wrote $MANIFEST_PATH" diff --git a/global_countries.json b/v1/global_countries.json similarity index 99% rename from global_countries.json rename to v1/global_countries.json index edcf28f..2e2c69d 100644 --- a/global_countries.json +++ b/v1/global_countries.json @@ -1,5 +1,4 @@ { - "// === AFRICA ===": "------------------------------------------------", "AO": { "id": "AO", "iso_3": "AGO", @@ -864,7 +863,6 @@ "longitude": 29.1549 } }, - "// === AMERICAS ===": "------------------------------------------------", "AG": { "id": "AG", "iso_3": "ATG", @@ -1427,7 +1425,6 @@ "longitude": -66.5897 } }, - "// === ASIA ===": "------------------------------------------------", "AF": { "id": "AF", "iso_3": "AFG", @@ -2068,7 +2065,6 @@ "longitude": 48.5164 } }, - "// === EUROPE ===": "------------------------------------------------", "AL": { "id": "AL", "iso_3": "ALB", @@ -2325,15 +2321,6 @@ ], "firefighters": [ "18" - ], - "beaten_children": [ - "119" - ], - "deaf": [ - "114" - ], - "missing_children": [ - "116000" ] }, "geo": { @@ -2888,7 +2875,6 @@ "longitude": 12.4534 } }, - "// === OCEANIA ===": "------------------------------------------------", "AU": { "id": "AU", "iso_3": "AUS", diff --git a/locales/de.json b/v1/locales/de.json similarity index 74% rename from locales/de.json rename to v1/locales/de.json index ad17940..18c82be 100644 --- a/locales/de.json +++ b/v1/locales/de.json @@ -1,6 +1,6 @@ { "police": "Polizei", - "medical": "Rettugsdienst", + "medical": "Rettungsdienst", "firefighters": "Feuerwehr", "dispatch": "Allgemeiner Notruf" } \ No newline at end of file diff --git a/locales/en.json b/v1/locales/en.json similarity index 100% rename from locales/en.json rename to v1/locales/en.json diff --git a/locales/es.json b/v1/locales/es.json similarity index 100% rename from locales/es.json rename to v1/locales/es.json diff --git a/locales/fr.json b/v1/locales/fr.json similarity index 100% rename from locales/fr.json rename to v1/locales/fr.json diff --git a/locales/it.json b/v1/locales/it.json similarity index 100% rename from locales/it.json rename to v1/locales/it.json