{"id":35841273,"url":"https://github.com/andreashasse/elli_openapi","last_synced_at":"2026-04-02T14:54:23.179Z","repository":{"id":315993025,"uuid":"1061514530","full_name":"andreashasse/elli_openapi","owner":"andreashasse","description":"Demo erldantic using elli","archived":false,"fork":false,"pushed_at":"2026-03-27T12:13:42.000Z","size":119,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-31T11:43:13.160Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/andreashasse.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-22T02:55:20.000Z","updated_at":"2026-03-27T12:13:46.000Z","dependencies_parsed_at":"2025-09-22T04:32:20.111Z","dependency_job_id":"df4d530d-43ea-4738-94fb-1665ab1c5b41","html_url":"https://github.com/andreashasse/elli_openapi","commit_stats":null,"previous_names":["andreashasse/elli_openapi"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/andreashasse/elli_openapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreashasse%2Felli_openapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreashasse%2Felli_openapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreashasse%2Felli_openapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreashasse%2Felli_openapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andreashasse","download_url":"https://codeload.github.com/andreashasse/elli_openapi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andreashasse%2Felli_openapi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31308447,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":"2026-01-08T03:19:58.594Z","updated_at":"2026-04-02T14:54:23.174Z","avatar_url":"https://github.com/andreashasse.png","language":"Erlang","funding_links":[],"categories":[],"sub_categories":[],"readme":"# elli_openapi\nLibrary for building type-safe HTTP APIs with automatic OpenAPI documentation generation using Elli and Spectra.\nThis library is not ready for production use, but it wont take long to finish it.\n\n## Usage\n\n1. **Add to your rebar.config dependencies:**\n\n```erlang\n{deps, [\n    {elli_openapi, \"~\u003e 0.1.1\"}\n]}.\n```\n\n2. **Start Elli with elli_openapi_handler as the callback and your routes as arguments to elli_openapi_handler:**\n\n```erlang\n%% Define your routes\nRoutes = [\n    {\u003c\u003c\"POST\"\u003e\u003e, \u003c\u003c\"/api/users\"\u003e\u003e, fun user_handler:create_user/4},\n    {\u003c\u003c\"GET\"\u003e\u003e, \u003c\u003c\"/api/users/{userId}\"\u003e\u003e, fun user_handler:get_user/4}\n],\n\n%% Configure and start Elli, preferably in your supervisor spec.\nElliOpts = [\n    {callback, elli_openapi_handler},\n    {callback_args, Routes},\n    {port, 3000}\n],\n\n{ok, Pid} = elli:start_link(ElliOpts).\n```\n\nYou can optionally pass custom OpenAPI metadata by wrapping `callback_args` in a `{MetaData, Routes}` tuple:\n\n```erlang\nMetaData = #{title =\u003e \u003c\u003c\"My API\"\u003e\u003e, version =\u003e \u003c\u003c\"1.0.0\"\u003e\u003e},\nElliOpts = [\n    {callback, elli_openapi_handler},\n    {callback_args, {MetaData, Routes}},\n    {port, 3000}\n].\n```\n\nSee the `example/` directory for a runnable example application with handler implementations.\n\n## Handler Functions\n\nAll handler functions must follow this signature:\n\n```erlang\nhandler_name(PathArgs, QueryArgs, Headers, Body) -\u003e {StatusCode, ResponseHeaders, ResponseBody}\n```\n\n### Arguments\n\n1. **PathArgs** (`map()`): URL path parameters extracted from the route\n   - Example: For route `\u003c\u003c\"/api/users/{userId}\"\u003e\u003e`, PathArgs would be `#{userId =\u003e \u003c\u003c\"42\"\u003e\u003e}`\n   - Empty map `#{}` if no path parameters\n\n2. **QueryArgs** (`map()`): URL query parameters\n   - Example: `#{page =\u003e 1, per_page =\u003e 20}`\n   - Declare expected query params in the function spec; undeclared params are ignored\n\n3. **Headers** (`map()`): HTTP request headers with atom keys\n   - Example: `#{'Authorization' =\u003e \u003c\u003c\"Bearer ...\"\u003e\u003e, 'Content-Type' =\u003e \u003c\u003c\"application/json\"\u003e\u003e}`\n   - Required headers must be declared in the function spec\n\n4. **Body** (`any()`): Request body, automatically decoded based on the type in your function spec\n   - JSON requests: `map()` or record type\n   - Plain text requests: `binary()`\n   - Bodyless methods (GET, HEAD, etc.): declare as `binary()` — an empty body decodes cleanly to `\u003c\u003c\"\"\u003e\u003e`\n   - The library validates and decodes the body according to your spec\n\n### Return Value\n\nMust be a 3-tuple: `{StatusCode, ResponseHeaders, ResponseBody}`\n\n- **StatusCode**: HTTP status code integer (200, 201, 400, etc.)\n- **ResponseHeaders**: Map with atom keys (e.g., `#{'Location' =\u003e \u003c\u003c\"...\"\u003e\u003e, 'ETag' =\u003e \u003c\u003c\"...\"\u003e\u003e}`)\n- **ResponseBody**: Response body (record, map, or binary) - will be encoded based on content type\n\nTo return different status codes from the same handler, use union types in your function spec where each branch represents a possible response:\n\n```erlang\n-spec my_handler(PathArgs, QueryArgs, Headers, Body) -\u003e\n    {200, Headers1, SuccessBody}\n    | {400, Headers2, ErrorBody}\n    | {404, Headers3, NotFoundBody}.\n```\n\n### Spec placement\n\n`-spectra()` metadata attributes and `-spec` declarations must appear **before any function clause** in the file. The Erlang compiler processes attributes in declaration order — placing them after a function clause will cause them to be ignored or crash at startup.\n\n```erlang\n%% Correct order\n-spectra(#{summary =\u003e \u003c\u003c\"Create user\"\u003e\u003e}).\n-spec create_user(#{}, #{}, #{}, #user{}) -\u003e {201, #{}, #user{}}.\ncreate_user(#{}, #{}, #{}, User) -\u003e ...\n\n%% Wrong — attributes after a function clause are not processed\nsome_other_function() -\u003e ...\n-spectra(#{summary =\u003e \u003c\u003c\"Create user\"\u003e\u003e}).   %% too late\n-spec create_user(...) -\u003e ...\ncreate_user(...) -\u003e ...\n```\n\nHandler specs use Spectra's type system. See the [Spectra documentation](https://hexdocs.pm/spectra/readme.html) for supported types and serialization rules.\n\nFor complete handler examples, see `example/src/elli_openapi_demo.erl`.\n\n## How Routing Works\n\nRoutes are compiled into [ETS match specifications](https://www.erlang.org/doc/apps/erts/match_spec.html) at startup. Each path template like `\u003c\u003c\"/api/users/{userId}\"\u003e\u003e` is translated into a match spec pattern where `{userId}` becomes a match variable. At request time, the incoming path is converted to a tuple of segments and dispatched with a single `ets:match_spec_run/2` call, which simultaneously selects the correct handler and extracts all path variable bindings — using the VM's native pattern-matching engine rather than iterating over a list of routes.\n\n## Example Application\n\nThe `example/` directory contains a runnable demo application showcasing multiple handler implementations including user management, echo, status updates, and item updates with conflict detection.\n\nTo run the example:\n\n```bash\nmake demo\n```\n\nThe demo starts on port 3000. Access the API documentation at:\n- Swagger UI: http://localhost:3000/swagger\n- ReDoc: http://localhost:3000/redoc\n- OpenAPI JSON: http://localhost:3000/api-docs\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreashasse%2Felli_openapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandreashasse%2Felli_openapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandreashasse%2Felli_openapi/lists"}