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', () => {
|
||||
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', () => {
|
||||
|
||||
+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: {
|
||||
schemas: {
|
||||
@@ -147,6 +215,74 @@ export const openApiSpec = {
|
||||
},
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
Reference in New Issue
Block a user