{"id":32167038,"url":"https://github.com/nobrick/exaop","last_synced_at":"2026-02-19T13:34:47.055Z","repository":{"id":57498823,"uuid":"285037785","full_name":"nobrick/exaop","owner":"nobrick","description":"A minimal elixir library for aspect-oriented programming.","archived":false,"fork":false,"pushed_at":"2020-08-06T19:33:47.000Z","size":26,"stargazers_count":22,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-21T15:30:47.140Z","etag":null,"topics":["aop","aspect","aspect-oriented-programming","decorators","injection","interceptor","workflow"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nobrick.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-08-04T16:30:34.000Z","updated_at":"2022-11-21T03:29:53.000Z","dependencies_parsed_at":"2022-09-06T17:11:24.154Z","dependency_job_id":null,"html_url":"https://github.com/nobrick/exaop","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/nobrick/exaop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nobrick%2Fexaop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nobrick%2Fexaop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nobrick%2Fexaop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nobrick%2Fexaop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nobrick","download_url":"https://codeload.github.com/nobrick/exaop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nobrick%2Fexaop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29614986,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T13:04:20.082Z","status":"ssl_error","status_checked_at":"2026-02-19T13:03:33.775Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6: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":["aop","aspect","aspect-oriented-programming","decorators","injection","interceptor","workflow"],"created_at":"2025-10-21T15:18:54.149Z","updated_at":"2026-02-19T13:34:47.050Z","avatar_url":"https://github.com/nobrick.png","language":"Elixir","readme":"# Exaop\n\n[![Build Status](https://github.com/nobrick/exaop/workflows/CI/badge.svg)](https://github.com/nobrick/exaop/actions?query=workflow%3ACI)\n\nA minimal elixir library for aspect-oriented programming.\n\n## Installation\n\nAdd `exaop` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:exaop, \"~\u003e 0.1\"}\n  ]\nend\n```\n\n## Usage\n\nUnlike common AOP patterns, Exaop does not introduce any additional behavior to\nexisting functions, as it may bring complexity and make the control flow\nobscured. Elixir developers prefer explicit over implicit, thus invoking the\ncross-cutting behavior by simply calling the plain old function generated by\npointcut definitions is better than using some magic like module attributes and\nmacros to decorate and weave a function.\n\n### Hello World\n\nUse Exaop in a module, then define some pointcuts to separate the cross-cutting\nlogic:\n\n```elixir\ndefmodule Foo do\n  use Exaop\n\n  check :validity\n  set :compute\nend\n```\n\nWhen you compile the file, the following warnings would occur:\n\n```\nwarning: function check_validity/3 required by behaviour Foo.ExaopBehaviour is not implemented (in module Foo)\n  foo.exs:1: Foo (module)\n\nwarning: function set_compute/3 required by behaviour Foo.ExaopBehaviour is not implemented (in module Foo)\n  foo.exs:1: Foo (module)\n```\n\nIt reminds you to implement the corresponding callbacks required by your pointcut definitions:\n\n```elixir\ndefmodule Foo do\n  use Exaop\n\n  check :validity\n  set :compute\n\n  @impl true\n  def check_validity(%{b: b} = _params, _args, _acc) do\n    if b == 0 do\n      {:error, :divide_by_zero}\n    else\n      :ok\n    end\n  end\n\n  @impl true\n  def set_compute(%{a: a, b: b} = _params, _args, acc) do\n    Map.put(acc, :result, a / b)\n  end\nend\n```\n\nA function `__inject__/2` is generated in the above module `Foo`. When it is\ncalled, the callbacks are triggered in the order defined by your pointcut\ndefinitions.\n\nThroughout the execution of the pointcut callbacks, an accumulator is passed\nand updated after running each callback. The execution process may be halted\nby a return value of a callback.\n\nIf the execution is not halted by any callback, the final accumulator value is\nreturned by the `__inject__/2` function. Otherwise, the return value of the\ncallback that terminates the entire execution process is returned.\n\nIn the above example, the value of the accumulator is returned if the\n`check_validity` is passed:\n\n```elixir\niex\u003e params = %{a: 1, b: 2}\niex\u003e initial_acc = %{}\niex\u003e Foo.__inject__(params, initial_acc)\n%{result: 0.5}\n```\n\nThe halted error is returned if the execution is aborted:\n\n```elixir\niex\u003e params = %{a: 1, b: 0}\niex\u003e initial_acc = %{}\niex\u003e Foo.__inject__(params, initial_acc)\n{:error, :divide_by_zero}\n```\n\n### Pointcut definitions\n\n```elixir\ncheck :validity\nset :compute\n```\n\nWe've already seen the pointcut definitions in the example before.\n`check_validity/3` and `set_compute/3` are the pointcut callback functions\nrequired by these definitions.\n\nAdditional arguments can be set:\n\n```elixir\ncheck :validity, some_option: true\nset :compute, {:a, :b}\n```\n\n### Pointcut callbacks\n\n#### Naming and arguments\n\nAll types of pointcut callbacks have the same function signature. Each callback\nfunction following the naming convention in the example, using an underscore\nto connect the pointcut type and the following atom as the callback function\nname.\n\nEach callback has three arguments and each argument can be of any Elixir term.\n\nThe first argument of the callback function is passed from the first argument\nof the caller `__inject__/2`. The argument remains unchanged in each callback\nduring the execution process.\n\nThe second argument of the callback function is passed from its pointcut\ndefinition, for example, `set :compute, :my_arg` passes `:my_arg` as the\nsecond argument of its callback function `set_compute/3`.\n\nThe third argument is the accumulator. It is initialized as the second\nargument of the caller `__inject__/2`. The value of accumulator is updated or\nremains the same after each callback execution, depending on the types and\nthe return values of the callback functions.\n\n#### Types and behaviours\n\nEach kind of pointcut has different impacts on the execution process and the\naccumulator.\n\n- `check`\n  - does not change the value of the accumulator.\n  - the execution of the generated function is halted if its callback\n    return value matches the pattern `{:error, _}`.\n  - the execution continues if its callback returns `:ok`.\n- `set`\n  - does not halt the execution process.\n  - sets the accumulator to its callback return value.\n- `preprocess`\n  - allows to change the value of the accumulator or halt the execution process.\n  - the execution of the generated function is halted if its callback return\n    value matches the pattern `{:error, _}`.\n  - the accumulator is updated to the wrapped `acc` if its callback return\n    value matches the pattern `{:ok, acc}`.\n\nView documentation of these macros for details.\n\n### A more in-depth example\n\nExaop is ready for production and makes complex application workflows simple\nand self-documenting. In practice, we combine it with some custom simple macros\nas a method to separate cross-cutting concerns and decouple business logic.\nNote that we do not recommend overusing it, it is only needed when the workflow\ngets complicated, and the pointcuts should be strictly restricted to the domain\nof cross-cutting logic, not the business logic body itself.\n\nHere's a more complex example, a wallet balance transfer. The configuration\nloading, context setting and transfer validations are separated, but the main\ntransfer logic remains untouched. The example also introduces an external\ncallback, which is defined in a module other than its pointcut definition.\n\n```elixir\ndefmodule Wallet do\n  @moduledoc false\n\n  use Exaop\n  alias Wallet.AML\n  require Logger\n\n  ## Definitions for cross-cutting concerns\n\n  set :config, [:max_allowed_amount, :fee_rate]\n  set :accounts\n\n  check :amount, guard: :positive\n  check :amount, guard: {:lt_or_eq, :max_allowed_amount}\n  check :recipient, :not_equal_to_sender\n  check AML\n\n  set :fee\n  check :balance\n\n  @doc \"\"\"\n  A function injected by explicitly calling __inject__/2 generated by Exaop.\n  \"\"\"\n  def transfer(%{from: _, to: _, amount: _} = info) do\n    info\n    |\u003e __inject__(%{})\n    |\u003e handle_inject(info)\n  end\n\n  defp handle_inject({:error, _} = error, info) do\n    Logger.error(\"transfer failed\", error: error, info: info)\n  end\n\n  defp handle_inject(_acc, info) do\n    # Put the actual transfer logic here:\n    # Wallet.transfer!(acc, info)\n    Logger.info(\"transfer validated and completed\", info: info)\n  end\n\n  ## Setters required by the above concern definitions.\n\n  @impl true\n  def set_accounts(%{from: from, to: to}, _args, acc) do\n    balances = %{\"Alice\" =\u003e 100, \"Bob\" =\u003e 30}\n\n    acc\n    |\u003e Map.put(:sender_balance, balances[from])\n    |\u003e Map.put(:recipient_balance, balances[to])\n  end\n\n  @impl true\n  def set_config(_params, keys, acc) do\n    keys\n    |\u003e Enum.map(\u0026{\u00261, Application.get_env(:my_app, \u00261, default_config(\u00261))})\n    |\u003e Enum.into(acc)\n  end\n\n  defp default_config(key) do\n    Map.get(%{fee_rate: 0.01, max_allowed_amount: 1_000}, key)\n  end\n\n  @impl true\n  def set_fee(%{amount: amount}, _args, %{fee_rate: fee_rate} = acc) do\n    Map.put(acc, :fee, amount * fee_rate)\n  end\n\n  ## Checkers required by the above concern definitions.\n\n  @impl true\n  def check_amount(%{amount: amount}, args, acc) do\n    args\n    |\u003e Keyword.fetch!(:guard)\n    |\u003e do_check_amount(amount, acc)\n  end\n\n  defp do_check_amount(:positive, amount, _acc) do\n    if amount \u003e 0 do\n      :ok\n    else\n      {:error, :amount_not_positive}\n    end\n  end\n\n  defp do_check_amount({:lt_or_eq, key}, amount, acc)\n       when is_atom(key) do\n    max = Map.fetch!(acc, key)\n\n    if max \u0026\u0026 amount \u003c= max do\n      :ok\n    else\n      {:error, :amount_exceeded}\n    end\n  end\n\n  @impl true\n  def check_recipient(%{from: from, to: to}, :not_equal_to_sender, _acc) do\n    if from == to do\n      {:error, :invalid_recipient}\n    else\n      :ok\n    end\n  end\n\n  @impl true\n  def check_balance(%{amount: amount}, _args, %{fee: fee, sender_balance: balance}) do\n    if balance \u003e= amount + fee do\n      :ok\n    else\n      {:error, :insufficient_balance}\n    end\n  end\nend\n\ndefmodule Wallet.AML do\n  @moduledoc \"\"\"\n  A module defining external Exaop callbacks.\n  \"\"\"\n\n  @behaviour Exaop.Checker\n\n  @aml_blacklist ~w(Trump)\n\n  @impl true\n  def check(%{from: from, to: to}, _args, _acc) do\n    cond do\n      from in @aml_blacklist -\u003e\n        {:error, {:aml_check_failed, from}}\n\n      to in @aml_blacklist -\u003e\n        {:error, {:aml_check_failed, to}}\n\n      true -\u003e\n        :ok\n    end\n  end\nend\n```\n\n## License\n\n[The MIT License](https://github.com/nobrick/exaop/blob/master/LICENSE)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnobrick%2Fexaop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnobrick%2Fexaop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnobrick%2Fexaop/lists"}