Skip to content

Commit

Permalink
feat: add rest-api exercise (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
vaeng authored Jun 10, 2024
1 parent ce38147 commit a2534af
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,14 @@
"practices": [],
"prerequisites": [],
"difficulty": 2
},
{
"slug": "rest-api",
"name": "REST API",
"uuid": "51275817-1b95-48d9-b7ab-bbbf8720acb7",
"practices": [],
"prerequisites": [],
"difficulty": 1
}
]
},
Expand Down
48 changes: 48 additions & 0 deletions exercises/practice/rest-api/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Instructions

Implement a RESTful API for tracking IOUs.

Four roommates have a habit of borrowing money from each other frequently, and have trouble remembering who owes whom, and how much.

Your task is to implement a simple [RESTful API][restful-wikipedia] that receives [IOU][iou]s as POST requests, and can deliver specified summary information via GET requests.

## API Specification

### User object

```json
{
"name": "Adam",
"owes": {
"Bob": 12.0,
"Chuck": 4.0,
"Dan": 9.5
},
"owed_by": {
"Bob": 6.5,
"Dan": 2.75
},
"balance": "<(total owed by other users) - (total owed to other users)>"
}
```

### Methods

| Description | HTTP Method | URL | Payload Format | Response w/o Payload | Response w/ Payload |
| ------------------------ | ----------- | ------ | ------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------- |
| List of user information | GET | /users | `{"users":["Adam","Bob"]}` | `{"users":<List of all User objects>}` | `{"users":<List of User objects for <users> (sorted by name)}` |
| Create user | POST | /add | `{"user":<name of new user (unique)>}` | N/A | `<User object for new user>` |
| Create IOU | POST | /iou | `{"lender":<name of lender>,"borrower":<name of borrower>,"amount":5.25}` | N/A | `{"users":<updated User objects for <lender> and <borrower> (sorted by name)>}` |

## Other Resources

- [REST API Tutorial][restfulapi]
- Example RESTful APIs
- [GitHub][github-rest]
- [Reddit][reddit-rest]

[restful-wikipedia]: https://en.wikipedia.org/wiki/Representational_state_transfer
[iou]: https://en.wikipedia.org/wiki/IOU
[github-rest]: https://developer.github.com/v3/
[reddit-rest]: https://web.archive.org/web/20231202231149/https://www.reddit.com/dev/api/
[restfulapi]: https://restfulapi.net/
18 changes: 18 additions & 0 deletions exercises/practice/rest-api/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"authors": [
"steffan153",
"vaeng"
],
"files": {
"solution": [
"rest-api.sql"
],
"test": [
"rest-api_test.sql"
],
"example": [
".meta/example.sql"
]
},
"blurb": "Implement a RESTful API for tracking IOUs."
}
92 changes: 92 additions & 0 deletions exercises/practice/rest-api/.meta/example.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
UPDATE 'rest-api' AS CURRENT
SET result = (
SELECT json_object('users', IIF(json_type(DB.value) IS NULL, json_array(), json_array(DB.value)))
FROM 'rest-api' AS RA
LEFT JOIN json_each(RA.payload, '$.users') AS PL ON RA.payload = PL.json
LEFT JOIN json_each(RA.database, '$.users') AS DB ON RA.database = DB.json AND json_extract(DB.value, '$.name') = PL.value
WHERE (RA.database, RA.payload) = (CURRENT.database, CURRENT.payload)
)
WHERE url = '/users';


UPDATE 'rest-api' AS CURRENT
SET result = json_object('name', json_extract(payload, '$.user'), 'owes', json_object(), 'owed_by', json_object(), 'balance', 0)
WHERE url = '/add';


