diff --git a/public/api-proxy.php b/public/api-proxy.php
index 06524ca..54027cf 100644
--- a/public/api-proxy.php
+++ b/public/api-proxy.php
@@ -2,13 +2,12 @@
declare(strict_types=1);
require_once __DIR__ . '/../src/Calco2latoApiClient.php';
-// --- Basic CORS (adjust origin to your site/domain) ---
+// --- CORS (restrict to your site) ---
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
-$allowedOrigin = preg_match('#^https://(www\.)?your-frontend\.example$#', $origin) ? $origin : '';
-if ($allowedOrigin) {
- header('Access-Control-Allow-Origin: ' . $allowedOrigin);
+if (preg_match('#^https://(www\.)?your-frontend\.example$#', $origin)) {
+ header('Access-Control-Allow-Origin: ' . $origin);
header('Vary: Origin');
- header('Access-Control-Allow-Credentials: true');
+ header('Access-Control-Allow-Credentials', 'true');
}
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
@@ -55,7 +54,9 @@ if (!$key) {
}
// --- Instantiate API client ---
-$client = new Calco2latoApiClient($base, $key);
+// Version from query (?ver=v1|test|latest), default latest
+$ver = $_GET['ver'] ?? $_POST['ver'] ?? Calco2latoApiVersion::LATEST;
+$client = new Calco2latoApiClient($base, $key, (string)$ver);
// --- Whitelist router ---
$input = json_decode(file_get_contents('php://input') ?: '[]', true) ?: [];
@@ -66,21 +67,32 @@ header('Content-Type: application/json; charset=utf-8');
try {
switch ($endpoint) {
- case 'airports.search':
- // GET /?endpoint=airports.search&q=FRA&per_page=10
- $q = $_GET['q'] ?? '';
- $per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 20;
- $page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
- $data = $client->searchAirports($q, $per_page, $page);
- echo json_encode($data);
+ case 'health':
+ echo json_encode(['ok'=>true,'ver'=>$ver]);
break;
+ case 'airports.search':
+ if ($method === 'GET') {
+ $q = [
+ 'page' => isset($_GET['page']) ? (int)$_GET['page'] : null,
+ 'per_page' => isset($_GET['per_page']) ? (int)$_GET['per_page'] : null,
+ 'sort_by' => $_GET['sort_by'] ?? null,
+ 'order' => $_GET['order'] ?? null,
+ 'iata' => $_GET['iata'] ?? null,
+ ];
+ echo json_encode($client->airports_get($q));
+ break;
+ } elseif ($method === 'POST') {
+ $params = $input['params'] ?? [];
+ echo json_encode($client->airports_post($params));
+ break;
+ }
+
case 'flights.estimate':
// POST with JSON body: { endpoint: "flights.estimate", params: {...} }
if ($method !== 'POST') throw new RuntimeException('Use POST');
$params = $input['params'] ?? [];
- $data = $client->flightEstimate($params);
- echo json_encode($data);
+ echo json_encode($client->flight_post($params));
break;
default:
diff --git a/public/example.html b/public/example.html
index bdc7382..fab6cbd 100644
--- a/public/example.html
+++ b/public/example.html
@@ -19,7 +19,7 @@
Search for airports
@@ -55,8 +55,8 @@
e.preventDefault();
list.innerHTML = 'Loading…';
try {
- const q = new FormData(searchform).get('q');
- const airports = await api.searchAirports(q, 10, 1);
+ const iata = new FormData(searchform).get('iata');
+ const airports = await api.searchAirports(iata, 10, 1);
list.innerHTML = airports.map(a => `
${a.display}`).join('');
} catch (err) {
list.innerHTML = `${err.message}`;
@@ -72,7 +72,7 @@
try {
const f = new FormData(flightform);
const flight = await api.estimateFlight({"flights": [{"departure": f.get('departure'), "arrival": f.get('arrival'), "passengerCount": 1, "travelClass": f.get('cabinclass'), "departureDate": f.get('departuredate')}]});
- flightresult.innerHTML = JSON.stringify(flight, null, 2);
+ flightresult.innerHTML = flight.summary();
} catch (err) {
flightresult.innerHTML = `${err.message}`;
}
diff --git a/public/js/calco2lato.js b/public/js/calco2lato.js
index 1a7d868..a2a60a2 100644
--- a/public/js/calco2lato.js
+++ b/public/js/calco2lato.js
@@ -17,7 +17,7 @@ export class Flight {
}
summary() {
const s = [];
- if (this.departureAirport?.iata) s.push(this.departureAirport.iata);
+ if (this.departureAirport?.iata) s.push(this.departureAirport.iata);
if (this.arrivalAirport?.iata) s.push(this.arrivalAirport.iata);
const route = s.length ? s.join(' → ') : 'Flight';
const co2 = this.emissions?.co2_total ?? this.co2 ?? null;
@@ -77,9 +77,9 @@ export class Calco2latoClient {
async searchAirports(q, limit = 20, offset = 1) {
const data = await this._fetchJSON({
method: 'GET',
- query: { endpoint: 'airports.search', q, limit, offset }
+ query: { endpoint: 'airports.search', iata: q, per_page: limit, page: offset }
});
- const items = Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : []);
+ const items = Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : []);
return items.map(a => new Airport(a));
}
@@ -94,6 +94,6 @@ export class Calco2latoClient {
method: 'POST',
body: { endpoint: 'flights.estimate', params }
});
- return new Flight(data);
+ return new Flight(data.flights[0]);
}
}
diff --git a/src/Calco2latoApiClient.php b/src/Calco2latoApiClient.php
index c60bf23..cf87382 100644
--- a/src/Calco2latoApiClient.php
+++ b/src/Calco2latoApiClient.php
@@ -1,27 +1,56 @@
baseUrl = rtrim($baseUrl, '/');
$this->apiKey = $apiKey;
+ $this->version = Calco2latoApiVersion::ensure($version);
$this->timeout = $timeoutSeconds;
}
- /**
- * Low-level request wrapper (GET/POST/DELETE/PUT/PATCH).
- * $query is appended to URL; $body (if provided) is JSON-encoded.
- */
- private function request(string $method, string $path, array $query = [], ?array $body = null): array
+ public function withVersion(string $version): self
{
- $url = $this->baseUrl . '/' . ltrim($path, '/');
- if (!empty($query)) {
+ $clone = clone $this;
+ $clone->version = Calco2latoApiVersion::ensure($version);
+ return $clone;
+ }
+
+ private function url(string $path): string
+ {
+ $path = '/' . ltrim($path, '/');
+ return "{$this->baseUrl}/{$this->version}{$path}";
+ }
+
+ /** @return array */
+ private function request(string $method, string $path, array $query = [], ?array $json = null): array
+ {
+ $url = $this->url($path);
+ if ($query) {
$url .= (str_contains($url, '?') ? '&' : '?') . http_build_query($query);
}
@@ -30,62 +59,73 @@ final class Calco2latoApiClient
'Accept: application/json',
'Authorization: Bearer ' . $this->apiKey,
];
- curl_setopt_array($ch, [
+
+ $opts = [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => strtoupper($method),
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => $this->timeout,
- ]);
+ ];
- if ($body !== null) {
- $json = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ if ($json !== null) {
+ $payload = json_encode($json, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$headers[] = 'Content-Type: application/json';
- curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
+ $opts[CURLOPT_HTTPHEADER] = $headers;
+ $opts[CURLOPT_POSTFIELDS] = $payload;
}
- $responseBody = curl_exec($ch);
- $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
- $err = curl_error($ch);
+ curl_setopt_array($ch, $opts);
+ $raw = curl_exec($ch);
+ $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $err = curl_error($ch);
curl_close($ch);
- if ($responseBody === false) {
+ if ($raw === false) {
throw new RuntimeException('cURL error: ' . $err);
}
- $decoded = json_decode($responseBody, true);
+ $decoded = json_decode($raw, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
- throw new RuntimeException('Invalid JSON from API (HTTP ' . $status . '): ' . $responseBody);
+ throw new RuntimeException("Invalid JSON from API (HTTP {$status}): {$raw}");
}
-
if ($status < 200 || $status >= 300) {
- $msg = $decoded['error'] ?? $decoded['message'] ?? 'Upstream API error';
- throw new RuntimeException('API error (HTTP ' . $status . '): ' . $msg);
+ $msg = is_array($decoded) ? ($decoded['error'] ?? $decoded['message'] ?? 'Upstream API error') : 'Upstream API error';
+ throw new RuntimeException("API error (HTTP {$status}): {$msg}");
}
+ /** @var array $decoded */
return $decoded;
}
- // ---------- Airports ----------
+ // ===================== Airports =====================
- /** Search airports by free-text (IATA/ICAO/name/city/country) */
- public function searchAirports(string $q, int $per_page = 20, int $page = 0): array
+ /** GET /{v}/airports — query: page, per_page, sort_by, order, iata */
+ public function airports_get(array $query = []): array
{
- return $this->request('GET', '/latest/transport/airports', [
- 'iata' => $q,
- 'per_page' => $per_page,
- 'page' => $page,
- ]);
+ // Only pass the fields your spec declares (extra keys are harmless but we keep it tidy)
+ $allowed = ['page','per_page','sort_by','order','iata'];
+ $q = [];
+ foreach ($allowed as $k) {
+ if (array_key_exists($k, $query) && $query[$k] !== null) {
+ $q[$k] = $query[$k];
+ }
+ }
+ return $this->request('GET', '/transport/airports', $q, null);
}
- // ---------- Flights ----------
-
- /**
- * Get flight estimate / emissions (example – adjust to your API)
- * $params might include origin, destination, date, pax, class, etc.
- */
- public function flightEstimate(array $params): array
+ /** POST /{v}/airports — body: AirportsRequest */
+ public function airports_post(array $airportsRequest = []): array
{
- return $this->request('POST', '/latest/transport/flight', [], $params);
+ // Accepts the exact schema: page, per_page, sort_by, order, iata
+ return $this->request('POST', '/transport/airports', [], $airportsRequest);
}
-}
+
+ // ===================== Flight =====================
+
+ /** POST /{v}/flight — body: FlightRequest */
+ public function flight_post(array $flightRequest): array
+ {
+ // The spec requires "flights" in the body; api_key could be included, but we already send Bearer
+ return $this->request('POST', '/transport/flight', [], $flightRequest);
+ }
+}
\ No newline at end of file