diff --git a/bin/generate-spec b/bin/generate-spec index c0dc900..ada3fa0 100755 --- a/bin/generate-spec +++ b/bin/generate-spec @@ -132,7 +132,7 @@ canonical_data = json.decode read_file(canonical_data_path), 1, json.null tests_toml_path = exercise_directory .. '/.meta/tests.toml' included_tests = included_tests_from_toml tests_toml_path -package.moonpath = "#{exercise_directory}/.meta/?.moon;#{package.moonpath}" +package.moonpath = "#{exercise_directory}/.meta/?.moon;./lib/?.moon;#{package.moonpath}" spec_generator = require 'spec_generator' local spec diff --git a/config.json b/config.json index a3a2a02..b2d9c9c 100644 --- a/config.json +++ b/config.json @@ -826,6 +826,14 @@ "prerequisites": [], "difficulty": 7 }, + { + "slug": "rest-api", + "name": "REST API", + "uuid": "a999597a-d5db-4a72-83f7-133a2b7f3add", + "practices": [], + "prerequisites": [], + "difficulty": 7 + }, { "slug": "satellite", "name": "Satellite", diff --git a/exercises/practice/rest-api/.busted b/exercises/practice/rest-api/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/rest-api/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/rest-api/.docs/instructions.md b/exercises/practice/rest-api/.docs/instructions.md new file mode 100644 index 0000000..e889b1b --- /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://docs.github.com/en/rest +[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..62bdf87 --- /dev/null +++ b/exercises/practice/rest-api/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "rest_api.moon" + ], + "test": [ + "rest_api_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "blurb": "Implement a RESTful API for tracking IOUs." +} diff --git a/exercises/practice/rest-api/.meta/example.moon b/exercises/practice/rest-api/.meta/example.moon new file mode 100644 index 0000000..169a591 --- /dev/null +++ b/exercises/practice/rest-api/.meta/example.moon @@ -0,0 +1,63 @@ +contains = (list, elem) -> + for item in *list + return true if item == elem + false + +sort_users = (list) -> + table.sort list, (a, b) -> a.name < b.name + list + +-- The tests do not require the payload to be validated. +-- Also, I'm not concerned about accepting references to user's tables, +-- or returning references to internal tables. + +class RestApi + new: (@db) => + + GET: (url, payload) => @route 'GET', url, payload + POST: (url, payload) => @route 'POST', url, payload + + route: (method, url, payload) => + subject = url\match '^/(%w+)' + func = "#{subject}_#{method}" + return {error: "Unknown endpoint: #{method} #{url}"} if not @[func] + @[func] @, payload + + users_GET: (payload) => + filter = if payload + (user) -> contains payload.users, user.name + else + (user) -> true + {users: sort_users [u for u in *@db.users when filter u]} + + add_POST: (payload) => + user = { + name: payload.user + balance: 0 + owes: {} + owed_by: {} + } + table.insert @db.users, user + user + + iou_POST: (payload) => + {users: {lender}} = @users_GET {users: {payload.lender}} + {users: {borrower}} = @users_GET {users: {payload.borrower}} + borrower.balance -= payload.amount + lender.balance += payload.amount + + debt = (lender.owes[borrower.name] or 0) - (lender.owed_by[borrower.name] or 0) - payload.amount + + lender.owes[borrower.name] = nil + lender.owed_by[borrower.name] = nil + borrower.owes[lender.name] = nil + borrower.owed_by[lender.name] = nil + + if debt > 0 + lender.owes[borrower.name] = debt + borrower.owed_by[lender.name] = debt + elseif debt < 0 + lender.owed_by[borrower.name] = -debt + borrower.owes[lender.name] = -debt + + {users: sort_users {lender, borrower}} diff --git a/exercises/practice/rest-api/.meta/spec_generator.moon b/exercises/practice/rest-api/.meta/spec_generator.moon new file mode 100644 index 0000000..4ac3829 --- /dev/null +++ b/exercises/practice/rest-api/.meta/spec_generator.moon @@ -0,0 +1,25 @@ +test_helpers = require 'test_helpers' +import table_dump from test_helpers + +{ + module_name: 'RestApi', + + generate_test: (case, level) -> + lines = { + "database = #{table_dump case.input.database, level}", + "api = RestApi database" + } + call = "result = api\\#{case.property\upper!} #{quote case.input.url}" + if case.input.payload + table.insert lines, "payload = #{table_dump case.input.payload, level}" + call ..= ", payload" + table.insert lines, call + table.insert lines, "expected = #{table_dump case.expected, level}" + table.insert lines, "assert.are.same expected, result" + table.concat [indent line, level for line in *lines], '\n' + + -- we have deep tables to compare, display it all when not the same + test_helpers: [[ + assert\set_parameter "TableFormatLevel", 4 +]] +} 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/rest_api.moon b/exercises/practice/rest-api/rest_api.moon new file mode 100644 index 0000000..7fe37bd --- /dev/null +++ b/exercises/practice/rest-api/rest_api.moon @@ -0,0 +1 @@ +error 'Implement me' \ No newline at end of file diff --git a/exercises/practice/rest-api/rest_api_spec.moon b/exercises/practice/rest-api/rest_api_spec.moon new file mode 100644 index 0000000..f2e42e2 --- /dev/null +++ b/exercises/practice/rest-api/rest_api_spec.moon @@ -0,0 +1,332 @@ +RestApi = require 'rest_api' + +describe 'rest-api', -> + assert\set_parameter "TableFormatLevel", 4 + + describe 'user management', -> + it 'no users', -> + database = { + users: {} + } + api = RestApi database + result = api\GET '/users' + expected = { + users: {} + } + assert.are.same expected, result + + pending 'add user', -> + database = { + users: {} + } + api = RestApi database + payload = { + user: "Adam" + } + result = api\POST '/add', payload + expected = { + owed_by: {} + balance: 0.0 + owes: {} + name: "Adam" + } + assert.are.same expected, result + + pending 'get single user', -> + database = { + users: {{ + owed_by: {} + balance: 0.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: 0.0 + owes: {} + name: "Bob" + }} + } + api = RestApi database + payload = { + users: {"Bob"} + } + result = api\GET '/users', payload + expected = { + users: {{ + owed_by: {} + balance: 0.0 + owes: {} + name: "Bob" + }} + } + assert.are.same expected, result + + describe 'iou', -> + pending 'both users have 0 balance', -> + database = { + users: {{ + owed_by: {} + balance: 0.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: 0.0 + owes: {} + name: "Bob" + }} + } + api = RestApi database + payload = { + borrower: "Bob" + lender: "Adam" + amount: 3.0 + } + result = api\POST '/iou', payload + expected = { + users: {{ + owed_by: { + Bob: 3.0 + } + balance: 3.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: -3.0 + owes: { + Adam: 3.0 + } + name: "Bob" + }} + } + assert.are.same expected, result + + pending 'borrower has negative balance', -> + database = { + users: {{ + owed_by: {} + balance: 0.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: -3.0 + owes: { + Chuck: 3.0 + } + name: "Bob" + }, { + owed_by: { + Bob: 3.0 + } + balance: 3.0 + owes: {} + name: "Chuck" + }} + } + api = RestApi database + payload = { + borrower: "Bob" + lender: "Adam" + amount: 3.0 + } + result = api\POST '/iou', payload + expected = { + users: {{ + owed_by: { + Bob: 3.0 + } + balance: 3.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: -6.0 + owes: { + Chuck: 3.0 + Adam: 3.0 + } + name: "Bob" + }} + } + assert.are.same expected, result + + pending 'lender has negative balance', -> + database = { + users: {{ + owed_by: {} + balance: 0.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: -3.0 + owes: { + Chuck: 3.0 + } + name: "Bob" + }, { + owed_by: { + Bob: 3.0 + } + balance: 3.0 + owes: {} + name: "Chuck" + }} + } + api = RestApi database + payload = { + borrower: "Adam" + lender: "Bob" + amount: 3.0 + } + result = api\POST '/iou', payload + expected = { + users: {{ + owed_by: {} + balance: -3.0 + owes: { + Bob: 3.0 + } + name: "Adam" + }, { + owed_by: { + Adam: 3.0 + } + balance: 0.0 + owes: { + Chuck: 3.0 + } + name: "Bob" + }} + } + assert.are.same expected, result + + pending 'lender owes borrower', -> + database = { + users: {{ + owed_by: {} + balance: -3.0 + owes: { + Bob: 3.0 + } + name: "Adam" + }, { + owed_by: { + Adam: 3.0 + } + balance: 3.0 + owes: {} + name: "Bob" + }} + } + api = RestApi database + payload = { + borrower: "Bob" + lender: "Adam" + amount: 2.0 + } + result = api\POST '/iou', payload + expected = { + users: {{ + owed_by: {} + balance: -1.0 + owes: { + Bob: 1.0 + } + name: "Adam" + }, { + owed_by: { + Adam: 1.0 + } + balance: 1.0 + owes: {} + name: "Bob" + }} + } + assert.are.same expected, result + + pending 'lender owes borrower less than new loan', -> + database = { + users: {{ + owed_by: {} + balance: -3.0 + owes: { + Bob: 3.0 + } + name: "Adam" + }, { + owed_by: { + Adam: 3.0 + } + balance: 3.0 + owes: {} + name: "Bob" + }} + } + api = RestApi database + payload = { + borrower: "Bob" + lender: "Adam" + amount: 4.0 + } + result = api\POST '/iou', payload + expected = { + users: {{ + owed_by: { + Bob: 1.0 + } + balance: 1.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: -1.0 + owes: { + Adam: 1.0 + } + name: "Bob" + }} + } + assert.are.same expected, result + + pending 'lender owes borrower same as new loan', -> + database = { + users: {{ + owed_by: {} + balance: -3.0 + owes: { + Bob: 3.0 + } + name: "Adam" + }, { + owed_by: { + Adam: 3.0 + } + balance: 3.0 + owes: {} + name: "Bob" + }} + } + api = RestApi database + payload = { + borrower: "Bob" + lender: "Adam" + amount: 3.0 + } + result = api\POST '/iou', payload + expected = { + users: {{ + owed_by: {} + balance: 0.0 + owes: {} + name: "Adam" + }, { + owed_by: {} + balance: 0.0 + owes: {} + name: "Bob" + }} + } + assert.are.same expected, result diff --git a/exercises/shared/templates/spec_generator.moon b/exercises/shared/templates/spec_generator.moon index 2724a76..7bd104e 100644 --- a/exercises/shared/templates/spec_generator.moon +++ b/exercises/shared/templates/spec_generator.moon @@ -1,6 +1,5 @@ --- helper functions can go here at the top, like: -int_list = (list) -> "{#{table.concat list, ', '}}" --- see `exercises/test_helpers.md` for some examples +test_helpers = require 'test_helpers' +import int_list, word_list from test_helpers { module_name: '${camel_slug}', @@ -31,4 +30,5 @@ int_list = (list) -> "{#{table.concat list, ', '}}" "Bonus tests are indented 6 spaces" } ]] + } diff --git a/exercises/test_helpers.md b/exercises/test_helpers.md index 72e2eb4..fafae98 100644 --- a/exercises/test_helpers.md +++ b/exercises/test_helpers.md @@ -13,183 +13,66 @@ These functions are exported from generate-spec for use in spec_generator module ## Helper functions Useful for generating pretty tables mostly. +The functions are stored in `/lib/test_helpers.md` -### List of ints +An example: ```moonscript -int_list = (list) -> "{#{table.concat list, ', '}}" +test_helpers = require 'test_helpers' +import int_list, word_list from test_helpers +... +generate_test: (case, level) -> + lines = { + "input = #{int_list case.input.numbers}" + "expected = #{word_list case.expected}" + ... ``` -Used in: all-your-base - -### List of lists of ints - -```moonscript -int_lists = (lists, level) -> - if #lists == 1 - "{#{int_list lists[1]}}" - else - rows = [indent int_list(row) .. ',', level + 1 for row in *lists] - table.insert rows, 1, '{' - table.insert rows, indent '}', level - table.concat rows, '\n' -``` - -Used in: saddle-points, spiral-matrix - -### List of strings - -```moonscript -string_list = (list) -> - "{#{table.concat [quote word for word in *list], ', '}}" +## Comparing tables deeply + +The `assert.are.same t1, t2` assertion is used to compare tables deeply. +However when they don't match, by default busted does not show the whole object, which makes it hard for the student: "What is different?" + +```shnone +Failure → ./rest_api_spec.moon @ 251 +rest-api iou lender owes borrower less than new loan +...uarocks/lib/luarocks/rocks-5.4/busted/2.3.0-1/bin/busted:7: Expected objects to be the same. +Passed in: +(table: 0x641e0548a540) { + *[users] = { + [1] = { + [balance] = 1.0 + [name] = 'Adam' + [owed_by] = { ... more } + [owes] = { } } + *[2] = { + [balance] = -1.0 + [name] = 'Bob' + [owed_by] = { } + *[owes] = { ... more } } } } +Expected: +(table: 0x641e0548a620) { + *[users] = { + [1] = { + [balance] = 1.0 + [name] = 'Adam' + [owed_by] = { ... more } + [owes] = { } } + *[2] = { + [balance] = -1.0 + [name] = 'Bob' + [owed_by] = { } + *[owes] = { ... more } } } } ``` -The `quote` function is defined in bin/generate-spec - -Used in: anagram -### Indented multi-line list of strings +The solution here is to configure the `assert` object,. +In the spec_generator.moon module, add this (adjust the value `4` as needed): -```moonscript -string_list = (list, level) -> - if #list <= 2 - "{#{table.concat [quote elem for elem in *list], ', '}}" - else - lines = [indent quote(elem) .. ',', level + 1 for elem in *list] - table.insert lines, 1, '{' - table.insert lines, indent('}', level) - table.concat lines, '\n' -``` -The `indent` function is defined in bin/generate-spec - -This returns a multi-line string without the first line indented. -The returned string will be added to another string in the test case body, -and then that string (without regard to internal newlines) will later be indented. - -Used in: forth, transpose - -### Key-Value list - -```moonscript -kv_table = (tbl, level) -> - lines = {'{'} - for k, v in pairs tbl - key = if k\match('^%a%w*$') then k else "[#{quote k}]" - table.insert lines, indent "#{key}: #{v},", level + 1 - table.insert lines, indent '}', level - table.concat lines, '\n' -``` - -Used in: word-count - -### key-value table as a one-line string - -```moonscript -table.tostring = (t) -> - s = [string.format '%s: %q', k, v for k, v in pairs t] - "{#{table.concat s, ', '}}" -``` - -Reminder: order of keys in indeterminate - -Used in: meetup - -### Show strings with escapes, when you want to keep the `\t`, `\n` in the test case. - -```moonscript -json = require 'dkjson' --- and then - "result = Bob.hey #{json.encode case.input.heyBob}", -``` -```moonscript -json = require 'dkjson' -json_string = (s) -> json.encode s -``` - -Used in: bob, matrix - -### Table contains an element - -```moonscript -table_contains = (list, target) -> - for elem in *list - return true if elem == target - false -``` - -Used in: forth - -### Wrapped list of ints - -sieve has a test with loads of numbers. -This _should_ work fine on Mac or Linux ("Works For Me™) - -```moonscript -formatted = (list, level) -> - joined = table.concat list, ', ' - cmd = "echo '#{joined}' | fold -s -w 76" - fh = io.popen cmd, 'r' - return "{#{joined}}" if not fh - - lines = [indent(line\gsub('%s+$', ''), level + 1) for line in fh\lines!] - - result = {fh\close!} - lines = {joined} if not result[1] - - if #lines == 1 - "{#{lines[1]\gsub('^%s+', '')}}" - else - table.insert lines, 1, "{" - table.insert lines, (indent "}", level) - table.concat lines, '\n' -``` - -### Arbitrarily deep nested tables - -The sgf-parsing exercises produces recursively nested structures. -I came up with this (which could probably be refactored) - -```moonscript -json = require 'dkjson' -json_string = (s) -> json.encode s - -is_sequence = (t) -> - return false if type(t) != 'table' - size = 0 - size += 1 for k, _ in pairs t when k != 'n' - size == #t - -is_empty = (t) -> not next t - --- mostly taken from: --- https://github.com/leafo/moonscript/blob/7b7899741c6c1e971e436d36c9aabb56f51dc3d5/moonscript/util.moon#L58 -to_string = (what, level = 0) -> - seen = {} - _dump = (what, depth = 0) -> - t = type what - if t == 'string' then - json_string what - elseif t != 'table' then - tostring what - else - if seen[what] then - return "" - seen[what] = true - if is_sequence what then - return "{" .. table.concat([to_string(v, level + depth + 1) for v in *what], ", ") .. "}" - - depth += 1 - lines = for k,v in pairs what do - key = if type(k) == 'number' then "[#{k}]" else k - (' ')\rep(depth * 2) .. "#{key}: " .. _dump(v, depth) - seen[what] = false - val = if not is_empty lines - table.concat [indent(line, level) .. "\n" for line in *lines] - else - "" - class_name = if type(what.__class) == 'table' and type(what.__class.__name) == 'string' - "<#{what.__class.__name}>" - "#{class_name or ""}{\n#{val}#{indent '}', level + depth - 1}" - _dump what +```none + -- we have deep tables to compare, display it all when not the same + test_helpers: [[ + assert\set_parameter "TableFormatLevel", 4 +]] ``` ## Custom assertions @@ -197,4 +80,3 @@ to_string = (what, level = 0) -> - dnd-character: `assert.between value, min, max` - space-age: `assert.approx_equal #{case.expected}, result` - word-count: `assert.has.same_kv result, expected` - diff --git a/lib/test_helpers.moon b/lib/test_helpers.moon new file mode 100644 index 0000000..6222578 --- /dev/null +++ b/lib/test_helpers.moon @@ -0,0 +1,138 @@ +json = require 'dkjson' + +--- ----------------------------------------------------------------------- +--- List of ints +int_list = (list) -> "{#{table.concat list, ', '}}" + +--- List of lists of ints +int_lists = (lists, level) -> + if #lists == 1 + "{#{int_list lists[1]}}" + else + rows = [indent int_list(row) .. ',', level + 1 for row in *lists] + table.insert rows, 1, '{' + table.insert rows, indent '}', level + table.concat rows, '\n' + +--- Wrapped list of ints +-- Example, sieve has a test with loads of numbers. +-- This _should_ work fine on Mac or Linux ("Works For Me™) +int_list_wrapped = (list, level) -> + error 'Provide a level for `int_list_wrapped`', 2 if not level + joined = table.concat list, ', ' + cmd = "echo '#{joined}' | fold -s -w 76" + fh = io.popen cmd, 'r' + return "{#{joined}}" if not fh + + lines = [indent(line\gsub('%s+$', ''), level + 1) for line in fh\lines!] + + result = {fh\close!} + lines = {joined} if not result[1] + + if #lines == 1 + "{#{lines[1]\gsub('^%s+', '')}}" + else + table.insert lines, 1, "{" + table.insert lines, (indent "}", level) + table.concat lines, '\n' + +--- ----------------------------------------------------------------------- +--- List of strings +word_list = (list) -> + "{#{table.concat [quote word for word in *list], ', '}}" + +--- Indented multi-line list of strings +-- This returns a multi-line string without the first line indented. +-- The returned string will be added to another string in the test case body, +-- and then that string (without regard to internal newlines) will later be indented. +string_list = (list, level) -> + error 'Provide a level for `string_list`', 2 if not level + if #list <= 2 + "{#{table.concat [quote elem for elem in *list], ', '}}" + else + lines = [indent quote(elem) .. ',', level + 1 for elem in *list] + table.insert lines, 1, '{' + table.insert lines, indent('}', level) + table.concat lines, '\n' + +--- Show strings with escapes, when you want to keep the `\t`, `\n` in the test case. +json_string = (s) -> json.encode s + +--- ----------------------------------------------------------------------- +--- Key-Value list +kv_table = (tbl, level) -> + error 'Provide a level for `kv_table`', 2 if not level + lines = {'{'} + for k, v in pairs tbl + key = if k\match('^%a%w*$') then k else "[#{quote k}]" + table.insert lines, indent "#{key}: #{v},", level + 1 + table.insert lines, indent '}', level + table.concat lines, '\n' + +--- key-value table as a one-line string +-- Reminder: order of keys in indeterminate +table_tostring = (t) -> + s = [string.format '%s: %q', k, v for k, v in pairs t] + "{#{table.concat s, ', '}}" + +--- Table contains an element +table_contains = (list, target) -> + for elem in *list + return true if elem == target + false + +--- ----------------------------------------------------------------------- +--- Arbitrarily deep nested tables + +is_sequence = (t) -> + return false if type(t) != 'table' + size = 0 + size += 1 for k, _ in pairs t when k != 'n' + size == #t + +is_empty = (t) -> not next t + +-- mostly taken from: +-- https://github.com/leafo/moonscript/blob/7b7899741c6c1e971e436d36c9aabb56f51dc3d5/moonscript/util.moon#L58 +table_dump = (what, level = 0) -> + seen = {} + _dump = (what, depth = 0) -> + t = type what + if t == 'string' then + json_string what + elseif t != 'table' then + tostring what + else + if seen[what] then + return "" + seen[what] = true + if is_sequence what then + return "{" .. table.concat([table_dump(v, level + depth + 1) for v in *what], ", ") .. "}" + + depth += 1 + lines = for k,v in pairs what do + key = if type(k) == 'number' then "[#{k}]" else k + (' ')\rep(depth * 2) .. "#{key}: " .. _dump(v, depth) + seen[what] = false + val = if not is_empty lines + table.concat [indent(line, level) .. "\n" for line in *lines] + else + "" + class_name = if type(what.__class) == 'table' and type(what.__class.__name) == 'string' + "<#{what.__class.__name}>" + "#{class_name or ""}{\n#{val}#{indent '}', level + depth - 1}" + _dump what + +--- ----------------------------------------------------------------------- +{ + :int_list + :int_lists + :int_list_wrapped + :word_list + :string_list + :kv_table + :table_tostring + :json_string + :table_contains + :table_dump +}