{"id":21526947,"url":"https://github.com/lambdaclass/erlang-katana","last_synced_at":"2025-04-09T23:34:10.101Z","repository":{"id":19411206,"uuid":"22653264","full_name":"lambdaclass/erlang-katana","owner":"lambdaclass","description":":ok_hand: erlang grab bag of useful functions. it should have been called swiss army knife but katanas are more deadlier ;)","archived":false,"fork":false,"pushed_at":"2017-10-13T16:51:30.000Z","size":352,"stargazers_count":62,"open_issues_count":3,"forks_count":19,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-04T13:54:21.909Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lambdaclass.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-08-05T17:46:22.000Z","updated_at":"2025-03-08T11:51:02.000Z","dependencies_parsed_at":"2022-09-05T16:00:24.375Z","dependency_job_id":null,"html_url":"https://github.com/lambdaclass/erlang-katana","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaclass%2Ferlang-katana","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaclass%2Ferlang-katana/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaclass%2Ferlang-katana/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaclass%2Ferlang-katana/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lambdaclass","download_url":"https://codeload.github.com/lambdaclass/erlang-katana/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248130154,"owners_count":21052707,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-24T01:47:19.322Z","updated_at":"2025-04-09T23:34:10.065Z","avatar_url":"https://github.com/lambdaclass.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"erlang katana\n======\n\n![samurai](https://raw.githubusercontent.com/unbalancedparentheses/katana/master/images/samurai.jpg)\n\nEven if you love Erlang as we do, from time to time you might ask yourself why\nsome functions you normally find in other languages are not part of the erlang's\nstandard library. When you ask yourself that type of question you should\nremember that an estimated 2 million people are currently working in COBOL and\n1.5 million new lines of COBOL code are written every day. After feeling bad for\nthose developers, you should send a pull request to erlang katana with the\nfunctions you use on a daily basis.\n\nTo sum up this is a grab bag of useful functions (ideally).\n![grabbag](https://raw.githubusercontent.com/unbalancedparentheses/erlang-katana/master/images/bagofcat.jpg)\n\n# Contact Us\n\nIf you find any **bugs** or have a **problem** while using this library, please\n[open an issue](https://github.com/inaka/erlang-katana/issues/new) in this repo\n(or a pull request :)).\n\nAnd you can check all of our open-source projects at\n[inaka.github.io](http://inaka.github.io)\n\n#Objective\n- [20 cool Clojure functions](https://daveyarwood.github.io/2014/07/30/20-cool-clojure-functions/)\n- [Prismatic's Clojure utility belt](https://github.com/Prismatic/plumbing)\n\n## Related Projects\nCheck out as well [katana-code](https://github.com/inaka/katana-code) and [katana-test](https://github.com/inaka/katana-test)\n\n## Included goodies:\n\n* `ktn_date`: functions useful for handling dates and time values.\n* `ktn_debug`: functions useful for debugging.\n* `ktn_fsm`: a _nice_ wrapper on top of `gen_fsm`\n* `ktn_json`: functions useful for processing \u0026 creating JSON.\n* `ktn_maps`: functions useful for handling maps.\n* `ktn_numbers`: functions useful for processing numeric values.\n* `ktn_recipe`: A tool to structure code that consists of sequential steps in which decisions are made.\n* `ktn_rpc`: functions useful for RPC mechanisms.\n* `ktn_task`: functions useful for managing asyncronous tasks.\n* `ktn_user_default`: useful functions for your erlang shell.\n\n### `ktn_user_default`\n\nThis module contains functions that are nice to have in your user default\nmodule, and thereby added to your shell. To use them, copy the ones you want\ninto your `~/user_default.erl` module.\n\n### `ktn_test_utils`\n\nThe Katana Test Utilities includes two functions useful for testing REST APIs:\n`test_response/4` and `assert_response/4`.\n\n##### assert_response(Test, MatchType, Params, Response)\n\n`assert_response/4` uses `test_response/4` to check that a given assertion\nholds. It's usage is identical to `test_response/4`, except that it fails when\nthe assertion fails.\n\n##### test_response(Test, MatchType, Params, Response)\n\n`test_response/3` provides some useful checks regarding request responses. The\ncall to test itself does not fail, `test_response` will return ok if the test\npassed and {error, Reason} when it does not.\n\nTo have it fail on the same test use `assert_response/4`. `test_response/4` is\nintended to be called as:\n\n```erlang\nok = test_response(status, Response, \"201\"),\n{error, {nomatch, \"204\", _}} = test_response(status, partial, Response, \"40?\"),\n```\n\nThe first argument is what part of the response must be tested, and may be one\nof: `status`, `headers` or `body`.\n\nThe second argument must be a map with at least three keys:\n- `status`, the reponse status\n- `headers`, the list of headers in the response\n- `body`, the response body\n\nThe status test matches the response status agains some regex pattern. It has a\nshort form for specifying return codes: the third argument must be a string,\neither representing the exact return code, a string representing a partial\nreturn code (using the ? character as a wildcard, e.g. \"20?\", \"4??\"), or a\nregular expression as processed by the re module. You cannot specify patterns\nwith a ? in the second digit position, e.g. \"2?1\".\n\nIf the status test fails, it returns {error, {nomatch, Pattern, Status}} where\nPattern is the provided pattern to match agains and Status is the response\nstatus.\n\nThe headers test compares a list of headers agains those present in a response.\nThe comparison can check whether the set of headers provided matches exactly\n(with exact) or if it is a subset of the response headers (with partial). The\nheaders list of is a list of {Header, Value} tuples. Note that the elements in\nboth headers lists do not have to be in the same order, nor have the same case.\n\nIf the test should fail, the possible error values returned are:\n- {missing_headers, Headers, ResHeaders} where Headers is the list\n of headers missing from the set of headers in the response ResHeaders.\n- {nomatch, Headers, ResHeaders} if the test type was exact and the two sets\n of headers do not contain the same elements.\n\nThe body test's third argument may specify whether the body must match a\nprovided regex ({partial, Pattern}) or match a given body exactly ({match,\nText}).\n\nPossible error values for the body assert are:\n- {regex_compile_fail, Error}} if the regular expression fails to compile.\n- {error, {nomatch, Pattern, Body}} if the body does not match the regex.\n- {nomatch, ResBody, Body}} if the body does not match the text.\n\n\n### `ktn_recipe`\n\n#### What is a recipe?\n\n*Recipe* (noun): A set of conditions and parameters of an industrial process\nto obtain a given result.\n\nA recipe is a series of steps to obtain a result. This word was chosen\nbecause 'procedure' is overloaded in computer sciences, and it is not\nexactly a finite state machine.\n\nktn_recipe arose from the need to restructure code implementing large\napplication business logic decision trees.\nLong functions, deeply nested case expressions, code with several\nresponsibilities, all make for unreadable and unmaintainable code.\nEvery time one needs to make a change or add a feature, one must think about\nthe non-obvious data flow, complex logic, possible side effects.\nExceptions pile upon exceptions, until the code is a house of cards.\nIf one is lucky (or responsible), one has comprehensive test suites covering\nall cases.\n\nA better way to structure the code would be preferable, one that makes the\nflow obvious and separates responsibilities into appropriate code segments.\nOne way is a finite state machine. OTP even has a behaviour for exactly this\npurpose. However, gen_fsm does not exactly fit our needs:\nTo begin with, gen_fsms run in their own process, which may not be what is\nneeded. Second, logic flow is not immediately obvious because the fsm's\nstate depends on the result of each state function. Finally, a gen_fsm\ntransitions only on receiving input, whereas we are looking for something\nthat runs like normal code, \"on its own\". So, our fsm will be defined by\nsomething like a transition table, a single place you can look at and know\nhow it executes. This specification will \"drive\" the recipe.\n\nThe most common case envisioned is a sequential series of steps, each of\nwhich makes a decision affecting later outcomes, so our design should be\noptimized for a single, linear execution flow, allowing for writing the\nminimum amount of code for this particular case. Keep the common case fast.\n\n#### How do I use it?\n\nThe simplest use case: create a module using the ktn_recipe behaviour.\nIt must export `transitions/0`, `process_result/1`, `process_error/1` and a\nseries of unary functions called step functions.\n`transitions/0` must return a list of atoms naming the step functions, in the\norder in which they must be called. Each step function takes a State\nvariable as input and if the step was succesful emits `{ok, NewState}` with\nthe updated state, or `{error, NewState}` if it was unsuccessful.\nAfter running all the steps, process_result will take the resulting state\nas input and should emit whatever you need.\nIf one of the state functions returns `{error, NewState}`, then\n`process_error/1` will be called taking the resulting state as input and\nshould handle all expected error conditions.\n\nTo run the recipe, call `ktn_recipe:run(CALLBACK_MODULE, INITIAL_STATE)`.\n\nFor more advanced uses, continue reading.\n\n#### How does it work?\n\nThe main functions are `ktn_recipe:run/2-4`. These will run a recipe and\nreturn the result.\n\nRecipes may be specified in two ways, which we call \"implicit\" and\n\"explicit\", for lack of better words. In implicit mode, we pass `run/2` a\ncallback module implementing the ktn_recipe behavior, which implements all\nnecessary functions. In explicit mode, we explicitly give `run/4` the\ntransition table, a function to process the state resulting from running all\nrecipe steps to completion, a function to process erroneous states, and the\ninitial state of the recipe.\n\n#### Step functions\n\nStep functions have the following type:\n\n```erlang\n-type state()  :: term().\n-type output() :: term().\n-spec Step(state()) -\u003e {ok, state()}\n                    | {output(), state()}\n                    | {error, state()}\n                    | {halt, state()}.\n```\n\nThe recipe state may be any value you need, a list, a proplist, a record, a\nmap, etc. ktn_recipe does not use maps itself, although the test suite does.\n\nThe initial state passed to `run/2-4` will be passed to the first step as-is,\nand the subsequent resulting states will be fed to each step.\n\nThe step functions should, if possible, have no side effects. If so, and the\nrecipe ends in an error state, it can be aborted without worrying about\nrolling back the side effects.\n\nFor example, if the recipe involves making changes in a database, these\nshould be made in the `process_result/1` function, once it is certain that\nall steps completed successfully.\n\n#### Result and Error processing\n\nIn implicit mode, the module must export `process_error/1` and\n`process_result/1`; in explicit mode you may pass whichever function you\ndesire.\nThe purpose of the 'result processing function' is to transform the\nresulting state into a usable value.\nThe purpose of the 'error processing function' is to extract the error value\nfrom the state and do something according. If you are unfortunate enough\nto have unavoidable side effects in your step functions, you may undo them\nhere.\n\n#### Specifying transitions\n\nTransition tables are lists.\nAs stated, the simplest transition table is a list of atoms naming the step\nfunctions, but this is not the only way recipe states may be specified.\nA transition table is a list of transitions.\nWhat is permissible as a transition depends on whether we are running in\nimplicit or explicit mode.\n\nIn explicit mode, a transition may be either be an external function, i.e.\n`fun module:function/arity`, or a ternary tuple `{F1, I, F2}` in which the _F1_\nand _F2_ elements are explicit functions and the _I_ is the transition input.\nThis form reprensents a transition from step _F1_ to step _F2_, if _F1_ outputs\n`{Input, State}` instead of `{ok, State}`.\n\nIn implicit mode, in addition to the se two forms of specifying step\nfunctions, an atom may be used. The module in which the function resides is\nassumed to be the provided callback module, hence it is 'implicit'.\n\nAlso as stated, in any mode, the return value of step functions must be one\nof:\n- `{ok, State}`\n- `{Output, State}`\n- `{halt, State}`\n- `{error, State}`\n\nIf `{error, State}` is returned, `run/2-4` will call the error processing\nfunction. If {halt, State} is called, `run/2-4` will call the result\nprocessing function.\nIf `{ok, State}` is returned, run will call the next function in the\ntransition table. That is, it will search the transition table until it\nfinds the current's functions position, if necessary skip over entries\ncorresponding to the current function, and call the first function with a\ndifferent name it encounters.\nIf `{Output, State}` is returned, run will search the transition table for an\nentry matching `{Step, Output, NextStep}`, and select NextStep as the function\nto call. In this way, non-linear recipes that jump ahead or loop back may\nbe specified. It is recommended that the transition table describe a DAG,\nunless the program logic really requires loops.\n\nBefore running a recipe, `run/2-4` will 'normalize' the transition table to\nremove implicit assumptions and so that it is composed only of ternary\ntuples explicitly listing every transition. As a debugging aid, this\nfunction `normalize/1` is exported by the `ktn_recipe` module.\n\n#### Example\n\nSuppose you have a web server with an endpoint `/conversations`, accepting the\n`DELETE` method to delete conversations between users. To delete a\nconversation, one must fetch the converation entity from the database, fetch\nthe contact (the other user in the conversation) specified by the user id\nin the conversation's contact_id attribute, check the contact's status (if\nit is blocked, etc) and also check whether the client is using version 1 of\nour REST API or version 2, since one really deletes the conversation from\nthe database and the other merely clears messages from the conversation.\n\nWe could implement this as a recipe with four steps:\n\n```erlang\ntransitions() -\u003e\n [ get_conversation\n , get_contact\n , get_status\n , check_api_version\n ].\n```\n\nget_conversation fetches it from the database. If it has already been\ndeleted, it could return `{error, NewState}` and store the error in the state.\nLikewise for `get_contact`, and `get_status`.\n`get_api_version` would extract the api version from the request header or\nurl, and store the result in the recipe state. If all steps complete\nsuccessfully, `process_result` will effectively make the call to delete the\nconversation from the database.\n\nUsing ktn_recipes, you could structure your application as:\n\n```\n+----------+   +-------------+   +---------+   +-----------+\n|          |   |             |   |         |   |           |\n| Endpoint +---\u003e Recipes for +---\u003e Entity  +---\u003e Databases |\n| handlers |   | endpoint    |   | modules |   |           |\n|          |   | actions     |   |         |   |           |\n|          |   |             |   |         |   |           |\n+----------+   +-------------+   +---------+   +-----------+\n```\n\nThe handlers would have the sole responsibility of handling the request,\ni. e. parsing URL, header and body parameters, verfying them and invoking\nthe correct recipe.\nRecipes would implement the business logic.\nEntity modules would abstract management of your systems entities, including\nissues such as caching, and eventually persisting the changes to the\nstorage medium or obtaining the required information.\n\n#### What if something goes wrong?\n\nThe function `ktn_recipe:verify/1` takes either a recipe module or an explicit\ntransition table and will run several checks to verify that it will run\ncorrectly. You may use this function to test your transition tables.\nNote that verify/1 is implemented as a `ktn_recipe`, so you can use it as an\nexample.\n\nIf a step throws an exception, it will not be caught. That means you may see\nsome of ktn_recipe's internal functions in the stacktrace. In general, the\nmost common errors will be:\n1) badly formed transition tables\n2) not exporting step functions\n3) bad return values from step functions\n4) bad state values set by functions\n\n1) and 2) should be detected by running verify on your recipe. 3) and 4)\nwill be detected at run time and an appropriate error will be returned by\n`run/2-4`. Of course, your tests should detect these cases. You do have\ntests, right?\n\nFinally, there is one more function, `ktn_recipe:pretty_print/1`, which takes\neither a callback module or a transition table, and prints the normalized\ntransition table.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaclass%2Ferlang-katana","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flambdaclass%2Ferlang-katana","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaclass%2Ferlang-katana/lists"}