feat: v0.5.0 — machine-readable API docs: GET /api/version + OpenAPI 3.1 spec at /api/openapi; AGENTS.md points agents at the live spec
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import pkg from '@/package.json';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { openApiSpec } from './openapi';
|
||||
|
||||
describe('OpenAPI spec', () => {
|
||||
it('documents exactly the two public endpoints', () => {
|
||||
expect(Object.keys(openApiSpec.paths).sort()).toEqual(['/api/airports', '/api/version']);
|
||||
});
|
||||
|
||||
it('keeps info.version in sync with package.json', () => {
|
||||
expect(openApiSpec.info.title).toBe('Flight Ops API');
|
||||
expect(openApiSpec.info.version).toBe(pkg.version);
|
||||
});
|
||||
|
||||
it('uses an empty server url so "try it" targets the current origin', () => {
|
||||
expect(openApiSpec.servers).toEqual([{ url: '' }]);
|
||||
});
|
||||
|
||||
it('describes /api/airports as a GeoJSON FeatureCollection of airport features', () => {
|
||||
const schema = openApiSpec.paths['/api/airports'].get.responses[200].content['application/json'].schema;
|
||||
expect(schema.$ref).toBe('#/components/schemas/AirportFeatureCollection');
|
||||
|
||||
const fc = openApiSpec.components.schemas.AirportFeatureCollection.properties;
|
||||
expect(fc.features.items.$ref).toBe('#/components/schemas/AirportFeature');
|
||||
|
||||
const feature = openApiSpec.components.schemas.AirportFeature.properties;
|
||||
expect(feature.geometry.$ref).toBe('#/components/schemas/PointGeometry');
|
||||
expect(feature.properties.$ref).toBe('#/components/schemas/AirportProperties');
|
||||
|
||||
const props = openApiSpec.components.schemas.AirportProperties.properties;
|
||||
expect(Object.keys(props).sort()).toEqual(['icao', 'lat', 'lon', 'name']);
|
||||
expect(props.name.type).toBe('string');
|
||||
expect(props.icao.type).toBe('string');
|
||||
expect(props.lat.type).toBe('number');
|
||||
expect(props.lon.type).toBe('number');
|
||||
|
||||
const coords = openApiSpec.components.schemas.PointGeometry.properties.coordinates;
|
||||
expect(coords.items.type).toBe('number');
|
||||
});
|
||||
|
||||
it('describes /api/version as { version } with a string version', () => {
|
||||
const schema = openApiSpec.paths['/api/version'].get.responses[200].content['application/json'].schema;
|
||||
expect(schema.$ref).toBe('#/components/schemas/AppVersion');
|
||||
|
||||
const props = openApiSpec.components.schemas.AppVersion.properties;
|
||||
expect(Object.keys(props)).toEqual(['version']);
|
||||
expect(props.version.type).toBe('string');
|
||||
});
|
||||
});
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import pkg from '@/package.json';
|
||||
|
||||
// OpenAPI 3.1 document for the app's public JSON API, kept in code so it can be
|
||||
// served machine-readable at /api/openapi (see AGENTS.md "## API"). Must be updated
|
||||
// whenever an endpoint changes.
|
||||
export const openApiSpec = {
|
||||
openapi: '3.1.0',
|
||||
info: {
|
||||
title: 'Flight Ops API',
|
||||
description:
|
||||
'JSON API for Flight Ops — EU airport data for the map, plus app metadata.',
|
||||
// Sourced from package.json so it stays in sync with every version bump.
|
||||
version: pkg.version,
|
||||
},
|
||||
// Empty url = current origin (the spec is served from the same host that runs the API).
|
||||
servers: [{ url: '' }],
|
||||
paths: {
|
||||
'/api/airports': {
|
||||
get: {
|
||||
summary: 'List EU airports',
|
||||
description:
|
||||
'All fixed-wing EU airports (large, medium and seaplane bases) as a GeoJSON FeatureCollection.',
|
||||
operationId: 'listAirports',
|
||||
responses: {
|
||||
200: {
|
||||
description: 'GeoJSON FeatureCollection of EU airports',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/AirportFeatureCollection' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/version': {
|
||||
get: {
|
||||
summary: 'App version',
|
||||
description: 'The running app version, read from package.json.',
|
||||
operationId: 'getVersion',
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Current app version',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/AppVersion' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
AppVersion: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
version: { type: 'string', description: 'Semver of the running build' },
|
||||
},
|
||||
required: ['version'],
|
||||
},
|
||||
AirportFeatureCollection: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { const: 'FeatureCollection' },
|
||||
features: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/AirportFeature' },
|
||||
},
|
||||
},
|
||||
required: ['type', 'features'],
|
||||
},
|
||||
AirportFeature: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { const: 'Feature' },
|
||||
geometry: { $ref: '#/components/schemas/PointGeometry' },
|
||||
properties: { $ref: '#/components/schemas/AirportProperties' },
|
||||
},
|
||||
required: ['type', 'geometry', 'properties'],
|
||||
},
|
||||
PointGeometry: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: { const: 'Point' },
|
||||
coordinates: {
|
||||
type: 'array',
|
||||
description: '[longitude, latitude]',
|
||||
items: { type: 'number' },
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
},
|
||||
},
|
||||
required: ['type', 'coordinates'],
|
||||
},
|
||||
AirportProperties: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
icao: { type: 'string' },
|
||||
lat: { type: 'number' },
|
||||
lon: { type: 'number' },
|
||||
},
|
||||
required: ['name', 'icao', 'lat', 'lon'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user