{"id":28762326,"url":"https://github.com/inaka/sumo_rest","last_synced_at":"2025-06-17T08:07:54.864Z","repository":{"id":57554785,"uuid":"46345698","full_name":"inaka/sumo_rest","owner":"inaka","description":"Generic cowboy handlers to work with Sumo","archived":false,"fork":false,"pushed_at":"2018-05-15T13:05:41.000Z","size":207,"stargazers_count":59,"open_issues_count":11,"forks_count":10,"subscribers_count":39,"default_branch":"master","last_synced_at":"2025-05-21T03:40:03.770Z","etag":null,"topics":["cowboy","erlang","sumo-db","sumo-rest"],"latest_commit_sha":null,"homepage":"http://inaka.github.io/sumo_rest/","language":"Erlang","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/inaka.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":"2015-11-17T12:29:06.000Z","updated_at":"2024-01-10T12:22:12.000Z","dependencies_parsed_at":"2022-09-26T18:51:24.339Z","dependency_job_id":null,"html_url":"https://github.com/inaka/sumo_rest","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/inaka/sumo_rest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inaka%2Fsumo_rest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inaka%2Fsumo_rest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inaka%2Fsumo_rest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inaka%2Fsumo_rest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/inaka","download_url":"https://codeload.github.com/inaka/sumo_rest/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/inaka%2Fsumo_rest/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260318683,"owners_count":22991121,"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":["cowboy","erlang","sumo-db","sumo-rest"],"created_at":"2025-06-17T08:07:53.935Z","updated_at":"2025-06-17T08:07:54.855Z","avatar_url":"https://github.com/inaka.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sumo Rest\n\n[![Build Status](https://travis-ci.org/inaka/sumo_rest.svg?branch=master)](https://travis-ci.org/inaka/sumo_rest)\n\n\u003cimg src=\"http://www.technovelgy.com/graphics/content/sumo_robot.jpg\" align=\"right\" style=\"float:right\" height=\"400\" /\u003e\n\nGeneric **Cowboy** handlers to work with **Sumo DB**\n\n## Introduction\nWe, at Inaka, build our RESTful servers on top of [cowboy](https://github.com/ninenines/cowboy). We use [sumo_db](https://github.com/inaka/sumo_db) to manage our persistence and [trails](https://github.com/inaka/cowboy-trails) together with [cowboy-swagger](https://github.com/inaka/cowboy-swagger) for documentation.\n\nSoon enough, we realized that we were duplicating code everywhere. Not every endpoint in our APIs is just a CRUD for some entity, but there are definitely lots of them in every server. As an example, most of our servers provide something like the following list of endpoints:\n\n* `GET /users` - Returns the list of users\n* `POST /users` - Creates a new user\n* `PUT /users/:id` or `PATCH /users/:id` - Updates a user\n* `DELETE /users/:id` - Deletes a user\n* `GET /users/:id` - Retrieves an individual user\n\nTo avoid (or at least reduce) such duplication, we started using [mixer](https://github.com/chef/mixer). That way, we can have a *base_handler* in each application where all the common handler logic lives.\n\nEventually, all applications shared that same *base_handler*, so we decided to abstract that even further. Into its own app: **sumo_rest**.\n\n## Architecture\nThis project dependency tree is a great way to show the architecture behind it.\n\n![Architecture](https://docs.google.com/drawings/d/1mlJTIxd7mH_48hcWmip_zW6rfzglbmSprpGSsfhjcsM/pub?w=367\u0026amp;h=288)\n\nAs you'll see below, **Sumo Rest** gives you _base handlers_ that you can use on your **Cowboy** server to manage your **Sumo DB** entities easily. You just need to define your routes using **Trails** and provide proper metadata for each of them. In particular, you need to provide the same basic metadata **Swagger** requires. You can manually use the base handlers and call each of their functions when you need them, but you can also use **Mixer** to just _bring_ their functions to your own handlers easily.\n\n## Usage\nIn a nutshell, **Sumo Rest** provides 2 cowboy rest handlers:\n\n- [`sr_entities_handler`](src/sr_entities_handler.erl) that provides an implementation for\n    + `POST /entities` - to create a new entity\n    + `GET /entitites` - to retrieve the list of all entities\n- [`sr_single_entity_handler`](src/sr_single_entity_handler.erl) that provides implementation for\n    + `GET /entities/:id` - to retrieve an entity\n    + `PUT /entities/:id` - to update (or create) an entity\n    + `PATCH /entities/:id` - to update an entity\n    + `DELETE /entities/:id` - to delete an entity\n\n(Of course, the uris for those endpoints will not be exactly those, you have to define what _entities_ you want to manage.)\n\nTo use them you first have to define your models, by implementing the behaviours `sumo_doc` (from **Sumo DB**) and [`sumo_rest_doc`](src/sumo_rest_doc.erl).\n\nThen you have to create a module that implements the `trails_handler` behaviour (from **Trails**) and _mix in_ that module all the functions that you need from the provided handlers.\n\n## A Basic Example\nYou can find a very basic example of the usage of this app in the [tests](test/sr_test).\n\nThe app used for the tests (`sr_test`), makes no sense at all. Don't worry about that. It's just there to provide examples of usage (and of course to run the tests). It basically manages 2 totally independent entities:\n- _elements_: members of an extremely naïve key/value store\n- _sessions_: poorly-designed user sessions :trollface:\n\nLet me walk you through the process of creating such a simple app.\n\n### The application definition\nIn [sr_test.app](test/sr_test.app) file you'll find the usual stuff. The only particular pieces are:\n\n* The list of `applications`, which includes `cowboy`, `katana`, `cowboy_swagger` and `sumo_db`.\n* The list of `start_phases`. This is not a requirement, but we've found this is a nice way of getting **Sumo DB** up and running before **Cowboy** starts listening:\n```erlang\n  { start_phases\n  , [ {create_schema, []}\n    , {start_cowboy_listeners, []}\n    ]\n  }\n```\n\n### The configuration\nIn [test.config](test/test.config) we added the required configuration for the different apps to work:\n\n#### Swagger\nWe just defined the minimum required properties:\n```erlang\n, { cowboy_swagger\n  , [ { global_spec\n      , #{ swagger =\u003e \"2.0\"\n         , info =\u003e #{title =\u003e \"SumoRest Test API\"}\n         , basePath =\u003e \"\"\n         }\n      }\n    ]\n  }\n```\n\n#### Mnesia\nWe've chosen **Mnesia** as our backend, so we just enabled debug on it (not a requirement, but a nice thing to have on development environments):\n```erlang\n, { mnesia\n  , [{debug, true}]\n  }\n```\n\n#### Sumo DB\n**Sumo DB**'s **Mnesia** backend/store is really easy to set up. We will just have 2 models: _elements_ and _sessions_. We will store them both on **Mnesia**:\n```erlang\n, { sumo_db\n  , [ {wpool_opts, [{overrun_warning, 100}]}\n    , {log_queries, true}\n    , {query_timeout, 30000}\n    , {storage_backends, []}\n    , {stores, [{sr_store_mnesia, sumo_store_mnesia, [{workers, 10}]}]}\n    , { docs\n      , [ {elements, sr_store_mnesia, #{module =\u003e sr_elements}}\n        , {sessions, sr_store_mnesia, #{module =\u003e sr_sessions}}\n        ]\n      }\n    , {events, []}\n    ]\n  }\n```\n\n#### SR Test\nFinally we add some extremely naïve configuration to our own app. In our case, just a list of users we'll use for authentication purposes (:warning: **Do NOT do this at home, kids** :warning:):\n```erlang\n, { sr_test\n  , [ {users, [{\u003c\u003c\"user1\"\u003e\u003e, \u003c\u003c\"pwd1\"\u003e\u003e}, {\u003c\u003c\"user2\"\u003e\u003e, \u003c\u003c\"pwd2\"\u003e\u003e}]}\n    ]\n  }\n```\n\n### The application module\nThe next step is to come up with the main application module: [sr_test](test/sr_test/sr_test.erl). The interesting bits are all in the start phases.\n\n#### `create_schema`\nFor **Sumo DB** to work, we just need to make sure we create the schema. We need to do a little trick to setup **Mnesia** though, because for `create_schema` to properly work, **Mnesia** has to be stopped:\n```erlang\nstart_phase(create_schema, _StartType, []) -\u003e\n  _ = application:stop(mnesia),\n  Node = node(),\n  case mnesia:create_schema([Node]) of\n    ok -\u003e ok;\n    {error, {Node, {already_exists, Node}}} -\u003e ok\n  end,\n  {ok, _} = application:ensure_all_started(mnesia),\n  sumo:create_schema();\n```\n\n#### `start_cowboy_listeners`\nSince we're using **Trails**, we can let each module define its own ~~routes~~ trails. And, since we're using a single host we can use the fancy helper that comes with **Trails**:\n```erlang\n  Handlers =\n    [ sr_elements_handler\n    , sr_single_element_handler\n    , sr_sessions_handler\n    , sr_single_session_handler\n    , cowboy_swagger_handler\n    ],\n  Routes = trails:trails(Handlers),\n  trails:store(Routes),\n  Dispatch = trails:single_host_compile(Routes),\n```\nIt's crucial that we _store_ the trails. Otherwise, **Sumo Rest** will not be able to find them later.\n\nThen, we start our **Cowboy** server:\n```erlang\n  TransOpts = [{port, 4891}],\n  ProtoOpts = %% cowboy_protocol:opts()\n    [{compress, true}, {env, [{dispatch, Dispatch}]}],\n  case cowboy:start_http(sr_test_server, 1, TransOpts, ProtoOpts) of\n    {ok, _} -\u003e ok;\n    {error, {already_started, _}} -\u003e ok\n  end.\n```\n\n### The Models\nThe next step is to define our models (i.e. the entities our system will manage). We use a module for each model and all of them implement the required behaviours.\n\n#### Elements\n[Elements](test/sr_test/sr_elements.erl) are simple key/value pairs.\n```erlang\n-type key() :: integer().\n-type value() :: binary() | iodata().\n\n-opaque element() ::\n  #{ key        =\u003e key()\n   , value      =\u003e value()\n   , created_at =\u003e calendar:datetime()\n   , updated_at =\u003e calendar:datetime()\n   }.\n```\n\n`sumo_doc` requires us to add the schema, sleep and wakeup functions. Since we'll use maps for our internal representation (just like **Sumo DB** does), they're trivial:\n```erlang\n-spec sumo_schema() -\u003e sumo:schema().\nsumo_schema() -\u003e\n  sumo:new_schema(elements,\n    [ sumo:new_field(key,        string,   [id, not_null])\n    , sumo:new_field(value,      string,   [not_null])\n    , sumo:new_field(created_at, datetime, [not_null])\n    , sumo:new_field(updated_at, datetime, [not_null])\n    ]).\n\n-spec sumo_sleep(element()) -\u003e sumo:doc().\nsumo_sleep(Element) -\u003e Element.\n\n-spec sumo_wakeup(sumo:doc()) -\u003e element().\nsumo_wakeup(Element) -\u003e Element.\n```\n\n`sumo_rest_doc` on the other hand requires functions to convert to and from json (which should also validate user input):\n```erlang\n-spec to_json(element()) -\u003e sumo_rest_doc:json().\nto_json(Element) -\u003e\n  #{ key        =\u003e maps:get(key, Element)\n   , value      =\u003e maps:get(value, Element)\n   , created_at =\u003e sr_json:encode_date(maps:get(created_at, Element))\n   , updated_at =\u003e sr_json:encode_date(maps:get(updated_at, Element))\n   }.\n```\n\nIn order to convert from json we have two options: `from_json` or `from_ctx`. The difference is `from_json` accepts only a json body as a parameter, `from_ctx` receive a `context` structure which has the entire request and handler state besides the json body. We will see a `from_ctx` example in `sessions` section\n```erlang\n-spec from_json(sumo_rest_doc:json()) -\u003e {ok, element()} | {error, iodata()}.\nfrom_json(Json) -\u003e\n  Now = sr_json:encode_date(calendar:universal_time()),\n  try\n    { ok\n    , #{ key        =\u003e maps:get(\u003c\u003c\"key\"\u003e\u003e, Json)\n       , value      =\u003e maps:get(\u003c\u003c\"value\"\u003e\u003e, Json)\n       , created_at =\u003e\n          sr_json:decode_date(maps:get(\u003c\u003c\"created_at\"\u003e\u003e, Json, Now))\n       , updated_at =\u003e\n          sr_json:decode_date(maps:get(\u003c\u003c\"updated_at\"\u003e\u003e, Json, Now))\n       }\n    }\n  catch\n    _:{badkey, Key} -\u003e\n      {error, \u003c\u003c\"missing field: \", Key/binary\u003e\u003e}\n  end.\n```\n\nWe also need to provide an `update` function for `PUT` and `PATCH`:\n```erlang\n-spec update(element(), sumo_rest_doc:json()) -\u003e\n  {ok, element()} | {error, iodata()}.\nupdate(Element, Json) -\u003e\n  try\n    NewValue = maps:get(\u003c\u003c\"value\"\u003e\u003e, Json),\n    UpdatedElement =\n      Element#{value := NewValue, updated_at := calendar:universal_time()},\n    {ok, UpdatedElement}\n  catch\n    _:{badkey, Key} -\u003e\n      {error, \u003c\u003c\"missing field: \", Key/binary\u003e\u003e}\n  end.\n```\n\nFor **Sumo Rest** to provide urls to the callers, we need to specify the location URL:\n```erlang\n-spec location(element(), sumo_rest_doc:path()) -\u003e binary().\nlocation(Element, Path) -\u003e iolist_to_binary([Path, \"/\", key(Element)]).\n```\n\nTo let **Sumo Rest** avoid duplicate keys (and return `422 Conflict` in that case), we provide the optional callback `duplication_conditions/1`:\n```erlang\n-spec duplication_conditions(element()) -\u003e sumo_rest_doc:duplication_conditions().\nduplication_conditions(Element) -\u003e [{key, '==', key(Element)}].\n```\n\nIf your model has an `id` type different than integer, string or binary you have to implement `id_from_binding/1`. That function is needed in order to convert the `id` from `binary()` to your type. There is an example at `sr_elements` module for our test coverage. It only converts to `integer()` but that is the general idea behind that function.\n```erlang\n-spec id_from_binding(binary()) -\u003e key().\nid_from_binding(BinaryId) -\u003e\n  try binary_to_integer(BinaryId) of\n    Id -\u003e Id\n  catch\n    error:badarg -\u003e -1\n  end.\n```\n\nThe rest of the functions in the module are just helpers, particularly useful for our tests.\n\n#### Sessions\n[Sessions](test/sr_test/sr_sessions.erl) are very similar to elements. One difference is that session ids (unlike element keys) are auto-generated by the mnesia store. Therefore they're initially `undefined`. We don't need to provide a `duplication_conditions/1` function in this case since we don't need to avoid duplicates.\n\nThe most important difference with elements is sessions does't implement `from_json` callback. Remember, `from_json` only accepts the request body in json format. In sessions we also need the logged user in order to build our session. In this case we implement `from_ctx` instead of `from_json` since it accepts the entire request and the handler's state. That information is encapsulated in a `context` structure.\n\nThis is how the `context`'s spec looks like. It is composed by a `sr_request:req()` and a `sr_state:state()` structures. Modules `sr_state` and `sr_request` are available in order to manipulate them.\n```erlang\n-type context() :: #{req := sr_request:req(), state := sr_state:state()}\n\n.. In sr_request.erl ...\n\n-opaque req() ::\n  #{ body     =\u003e sr_json:json()\n   , headers  := [{binary(), iodata()}]\n   , path     := binary()\n   , bindings := #{atom() =\u003e any()}\n   }.\n\n... In sr_state.erl ...\n\n-opaque state() ::\n  #{ opts      := sr_state:options()\n   , id        =\u003e binary()\n   , entity    =\u003e sumo:user_doc()\n   , module    := module()\n   , user_opts := map()\n   }.\n```\n\nAnd this is the `from_ctx` implementation\n```erlang\n-spec from_ctx(sumo_rest_doc:context()) -\u003e {ok, session()} | {error, iodata()}.\nfrom_ctx(#{req := SrRequest, state := State}) -\u003e\n  Json = sr_request:body(SrRequest),\n  {User, _} = sr_state:retrieve(user, State, undefined),\n   case from_json_internal(Json) of\n     {ok, Session} -\u003e {ok, user(Session, User)};\n     MissingField  -\u003e MissingField\n   end.\n\n```\n\n### The Handlers\nNow, the juicy part: The cowboy handlers. We have 4, two of them built on top of `sr_entitites_handler` and the other two built on `sr_single_entity_handler`.\n\n#### Elements\n[sr_elements_handler](test/sr_test/sr_elements_handler.erl) is built on `sr_entities_handler` and handles the path `\"/elements\"`. As you can see, the code is really simple.\n\nFirst we _mix in_ the functions from `sr_entities_handler`:\n```erlang\n-include_lib(\"mixer/include/mixer.hrl\").\n-mixin([{ sr_entities_handler\n        , [ init/3\n          , rest_init/2\n          , allowed_methods/2\n          , resource_exists/2\n          , content_types_accepted/2\n          , content_types_provided/2\n          , handle_get/2\n          , handle_post/2\n          ]\n        }]).\n```\n\nThen, we only need to write the documentation for this module, and provide the proper `Opts` and that's all:\n```erlang\n-spec trails() -\u003e trails:trails().\ntrails() -\u003e\n  RequestBody =\n    #{ name =\u003e \u003c\u003c\"request body\"\u003e\u003e\n     , in =\u003e body\n     , description =\u003e \u003c\u003c\"request body (as json)\"\u003e\u003e\n     , required =\u003e true\n     },\n  Metadata =\n    #{ get =\u003e\n       #{ tags =\u003e [\"elements\"]\n        , description =\u003e \"Returns the list of elements\"\n        , produces =\u003e [\"application/json\"]\n        }\n     , post =\u003e\n       #{ tags =\u003e [\"elements\"]\n        , description =\u003e \"Creates a new element\"\n        , consumes =\u003e [\"application/json\"]\n        , produces =\u003e [\"application/json\"]\n        , parameters =\u003e [RequestBody]\n        }\n     },\n  Path = \"/elements\",\n  Opts = #{ path =\u003e Path\n          , model =\u003e elements\n          , verbose =\u003e true\n          },\n  [trails:trail(Path, ?MODULE, Opts, Metadata)].\n```\nThe `Opts` here include the trails path (so it can be found later) and the model behind it.\n\nAnd there you go, **_no more code!_**\n\n[`sr_single_element_handler`](test/sr_test/sr_single_element_handler.erl) is analogous but it's based on `sr_single_entity_handler`.\n\n#### Sessions\n[sr_sessions_handler](test/sr_test/sr_sessions_handler.erl) shows you what happens when you need to steer away from the default implementations in **Sumo Rest**. It's as easy as defining your own functions instead of _mixing_ them _in_ from the base handlers.\n\nIn this case we needed authentication, so we added an implementation for `is_authorized`:\n```erlang\n-spec is_authorized(cowboy_req:req(), state()) -\u003e\n  {boolean(), cowboy_req:req(), state()}.\nis_authorized(Req, State) -\u003e\n  case get_authorization(Req) of\n    {not_authenticated, Req1} -\u003e\n      {{false, auth_header()}, Req1, State};\n    {User, Req1} -\u003e\n      Users = application:get_env(sr_test, users, []),\n      case lists:member(User, Users) of\n        true -\u003e {true, Req1, State#{user =\u003e User}};\n        false -\u003e\n          ct:pal(\"Invalid user ~p not in ~p\", [User, Users]),\n          {{false, auth_header()}, Req1, State}\n      end\n  end.\n```\n\nFinally, we did something similar in [`sr_single_session_handler`](test/sr_test/sr_single_session_handler.erl). We needed the same authentication mechanism, so we just _mix_ it _in_:\n```erlang\n-mixin([{ sr_sessions_handler\n        , [ is_authorized/2\n          ]\n        }]).\n```\n\nBut we needed to prevent users from accessing other user's sessions, so we implemented `forbidden/2`:\n```erlang\n-spec forbidden(cowboy_req:req(), state()) -\u003e\n  {boolean(), cowboy_req:req(), state()}.\nforbidden(Req, State) -\u003e\n  #{user := {User, _}, id := Id} = State,\n  case sumo:fetch(sessions, Id) of\n    notfound -\u003e {false, Req, State};\n    Session -\u003e {User =/= sr_sessions:user(Session), Req, State}\n  end.\n```\n\nAnd, since sessions can not be created with `PUT` (because their keys are auto-generated):\n```erlang\n-spec is_conflict(cowboy_req:req(), state()) -\u003e\n  {boolean(), cowboy_req:req(), state()}.\nis_conflict(Req, State) -\u003e\n  {not maps:is_key(entity, State), Req, State}.\n```\n\n## A Full-Fledged App\nFor a more elaborated example on how to use this library, please check [lsl](https://github.com/inaka/lsl).\n\n---\n\n## Contact Us\nIf you find any **bugs** or have a **problem** while using this library, please\n[open an issue](https://github.com/inaka/sumo_rest/issues/new) in this repo\n(or a pull request :)).\n\nAnd you can check all of our open-source projects at [inaka.github.io](http://inaka.github.io).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finaka%2Fsumo_rest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Finaka%2Fsumo_rest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Finaka%2Fsumo_rest/lists"}