├── test ├── test_helper.exs └── oban │ └── notifiers │ └── phoenix_test.exs ├── .formatter.exs ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── CHANGELOG.md ├── README.md ├── mix.exs ├── lib └── oban │ └── notifiers │ └── phoenix.ex ├── mix.lock ├── .credo.exs └── LICENSE.txt /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | oban_notifiers_phoenix-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | ci: 7 | env: 8 | MIX_ENV: test 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | include: 14 | - pair: 15 | elixir: '1.15' 16 | otp: '24.3' 17 | - pair: 18 | elixir: '1.18' 19 | otp: '27.2' 20 | lint: lint 21 | 22 | runs-on: ubuntu-24.04 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: erlef/setup-beam@v1 28 | with: 29 | otp-version: ${{matrix.pair.otp}} 30 | elixir-version: ${{matrix.pair.elixir}} 31 | 32 | - uses: actions/cache@v4 33 | with: 34 | path: | 35 | deps 36 | _build 37 | key: ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}-${{ hashFiles('**/mix.lock') }} 38 | restore-keys: | 39 | ${{ runner.os }}-mix-${{matrix.pair.elixir}}-${{matrix.pair.otp}}- 40 | 41 | - name: Run mix deps.get 42 | run: mix deps.get 43 | 44 | - name: Run mix format 45 | run: mix format --check-formatted 46 | if: ${{ matrix.lint }} 47 | 48 | - name: Run mix deps.unlock 49 | run: mix deps.unlock --check-unused 50 | if: ${{ matrix.lint }} 51 | 52 | - name: Run mix deps.compile 53 | run: mix deps.compile 54 | 55 | - name: Run mix compile 56 | run: mix compile --warnings-as-errors 57 | if: ${{ matrix.lint }} 58 | 59 | - name: Run credo 60 | run: mix credo --strict 61 | if: ${{ matrix.lint }} 62 | 63 | - name: Run mix test 64 | run: mix test 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for Oban Notifier Phoenix 2 | 3 | ## v0.2.2 — 2025-10-23 4 | 5 | - Correct node scoping during dispatch 6 | 7 | The name/node from the config that originally published the message was re-used during local 8 | dispatch, which prevented the notifier's relay from properly scoping messages. In other words, 9 | it looked like every message originated locally because the `ident` always matched the current 10 | node. 11 | 12 | This eliminates the problem by grabbing the config for the local instance by name, rather than 13 | sharing the node/name and building a static config from it. 14 | 15 | ## v0.2.1 — 2025-06-24 16 | 17 | - Safely handle dispatches from older notifiers 18 | 19 | In order to have graceful upgrades, this restores a clause to handle the older `conf` based 20 | message format. 21 | 22 | ## v0.2.0 — 2025-06-04 23 | 24 | ### Enhancements 25 | 26 | - Change notifier to pass config name instead of full config. 27 | 28 | Serializing the config for every notification is needlessly wasteful. Instead pass the config 29 | name and reconstruct the config in the dispatch function. 30 | 31 | - Fetch notifier state directly from the registry. 32 | 33 | State can be fetched from the Oban.Registry rather than with a GenServer call. Even if the call 34 | is fast, concurrent `notify` calls would be serialized behind GenServer state fetching. 35 | 36 | - Minimize data passing with hollow config. 37 | 38 | The notifier's `relay` function only needs the `name` and `node` values from the conf. The 39 | notifier drops all other fields and manually reconstructs a minimal config rather than pulling 40 | any data from the registry at runtime. 41 | 42 | ## v0.1.0 — 2024-02-02 43 | 44 | Initial release with full functionality! 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Oban.Notifiers.Phoenix 2 | 3 | 4 | 5 | An [Oban.Notifier][no] that uses [Phoenix.PubSub][pp] for notifications. 6 | 7 | The `Phoenix` notifier allows Oban to share a Phoenix application's `PubSub` for notifications. In 8 | addition to centralizing PubSub communications, it opens up the possible transports to all PubSub 9 | adapters. 10 | 11 | Most importantly, as Oban already provides `Postgres` and `PG` notifiers, this package enables 12 | Redis notifications via the [Phoenix.PubSub.Redis][pr] adapter. 13 | 14 | [no]: https://hexdocs.pm/oban/Oban.Notifier.html 15 | [pp]: https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html 16 | [pr]: https://hex.pm/packages/phoenix_pubsub_redis 17 | 18 | ## Usage 19 | 20 | This package requires a minimum of Oban v2.17 for a few enhancements: 21 | 22 | ```elixir 23 | defp deps do 24 | [ 25 | {:oban_notifiers_phoenix, "~> 0.1"}, 26 | ... 27 | ] 28 | end 29 | ``` 30 | 31 | Make note of your application's `Phoenix.PubSub` instance name from the primary supervision tree: 32 | 33 | ```elixir 34 | def start(_type, _args) do 35 | children = [ 36 | {Phoenix.PubSub, name: MyApp.PubSub}, 37 | ... 38 | ``` 39 | 40 | Finally, configure Oban to use `Oban.Notifiers.Phoenix` as the notifier with the `PubSub` 41 | intance name as the `:pubusb` option: 42 | 43 | ```elixir 44 | config :my_app, Oban, 45 | notifier: {Oban.Notifiers.Phoenix, pubsub: MyApp.PubSub}, 46 | ... 47 | ``` 48 | 49 | 50 | 51 | ## Contributing 52 | 53 | Run `mix test.ci` locally to ensure changes will pass in CI. That alias executes the following 54 | commands: 55 | 56 | * Check formatting (`mix format --check-formatted`) 57 | * Check unused deps (`mix deps.unlock --check-unused`) 58 | * Lint with Credo (`mix credo --strict`) 59 | * Run all tests (`mix test --raise`) 60 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ObanNotifiersPhoenix.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/sorentwo/oban_notifiers_phoenix" 5 | @version "0.2.2" 6 | 7 | def project do 8 | [ 9 | app: :oban_notifiers_phoenix, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | start_permanent: Mix.env() == :prod, 13 | aliases: aliases(), 14 | deps: deps(), 15 | docs: docs(), 16 | preferred_cli_env: ["test.ci": :test], 17 | 18 | # Hex 19 | name: "Oban Notifiers Phoenix", 20 | package: package(), 21 | description: "An Oban Notifier built on Phoenix.PubSub" 22 | ] 23 | end 24 | 25 | def application do 26 | [extra_applications: []] 27 | end 28 | 29 | defp package do 30 | [ 31 | maintainers: ["Parker Selbert"], 32 | licenses: ["Apache-2.0"], 33 | files: ~w(lib .formatter.exs mix.exs README* CHANGELOG* LICENSE*), 34 | links: %{ 35 | Changelog: "#{@source_url}/blob/main/CHANGELOG.md", 36 | GitHub: @source_url 37 | } 38 | ] 39 | end 40 | 41 | defp docs do 42 | [ 43 | main: "Oban.Notifiers.Phoenix", 44 | api_reference: false, 45 | source_ref: "v#{@version}", 46 | source_url: @source_url, 47 | formatters: ["html"], 48 | extras: ["CHANGELOG.md": [title: "Changelog"]], 49 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"] 50 | ] 51 | end 52 | 53 | defp deps do 54 | [ 55 | {:oban, "~> 2.16"}, 56 | {:phoenix_pubsub, "~> 2.0"}, 57 | {:credo, "~> 1.7", only: [:test, :dev], runtime: false}, 58 | {:ex_doc, "~> 0.30", only: [:test, :dev], runtime: false} 59 | ] 60 | end 61 | 62 | defp aliases do 63 | [ 64 | release: [ 65 | "cmd git tag v#{@version}", 66 | "cmd git push", 67 | "cmd git push --tags", 68 | "hex.publish --yes" 69 | ], 70 | "test.ci": [ 71 | "format --check-formatted", 72 | "deps.unlock --check-unused", 73 | "credo --strict", 74 | "test --raise" 75 | ] 76 | ] 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/oban/notifiers/phoenix.ex: -------------------------------------------------------------------------------- 1 | defmodule Oban.Notifiers.Phoenix do 2 | @external_resource readme = Path.join([__DIR__, "../../../README.md"]) 3 | 4 | @moduledoc readme 5 | |> File.read!() 6 | |> String.split("") 7 | |> Enum.fetch!(1) 8 | 9 | @behaviour Oban.Notifier 10 | 11 | use GenServer 12 | 13 | alias Oban.Notifier 14 | alias Phoenix.PubSub 15 | 16 | defstruct [:conf, :pubsub] 17 | 18 | @doc false 19 | def child_spec(opts), do: super(opts) 20 | 21 | @impl Notifier 22 | def start_link(opts) do 23 | {name, opts} = Keyword.pop(opts, :name, __MODULE__) 24 | 25 | GenServer.start_link(__MODULE__, struct!(__MODULE__, opts), name: name) 26 | end 27 | 28 | @impl Notifier 29 | def listen(server, channels) do 30 | with {:ok, %{pubsub: pubsub}} <- get_state(server) do 31 | for channel <- channels, 32 | do: PubSub.subscribe(pubsub, to_string(channel), metadata: __MODULE__) 33 | 34 | :ok 35 | end 36 | end 37 | 38 | @impl Notifier 39 | def unlisten(server, channels) do 40 | with {:ok, %{pubsub: pubsub}} <- get_state(server) do 41 | for channel <- channels, do: PubSub.unsubscribe(pubsub, to_string(channel)) 42 | 43 | :ok 44 | end 45 | end 46 | 47 | @impl Notifier 48 | def notify(server, channel, payload) do 49 | with {:ok, %{conf: conf, pubsub: pubsub}} <- get_state(server) do 50 | PubSub.broadcast( 51 | pubsub, 52 | to_string(channel), 53 | {conf.name, channel, payload}, 54 | __MODULE__ 55 | ) 56 | 57 | :ok 58 | end 59 | end 60 | 61 | @impl GenServer 62 | def init(state) do 63 | put_state(state) 64 | 65 | {:ok, state} 66 | end 67 | 68 | @doc false 69 | def dispatch(entries, :none, {name, channel, payload}) do 70 | pids = Enum.map(entries, &elem(&1, 0)) 71 | conf = Oban.config(name) 72 | 73 | for message <- payload, do: Notifier.relay(conf, pids, channel, message) 74 | end 75 | 76 | # Backward compatibility with 0.2.1 77 | def dispatch(entries, :none, {name, _node, channel, payload}) do 78 | dispatch(entries, :none, {name, channel, payload}) 79 | end 80 | 81 | # Backward compatibility with < 0.2.0 82 | def dispatch(entries, _from, {conf, channel, payload}) do 83 | dispatch(entries, :none, {conf.name, conf.node, channel, payload}) 84 | end 85 | 86 | defp put_state(state) do 87 | Registry.update_value(Oban.Registry, {state.conf.name, Oban.Notifier}, fn _ -> state end) 88 | end 89 | 90 | defp get_state(server) do 91 | [name] = Registry.keys(Oban.Registry, server) 92 | 93 | case Oban.Registry.lookup(name) do 94 | {_pid, state} -> {:ok, state} 95 | nil -> {:error, RuntimeError.exception("no notifier running as #{inspect(name)}")} 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/oban/notifiers/phoenix_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Oban.Notifiers.PhoenixTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Oban.{Config, Notifier} 5 | 6 | defmodule Repo do 7 | use Ecto.Repo, otp_app: :oban, adapter: Ecto.Adapters.Postgres 8 | end 9 | 10 | setup config do 11 | start_supervised!({Phoenix.PubSub, name: config.test}) 12 | 13 | opts = [ 14 | notifier: {Oban.Notifiers.Phoenix, pubsub: config.test}, 15 | peer: Oban.Peers.Isolated, 16 | stage_interval: :infinity, 17 | repo: Repo 18 | ] 19 | 20 | start_supervised!({Oban, opts}) 21 | 22 | :ok 23 | end 24 | 25 | test "broadcasting notifications to subscribers" do 26 | :ok = Notifier.listen(:signal) 27 | :ok = Notifier.notify(:signal, %{incoming: "message"}) 28 | 29 | assert_receive {:notification, :signal, %{"incoming" => "message"}} 30 | end 31 | 32 | test "notifying with complex types" do 33 | Notifier.listen([:insert, :gossip, :signal]) 34 | 35 | Notifier.notify(:signal, %{ 36 | date: ~D[2021-08-09], 37 | keyword: [a: 1, b: 1], 38 | map: %{tuple: {1, :second}}, 39 | tuple: {1, :second} 40 | }) 41 | 42 | assert_receive {:notification, :signal, notice} 43 | assert %{"date" => "2021-08-09", "keyword" => [["a", 1], ["b", 1]]} = notice 44 | assert %{"map" => %{"tuple" => [1, "second"]}, "tuple" => [1, "second"]} = notice 45 | end 46 | 47 | test "broadcasting on select channels" do 48 | :ok = Notifier.listen([:signal, :gossip]) 49 | :ok = Notifier.unlisten([:gossip]) 50 | 51 | :ok = Notifier.notify(:gossip, %{foo: "bar"}) 52 | :ok = Notifier.notify(:signal, %{baz: "bat"}) 53 | 54 | assert_receive {:notification, :signal, _} 55 | refute_received {:notification, :gossip, _} 56 | end 57 | 58 | test "broadcasting scoped notifications", config do 59 | :ok = Notifier.listen(:signal) 60 | 61 | # We can't start two instances with the same name on a single node, so fake it by sending a 62 | # direct broadcast. 63 | broadcast = fn payload -> 64 | Phoenix.PubSub.broadcast( 65 | config.test, 66 | "signal", 67 | {Oban, :signal, [payload]}, 68 | Oban.Notifiers.Phoenix 69 | ) 70 | end 71 | 72 | real_ident = Config.to_ident(Oban.config()) 73 | fake_ident = "Oban.other-node@localhost" 74 | 75 | broadcast.(~s({"action":"real", "ident": "#{real_ident}"})) 76 | broadcast.(~s({"action":"fake", "ident": "#{fake_ident}"})) 77 | 78 | assert_receive {:notification, :signal, %{"action" => "real"}} 79 | refute_receive {:notification, :signal, %{"action" => "fake"}}, 50 80 | end 81 | 82 | test "safely handling dispatches from older notifier versions" do 83 | :ok = Notifier.listen(:signal) 84 | 85 | conf = Oban.config() 86 | 87 | assert [] = Oban.Notifiers.Phoenix.dispatch(%{}, self(), {conf, :signal, %{}}) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 4 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, 5 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 7 | "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, 8 | "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, 9 | "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 11 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 12 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 14 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 15 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 16 | "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"}, 17 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 18 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 19 | } 20 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for Credo and you are probably reading 2 | # this after creating it with `mix credo.gen.config`. 3 | # 4 | # If you find anything wrong or unclear in this file, please report an 5 | # issue on GitHub: https://github.com/rrrene/credo/issues 6 | # 7 | %{ 8 | # 9 | # You can have as many configs as you like in the `configs:` field. 10 | configs: [ 11 | %{ 12 | # 13 | # Run any config using `mix credo -C `. If no config name is given 14 | # "default" is used. 15 | # 16 | name: "default", 17 | # 18 | # These are the files included in the analysis: 19 | files: %{ 20 | # 21 | # You can give explicit globs or simply directories. 22 | # In the latter case `**/*.{ex,exs}` will be used. 23 | # 24 | included: [ 25 | "lib/", 26 | "src/", 27 | "test/", 28 | "web/", 29 | "apps/*/lib/", 30 | "apps/*/src/", 31 | "apps/*/test/", 32 | "apps/*/web/" 33 | ], 34 | excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] 35 | }, 36 | # 37 | # Load and configure plugins here: 38 | # 39 | plugins: [], 40 | # 41 | # If you create your own checks, you must specify the source files for 42 | # them here, so they can be loaded by Credo before running the analysis. 43 | # 44 | requires: [], 45 | # 46 | # If you want to enforce a style guide and need a more traditional linting 47 | # experience, you can change `strict` to `true` below: 48 | # 49 | strict: false, 50 | # 51 | # To modify the timeout for parsing files, change this value: 52 | # 53 | parse_timeout: 5000, 54 | # 55 | # If you want to use uncolored output by default, you can change `color` 56 | # to `false` below: 57 | # 58 | color: true, 59 | # 60 | # You can customize the parameters of any check by adding a second element 61 | # to the tuple. 62 | # 63 | # To disable a check put `false` as second element: 64 | # 65 | # {Credo.Check.Design.DuplicatedCode, false} 66 | # 67 | checks: %{ 68 | enabled: [ 69 | # 70 | ## Consistency Checks 71 | # 72 | {Credo.Check.Consistency.ExceptionNames, []}, 73 | {Credo.Check.Consistency.LineEndings, []}, 74 | {Credo.Check.Consistency.ParameterPatternMatching, []}, 75 | {Credo.Check.Consistency.SpaceAroundOperators, []}, 76 | {Credo.Check.Consistency.SpaceInParentheses, []}, 77 | {Credo.Check.Consistency.TabsOrSpaces, []}, 78 | 79 | # 80 | ## Design Checks 81 | # 82 | # You can customize the priority of any check 83 | # Priority values are: `low, normal, high, higher` 84 | # 85 | {Credo.Check.Design.AliasUsage, 86 | [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 3]}, 87 | {Credo.Check.Design.TagFIXME, []}, 88 | # You can also customize the exit_status of each check. 89 | # If you don't want TODO comments to cause `mix credo` to fail, just 90 | # set this value to 0 (zero). 91 | # 92 | {Credo.Check.Design.TagTODO, [exit_status: 2]}, 93 | 94 | # 95 | ## Readability Checks 96 | # 97 | {Credo.Check.Readability.AliasOrder, []}, 98 | {Credo.Check.Readability.FunctionNames, []}, 99 | {Credo.Check.Readability.LargeNumbers, []}, 100 | {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, 101 | {Credo.Check.Readability.ModuleAttributeNames, []}, 102 | {Credo.Check.Readability.ModuleDoc, []}, 103 | {Credo.Check.Readability.ModuleNames, []}, 104 | {Credo.Check.Readability.ParenthesesInCondition, []}, 105 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, 106 | {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, 107 | {Credo.Check.Readability.PredicateFunctionNames, []}, 108 | {Credo.Check.Readability.PreferImplicitTry, []}, 109 | {Credo.Check.Readability.RedundantBlankLines, []}, 110 | {Credo.Check.Readability.Semicolons, []}, 111 | {Credo.Check.Readability.SpaceAfterCommas, []}, 112 | {Credo.Check.Readability.StringSigils, []}, 113 | {Credo.Check.Readability.TrailingBlankLine, []}, 114 | {Credo.Check.Readability.TrailingWhiteSpace, []}, 115 | {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, 116 | {Credo.Check.Readability.VariableNames, []}, 117 | {Credo.Check.Readability.WithSingleClause, []}, 118 | 119 | # 120 | ## Refactoring Opportunities 121 | # 122 | {Credo.Check.Refactor.Apply, []}, 123 | {Credo.Check.Refactor.CondStatements, []}, 124 | {Credo.Check.Refactor.CyclomaticComplexity, []}, 125 | {Credo.Check.Refactor.FilterCount, []}, 126 | {Credo.Check.Refactor.FilterFilter, []}, 127 | {Credo.Check.Refactor.FunctionArity, []}, 128 | {Credo.Check.Refactor.LongQuoteBlocks, []}, 129 | {Credo.Check.Refactor.MapJoin, []}, 130 | {Credo.Check.Refactor.MatchInCondition, []}, 131 | {Credo.Check.Refactor.NegatedConditionsInUnless, []}, 132 | {Credo.Check.Refactor.NegatedConditionsWithElse, []}, 133 | {Credo.Check.Refactor.Nesting, []}, 134 | {Credo.Check.Refactor.RedundantWithClauseResult, []}, 135 | {Credo.Check.Refactor.RejectReject, []}, 136 | {Credo.Check.Refactor.UnlessWithElse, []}, 137 | {Credo.Check.Refactor.WithClauses, []}, 138 | 139 | # 140 | ## Warnings 141 | # 142 | {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, 143 | {Credo.Check.Warning.BoolOperationOnSameValues, []}, 144 | {Credo.Check.Warning.Dbg, []}, 145 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, 146 | {Credo.Check.Warning.IExPry, []}, 147 | {Credo.Check.Warning.IoInspect, []}, 148 | {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, 149 | {Credo.Check.Warning.OperationOnSameValues, []}, 150 | {Credo.Check.Warning.OperationWithConstantResult, []}, 151 | {Credo.Check.Warning.RaiseInsideRescue, []}, 152 | {Credo.Check.Warning.SpecWithStruct, []}, 153 | {Credo.Check.Warning.UnsafeExec, []}, 154 | {Credo.Check.Warning.UnusedEnumOperation, []}, 155 | {Credo.Check.Warning.UnusedFileOperation, []}, 156 | {Credo.Check.Warning.UnusedKeywordOperation, []}, 157 | {Credo.Check.Warning.UnusedListOperation, []}, 158 | {Credo.Check.Warning.UnusedPathOperation, []}, 159 | {Credo.Check.Warning.UnusedRegexOperation, []}, 160 | {Credo.Check.Warning.UnusedStringOperation, []}, 161 | {Credo.Check.Warning.UnusedTupleOperation, []}, 162 | {Credo.Check.Warning.WrongTestFileExtension, []} 163 | ], 164 | disabled: [ 165 | # 166 | # Checks scheduled for next check update (opt-in for now) 167 | {Credo.Check.Refactor.UtcNowTruncate, []}, 168 | 169 | # 170 | # Controversial and experimental checks (opt-in, just move the check to `:enabled` 171 | # and be sure to use `mix credo --strict` to see low priority checks) 172 | # 173 | {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, 174 | {Credo.Check.Consistency.UnusedVariableNames, []}, 175 | {Credo.Check.Design.DuplicatedCode, []}, 176 | {Credo.Check.Design.SkipTestWithoutComment, []}, 177 | {Credo.Check.Readability.AliasAs, []}, 178 | {Credo.Check.Readability.BlockPipe, []}, 179 | {Credo.Check.Readability.ImplTrue, []}, 180 | {Credo.Check.Readability.MultiAlias, []}, 181 | {Credo.Check.Readability.NestedFunctionCalls, []}, 182 | {Credo.Check.Readability.OneArityFunctionInPipe, []}, 183 | {Credo.Check.Readability.OnePipePerLine, []}, 184 | {Credo.Check.Readability.SeparateAliasRequire, []}, 185 | {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, 186 | {Credo.Check.Readability.SinglePipe, []}, 187 | {Credo.Check.Readability.Specs, []}, 188 | {Credo.Check.Readability.StrictModuleLayout, []}, 189 | {Credo.Check.Readability.WithCustomTaggedTuple, []}, 190 | {Credo.Check.Refactor.ABCSize, []}, 191 | {Credo.Check.Refactor.AppendSingleItem, []}, 192 | {Credo.Check.Refactor.DoubleBooleanNegation, []}, 193 | {Credo.Check.Refactor.FilterReject, []}, 194 | {Credo.Check.Refactor.IoPuts, []}, 195 | {Credo.Check.Refactor.MapMap, []}, 196 | {Credo.Check.Refactor.ModuleDependencies, []}, 197 | {Credo.Check.Refactor.NegatedIsNil, []}, 198 | {Credo.Check.Refactor.PassAsyncInTestCases, []}, 199 | {Credo.Check.Refactor.PipeChainStart, []}, 200 | {Credo.Check.Refactor.RejectFilter, []}, 201 | {Credo.Check.Refactor.VariableRebinding, []}, 202 | {Credo.Check.Warning.LazyLogging, []}, 203 | {Credo.Check.Warning.LeakyEnvironment, []}, 204 | {Credo.Check.Warning.MapGetUnsafePass, []}, 205 | {Credo.Check.Warning.MixEnv, []}, 206 | {Credo.Check.Warning.UnsafeToAtom, []} 207 | 208 | # {Credo.Check.Refactor.MapInto, []}, 209 | 210 | # 211 | # Custom checks can be created using `mix credo.gen.check`. 212 | # 213 | ] 214 | } 215 | } 216 | ] 217 | } 218 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Parker Selbert 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------