{"id":17491764,"url":"https://github.com/ndalmia/ex_runner","last_synced_at":"2025-04-22T20:14:51.520Z","repository":{"id":62429437,"uuid":"304239338","full_name":"ndalmia/ex_runner","owner":"ndalmia","description":"Elixir library that provides a macro which converts the modules into operations for encapsulating business logics.","archived":false,"fork":false,"pushed_at":"2020-10-16T08:00:48.000Z","size":12,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-22T20:14:45.321Z","etag":null,"topics":["business-logic","ecto","elixir","macro","operation","parameters","validation"],"latest_commit_sha":null,"homepage":"","language":"Elixir","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/ndalmia.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":"2020-10-15T07:06:22.000Z","updated_at":"2022-02-03T01:28:54.000Z","dependencies_parsed_at":"2022-11-01T20:05:50.147Z","dependency_job_id":null,"html_url":"https://github.com/ndalmia/ex_runner","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/ndalmia%2Fex_runner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndalmia%2Fex_runner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndalmia%2Fex_runner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ndalmia%2Fex_runner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ndalmia","download_url":"https://codeload.github.com/ndalmia/ex_runner/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250316065,"owners_count":21410476,"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":["business-logic","ecto","elixir","macro","operation","parameters","validation"],"created_at":"2024-10-19T08:04:51.066Z","updated_at":"2025-04-22T20:14:51.498Z","avatar_url":"https://github.com/ndalmia.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ExRunner\n\nElixir library that provides a macro which converts the modules into operations for encapsulating business logics. It uses **[Ecto Schema](https://hexdocs.pm/ecto/Ecto.Schema.html)** (`embedded_schema`) for defining input / output and **[Ecto Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html)** for the validations. :)\n\n## Installation\n\nIt can be installed by adding `ex_runner` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:ex_runner, \"~\u003e 0.2.0\"}\n  ]\nend\n```\n\n## Usage\n\nLet's take an example to understand how this library works. \n\n**Example - Write an operation which creates the user session by taking email and password.**\n\n**Step 1** - Define a module and add `use ExRunner`.\n\n```elixir\ndefmodule CreateSession do\n  use ExRunner\nend\n```\n\n**Step 2** - Define input and output for the operation. Input is what will be given as parameters to this operation and Output is what will get returned from the operation as a result.\n\n```elixir\ndefmodule CreateSession do\n  use ExRunner\n\n  input do\n    field :email, :string\n    field :password, :string\n  end\n\n  output do\n    field :session_id, Ecto.UUID\n  end\nend\n```\n\nThis is just [Ecto Schema](https://hexdocs.pm/ecto/Ecto.Schema.html) For complex schema definition, [embeds_one](https://hexdocs.pm/ecto/Ecto.Schema.html#embeds_one/3) and [embeds_many](https://hexdocs.pm/ecto/Ecto.Schema.html#embeds_many/3) can be used.\n\n**Step 3** - Define validate function which adds the required validations on the inputs passed to the operation.\n\n```elixir\ndefmodule CreateSession do\n  use ExRunner\n\n  input do\n    field :email, :string\n    field :password, :string\n  end\n\n  output do\n    field :session_id, Ecto.UUID\n  end\n\n  defp validate(changeset) do\n    changeset\n    |\u003e validate_required([:email, :password])\n    |\u003e validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$/)\n    |\u003e validate_length(:password, min: 4)\n  end\nend\n```\n\nValidate function takes changeset as argument, `params` of which contains the input passed and schema is of type input defined in step 2.\n\nFor complex validations, read [Ecto Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html)\n\n**Step 4**- Define execute function which performs business logic with the inputs provided. \n\n```elixir\ndefmodule CreateSession do\n  use ExRunner\n\n  input do\n    field :email, :string\n    field :password, :string\n  end\n\n  output do\n    field :session_id, Ecto.UUID\n  end\n\n  defp validate(changeset) do\n    changeset\n    |\u003e validate_required([:email, :password])\n    |\u003e validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$/)\n    |\u003e validate_length(:password, min: 4)\n  end\n\n  defp execute(changeset) do\n    params = changeset.params\n\n    case (params.email == \"test@test.com\" and params.password == \"test\") do\n      true -\u003e %{session_id: \"870df8e8-3107-4487-8316-81e089b8c2cf\"}\n      false -\u003e add_error(changeset, :credentials, \"are invalid\")\n    end\n  end\nend\n```\n\nExecute function takes changeset as argument. Inputs / parameters can be found in `changeset.params`.\nTo add an error, just call add_error of Ecto.Changeset. \n\nIn case of :ok, return the needed response of type output defined in step 4. \n\nIn case of :error, return Ecto.Changeset.\n\n**Step 5** - Try running the operation.\n\n```elixir\n# run with valid credentials\n\u003e CreateSession.run(email: \"test@test.com\", password: \"test\")\n{:ok, %{session_id: \"870df8e8-3107-4487-8316-81e089b8c2cf\"}}\n\n# run! with valid credentials\n\u003e CreateSession.run!(email: \"test@test.com\", password: \"test\")\n%{session_id: \"870df8e8-3107-4487-8316-81e089b8c2cf\"}\n\n# run with invalid email format\n\u003e CreateSession.run(email: \"test\", password: \"test\")\n{:error,\n#Ecto.Changeset\u003c\n  action: nil,\n  changes: %{email: \"test\", password: \"testi\"},\n  errors: [email: {\"has invalid format\", [validation: :format]}],\n  data: #CreateSession.Input\u003c\u003e,\n  valid?: false\n\u003e}\n\n# run! with invalid email format\n\u003e CreateSession.run(email: \"test\", password: \"test\")\n# raises Ecto.InvalidChangesetError \n\n# run with invalid credentials\n\u003e CreateSession.run(email: \"test@test.com\", password: \"test1\")\n{:error,\n#Ecto.Changeset\u003c\n  action: nil,\n  changes: %{email: \"test@test.com\", password: \"testi\"},\n  errors: [credentials: {\"are invalid\", []}],\n  data: #CreateSession.Input\u003c\u003e,\n  valid?: false\n\u003e}\n\n# run! with invalid credentials\n\u003e CreateSession.run!(email: \"test@test.com\", password: \"test1\")\n# raises Ecto.InvalidChangesetError \n```\n\nI recommend reading [How does the library work internally ?](#how-does-the-library-work-internally) to understand in detail.\n\n## Examples\n\n1 - Write an operation which creates the user session by taking email and password.\n\n```elixir\ndefmodule CreateSession do\n  use ExRunner\n\n  input do\n    field :email, :string\n    field :password, :string\n  end\n\n  output do\n    field :session_id, Ecto.UUID\n  end\n\n  defp validate(changeset) do\n    changeset\n    |\u003e validate_required([:email, :password])\n    |\u003e validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}$/)\n    |\u003e validate_length(:password, min: 4)\n  end\n\n  defp execute(changeset) do\n    params = changeset.params\n\n    case (params.email == \"test@test.com\" and params.password == \"test\") do\n      true -\u003e %{session_id: \"870df8e8-3107-4487-8316-81e089b8c2cf\"}\n      false -\u003e add_error(changeset, :credentials, \"are invalid\")\n    end\n  end\nend\n```\n\n2 - (Embed Example) - Write an operation which takes the user data, process and return them.\n\n```elixir\ndefmodule ProcessUserData do\n  use ExRunner\n\n  embed_object Profile do\n    field :name, :string\n    field :picture, :string\n    field :mobile_numbers, {:array, :string}\n  end\n\n  embed_object Address do\n    field :address, :string\n    field :country, :string\n  end\n\n  input do\n    field :id, :integer\n    embeds_one :profile, ProcessUserData.Profile\n    embeds_many :addresses, ProcessUserData.Address\n  end\n\n  output do\n    field :id, :integer\n    embeds_one :profile, ProcessUserData.Profile\n    embeds_many :addresses, ProcessUserData.Address\n  end\n\n  defp validate(changeset) do\n    changeset\n    |\u003e validate_required([:id])\n    |\u003e EctoMorph.validate_nested_changeset([:profile], fn changeset -\u003e\n      changeset\n      |\u003e validate_required([:name, :picture])\n    end)\n    |\u003e EctoMorph.validate_nested_changeset([:addresses], fn changeset -\u003e\n      changeset\n      |\u003e validate_required([:address, :country])\n      |\u003e validate_inclusion(:country, [\"US\", \"NL\"])\n    end)\n  end\n\n  defp execute(changeset) do\n    changeset.params |\u003e process_data\n  end\n\n  defp process_data(data) do\n    profile = data.profile\n    processed_picture = \"processed_picture\"\n    profile = Map.put(profile, :picture, processed_picture)\n    Map.put(data, :profile, profile)\n  end\nend\n```\n\n\n## How does the library work internally ?\n\n`run ` can be called either with a keyword list or a map.\n\n1 - It first filters the input and permits only the ones defined in input schema. Even if string keys gets passed to run, it converts them to atoms. This is also true for embeds_one and embeds_many.\n\n2 - It checks the input against the field types defined in input. If invalid, it returns {:error, changeset}\n\n3 - It calls validate which has been defined in the module. If invalid, it returns {:error, changeset}\n\n4 - It calls execute which has been defined in the module. If execute returns changeset, it returns {:error, changeset}. If execute returns other than changeset, it stores it as output.\n\n5 - It filters the output and permits only the ones defined in output schema. This is also true for embeds_one and embeds_many.\n\n6 - It checks the output against the field types defined in output. If invalid, it raises Ecto.InvalidChangesetError.\n\n7 - It returns the output as map finally. {:ok, output}\n\nIn case of `run!`, if the returned tuple is of {:error}, it raises errors. if the returned tuple is of {:ok}, it returns output.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndalmia%2Fex_runner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fndalmia%2Fex_runner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fndalmia%2Fex_runner/lists"}