UPDATE 'rest-api' AS CURRENT
SET result = json_array(
-- update the lender:
(
SELECT json_object(
'name', json_extract(payload, '$.lender'),
'owes', json_patch(
json_extract(value, '$.owes'),
json_object(
json_extract(payload, '$.borrower'),
IIF(relative_balance < 0, -relative_balance, NULL)
)
),
'owed_by', json_patch(
json_extract(value, '$.owed_by'),
json_object(
json_extract(payload, '$.borrower'),
IIF(relative_balance > 0, relative_balance, NULL)
)
),
'balance', json_extract(value, '$.balance') + json_extract(payload, '$.amount')
)
FROM (
SELECT
*,
IFNULL(json_extract(json_extract(value, '$.owed_by'), '$.' || json_extract(payload, '$.borrower')), 0) -
IFNULL(json_extract(json_extract(value, '$.owes'), '$.' || json_extract(payload, '$.borrower')), 0) +
json_extract(payload, '$.amount') AS relative_balance
FROM json_each(database, '$.users')
WHERE json = database AND json_extract(value, '$.name') == json_extract(payload, '$.lender')
)
),
-- update the borrower:
(
SELECT json_object(
'name', json_extract(payload, '$.borrower'),
'owes', json_patch(
json_extract(value, '$.owes'),
json_object(
json_extract(payload, '$.lender'),
IIF(relative_balance < 0, -relative_balance, NULL)
)
),
'owed_by', json_patch(
json_extract(value, '$.owed_by'),
json_object(
json_extract(payload, '$.lender'),
IIF(relative_balance > 0, relative_balance, NULL)
)
),
'balance', json_extract(value, '$.balance') - json_extract(payload, '$.amount')
)
FROM (
SELECT
*,
IFNULL(json_extract(json_extract(value, '$.owed_by'), '$.' || json_extract(payload, '$.lender')), 0) -
IFNULL(json_extract(json_extract(value, '$.owes'), '$.' || json_extract(payload, '$.lender')), 0) -
json_extract(payload, '$.amount') AS relative_balance
FROM json_each(database, '$.users')
WHERE json = database AND json_extract(value, '$.name') == json_extract(payload, '$.borrower')
)
)
)
WHERE url = '/iou';

-- order the result
UPDATE 'rest-api' AS CURRENT
SET result = (
SELECT json_object('users', json_group_array(json(value)))
FROM (
SELECT value
FROM json_each(result)
ORDER BY value
)
)
WHERE url = '/iou';
37 changes: 37 additions & 0 deletions exercises/practice/rest-api/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This is an auto-generated file.
#
# Regenerating this file via `configlet sync` will:
# - Recreate every `description` key/value pair
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
# - Preserve any other key/value pair
#
# As user-added comments (using the # character) will be removed when this file
# is regenerated, comments can be added via a `comment` key.

[5be01ffb-a814-47a8-a19f-490a5622ba07]
description = "user management -> no users"

[382b70cc-9f6c-486d-9bee-fda2df81c803]
description = "user management -> add user"

[d624e5e5-1abb-4f18-95b3-45d55c818dc3]
description = "user management -> get single user"

[7a81b82c-7276-433e-8fce-29ce983a7c56]
description = "iou -> both users have 0 balance"

[1c61f957-cf8c-48ba-9e77-b221ab068803]
description = "iou -> borrower has negative balance"

[8a8567b3-c097-468a-9541-6bb17d5afc85]
description = "iou -> lender has negative balance"

[29fb7c12-7099-4a85-a7c4-9c290d2dc01a]
description = "iou -> lender owes borrower"

[ce969e70-163c-4135-a4a6-2c3a5da286f5]
description = "iou -> lender owes borrower less than new loan"

[7f4aafd9-ae9b-4e15-a406-87a87bdf47a4]
description = "iou -> lender owes borrower same as new loan"
10 changes: 10 additions & 0 deletions exercises/practice/rest-api/create_fixture.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DROP TABLE IF EXISTS "rest-api";
CREATE TABLE "rest-api" (
"database" TEXT,
"payload" TEXT,
"url" TEXT,
"result" TEXT
);

.mode csv
.import ./data.csv "rest-api"
84 changes: 84 additions & 0 deletions exercises/practice/rest-api/create_test_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
DROP TABLE IF EXISTS tests;
CREATE TABLE IF NOT EXISTS tests (
-- uuid and name are taken from the test.toml file
uuid TEXT PRIMARY KEY,
name TEXT NOT NULL,
-- The following section is needed by the online test-runner
status TEXT DEFAULT 'fail',
message TEXT,
output TEXT,
test_code TEXT,
task_id INTEGER DEFAULT NULL,
-- Here are columns for the actual tests
database TEXT NOT NULL,
payload TEXT NOT NULL,
url TEXT INT NOT NULL,
expected TEXT NOT NULL
);

