diff --git a/config.json b/config.json index 1a73e2a..532ec1c 100644 --- a/config.json +++ b/config.json @@ -225,6 +225,14 @@ "practices": [], "prerequisites": [], "difficulty": 2 + }, + { + "slug": "rest-api", + "name": "REST API", + "uuid": "51275817-1b95-48d9-b7ab-bbbf8720acb7", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ] }, diff --git a/exercises/practice/rest-api/.docs/instructions.md b/exercises/practice/rest-api/.docs/instructions.md new file mode 100644 index 0000000..af223ba --- /dev/null +++ b/exercises/practice/rest-api/.docs/instructions.md @@ -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":}` | `{"users": (sorted by name)}` | +| Create user | POST | /add | `{"user":}` | N/A | `` | +| Create IOU | POST | /iou | `{"lender":,"borrower":,"amount":5.25}` | N/A | `{"users": and (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/ diff --git a/exercises/practice/rest-api/.meta/config.json b/exercises/practice/rest-api/.meta/config.json new file mode 100644 index 0000000..2706980 --- /dev/null +++ b/exercises/practice/rest-api/.meta/config.json @@ -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." +} diff --git a/exercises/practice/rest-api/.meta/example.sql b/exercises/practice/rest-api/.meta/example.sql new file mode 100644 index 0000000..72daffa --- /dev/null +++ b/exercises/practice/rest-api/.meta/example.sql @@ -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'; diff --git a/exercises/practice/rest-api/.meta/tests.toml b/exercises/practice/rest-api/.meta/tests.toml new file mode 100644 index 0000000..8b2cd1a --- /dev/null +++ b/exercises/practice/rest-api/.meta/tests.toml @@ -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" diff --git a/exercises/practice/rest-api/create_fixture.sql b/exercises/practice/rest-api/create_fixture.sql new file mode 100644 index 0000000..47515fb --- /dev/null +++ b/exercises/practice/rest-api/create_fixture.sql @@ -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" diff --git a/exercises/practice/rest-api/create_test_table.sql b/exercises/practice/rest-api/create_test_table.sql new file mode 100644 index 0000000..c72a65c --- /dev/null +++ b/exercises/practice/rest-api/create_test_table.sql @@ -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}]}"); diff --git a/exercises/practice/rest-api/data.csv b/exercises/practice/rest-api/data.csv new file mode 100644 index 0000000..f378108 --- /dev/null +++ b/exercises/practice/rest-api/data.csv @@ -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","" diff --git a/exercises/practice/rest-api/rest-api.sql b/exercises/practice/rest-api/rest-api.sql new file mode 100644 index 0000000..1eefe52 --- /dev/null +++ b/exercises/practice/rest-api/rest-api.sql @@ -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. diff --git a/exercises/practice/rest-api/rest-api_test.sql b/exercises/practice/rest-api/rest-api_test.sql new file mode 100644 index 0000000..35fe36d --- /dev/null +++ b/exercises/practice/rest-api/rest-api_test.sql @@ -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;