diff --git a/README.md b/README.md index eeda85e..79997e0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ and in an adopter's first run. It is small on purpose, so a job is cheap and fas | GET | `/health` | Says the server is up | | GET | `/tasks` | Lists every task | | POST | `/tasks` | Adds a task, body `{"title": "..."}` | +| GET | `/tasks/count` | Says how many tasks there are, and how many are done | | GET | `/tasks/:id` | Returns one task | | POST | `/tasks/:id/complete` | Marks a task done | | DELETE | `/tasks/:id` | Removes a task | diff --git a/src/server.ts b/src/server.ts index fce3baf..024a1cb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,6 +7,7 @@ import { TaskList, ValidationError } from './tasks.ts'; * GET /health is the server up * GET /tasks every task * POST /tasks add a task, body {"title": "..."} + * GET /tasks/count how many tasks, and how many are done * GET /tasks/:id one task * POST /tasks/:id/complete mark a task done * DELETE /tasks/:id remove a task @@ -35,6 +36,8 @@ async function handle(tasks: TaskList, request: IncomingMessage, response: Serve return send(response, 405, { error: 'method not allowed' }); } + if (parts.length === 2 && parts[1] === 'count' && method === 'GET') return send(response, 200, tasks.count()); + const id = Number(parts[1]); if (!Number.isInteger(id) || id < 1) return send(response, 404, { error: 'not found' }); diff --git a/src/tasks.ts b/src/tasks.ts index 491c652..e9e84c5 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -36,4 +36,11 @@ export class TaskList { remove(id: number): boolean { return this.tasks.delete(id); } + + /** How many tasks there are, and how many of them are done. */ + count(): { total: number; done: number } { + let done = 0; + for (const task of this.tasks.values()) if (task.done) done++; + return { total: this.tasks.size, done }; + } } diff --git a/test/server.test.ts b/test/server.test.ts index 8203b79..8aaa404 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -40,6 +40,23 @@ describe('the HTTP API', () => { assert.equal((await call('GET', `/tasks/${id}`)).status, 404); }); + it('counts the tasks, and the ones that are done', async () => { + const start = await call('GET', '/tasks/count'); + assert.equal(start.status, 200); + const { total, done } = start.json as { total: number; done: number }; + + const first = (await call('POST', '/tasks', { title: 'Count me' })).json!.id as number; + const second = (await call('POST', '/tasks', { title: 'Count me too' })).json!.id as number; + assert.deepEqual(await call('GET', '/tasks/count'), { status: 200, json: { total: total + 2, done } }); + + await call('POST', `/tasks/${first}/complete`); + assert.deepEqual(await call('GET', '/tasks/count'), { status: 200, json: { total: total + 2, done: done + 1 } }); + + await call('DELETE', `/tasks/${first}`); + await call('DELETE', `/tasks/${second}`); + assert.deepEqual(await call('GET', '/tasks/count'), { status: 200, json: { total, done } }); + }); + it('refuses a task without a title, and a body that is not JSON', async () => { assert.deepEqual(await call('POST', '/tasks', { title: '' }), { status: 400, json: { error: 'title must be text' } }); assert.deepEqual(await call('POST', '/tasks', 'not json'), { status: 400, json: { error: 'the body must be JSON' } }); diff --git a/test/tasks.test.ts b/test/tasks.test.ts index dd5bb48..85c072e 100644 --- a/test/tasks.test.ts +++ b/test/tasks.test.ts @@ -33,4 +33,20 @@ describe('the task list', () => { assert.equal(tasks.remove(id), false); assert.equal(tasks.get(id), undefined); }); + + it('counts the tasks, and the ones that are done', () => { + const tasks = new TaskList(); + assert.deepEqual(tasks.count(), { total: 0, done: 0 }); + + const { id } = tasks.add('Write the spec'); + tasks.add('Review it'); + tasks.add('Ship it'); + assert.deepEqual(tasks.count(), { total: 3, done: 0 }); + + tasks.complete(id); + assert.deepEqual(tasks.count(), { total: 3, done: 1 }); + + tasks.remove(id); + assert.deepEqual(tasks.count(), { total: 2, done: 0 }); + }); });