api: GET /api/system + POST /api/explore, OpenAPI spec updated in code
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { getGameDb } from '@/lib/db/client'
|
||||||
|
import { ExploreError, explorePlanet } from '@/lib/game/save'
|
||||||
|
|
||||||
|
// Scan one body of the current system. Body: { "planetId": number }. Returns the fresh
|
||||||
|
// system state plus what this scan found. Costs a turn; each body can only be scanned once.
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
let body: unknown
|
||||||
|
try {
|
||||||
|
body = await req.json()
|
||||||
|
} catch {
|
||||||
|
return Response.json({ error: 'Invalid JSON body' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const planetId = (body as { planetId?: unknown }).planetId
|
||||||
|
if (typeof planetId !== 'number' || !Number.isInteger(planetId)) {
|
||||||
|
return Response.json({ error: 'planetId must be an integer' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await getGameDb()
|
||||||
|
return Response.json(await explorePlanet(db, planetId))
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ExploreError) {
|
||||||
|
return Response.json({ error: err.message }, { status: err.status })
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { getGameDb } from '@/lib/db/client'
|
||||||
|
import { getSystemState } from '@/lib/game/save'
|
||||||
|
|
||||||
|
// The current system's bodies (planets + moons) and the save's inventory; auto-initializes on first hit.
|
||||||
|
export async function GET() {
|
||||||
|
const db = await getGameDb()
|
||||||
|
return Response.json(await getSystemState(db))
|
||||||
|
}
|
||||||
+22
-1
@@ -4,7 +4,28 @@ import { openApiSpec } from './openapi';
|
|||||||
|
|
||||||
describe('OpenAPI spec', () => {
|
describe('OpenAPI spec', () => {
|
||||||
it('documents every public endpoint', () => {
|
it('documents every public endpoint', () => {
|
||||||
expect(Object.keys(openApiSpec.paths).sort()).toEqual(['/api/jump', '/api/version', '/api/world']);
|
expect(Object.keys(openApiSpec.paths).sort()).toEqual([
|
||||||
|
'/api/explore',
|
||||||
|
'/api/jump',
|
||||||
|
'/api/system',
|
||||||
|
'/api/version',
|
||||||
|
'/api/world',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('describes the system endpoints as SystemState in and out', () => {
|
||||||
|
const system = openApiSpec.paths['/api/system'].get.responses[200].content['application/json'].schema;
|
||||||
|
expect(system.$ref).toBe('#/components/schemas/SystemState');
|
||||||
|
|
||||||
|
const explore = openApiSpec.paths['/api/explore'].post;
|
||||||
|
expect(explore.requestBody.content['application/json'].schema.$ref).toBe('#/components/schemas/ExploreRequest');
|
||||||
|
expect(explore.responses[200].content['application/json'].schema.$ref).toBe('#/components/schemas/ExploreResult');
|
||||||
|
// ExploreResult extends SystemState with the scan outcome.
|
||||||
|
const exploreResult = openApiSpec.components.schemas.ExploreResult as { allOf: { $ref?: string }[] };
|
||||||
|
expect(exploreResult.allOf[0].$ref).toBe('#/components/schemas/SystemState');
|
||||||
|
for (const status of ['400', '404', '409'] as const) {
|
||||||
|
expect(explore.responses[status].content['application/json'].schema.$ref).toBe('#/components/schemas/Error');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('describes the world endpoints as WorldState in and out', () => {
|
it('describes the world endpoints as WorldState in and out', () => {
|
||||||
|
|||||||
+136
@@ -100,6 +100,74 @@ export const openApiSpec = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'/api/system': {
|
||||||
|
get: {
|
||||||
|
summary: 'Current system state',
|
||||||
|
description:
|
||||||
|
'The bodies of the current solar system — planets and their moons (nested via parent) with hazard/explored flags — plus the save inventory and turn counter. Auto-initializes a new game when no save exists yet.',
|
||||||
|
operationId: 'getSystem',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Current system state',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/SystemState' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/explore': {
|
||||||
|
post: {
|
||||||
|
summary: 'Explore a planet or moon',
|
||||||
|
description:
|
||||||
|
'Scans one body of the save with a deterministic haul (a resource and/or a part, or nothing), marks it explored, increments the turn counter, accumulates finds into inventory and writes an explore event. Returns the fresh system state plus this scan outcome.',
|
||||||
|
operationId: 'explorePlanet',
|
||||||
|
requestBody: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/ExploreRequest' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Fresh system state plus the scan outcome',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/ExploreResult' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Malformed body or non-integer planetId',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: 'Unknown planet id',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
409: {
|
||||||
|
description: 'Body was already explored (each can only be scanned once)',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
@@ -147,6 +215,74 @@ export const openApiSpec = {
|
|||||||
},
|
},
|
||||||
required: ['systemId'],
|
required: ['systemId'],
|
||||||
},
|
},
|
||||||
|
Planet: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'integer' },
|
||||||
|
name: { type: 'string' },
|
||||||
|
type: { type: 'string', description: 'Body class, e.g. TERRESTRIAL or GAS GIANT' },
|
||||||
|
hazard: { type: 'boolean' },
|
||||||
|
explored: { type: 'boolean', description: 'True once scanned — each body can only be explored once' },
|
||||||
|
parent: { type: ['integer', 'null'], nullable: true, description: 'NULL for a planet, the parent planet id for a moon' },
|
||||||
|
},
|
||||||
|
required: ['id', 'name', 'type', 'hazard', 'explored', 'parent'],
|
||||||
|
},
|
||||||
|
InventoryItem: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
kind: { type: 'string', enum: ['resource', 'part'] },
|
||||||
|
name: { type: 'string' },
|
||||||
|
qty: { type: 'integer' },
|
||||||
|
},
|
||||||
|
required: ['kind', 'name', 'qty'],
|
||||||
|
},
|
||||||
|
FoundItem: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
kind: { type: 'string', enum: ['resource', 'part'] },
|
||||||
|
name: { type: 'string' },
|
||||||
|
qty: { type: 'integer' },
|
||||||
|
},
|
||||||
|
required: ['kind', 'name', 'qty'],
|
||||||
|
},
|
||||||
|
SystemState: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
systemId: { type: 'integer' },
|
||||||
|
systemName: { type: 'string' },
|
||||||
|
planets: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Planets and moons in display order — a moon always follows its parent row',
|
||||||
|
items: { $ref: '#/components/schemas/Planet' },
|
||||||
|
},
|
||||||
|
inventory: { type: 'array', items: { $ref: '#/components/schemas/InventoryItem' } },
|
||||||
|
turn: { type: 'integer', description: 'Turn counter — one per jump or exploration' },
|
||||||
|
},
|
||||||
|
required: ['systemId', 'systemName', 'planets', 'inventory', 'turn'],
|
||||||
|
},
|
||||||
|
ExploreRequest: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
planetId: { type: 'integer', description: 'planets.id of the body to scan (planet or moon)' },
|
||||||
|
},
|
||||||
|
required: ['planetId'],
|
||||||
|
},
|
||||||
|
ExploreResult: {
|
||||||
|
allOf: [
|
||||||
|
{ $ref: '#/components/schemas/SystemState' },
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
outcome: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'What this scan found — may be empty (some scans find nothing)',
|
||||||
|
items: { $ref: '#/components/schemas/FoundItem' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['outcome'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
Error: {
|
Error: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
Reference in New Issue
Block a user