-- Note: the strings below _may_ contain literal tab, newline, or carriage returns.

INSERT INTO tests (uuid, name, database, payload, url, expected)
VALUES
("5be01ffb-a814-47a8-a19f-490a5622ba07",
"no users",
"{""users"":[]}",
"{}",
"/users",
"{""users"":[]}"),

("382b70cc-9f6c-486d-9bee-fda2df81c803",
"add user",
"{""users"":[]}","{""user"":""Adam""}",
"/add",
"{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0}"),

("d624e5e5-1abb-4f18-95b3-45d55c818dc3",
"get single user",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{},""owed_by"":{},""balance"":0}]}",
"{""users"":[""Bob""]}",
"/users",
"{""users"":[{""name"":""Bob"",""owes"":{},""owed_by"":{},""balance"":0}]}"),

("7a81b82c-7276-433e-8fce-29ce983a7c56",
"both users have 0 balance",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{},""owed_by"":{},""balance"":0}]}",
"{""lender"":""Adam"",""borrower"":""Bob"",""amount"":3}",
"/iou",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{""Bob"":3},""balance"":3},{""name"":""Bob"",""owes"":{""Adam"":3},""owed_by"":{},""balance"":-3}]}"),

("1c61f957-cf8c-48ba-9e77-b221ab068803",
"borrower has negative balance",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{""Chuck"":3},""owed_by"":{},""balance"":-3},{""name"":""Chuck"",""owes"":{},""owed_by"":{""Bob"":3},""balance"":3}]}",
"{""lender"":""Adam"",""borrower"":""Bob"",""amount"":3}",
"/iou",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{""Bob"":3},""balance"":3},{""name"":""Bob"",""owes"":{""Adam"":3,""Chuck"":3},""owed_by"":{},""balance"":-6}]}"
),

("8a8567b3-c097-468a-9541-6bb17d5afc85",
"lender has negative balance",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{""Chuck"":3},""owed_by"":{},""balance"":-3},{""name"":""Chuck"",""owes"":{},""owed_by"":{""Bob"":3},""balance"":3}]}",
"{""lender"":""Bob"",""borrower"":""Adam"",""amount"":3}",
"/iou",
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":3},""owed_by"":{},""balance"":-3},{""name"":""Bob"",""owes"":{""Chuck"":3},""owed_by"":{""Adam"":3},""balance"":0}]}"),

("29fb7c12-7099-4a85-a7c4-9c290d2dc01a",
"lender owes borrower",
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":3},""owed_by"":{},""balance"":-3},{""name"":""Bob"",""owes"":{},""owed_by"":{""Adam"":3},""balance"":3}]}",
"{""lender"":""Adam"",""borrower"":""Bob"",""amount"":2}",
"/iou",
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":1},""owed_by"":{},""balance"":-1},{""name"":""Bob"",""owes"":{},""owed_by"":{""Adam"":1},""balance"":1}]}"),

("ce969e70-163c-4135-a4a6-2c3a5da286f5",
"lender owes borrower less than new loan",
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":3},""owed_by"":{},""balance"":-3},{""name"":""Bob"",""owes"":{},""owed_by"":{""Adam"":3},""balance"":3}]}",
"{""lender"":""Adam"",""borrower"":""Bob"",""amount"":4}",
"/iou",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{""Bob"":1},""balance"":1},{""name"":""Bob"",""owes"":{""Adam"":1},""owed_by"":{},""balance"":-1}]}"),

