├── test ├── test_helper.exs ├── support │ ├── test_config.ex │ ├── test_notification.ex │ └── test_strategy.ex ├── ravenx_notification_test.exs └── ravenx_test.exs ├── .tool-versions ├── .formatter.exs ├── lib ├── ravenx │ ├── strategy_behaviour.ex │ ├── notification_behaviour.ex │ ├── strategy │ │ └── dummy.ex │ └── notification.ex └── ravenx.ex ├── .travis.yml ├── config ├── test.exs └── config.exs ├── LICENSE ├── CHANGELOG.md ├── mix.exs ├── .gitignore ├── mix.lock ├── .credo.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 20.2 2 | elixir 1.6.0 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "lib/**/*.{ex,exs}", 4 | "test/**/*.{ex,exs}", 5 | "mix.exs" 6 | ], 7 | 8 | locals_without_parens: [] 9 | ] 10 | -------------------------------------------------------------------------------- /lib/ravenx/strategy_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.StrategyBehaviour do 2 | @moduledoc """ 3 | Provides an interface for implementations of Ravenx strategies. 4 | """ 5 | 6 | @callback call(Ravenx.notif_payload(), Ravenx.notif_options()) :: Ravenx.notif_result() 7 | end 8 | -------------------------------------------------------------------------------- /lib/ravenx/notification_behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.NotificationBehaviour do 2 | @moduledoc """ 3 | Provides an interface for implementations of Ravenx notifications. 4 | """ 5 | 6 | @callback get_notifications_config(any) :: [{Ravenx.notif_id(), Ravenx.notif_config()}] 7 | end 8 | -------------------------------------------------------------------------------- /test/support/test_config.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.Test.TestConfig do 2 | def test_multiple(_) do 3 | %{ 4 | token2: "MySecondSecretToken" 5 | } 6 | end 7 | 8 | def test_module(_) do 9 | %{ 10 | token2: "MySecondSecretToken" 11 | } 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/support/test_notification.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.Test.TestNotification do 2 | use Ravenx.Notification 3 | 4 | @doc """ 5 | Test notification that delivers a dummy notification with the expected result 6 | and one with the opposite. 7 | """ 8 | 9 | def get_notifications_config(result) when is_boolean(result) do 10 | [ 11 | dummy: {:dummy, %{result: result}}, 12 | dummy_not: {:dummy, %{result: !result}} 13 | ] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.5.0 4 | - 1.6.0 5 | otp_release: 6 | - 19.2 7 | - 20.2 8 | 9 | matrix: 10 | include: 11 | - elixir: 1.4 12 | otp_release: 19.2 13 | env: 14 | - MIX_ENV=test 15 | 16 | cache: 17 | directories: 18 | - deps 19 | - _build 20 | 21 | script: 22 | - mix compile --warning-as-errors 23 | - if [ "$TRAVIS_ELIXIR_VERSION" == "1.6.0" ]; then mix format --check-formatted; fi 24 | - mix credo 25 | - mix test 26 | -------------------------------------------------------------------------------- /test/support/test_strategy.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.Test.TestStrategy do 2 | @moduledoc """ 3 | Ravenx Test strategy. 4 | 5 | Used to test if configuration is read. 6 | """ 7 | 8 | @behaviour Ravenx.StrategyBehaviour 9 | 10 | @doc """ 11 | Function used to send a notification. 12 | 13 | This is a dummy one: if it receives a payload with result equal to true, returns 14 | true. Otherwise, return false. 15 | 16 | """ 17 | @spec call(map, map) :: {:ok, Bamboo.Email.t()} | {:error, {atom, any}} 18 | def call(_, opts), do: {:ok, opts} 19 | end 20 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :ravenx, 4 | strategies: [ 5 | test: Ravenx.Test.TestStrategy, # Used to test custom strategies and runtime configuration 6 | test_config: Ravenx.Test.TestStrategy, # Used to test in-app configuration 7 | test_module: Ravenx.Test.TestStrategy, # Used to test module configuration 8 | test_multiple: Ravenx.Test.TestStrategy # Used to tes configuration from module, in-app and runtime 9 | ], 10 | config: Ravenx.Test.TestConfig 11 | 12 | config :ravenx, :test_config, 13 | token: "MySecretToken" 14 | 15 | config :ravenx, :test_multiple, 16 | token: "MySecretToken" 17 | -------------------------------------------------------------------------------- /lib/ravenx/strategy/dummy.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.Strategy.Dummy do 2 | @moduledoc """ 3 | Ravenx Dummy strategy. 4 | 5 | Used to avoid dispatching real notifications. 6 | """ 7 | 8 | @behaviour Ravenx.StrategyBehaviour 9 | 10 | @doc """ 11 | Function used to send a notification. 12 | 13 | This is a dummy one: if it receives a payload with result equal to true, returns 14 | true. Otherwise, return false. 15 | 16 | """ 17 | @spec call(map, map) :: {:ok, Bamboo.Email.t()} | {:error, {atom, any}} 18 | def call(%{result: true}, _), do: get_ok_result() 19 | def call(_, _), do: get_error_result() 20 | 21 | def get_ok_result, do: {:ok, true} 22 | def get_error_result, do: {:error, {:expected_error, false}} 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Acutar.io. http://www.acutar.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Ravenx Changelog 2 | 3 | ## v 2.0.0 4 | 5 | * Strategies come in separate packages 6 | 7 | ## v 1.1.3 8 | 9 | * Format base code using Elixir 1.6 formatter 10 | * Update dependencies 11 | * Simpler supervision tree 12 | * Correct synchronous notifications (thanks to @ponty96) 13 | 14 | ## v 1.1.2 15 | 16 | * Allow unlinked notifications (thanks to @belaustegui) 17 | 18 | ## v 1.1.1 19 | 20 | * Fix circular dependencies (thanks to @mkaravaev) 21 | * Update dependencies for Elixir 1.5.0 22 | * Use non-retired version of hackney 23 | 24 | ## v 1.1.0 25 | 26 | * Use HTTPoison instead of HTTPotion 27 | * Fix error when using tuple contacts in e-mail strategy 28 | 29 | ## v 1.0.0 30 | 31 | * Add custom strategies support 32 | * Normalize error responses 33 | * Use identifiers as keys in multiple notification dispatching 34 | * Fix issue with e-mail strategy. 35 | * Add tests 36 | 37 | ## v 0.1.2 38 | 39 | * Fix bug that avoid email dispatching using SMTP. 40 | 41 | ## v 0.1.1 42 | 43 | * Add compatibility with `~> 2.0` version of `poison`, that is the required in actual versions of `phoenix`. 44 | 45 | ## v 0.1.0 46 | 47 | * Slack integration 48 | * E-mail integration 49 | * Notification modules support 50 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :ravenx, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:ravenx, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | if File.exists?("config/#{Mix.env}.exs") do 30 | import_config("#{Mix.env}.exs") 31 | end 32 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ravenx, 7 | version: "2.0.0", 8 | elixir: "~> 1.4", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | description: description(), 12 | package: package(), 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | deps: deps(), 15 | docs: docs(), 16 | dialyzer: [plt_add_deps: :transitive] 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application 21 | # 22 | # Type "mix help compile.app" for more information 23 | def application do 24 | [ 25 | mod: {Ravenx, []} 26 | ] 27 | end 28 | 29 | # Dependencies can be Hex packages: 30 | # 31 | # {:mydep, "~> 0.3.0"} 32 | # 33 | # Or git/path repositories: 34 | # 35 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 36 | # 37 | # Type "mix help deps" for more examples and options 38 | defp deps do 39 | [ 40 | {:ex_doc, ">= 0.0.0", only: :dev}, 41 | {:dialyxir, "~> 0.4", only: :dev}, 42 | {:credo, ">= 0.8.0", only: [:dev, :test]} 43 | ] 44 | end 45 | 46 | defp docs do 47 | [ 48 | main: "readme", 49 | source_url: "https://github.com/acutario/ravenx", 50 | extras: ["README.md"] 51 | ] 52 | end 53 | 54 | defp description do 55 | """ 56 | Notification dispatch library for Elixir applications. 57 | """ 58 | end 59 | 60 | defp package do 61 | # These are the default files included in the package 62 | [ 63 | name: :ravenx, 64 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 65 | maintainers: ["Óscar de Arriba"], 66 | licenses: ["MIT"], 67 | links: %{"GitHub" => "https://github.com/acutario/ravenx"} 68 | ] 69 | end 70 | 71 | # Always compile files in "lib". In tests compile also files in 72 | # "test/support" 73 | def elixirc_paths(:test), do: elixirc_paths() ++ ["test/support"] 74 | def elixirc_paths(_), do: elixirc_paths() 75 | def elixirc_paths, do: ["lib"] 76 | end 77 | -------------------------------------------------------------------------------- /test/ravenx_notification_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RavenxNotificationTest do 2 | use ExUnit.Case 3 | 4 | alias Ravenx.Strategy.Dummy, as: DummyStrategy 5 | 6 | test "dispatch multiple notifications synchronously returns expected keys" do 7 | result = Ravenx.Test.TestNotification.dispatch(true) 8 | 9 | assert Keyword.has_key?(result, :dummy) 10 | assert Keyword.has_key?(result, :dummy_not) 11 | end 12 | 13 | test "dispatch multiple notifications synchronously returns expected results" do 14 | result = Ravenx.Test.TestNotification.dispatch(true) 15 | 16 | dummy_result = Keyword.get(result, :dummy) 17 | assert dummy_result == DummyStrategy.get_ok_result() 18 | 19 | dummy_not_result = Keyword.get(result, :dummy_not) 20 | assert dummy_not_result == DummyStrategy.get_error_result() 21 | end 22 | 23 | test "dispatch multiple notifications asynchronously returns expected keys" do 24 | result = Ravenx.Test.TestNotification.dispatch_async(true) 25 | 26 | assert Keyword.has_key?(result, :dummy) 27 | assert Keyword.has_key?(result, :dummy_not) 28 | end 29 | 30 | test "dispatch multiple notifications asynchronously returns expected tasks" do 31 | result = Ravenx.Test.TestNotification.dispatch_async(true) 32 | 33 | dummy_result = Keyword.get(result, :dummy) 34 | assert {:ok, %Task{}} = dummy_result 35 | 36 | dummy_not_result = Keyword.get(result, :dummy_not) 37 | assert {:ok, %Task{}} = dummy_not_result 38 | end 39 | 40 | test "dispatch multiple notifications asynchronously returns expected results" do 41 | result = Ravenx.Test.TestNotification.dispatch_async(true) 42 | 43 | {:ok, task} = Keyword.get(result, :dummy) 44 | assert Task.await(task) == DummyStrategy.get_ok_result() 45 | 46 | {:ok, task} = Keyword.get(result, :dummy_not) 47 | assert Task.await(task) == DummyStrategy.get_error_result() 48 | end 49 | 50 | test "dispatch multiple notifications unlinked returns expected keys" do 51 | result = Ravenx.Test.TestNotification.dispatch_nolink(true) 52 | 53 | assert Keyword.has_key?(result, :dummy) 54 | assert Keyword.has_key?(result, :dummy_not) 55 | end 56 | 57 | test "dispatch multiple notifications asynchronously returns expected PIDs" do 58 | result = Ravenx.Test.TestNotification.dispatch_nolink(true) 59 | 60 | dummy_result = Keyword.get(result, :dummy) 61 | assert {:ok, dummy_result_pid} = dummy_result 62 | assert is_pid(dummy_result_pid) 63 | 64 | dummy_not_result = Keyword.get(result, :dummy_not) 65 | assert {:ok, dummy_not_result_pid} = dummy_not_result 66 | assert is_pid(dummy_not_result_pid) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/windows,linux,osx,sublimetext,visualstudiocode,erlang,elixir 3 | 4 | ### Windows ### 5 | # Windows image file caches 6 | Thumbs.db 7 | ehthumbs.db 8 | 9 | # Folder config file 10 | Desktop.ini 11 | 12 | # Recycle Bin used on file shares 13 | $RECYCLE.BIN/ 14 | 15 | # Windows Installer files 16 | *.cab 17 | *.msi 18 | *.msm 19 | *.msp 20 | 21 | # Windows shortcuts 22 | *.lnk 23 | 24 | 25 | ### Linux ### 26 | *~ 27 | 28 | # temporary files which can be created if a process still has a handle open of a deleted file 29 | .fuse_hidden* 30 | 31 | # KDE directory preferences 32 | .directory 33 | 34 | # Linux trash folder which might appear on any partition or disk 35 | .Trash-* 36 | 37 | # .nfs files are created when an open file is removed but is still being accessed 38 | .nfs* 39 | 40 | 41 | ### OSX ### 42 | *.DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | # Thumbnails 49 | ._* 50 | # Files that might appear in the root of a volume 51 | .DocumentRevisions-V100 52 | .fseventsd 53 | .Spotlight-V100 54 | .TemporaryItems 55 | .Trashes 56 | .VolumeIcon.icns 57 | .com.apple.timemachine.donotpresent 58 | # Directories potentially created on remote AFP share 59 | .AppleDB 60 | .AppleDesktop 61 | Network Trash Folder 62 | Temporary Items 63 | .apdisk 64 | 65 | 66 | ### SublimeText ### 67 | # cache files for sublime text 68 | *.tmlanguage.cache 69 | *.tmPreferences.cache 70 | *.stTheme.cache 71 | 72 | # workspace files are user-specific 73 | *.sublime-workspace 74 | 75 | # project files should be checked into the repository, unless a significant 76 | # proportion of contributors will probably not be using SublimeText 77 | # *.sublime-project 78 | 79 | # sftp configuration file 80 | sftp-config.json 81 | 82 | # Package control specific files 83 | Package Control.last-run 84 | Package Control.ca-list 85 | Package Control.ca-bundle 86 | Package Control.system-ca-bundle 87 | Package Control.cache/ 88 | Package Control.ca-certs/ 89 | bh_unicode_properties.cache 90 | 91 | # Sublime-github package stores a github token in this file 92 | # https://packagecontrol.io/packages/sublime-github 93 | GitHub.sublime-settings 94 | 95 | 96 | ### VisualStudioCode ### 97 | .vscode/* 98 | !.vscode/settings.json 99 | !.vscode/tasks.json 100 | !.vscode/launch.json 101 | 102 | 103 | ### Erlang ### 104 | .eunit 105 | deps 106 | *.o 107 | *.beam 108 | *.plt 109 | erl_crash.dump 110 | ebin/*.beam 111 | rel/example_project 112 | .concrete/DEV_MODE 113 | .rebar 114 | 115 | 116 | ### Elixir ### 117 | /_build 118 | /cover 119 | /deps 120 | /doc 121 | *.ez 122 | -------------------------------------------------------------------------------- /test/ravenx_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RavenxTest do 2 | use ExUnit.Case 3 | 4 | alias Ravenx.Strategy.Dummy, as: DummyStrategy 5 | 6 | test "dispatch synchronously with unknown strategy will return error" do 7 | result = Ravenx.dispatch(:wadus, %{}) 8 | 9 | assert result == {:error, {:unknown_strategy, :wadus}} 10 | end 11 | 12 | test "dispatch asynchronously with unknown strategy will return error" do 13 | result = Ravenx.dispatch_async(:wadus, %{}) 14 | 15 | assert result == {:error, {:unknown_strategy, :wadus}} 16 | end 17 | 18 | test "dispatch unlinked with unknown strategy will return error" do 19 | result = Ravenx.dispatch_nolink(:wadus, %{}) 20 | 21 | assert result == {:error, {:unknown_strategy, :wadus}} 22 | end 23 | 24 | test "dispatch synchronously :ok notification" do 25 | result = Ravenx.dispatch(:dummy, %{result: true}) 26 | 27 | assert result == DummyStrategy.get_ok_result() 28 | end 29 | 30 | test "dispatch synchronously :error notification" do 31 | result = Ravenx.dispatch(:dummy, %{result: false}) 32 | 33 | assert result == DummyStrategy.get_error_result() 34 | end 35 | 36 | test "dispatch async should return a Task object" do 37 | {status, task} = Ravenx.dispatch_async(:dummy, %{result: true}) 38 | 39 | assert {:ok, %Task{}} = {status, task} 40 | assert is_pid(task.pid) 41 | end 42 | 43 | test "dispatch unlink should return a PID" do 44 | {:ok, pid} = Ravenx.dispatch_nolink(:dummy, %{result: true}) 45 | 46 | assert is_pid(pid) 47 | end 48 | 49 | test "dispatch asynchronously :ok notification" do 50 | {status, task} = Ravenx.dispatch_async(:dummy, %{result: true}) 51 | 52 | assert {:ok, task} = {status, task} 53 | assert Task.await(task) == DummyStrategy.get_ok_result() 54 | end 55 | 56 | test "dispatch asynchronously :error notification" do 57 | {status, task} = Ravenx.dispatch_async(:dummy, %{result: false}) 58 | 59 | assert {:ok, task} = {status, task} 60 | assert Task.await(task) == DummyStrategy.get_error_result() 61 | end 62 | 63 | test "custom strategies can be added using configuration" do 64 | strategies = Application.get_env(:ravenx, :strategies, []) 65 | available_strategies = Ravenx.available_strategies() 66 | 67 | strategies 68 | |> Keyword.keys() 69 | |> Enum.each(fn strategy -> assert Keyword.has_key?(available_strategies, strategy) end) 70 | end 71 | 72 | test "test custom runtime options in configuration" do 73 | configuration = %{foo: "bar"} 74 | 75 | {:ok, result} = Ravenx.dispatch(:test, %{}, configuration) 76 | 77 | assert configuration == result 78 | end 79 | 80 | test "test custom options in configuration" do 81 | configuration = 82 | Application.get_env(:ravenx, :test_config, []) 83 | |> Enum.into(%{}) 84 | 85 | {:ok, result} = Ravenx.dispatch(:test_config, %{}, %{}) 86 | 87 | assert configuration == result 88 | end 89 | 90 | test "test custom options in module" do 91 | configuration = Ravenx.Test.TestConfig.test_module(%{}) 92 | {:ok, result} = Ravenx.dispatch(:test_module, %{}, %{}) 93 | 94 | assert configuration == result 95 | end 96 | 97 | test "test custom multiple options in module" do 98 | # Get config from the three possible vias 99 | runtime_config = %{foo: "bar"} 100 | 101 | app_config = 102 | Application.get_env(:ravenx, :test_multiple, []) 103 | |> Enum.into(%{}) 104 | 105 | module_config = Ravenx.Test.TestConfig.test_multiple(%{}) 106 | 107 | # Merge them 108 | configuration = 109 | runtime_config 110 | |> Map.merge(app_config) 111 | |> Map.merge(module_config) 112 | 113 | # Call and check the result 114 | {:ok, result} = Ravenx.dispatch(:test_multiple, %{}, runtime_config) 115 | assert configuration == result 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bamboo": {:hex, :bamboo, "0.8.0", "573889a3efcb906bb9d25a1c4caa4ca22f479235e1b8cc3260d8b88dabeb4b14", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "bamboo_smtp": {:hex, :bamboo_smtp, "1.4.0", "a01d91406f3a46b3452c84d345d50f75d6facca5e06337358287a97da0426240", [:mix], [{:bamboo, "~> 0.8.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.12.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 5 | "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, 6 | "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "dialyxir": {:hex, :dialyxir, "0.5.0", "5bc543f9c28ecd51b99cc1a685a3c2a1a93216990347f259406a910cf048d1d7", [:mix], [], "hexpm"}, 8 | "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"}, 9 | "ex_doc": {:hex, :ex_doc, "0.16.2", "3b3e210ebcd85a7c76b4e73f85c5640c011d2a0b2f06dcdf5acdb2ae904e5084", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"}, 11 | "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "httpoison": {:hex, :httpoison, "1.0.0", "1f02f827148d945d40b24f0b0a89afe40bfe037171a6cf70f2486976d86921cd", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "httpotion": {:hex, :httpotion, "3.0.2", "525b9bfeb592c914a61a8ee31fdde3871e1861dfe805f8ee5f711f9f11a93483", [:mix], [{:ibrowse, "~> 4.2", [hex: :ibrowse, optional: false]}]}, 14 | "ibrowse": {:hex, :ibrowse, "4.2.2", "b32b5bafcc77b7277eff030ed32e1acc3f610c64e9f6aea19822abcadf681b4b", [:rebar3], []}, 15 | "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 17 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], [], "hexpm"}, 18 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 19 | "plug": {:hex, :plug, "1.3.5", "7503bfcd7091df2a9761ef8cecea666d1f2cc454cbbaf0afa0b6e259203b7031", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, 20 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 21 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 22 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 23 | } 24 | -------------------------------------------------------------------------------- /lib/ravenx/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx.Notification do 2 | @moduledoc """ 3 | Base module for notifications implemented using Ravenx strategies. 4 | """ 5 | 6 | @doc """ 7 | Macro to inject notification features in application's modules. 8 | """ 9 | defmacro __using__(_) do 10 | quote do 11 | # Notification implementations should implement required callbacks. 12 | @behaviour Ravenx.NotificationBehaviour 13 | 14 | alias Ravenx.Notification 15 | 16 | @doc """ 17 | Function dispatch the notification synchronously. 18 | 19 | The object received will be used as the `get_notifications_config` argument, 20 | which should return a keyword list of notification configs that have the 21 | notification IDs as keys and the configuration tuple as value. 22 | 23 | It will respond with a keyword list that have the notification IDs as keys, 24 | and a tuple indicating final state as value. 25 | That tuple follows standard notification dispatch response. 26 | 27 | """ 28 | @spec dispatch(any) :: [{Ravenx.notif_id(), Ravenx.notif_result()}] 29 | def dispatch(opts) do 30 | opts 31 | |> get_notifications_config 32 | |> Enum.map(fn {k, opts} -> 33 | {k, Notification.dispatch_notification(opts, :sync)} 34 | end) 35 | end 36 | 37 | @doc """ 38 | Function dispatch the notification asynchronously and linked. 39 | 40 | Notifications dispatched with this function are linked to their caller processes. If you are 41 | not interested in the result of the notification dispatch you should use `dispatch_nolink/1`. 42 | 43 | The object received will be used as the `get_notifications_config` argument, 44 | which should return a keyword list of notification configs that have the 45 | notification IDs as keys and the configuration tuple as value. 46 | 47 | It will respond with a keyword list that have the notification IDs as keys, 48 | and a tuple indicating final state as value. 49 | That tuple follows standard notification dispatch response. 50 | 51 | """ 52 | @spec dispatch_async(any) :: [{Ravenx.notif_id(), Ravenx.notif_result()}] 53 | def dispatch_async(opts) do 54 | opts 55 | |> get_notifications_config 56 | |> Enum.map(fn {k, opts} -> 57 | {k, Notification.dispatch_notification(opts, :async)} 58 | end) 59 | end 60 | 61 | @doc """ 62 | Function dispatch the notification asynchronously and unlinked. 63 | 64 | Notifications dispatched with this function are not linked to their caller processes. If you 65 | are interested in the result of the notification dispatch you should use `dispatch_async/1`. 66 | 67 | The object received will be used as the `get_notifications_config` argument, 68 | which should return a keyword list of notification configs that have the 69 | notification IDs as keys and the configuration tuple as value. 70 | 71 | It will respond with a keyword list that have the notification IDs as keys, 72 | and a tuple indicating final state as value. 73 | That tuple follows standard notification dispatch response. 74 | """ 75 | @spec dispatch_nolink(any) :: [{Ravenx.notif_id(), Ravenx.notif_result()}] 76 | def dispatch_nolink(opts) do 77 | opts 78 | |> get_notifications_config 79 | |> Enum.map(fn {k, opts} -> 80 | {k, Notification.dispatch_notification(opts, :nolink)} 81 | end) 82 | end 83 | end 84 | end 85 | 86 | @doc """ 87 | Function used to send a using a configuration tuple like the ones that `get_notifications_config` 88 | should return. 89 | 90 | The tuple should have this objects: 91 | 92 | 1. Strategy atom: defining which strategy to use 93 | 2. Payload map: including the payload data of the notification. 94 | 3. Options map _(optional)_: the special configuration of the notification 95 | 96 | It will respond with a tuple, with an atom that could be `:ok` or `:error` and 97 | the result of the operation, as an standard notification dispatch returns. 98 | """ 99 | @spec dispatch_notification(Ravenx.notif_config(), Ravenx.dispatch_type()) :: 100 | Ravenx.notif_result() 101 | def dispatch_notification(notification, dispatch_type) do 102 | dispatcher = get_dispatcher(dispatch_type) 103 | 104 | case notification do 105 | {strategy, payload, options} when is_atom(strategy) and is_map(payload) and is_map(options) -> 106 | dispatcher.(strategy, payload, options) 107 | 108 | {strategy, payload} when is_atom(strategy) and is_map(payload) -> 109 | dispatcher.(strategy, payload, %{}) 110 | 111 | [_] -> 112 | {:error, {:missing, :payload}} 113 | 114 | _ -> 115 | {:error, {:invalid, :notification}} 116 | end 117 | end 118 | 119 | defp get_dispatcher(:sync), do: &Ravenx.dispatch/3 120 | defp get_dispatcher(:async), do: &Ravenx.dispatch_async/3 121 | defp get_dispatcher(:nolink), do: &Ravenx.dispatch_nolink/3 122 | end 123 | -------------------------------------------------------------------------------- /.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 exec using `mix credo -C `. If no exec 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: ["lib/", "src/", "web/", "apps/"], 25 | excluded: [~r"/_build/", ~r"/deps/"] 26 | }, 27 | # 28 | # If you create your own checks, you must specify the source files for 29 | # them here, so they can be loaded by Credo before running the analysis. 30 | # 31 | requires: [], 32 | # 33 | # If you want to enforce a style guide and need a more traditional linting 34 | # experience, you can change `strict` to `true` below: 35 | # 36 | strict: true, 37 | # 38 | # If you want to use uncolored output by default, you can change `color` 39 | # to `false` below: 40 | # 41 | color: true, 42 | # 43 | # You can customize the parameters of any check by adding a second element 44 | # to the tuple. 45 | # 46 | # To disable a check put `false` as second element: 47 | # 48 | # {Credo.Check.Design.DuplicatedCode, false} 49 | # 50 | checks: [ 51 | {Credo.Check.Consistency.ExceptionNames}, 52 | {Credo.Check.Consistency.LineEndings}, 53 | {Credo.Check.Consistency.ParameterPatternMatching}, 54 | {Credo.Check.Consistency.SpaceAroundOperators}, 55 | {Credo.Check.Consistency.SpaceInParentheses}, 56 | {Credo.Check.Consistency.TabsOrSpaces}, 57 | 58 | # You can customize the priority of any check 59 | # Priority values are: `low, normal, high, higher` 60 | # 61 | {Credo.Check.Design.AliasUsage, priority: :low}, 62 | 63 | # For some checks, you can also set other parameters 64 | # 65 | # If you don't want the `setup` and `test` macro calls in ExUnit tests 66 | # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just 67 | # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. 68 | # 69 | {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, 70 | 71 | # You can also customize the exit_status of each check. 72 | # If you don't want TODO comments to cause `mix credo` to fail, just 73 | # set this value to 0 (zero). 74 | # 75 | {Credo.Check.Design.TagTODO, exit_status: 2}, 76 | {Credo.Check.Design.TagFIXME}, 77 | 78 | {Credo.Check.Readability.FunctionNames}, 79 | {Credo.Check.Readability.LargeNumbers}, 80 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100}, 81 | {Credo.Check.Readability.ModuleAttributeNames}, 82 | {Credo.Check.Readability.ModuleDoc}, 83 | {Credo.Check.Readability.ModuleNames}, 84 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, 85 | {Credo.Check.Readability.ParenthesesInCondition}, 86 | {Credo.Check.Readability.PredicateFunctionNames}, 87 | {Credo.Check.Readability.PreferImplicitTry}, 88 | {Credo.Check.Readability.RedundantBlankLines}, 89 | {Credo.Check.Readability.StringSigils}, 90 | {Credo.Check.Readability.TrailingBlankLine}, 91 | {Credo.Check.Readability.TrailingWhiteSpace}, 92 | {Credo.Check.Readability.VariableNames}, 93 | {Credo.Check.Readability.Semicolons}, 94 | {Credo.Check.Readability.SpaceAfterCommas}, 95 | 96 | {Credo.Check.Refactor.DoubleBooleanNegation}, 97 | {Credo.Check.Refactor.CondStatements}, 98 | {Credo.Check.Refactor.CyclomaticComplexity}, 99 | {Credo.Check.Refactor.FunctionArity}, 100 | {Credo.Check.Refactor.LongQuoteBlocks}, 101 | {Credo.Check.Refactor.MatchInCondition}, 102 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 103 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 104 | {Credo.Check.Refactor.Nesting}, 105 | {Credo.Check.Refactor.PipeChainStart}, 106 | {Credo.Check.Refactor.UnlessWithElse}, 107 | 108 | {Credo.Check.Warning.BoolOperationOnSameValues}, 109 | {Credo.Check.Warning.ExpensiveEmptyEnumCheck}, 110 | {Credo.Check.Warning.IExPry}, 111 | {Credo.Check.Warning.IoInspect}, 112 | {Credo.Check.Warning.LazyLogging}, 113 | {Credo.Check.Warning.OperationOnSameValues}, 114 | {Credo.Check.Warning.OperationWithConstantResult}, 115 | {Credo.Check.Warning.UnusedEnumOperation}, 116 | {Credo.Check.Warning.UnusedFileOperation}, 117 | {Credo.Check.Warning.UnusedKeywordOperation}, 118 | {Credo.Check.Warning.UnusedListOperation}, 119 | {Credo.Check.Warning.UnusedPathOperation}, 120 | {Credo.Check.Warning.UnusedRegexOperation}, 121 | {Credo.Check.Warning.UnusedStringOperation}, 122 | {Credo.Check.Warning.UnusedTupleOperation}, 123 | {Credo.Check.Warning.RaiseInsideRescue}, 124 | 125 | # Controversial and experimental checks (opt-in, just remove `, false`) 126 | # 127 | {Credo.Check.Refactor.ABCSize, false}, 128 | {Credo.Check.Refactor.AppendSingleItem, false}, 129 | {Credo.Check.Refactor.VariableRebinding, false}, 130 | {Credo.Check.Warning.MapGetUnsafePass, false}, 131 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 132 | 133 | # Deprecated checks (these will be deleted after a grace period) 134 | # 135 | {Credo.Check.Readability.Specs, false}, 136 | {Credo.Check.Warning.NameRedeclarationByAssignment, false}, 137 | {Credo.Check.Warning.NameRedeclarationByCase, false}, 138 | {Credo.Check.Warning.NameRedeclarationByDef, false}, 139 | {Credo.Check.Warning.NameRedeclarationByFn, false}, 140 | 141 | # Custom checks can be created using `mix credo.gen.check`. 142 | # 143 | ] 144 | } 145 | ] 146 | } 147 | -------------------------------------------------------------------------------- /lib/ravenx.ex: -------------------------------------------------------------------------------- 1 | defmodule Ravenx do 2 | @moduledoc """ 3 | Ravenx main module. 4 | 5 | It includes and manages dispatching of messages through registered strategies. 6 | """ 7 | use Application 8 | 9 | @type notif_id :: atom 10 | @type notif_strategy :: atom 11 | @type notif_payload :: map 12 | @type notif_options :: map 13 | @type notif_result :: {:ok, any} | {:error, {atom, any}} 14 | @type notif_config :: 15 | {notif_strategy, notif_payload} 16 | | {notif_strategy, notif_payload, notif_options} 17 | @type dispatch_type :: :sync | :async | :nolink 18 | 19 | def start(_type, _args) do 20 | Task.Supervisor.start_link(name: Ravenx.Supervisor, max_restarts: 2) 21 | end 22 | 23 | @doc """ 24 | Dispatch a notification `payload` to a specified `strategy`. 25 | 26 | Custom options for this call can be passed in `options` parameter. 27 | 28 | Returns a tuple with `:ok` or `:error` indicating the final state. 29 | 30 | ## Examples 31 | 32 | iex> Ravenx.dispatch(:slack, %{title: "Hello world!", body: "Science is cool"}) 33 | {:ok, "ok"} 34 | 35 | iex> Ravenx.dispatch(:wadus, %{title: "Hello world!", body: "Science is cool"}) 36 | {:error, {:unknown_strategy, :wadus}} 37 | 38 | """ 39 | @spec dispatch(notif_strategy, notif_payload, notif_options) :: notif_result 40 | def dispatch(strategy, payload, options \\ %{}) do 41 | handler = Keyword.get(available_strategies(), strategy) 42 | 43 | opts = get_options(strategy, payload, options) 44 | 45 | if is_nil(handler) do 46 | {:error, {:unknown_strategy, strategy}} 47 | else 48 | handler.call(payload, opts) 49 | end 50 | end 51 | 52 | @doc """ 53 | Dispatch a notification `payload` to a specified `strategy` asynchronously. 54 | 55 | This function should be used when the caller has an interest in the notification dispatch result, 56 | which must be received using `Task.await/2`. Keep in mind that this function links the notification 57 | dispatch task with the caller process, so if one fails the other will fail also. 58 | 59 | If you simply want to dispatch an asynchronous notification without having any interest in the 60 | result, take a look at `dispatch_nolink/3`. 61 | 62 | Custom options for this call can be passed in `options` parameter. 63 | 64 | Returns a tuple with `:ok` or `:error` indicating the task launch result. 65 | If the result was `:ok`, the Task of the process launched is also returned 66 | 67 | ## Examples 68 | 69 | iex> {status, task} = Ravenx.dispatch_async(:slack, %{title: "Hello world!", body: "Science is cool"}) 70 | {:ok, %Task{owner: #PID<0.165.0>, pid: #PID<0.183.0>, ref: #Reference<0.0.4.418>}} 71 | 72 | iex> Task.await(task) 73 | {:ok, "ok"} 74 | 75 | iex> Ravenx.dispatch_async(:wadus, %{title: "Hello world!", body: "Science is cool"}) 76 | {:error, {:unknown_strategy, :wadus}} 77 | 78 | """ 79 | @spec dispatch_async(notif_strategy, notif_payload, notif_options) :: notif_result 80 | def dispatch_async(strategy, payload, options \\ %{}) do 81 | handler = Keyword.get(available_strategies(), strategy) 82 | 83 | opts = get_options(strategy, payload, options) 84 | 85 | if is_nil(handler) do 86 | {:error, {:unknown_strategy, strategy}} 87 | else 88 | task = Task.Supervisor.async(Ravenx.Supervisor, fn -> handler.call(payload, opts) end) 89 | {:ok, task} 90 | end 91 | end 92 | 93 | @doc """ 94 | Dispatch a notification `payload` to a specified `strategy` unlinked. 95 | 96 | This function spawns a separated process for dispatching the notification in an unlinked way, 97 | meaning that the caller won't be able to know the notification dispatch result. 98 | If you want to dispatch an asynchronous notification and receive its result take a look at 99 | `dispatch_async/3`. 100 | 101 | Custom options for this call can be passed in `options` parameter. 102 | 103 | Returns a tuple with `:ok` or `:error` indicating the task launch result. 104 | If the result was `:ok`, the PID of the notification dispatch process is also returned. 105 | 106 | ## Examples 107 | 108 | iex> {status, pid} = Ravenx.dispatch_nolink(:slack, %{title: "Hello world!", body: "Science is cool"}) 109 | {:ok, #PID<0.165.0>} 110 | 111 | iex> Ravenx.dispatch_nolink(:wadus, %{title: "Hello world!", body: "Science is cool"}) 112 | {:error, {:unknown_strategy, :wadus}} 113 | """ 114 | def dispatch_nolink(strategy, payload, options \\ %{}) do 115 | handler = Keyword.get(available_strategies(), strategy) 116 | 117 | opts = get_options(strategy, payload, options) 118 | 119 | if is_nil(handler) do 120 | {:error, {:unknown_strategy, strategy}} 121 | else 122 | Task.Supervisor.start_child(Ravenx.Supervisor, fn -> handler.call(payload, opts) end) 123 | end 124 | end 125 | 126 | @doc """ 127 | Function to get a Keyword list of registered strategies. 128 | """ 129 | @spec available_strategies() :: keyword 130 | def available_strategies do 131 | bundled_strategies = [ 132 | slack: Ravenx.Strategy.Slack, 133 | email: Ravenx.Strategy.Email, 134 | dummy: Ravenx.Strategy.Dummy 135 | ] 136 | 137 | bundled_strategies 138 | |> Keyword.merge(Application.get_env(:ravenx, :strategies, [])) 139 | end 140 | 141 | # Private function to get definitive options keyword list by getting options 142 | # from three different places. 143 | # 144 | @spec get_options(notif_strategy, notif_payload, notif_options) :: notif_options 145 | defp get_options(strategy, payload, options) do 146 | # Get strategy configuration in application 147 | app_config_opts = Enum.into(Application.get_env(:ravenx, strategy, []), %{}) 148 | 149 | # Get config module and call the function of this strategy (if any) 150 | module_name = Application.get_env(:ravenx, :config, nil) 151 | config_module_opts = call_config_module(module_name, strategy, payload) 152 | 153 | # Merge options 154 | app_config_opts 155 | |> Map.merge(config_module_opts) 156 | |> Map.merge(options) 157 | end 158 | 159 | # Private function to call the config module if it's defined. 160 | # 161 | @spec call_config_module(atom, notif_strategy, notif_payload) :: notif_options 162 | defp call_config_module(module, _strategy, _payload) when is_nil(module), do: %{} 163 | 164 | defp call_config_module(module, strategy, payload) do 165 | if Keyword.has_key?(module.__info__(:functions), strategy) do 166 | apply(module, strategy, [payload]) 167 | else 168 | %{} 169 | end 170 | end 171 | end 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ravenx 2 | 3 | 4 | [![Current Version](https://img.shields.io/hexpm/v/ravenx.svg)](https://hex.pm/packages/ravenx) 5 | [![Build Status](https://travis-ci.org/acutario/ravenx.svg?branch=master)](https://travis-ci.org/acutario/ravenx) 6 | 7 | Notification dispatch library for Elixir applications (WIP). 8 | 9 | ## Installation 10 | 11 | 1. The package can be installed as simply as adding `ravenx` to your list of dependencies in `mix.exs`: 12 | 13 | ```elixir 14 | def deps do 15 | [{:ravenx, "~> 1.1.3"}] 16 | end 17 | ``` 18 | 19 | 2. Add Ravenx to your list of applications in `mix.exs`. This step is only needed if you are using a version older than Elixir 1.4.0 or you already have some applications listed under the `applications` key. In any other case applications are automatically inferred from dependencies (explained in the [Application inference](http://elixir-lang.github.io/blog/2017/01/05/elixir-v1-4-0-released/) section): 20 | 21 | ```elixir 22 | def application do 23 | [ 24 | applications: [ 25 | ..., 26 | :ravenx 27 | ] 28 | ] 29 | end 30 | ``` 31 | 32 | ## Strategies 33 | 34 | From version 2.0, strategies come in separate packages, so the dependencies 35 | needed are not added by default. 36 | 37 | To define strategies, just add their packages to your `mix.exs` file and add 38 | them to Ravenx configuration as follows: 39 | 40 | ```elixir 41 | config :ravenx, 42 | strategies: [ 43 | email: Ravenx.Strategy.Email 44 | slack: Ravenx.Strategy.Slack 45 | my_strategy: MyApp.Ravenx.MyStrategy 46 | ] 47 | ``` 48 | 49 | We currently maintain two strategies: 50 | 51 | * **Slack**: [hex.pm](https://hex.pm/packages/ravenx_slack) | [GitHub](https://github.com/acutario/ravenx_slack) 52 | * **E-mail** (based on Bamboo): [hex.pm](https://hex.pm/packages/ravenx_email) | [GitHub](https://github.com/acutario/ravenx_email) 53 | 54 | Also, 3rd party strategies are supported and listed below. 55 | 56 | ### 3rd party strategies 57 | 58 | Amazing people created 3rd party strategies to use Ravenx with more services: 59 | 60 | * **Pusher** (thanks to [@behind-design](https://github.com/behind-design)): [hex.pm](https://hex.pm/packages/ravenx_pusher) | [GitHub](https://github.com/behind-design/ravenx-pusher) 61 | * **Telegram** (thanks to [@maratgaliev](https://github.com/maratgaliev)): [hex.pm](https://hex.pm/packages/ravenx_telegram) | [GitHub](https://github.com/maratgaliev/ravenx_telegram) 62 | 63 | Anyone can create a strategy that works with Ravenx, so if you have one, please let us know to add it to this list. 64 | 65 | ### Custom strategies 66 | 67 | Maybe there is some internal service you need to call to send notifications, so there is a way to create custom strategies for yout projects. 68 | 69 | First of all, you need to create a module that meet the [required behaviour](https://github.com/acutario/ravenx/blob/master/lib/ravenx/strategy_behaviour.ex), like the example you can see [here](https://github.com/acutario/ravenx/blob/master/lib/ravenx/strategy/dummy.ex). 70 | 71 | Then you can define custom strategies in application configuration: 72 | 73 | ```elixir 74 | config :ravenx, 75 | strategies: [ 76 | my_strategy: YourApp.MyStrategy 77 | ] 78 | ``` 79 | 80 | and start using your strategy to deliver notifications using the atom assigned (in the example, `my_strategy`). 81 | 82 | ## Single notification 83 | 84 | Sending a single notification is as simply as calling this method: 85 | 86 | ```elixir 87 | iex> Ravenx.dispatch(strategy, payload) 88 | ``` 89 | 90 | In which `strategy` is an atom indicating one of the defined strategies and the 91 | `payload` is a map with information to dispatch the notification. 92 | 93 | For example: 94 | 95 | ```elixir 96 | iex> Ravenx.dispatch(:slack, %{title: "Hello world!", body: "Science is cool!"}) 97 | ``` 98 | 99 | Optionally, a third parameter containing a map of options (like URLs or 100 | secrets) can be passed depending on strategy configuration needs. 101 | 102 | ## Multiple notifications 103 | 104 | You can implement notification modules that `Ravenx` can use to know which strategies should use to send a specific notification. 105 | 106 | To do it, you just need to `use Ravenx.Notification` and implement a callback function: 107 | 108 | ```elixir 109 | defmodule YourApp.Notification.NotifyUser do 110 | use Ravenx.Notification 111 | 112 | def get_notifications_config(user) do 113 | # In this function you can define which strategies use for your user (or 114 | # whatever you want to pass as argument) and return something like: 115 | 116 | [ 117 | slack: {:slack, %{title: "Important notification!", body: "Wait..."}, %{channel: user.slack_username}}, 118 | email_user: {:email, %{subject: "Important notification!", html_body: "

Wait...

", to: user.email_address}}, 119 | email_company: {:email, %{subject: "Important notification about an user!", html_body: "

Wait...

", to: user.company.email_address}}, 120 | other_notification: {:invalid_strategy, %{text: "Important notification!"}, %{option1: value2}}, 121 | ] 122 | end 123 | end 124 | ``` 125 | 126 | As seen above, strategies can be used multiple times in a notification list (to send multiple e-mails that have different payload, for example). 127 | 128 | **Note:** each notification entry in the returned list should include: 129 | 130 | 1. Atom defining the notification ID. 131 | 2. A two or three element tuple containing: 132 | 1. Atom defining which strategy should be used. 133 | 2. Payload map with the data of the notification. 134 | 3. (Optional) Options map for that strategy. 135 | 136 | And then you can dispatch your notification using: 137 | 138 | ```elixir 139 | iex> YourApp.Notification.NotifyUser.dispatch(user) 140 | ``` 141 | 142 | or asynchronously: 143 | 144 | ```elixir 145 | iex> YourApp.Notification.NotifyUser.dispatch_async(user) 146 | ``` 147 | 148 | Both will return a list with the responses for each notification sent: 149 | 150 | ```elixir 151 | iex> YourApp.Notification.NotifyUser.dispatch(user) 152 | [ 153 | slack: {:ok, ...}, 154 | email_user: {:ok, ...}, 155 | email_company: {:ok, ...}, 156 | other_notification: {:error, {:unknown_strategy, :invalid_strategy}} 157 | ] 158 | ``` 159 | 160 | ## Configuration 161 | Strategies usually needs configuration options. To solve that, there are three 162 | ways in which you can configure a notification dispatch strategy: 163 | 164 | 1. Passing the options in the dispatch call: 165 | 166 | ```elixir 167 | iex> Ravenx.dispatch(:slack, %{title: "Hello world!", body: "Science is cool!"}, %{url: "...", icon: ":bird:"}) 168 | ``` 169 | 170 | 2. Specifying a configuration module in your application config: 171 | 172 | ```elixir 173 | config :ravenx, 174 | config: YourApp.RavenxConfig 175 | ``` 176 | 177 | and creating that module: 178 | 179 | ```elixir 180 | defmodule YourApp.RavenxConfig do 181 | def slack (_payload) do 182 | %{ 183 | url: "...", 184 | icon: ":bird:" 185 | } 186 | end 187 | end 188 | ``` 189 | 190 | **Note:** the module should contain a function called as the strategy yopu are 191 | configuring, receiving the payload and returning a configuration Keyword list. 192 | 193 | 3. Specifying the configuration directly on your application config file: 194 | 195 | ```elixir 196 | config :ravenx, :slack, 197 | url: "...", 198 | icon: ":bird:" 199 | ``` 200 | 201 | ### Mixing configurations 202 | Configuration can also be mixed by using the three methods: 203 | 204 | * Static configuration on application configuration. 205 | * Dynamic configuration common to more than one scenario using a configuration module. 206 | * Call-specific configuration sending a config Keyword list on `dispatch` method. 207 | 208 | ## Contribute 209 | 210 | All contributions are welcome, and we really hope this repo will serve for beginners as well for more advanced developers. 211 | 212 | If you have any doubt, feel free to ask, but always respecting our [Code of Conduct](https://github.com/acutario/ravenx_slack/blob/master/CODE_OF_CONDUCT.md). 213 | 214 | To contribute, create a fork of the repository, make your changes and create a PR. And remember, talking on PRs/issues is a must! 215 | --------------------------------------------------------------------------------