api+openapi: GET /api/ship + POST upgrade/expand/build with 400/404/409/422 mapping
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { getGameDb } from '@/lib/db/client'
|
||||||
|
import { ShipError, buildSystem } from '@/lib/game/ship'
|
||||||
|
|
||||||
|
// Build an unbuilt ship system type at base values. Body: { "type": string }.
|
||||||
|
// Costs inventory items + a turn; returns the fresh ship state.
|
||||||
|
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 type = (body as { type?: unknown }).type
|
||||||
|
if (typeof type !== 'string' || type.length === 0) {
|
||||||
|
return Response.json({ error: 'type must be a non-empty string' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await getGameDb()
|
||||||
|
return Response.json(await buildSystem(db, type))
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ShipError) {
|
||||||
|
return Response.json({ error: err.message }, { status: err.status })
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { getGameDb } from '@/lib/db/client'
|
||||||
|
import { ShipError, expandSystem } from '@/lib/game/ship'
|
||||||
|
|
||||||
|
// Raise a built ship system's capacity by one (capped at max capacity). Body: { "systemId": number }.
|
||||||
|
// Costs inventory items + a turn; returns the fresh ship state.
|
||||||
|
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 systemId = (body as { systemId?: unknown }).systemId
|
||||||
|
if (typeof systemId !== 'number' || !Number.isInteger(systemId)) {
|
||||||
|
return Response.json({ error: 'systemId must be an integer' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await getGameDb()
|
||||||
|
return Response.json(await expandSystem(db, systemId))
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ShipError) {
|
||||||
|
return Response.json({ error: err.message }, { status: err.status })
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { getGameDb } from '@/lib/db/client'
|
||||||
|
import { getShipState } from '@/lib/game/ship'
|
||||||
|
|
||||||
|
// The ship's systems + cargo hold + turn; auto-initializes on first hit.
|
||||||
|
export async function GET() {
|
||||||
|
const db = await getGameDb()
|
||||||
|
return Response.json(await getShipState(db))
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { getGameDb } from '@/lib/db/client'
|
||||||
|
import { ShipError, upgradeSystem } from '@/lib/game/ship'
|
||||||
|
|
||||||
|
// Level up a built ship system by one (capped at max level). Body: { "systemId": number }.
|
||||||
|
// Costs inventory items + a turn; returns the fresh ship state.
|
||||||
|
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 systemId = (body as { systemId?: unknown }).systemId
|
||||||
|
if (typeof systemId !== 'number' || !Number.isInteger(systemId)) {
|
||||||
|
return Response.json({ error: 'systemId must be an integer' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = await getGameDb()
|
||||||
|
return Response.json(await upgradeSystem(db, systemId))
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ShipError) {
|
||||||
|
return Response.json({ error: err.message }, { status: err.status })
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,37 @@ describe('OpenAPI spec', () => {
|
|||||||
'/api/crew',
|
'/api/crew',
|
||||||
'/api/explore',
|
'/api/explore',
|
||||||
'/api/jump',
|
'/api/jump',
|
||||||
|
'/api/ship',
|
||||||
|
'/api/ship/build',
|
||||||
|
'/api/ship/expand',
|
||||||
|
'/api/ship/upgrade',
|
||||||
'/api/system',
|
'/api/system',
|
||||||
'/api/version',
|
'/api/version',
|
||||||
'/api/world',
|
'/api/world',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('describes the ship endpoints as ShipState in and out, with error responses on the actions', () => {
|
||||||
|
const ship = openApiSpec.paths['/api/ship'].get.responses[200].content['application/json'].schema;
|
||||||
|
expect(ship.$ref).toBe('#/components/schemas/ShipState');
|
||||||
|
|
||||||
|
const system = openApiSpec.components.schemas.ShipSystem.properties;
|
||||||
|
expect(Object.keys(system)).toEqual(['id', 'type', 'level', 'capacity', 'built']);
|
||||||
|
|
||||||
|
for (const [path, request] of [
|
||||||
|
['/api/ship/upgrade', '#/components/schemas/UpgradeRequest'],
|
||||||
|
['/api/ship/expand', '#/components/schemas/ExpandRequest'],
|
||||||
|
['/api/ship/build', '#/components/schemas/BuildRequest'],
|
||||||
|
] as const) {
|
||||||
|
const post = openApiSpec.paths[path].post;
|
||||||
|
expect(post.requestBody.content['application/json'].schema.$ref).toBe(request);
|
||||||
|
expect(post.responses[200].content['application/json'].schema.$ref).toBe('#/components/schemas/ShipState');
|
||||||
|
for (const status of ['400', '404', '409', '422'] as const) {
|
||||||
|
expect(post.responses[status].content['application/json'].schema.$ref).toBe('#/components/schemas/Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('describes /api/crew as an array of CrewMember and crewFound on ExploreResult', () => {
|
it('describes /api/crew as an array of CrewMember and crewFound on ExploreResult', () => {
|
||||||
const crew = openApiSpec.paths['/api/crew'].get.responses[200].content['application/json'].schema;
|
const crew = openApiSpec.paths['/api/crew'].get.responses[200].content['application/json'].schema;
|
||||||
expect(crew.type).toBe('array');
|
expect(crew.type).toBe('array');
|
||||||
|
|||||||
+237
@@ -186,6 +186,198 @@ export const openApiSpec = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'/api/ship': {
|
||||||
|
get: {
|
||||||
|
summary: 'Ship state',
|
||||||
|
description:
|
||||||
|
"The ship's systems (built flag derived from level), the cargo hold and the turn counter. The starting ship is seeded on new game — core types built at base values, the rest unbuilt until a build action. Auto-initializes a new game when no save exists yet.",
|
||||||
|
operationId: 'getShip',
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Current ship state',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/ShipState' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/ship/upgrade': {
|
||||||
|
post: {
|
||||||
|
summary: 'Upgrade a ship system',
|
||||||
|
description:
|
||||||
|
"Level up a built ship system by one (capped at max level). Costs `level` units of any resource, consumed from the oldest cargo row first, plus a turn. Returns the fresh ship state.",
|
||||||
|
operationId: 'upgradeShipSystem',
|
||||||
|
requestBody: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/UpgradeRequest' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Fresh ship state after the upgrade',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/ShipState' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Malformed body or non-integer systemId',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: 'Unknown ship system id',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
409: {
|
||||||
|
description: 'System is not built or already at max level',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
422: {
|
||||||
|
description: 'Not enough resources in the cargo hold for the cost',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/ship/expand': {
|
||||||
|
post: {
|
||||||
|
summary: 'Expand a ship system',
|
||||||
|
description:
|
||||||
|
"Raise a built ship system's capacity by one (capped at max capacity). Costs `capacity` units of any resource, consumed from the oldest cargo row first, plus a turn. Returns the fresh ship state.",
|
||||||
|
operationId: 'expandShipSystem',
|
||||||
|
requestBody: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/ExpandRequest' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Fresh ship state after the expansion',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/ShipState' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Malformed body or non-integer systemId',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: 'Unknown ship system id',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
409: {
|
||||||
|
description: 'System is not built or already at max capacity',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
422: {
|
||||||
|
description: 'Not enough resources in the cargo hold for the cost',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/ship/build': {
|
||||||
|
post: {
|
||||||
|
summary: 'Build a ship system',
|
||||||
|
description:
|
||||||
|
'Build an unbuilt ship system type at base values. Costs a flat amount of parts, consumed from the oldest cargo row first, plus a turn. Returns the fresh ship state.',
|
||||||
|
operationId: 'buildShipSystem',
|
||||||
|
requestBody: {
|
||||||
|
required: true,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/BuildRequest' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'Fresh ship state after the build',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/ShipState' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
description: 'Malformed body or empty type',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: 'Unknown ship system type',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
409: {
|
||||||
|
description: 'System is already built',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
422: {
|
||||||
|
description: 'Not enough parts in the cargo hold for the cost',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: '#/components/schemas/Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
@@ -275,6 +467,51 @@ export const openApiSpec = {
|
|||||||
},
|
},
|
||||||
required: ['id', 'name', 'role', 'trait_good', 'trait_bad', 'origin'],
|
required: ['id', 'name', 'role', 'trait_good', 'trait_bad', 'origin'],
|
||||||
},
|
},
|
||||||
|
ShipSystem: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'integer' },
|
||||||
|
type: { type: 'string', description: "System type, e.g. REACTOR or WEAPONS" },
|
||||||
|
level: { type: 'integer', description: '0 when not built yet' },
|
||||||
|
capacity: { type: 'integer', description: '0 when not built yet' },
|
||||||
|
built: { type: 'boolean', description: 'Derived — true when level > 0' },
|
||||||
|
},
|
||||||
|
required: ['id', 'type', 'level', 'capacity', 'built'],
|
||||||
|
},
|
||||||
|
ShipState: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
systems: {
|
||||||
|
type: 'array',
|
||||||
|
description: "The ship's systems in display order",
|
||||||
|
items: { $ref: '#/components/schemas/ShipSystem' },
|
||||||
|
},
|
||||||
|
inventory: { type: 'array', items: { $ref: '#/components/schemas/InventoryItem' } },
|
||||||
|
turn: { type: 'integer', description: 'Turn counter — one per jump, exploration or ship action' },
|
||||||
|
},
|
||||||
|
required: ['systems', 'inventory', 'turn'],
|
||||||
|
},
|
||||||
|
UpgradeRequest: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
systemId: { type: 'integer', description: 'ship_systems.id of the built system to level up' },
|
||||||
|
},
|
||||||
|
required: ['systemId'],
|
||||||
|
},
|
||||||
|
ExpandRequest: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
systemId: { type: 'integer', description: 'ship_systems.id of the built system to expand' },
|
||||||
|
},
|
||||||
|
required: ['systemId'],
|
||||||
|
},
|
||||||
|
BuildRequest: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: { type: 'string', description: "Ship system type to build, e.g. WEAPONS (see ShipSystem.type)" },
|
||||||
|
},
|
||||||
|
required: ['type'],
|
||||||
|
},
|
||||||
SystemState: {
|
SystemState: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
Reference in New Issue
Block a user