("7f4aafd9-ae9b-4e15-a406-87a87bdf47a4",
"lender owes borrower same as new loan",
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":3},""owed_by"":{},""balance"":-3},{""name"":""Bob"",""owes"":{},""owed_by"":{""Adam"":3},""balance"":3}]}",
"{""lender"":""Adam"",""borrower"":""Bob"",""amount"":3}",
"/iou",
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{},""owed_by"":{},""balance"":0}]}");
9 changes: 9 additions & 0 deletions exercises/practice/rest-api/data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"{""users"":[]}","{}","/users",""
"{""users"":[]}","{""user"":""Adam""}","/add",""
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{},""owed_by"":{},""balance"":0}]}","{""users"":[""Bob""]}","/users",""
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{},""owed_by"":{},""balance"":0}]}","{""lender"":""Adam"",""borrower"":""Bob"",""amount"":3}","/iou",""
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{""Chuck"":3},""owed_by"":{},""balance"":-3},{""name"":""Chuck"",""owes"":{},""owed_by"":{""Bob"":3},""balance"":3}]}","{""lender"":""Adam"",""borrower"":""Bob"",""amount"":3}","/iou",""
"{""users"":[{""name"":""Adam"",""owes"":{},""owed_by"":{},""balance"":0},{""name"":""Bob"",""owes"":{""Chuck"":3},""owed_by"":{},""balance"":-3},{""name"":""Chuck"",""owes"":{},""owed_by"":{""Bob"":3},""balance"":3}]}","{""lender"":""Bob"",""borrower"":""Adam"",""amount"":3}","/iou",""
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":3},""owed_by"":{},""balance"":-3},{""name"":""Bob"",""owes"":{},""owed_by"":{""Adam"":3},""balance"":3}]}","{""lender"":""Adam"",""borrower"":""Bob"",""amount"":2}","/iou",""
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":3},""owed_by"":{},""balance"":-3},{""name"":""Bob"",""owes"":{},""owed_by"":{""Adam"":3},""balance"":3}]}","{""lender"":""Adam"",""borrower"":""Bob"",""amount"":4}","/iou",""
"{""users"":[{""name"":""Adam"",""owes"":{""Bob"":3},""owed_by"":{},""balance"":-3},{""name"":""Bob"",""owes"":{},""owed_by"":{""Adam"":3},""balance"":3}]}","{""lender"":""Adam"",""borrower"":""Bob"",""amount"":3}","/iou",""
2 changes: 2 additions & 0 deletions exercises/practice/rest-api/rest-api.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Schema: CREATE TABLE "rest-api" ("database" TEXT, "payload" TEXT, "url" TEXT, "result" TEXT);
-- Task: update the rest-api table and set the result based on the database, payload and url fields.
37 changes: 37 additions & 0 deletions exercises/practice/rest-api/rest-api_test.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- Create database:
.read ./create_fixture.sql

-- Read user student solution and save any output as markdown in user_output.md:
.mode markdown
.output user_output.md
.read ./rest-api.sql
.output

-- Create a clean testing environment:
.read ./create_test_table.sql

-- Comparison of user input and the tests updates the status for each test:
UPDATE tests
SET status = 'pass'
FROM (SELECT database, url, payload, result FROM 'rest-api') AS actual
WHERE (actual.database, actual.url, actual.payload) = (tests.database, tests.url, tests.payload)
AND (SELECT COUNT(*) FROM json_tree(actual.result) as a, json_tree(tests.expected) as b WHERE (a.fullkey, a.atom) IS (b.fullkey, b.atom)) =
(SELECT COUNT(*) FROM json_tree(tests.expected))
AND (SELECT COUNT(*) FROM json_tree(tests.expected)) = (SELECT COUNT(*) FROM json_tree(actual.result));

-- Update message for failed tests to give helpful information:
UPDATE tests
SET message = 'Result for ' || tests.database || ' as ' || tests.payload || ' is ' || actual.result || ', but should be ' || tests.expected
FROM (SELECT database, payload, url, result FROM 'rest-api') AS actual
WHERE (actual.database, actual.payload, actual.url) = (tests.database, tests.payload, tests.url) AND tests.status = 'fail';

-- Save results to ./output.json (needed by the online test-runner)
.mode json
.once './output.json'
SELECT name, status, message, output, test_code, task_id
FROM tests;

-- Display test results in readable form for the student:
.mode table
SELECT name, status, message
FROM tests;

0 comments on commit a2534af

Please sign in to comment.