├── extras
├── Chain.md
├── Naming.md
├── Usage.md
├── Getting Started.md
├── agala.svg.png
├── logo
│ ├── agala.ai
│ ├── icon.png
│ ├── horizontal.png
│ ├── icon.svg
│ └── horizontal.svg
├── Bot Initializing.md
├── Example for agala init.md
├── General Configuration.md
├── Handlers.md
├── Providers.md
├── Bots.md
└── agala.svg
├── lib
├── backbone
│ ├── ets.ex
│ └── mnesia.ex
├── config.ex
├── util.ex
├── chain
│ ├── loopback.ex
│ ├── chain.ex
│ └── chain_builder.ex
├── bot
│ ├── plug.ex
│ ├── poller.ex
│ ├── storage.ex
│ ├── config.ex
│ ├── common
│ │ └── poller.ex
│ └── supervisor.ex
├── provider
│ └── provider.ex
├── agala.ex
├── bot_params.ex
├── backbone.ex
└── conn
│ └── conn.ex
├── config
└── config.exs
├── test
├── test_helper.exs
├── unit
│ ├── chain
│ │ ├── loopback_test.exs
│ │ └── chain_builder_test.exs
│ ├── provider
│ │ └── behaviour_test.exs
│ └── conn
│ │ ├── bot_params_test.exs
│ │ └── conn_test.exs
└── util_test.exs
├── .tool-versions
├── .coveralls.yml
├── coveralls.json
├── .travis.yml
├── .gitignore
├── CHANGELOG.md
├── README.md
├── mix.exs
└── mix.lock
/extras/Chain.md:
--------------------------------------------------------------------------------
1 | ## Chain
--------------------------------------------------------------------------------
/lib/backbone/ets.ex:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
--------------------------------------------------------------------------------
/extras/Naming.md:
--------------------------------------------------------------------------------
1 | ## Naming
2 |
3 |
--------------------------------------------------------------------------------
/extras/Usage.md:
--------------------------------------------------------------------------------
1 | ## Getting started
2 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | erlang 20.3
2 | elixir 1.6.3
3 |
--------------------------------------------------------------------------------
/extras/Getting Started.md:
--------------------------------------------------------------------------------
1 | ## Getting started
2 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: dLGilq7KLz7HEaM35iJsO5cD52TQiNFXc
2 |
--------------------------------------------------------------------------------
/coveralls.json:
--------------------------------------------------------------------------------
1 | {
2 | "skip_files": [
3 | "test"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/extras/agala.svg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agalaframework/agala/HEAD/extras/agala.svg.png
--------------------------------------------------------------------------------
/extras/logo/agala.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agalaframework/agala/HEAD/extras/logo/agala.ai
--------------------------------------------------------------------------------
/extras/logo/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agalaframework/agala/HEAD/extras/logo/icon.png
--------------------------------------------------------------------------------
/extras/logo/horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agalaframework/agala/HEAD/extras/logo/horizontal.png
--------------------------------------------------------------------------------
/extras/Bot Initializing.md:
--------------------------------------------------------------------------------
1 | # Bot initializing
2 |
3 |
4 | ## Creating bot module
5 |
6 |
7 | ```elixir
8 | def MyBot do
9 | use Agala.Bot, otp_app: :my_application
10 | end
11 | ```
12 |
13 | ## Configuration
14 |
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: elixir
3 | elixir:
4 | - 1.6.3
5 | otp_release:
6 | - 20.3
7 | env:
8 | - MIX_ENV=test
9 | script: mix coveralls.travis
10 | after_script:
11 | - mix deps.get --only docs
12 | - MIX_ENV=docs mix inch.report
13 |
--------------------------------------------------------------------------------
/lib/config.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Config do
2 |
3 | @doc """
4 | Returns current configured backbone as `Agala.Backbone` module.
5 |
6 | Can be used to perform direct calls to defined backbone.
7 | """
8 | @spec get_backbone() :: Agala.Backbone.t()
9 | def get_backbone() do
10 | Application.get_env(:agala, :backbone)
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/extras/Example for agala init.md:
--------------------------------------------------------------------------------
1 | defmodule CtiOmni.TelegramBot do
2 | use Agala.Bot, otp_app: :cti_omni
3 | end
4 |
5 |
6 |
7 |
8 |
9 | #conifg.exs
10 | config :cti_omni, CtiOmni.TelegramBot,
11 | router: CtiOmni.TelegramRouter
12 | handler: CtiOmni.TelegramHandler
13 |
14 |
15 |
16 |
17 | message = %{user_id, chat_id, message}
18 |
19 |
20 | {bot_name, cid, message}
21 |
22 | {bot_name, cid} :queue
--------------------------------------------------------------------------------
/test/unit/chain/loopback_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Chain.LoopbackTest do
2 | use ExUnit.Case
3 | alias Agala.Chain.Loopback
4 |
5 | describe "Agala.Chain.Loopback :" do
6 | test ": init" do
7 | assert %{opts: :opts} == Loopback.init(%{opts: :opts})
8 | end
9 |
10 | test ": call" do
11 | assert %Agala.Conn{responser: Test} =
12 | Loopback.call(%Agala.Conn{request_bot_params: %{bot: Test}}, [])
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/extras/General Configuration.md:
--------------------------------------------------------------------------------
1 | ## General configuration
2 |
3 | General `Agala` workflow has and idea of creating custome modules, that that
4 | are injected all functionality by **__using__** macro.
5 |
6 | You can create:
7 | * **`Agala.Bot`** -
8 |
9 |
10 |
11 |
12 |
13 | ### Bot configuration
14 |
15 | ```
16 | defmodule MyApp.TelegramBot do
17 | use Agala.Bot.Poller, otp_app: :my_app
18 |
19 | def init(_type, config) do
20 | {:ok, Keyword.put(config, :provider, )
21 | end
22 | end
23 | ```
--------------------------------------------------------------------------------
/lib/util.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Util do
2 | @doc """
3 | Method is used to retrieve `module`'s behaviour list.
4 |
5 | ### Example:
6 | defmodule FooBar do
7 | @behaviour Foo
8 | @Behaviour Bar
9 | end
10 |
11 | iex> Agala.Util.behaviours_list(FooBar)
12 | [Foo, Bar]
13 | """
14 | @spec behaviours_list(module :: atom()) :: [atom()]
15 | def behaviours_list(module) do
16 | module.module_info[:attributes]
17 | |> Enum.filter(fn {key, _} -> key == :behaviour end)
18 | |> Enum.map(fn {_, [value]} -> value end)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/.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 3rd-party dependencies like ExDoc output generated docs.
11 | /doc
12 | # InchCI docs
13 | /docs
14 |
15 | # If the VM crashes, it generates a dump, let's ignore it too.
16 | erl_crash.dump
17 |
18 | # Also ignore archive artifacts (built via "mix archive.build").
19 | *.ez
20 |
21 | # Ignore environment files
22 | *.env
23 |
24 | # ElixirLS
25 | /.elixir_ls
26 |
27 | # Logs
28 | /log
29 |
--------------------------------------------------------------------------------
/test/util_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Agala.UtilTest do
2 | use ExUnit.Case, async: true
3 | alias Agala.Util
4 |
5 | describe "behaviours_list" do
6 | test "return empty list for void module" do
7 | defmodule A do
8 | end
9 |
10 | assert [] = Util.behaviours_list(A)
11 | end
12 |
13 | test "return valid list for module full of behaviours" do
14 | defmodule A do
15 | @behaviour Foo
16 | @behaviour Bar
17 | @behaviour A.B.C
18 | end
19 |
20 | assert Foo in Util.behaviours_list(A)
21 | assert Bar in Util.behaviours_list(A)
22 | assert A.B.C in Util.behaviours_list(A)
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### 3.0.0
2 |
3 | ### Changes
4 |
5 | * Sugnificant remainings with bot structure and operation syncronisation.
6 |
7 | ### 2.0.2
8 |
9 | [BUG] Change Bot's id from string to atom
10 |
11 | ### 2.0.0
12 |
13 | ### Changes
14 |
15 | * Total remaking of the framework. Changed everything.
16 |
17 | ### 1.0.3
18 |
19 | #### Bugs
20 |
21 | * Fixing logging configuration
22 |
23 | ### 1.0.2
24 |
25 | #### Features
26 |
27 | * Added some documentation
28 | * Added coverage and deps badges
29 |
30 | ### 1.0.0
31 |
32 | #### Features
33 |
34 | * Extracted Router to separate behaviour
35 | * Added generator to scaffolding Agala bots
36 |
37 | ### 0.1.2
38 |
39 | #### Features
40 |
41 | * Adding proxy support
42 |
43 | #### Bugs
44 |
45 | * Fixing bug with crashes if the response code is not 200 or 404
46 |
--------------------------------------------------------------------------------
/extras/Handlers.md:
--------------------------------------------------------------------------------
1 | ## Handlers
2 |
3 | Basicaly, `Agala.Backbone` splits event handling into 2 stages:
4 |
5 | * **receive->backbone** stage - on which events are getting inside our system,
6 | and are preparing to be pushed into backbone. This module will cover this stage.
7 | * **backbone->handle** stage - on which events are pulling from the backbone,
8 | then somehow handled by the system.
9 |
10 | You can handle events just inside **receive** stage - this will probably increase performance
11 | of an app, because you will not need to put and then pull data from **Backbone**.
12 | On the other hand, **Backbone** alows you to split incoming events stream
13 | to be handled with pool of handlers that can be started all over the cluster.
14 | Also, sometimes you will get events directly from the **Backbone**, because
15 | receiver is created in another language or technology.
--------------------------------------------------------------------------------
/test/unit/provider/behaviour_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Agala.Provider.Test do
2 | use ExUnit.Case
3 |
4 | defmodule Foo do
5 | use Agala.Provider
6 | end
7 |
8 | defmodule Bar do
9 | use Agala.Provider
10 |
11 | def get_bot(:poller) do
12 | Poller
13 | end
14 |
15 | def get_bot(:handler) do
16 | Handler
17 | end
18 |
19 | def get_bot(:plug) do
20 | Plug
21 | end
22 | end
23 |
24 | test "Using without override" do
25 | assert Agala.Provider.Test.Foo.Poller = Foo.get_bot(:poller)
26 | assert Agala.Provider.Test.Foo.Handler = Foo.get_bot(:handler)
27 | assert Agala.Provider.Test.Foo.Plug = Foo.get_bot(:plug)
28 | end
29 |
30 | test "Using with override" do
31 | assert Poller = Bar.get_bot(:poller)
32 | assert Handler = Bar.get_bot(:handler)
33 | assert Plug = Bar.get_bot(:plug)
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/chain/loopback.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Chain.Loopback do
2 | @moduledoc """
3 | Loopback automaticly sends response back to the bot, which got the request
4 |
5 | You can use this Chain in pipeline in order to simplify resolving name of
6 | the bot, that should response with your connections.
7 |
8 | ### Example:
9 | defmodule SimpleHandler
10 | chain Agala.Chain.Loopback
11 | chain :handle
12 |
13 | def handle(conn, _opts) do
14 | # Here, responser already the same, as receiver
15 | assert conn.responser == conn.request_bot_params.bot
16 | end
17 | end
18 | """
19 | import Agala.Conn
20 |
21 | @doc false
22 | def init(opts), do: opts
23 |
24 | @doc false
25 | @spec call(conn :: Agala.Conn.t, opts :: any) :: Agala.Conn.t
26 | def call(conn = %Agala.Conn{request_bot_params: %{bot: bot}}, _opts) do
27 | send_to(conn, bot)
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/unit/conn/bot_params_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Agala.BotParamsTest do
2 | use ExUnit.Case
3 |
4 | setup do
5 | %{bot_params: %Agala.BotParams{
6 | private: %{},
7 | bot: Test,
8 | provider: FooProvider,
9 | chain: FooHandler,
10 | provider_params: nil
11 | }}
12 | end
13 |
14 | describe "Agala.BotParams :" do
15 | test ": access is working proper", %{bot_params: bot_params} do
16 | assert {:ok, %{}} = Access.fetch(bot_params, :private)
17 | assert {:ok, FooProvider} = Access.fetch(bot_params, :provider)
18 | assert {:ok, nil} = Access.fetch(bot_params, :provider_params)
19 |
20 | assert FooHandler = Access.get(bot_params, :chain)
21 | assert nil == Access.get(bot_params, :foo)
22 |
23 | assert {Test, %{bot: Shmest}} = Access.get_and_update(bot_params, :bot, fn _ -> {Test, Shmest} end)
24 |
25 | assert {Test, %{bot: Test}} = Access.pop(bot_params, :bot)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/bot/plug.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Bot.Plug do
2 | @moduledoc """
3 | This module can be used to build **Bots** to retrieve updates from third-parties
4 | as webhooks.
5 | """
6 | defmacro __using__(opts) do
7 | quote bind_quoted: [opts: opts] do
8 | use Plug.Builder
9 |
10 | {otp_app, provider, config} = Agala.Bot.Config.compile_config(:plug, __MODULE__, opts)
11 |
12 | @otp_app otp_app
13 | @provider provider
14 | @config config
15 |
16 | @doc """
17 | Retreives module's configuration.
18 | """
19 | @spec config() :: Map.t()
20 | def config, do: @config
21 |
22 | # First we need to store bot options inside connection
23 | plug :config_to_private
24 |
25 | # Then just passing control to plug, specified in provider,
26 | # and passing there bot's configuration
27 | plug provider.get_bot(:plug), config
28 |
29 | @doc false
30 | def config_to_private(conn, _opts) do
31 | conn
32 | |> Plug.Conn.put_private(:agala_bot_config, @config)
33 | end
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/unit/chain/chain_builder_test.exs:
--------------------------------------------------------------------------------
1 | defmodule FooGood do
2 | import Agala.Conn
3 |
4 | def init(opts), do: opts
5 |
6 | def call(conn, _opts) do
7 | %{conn | request: :foo}
8 | end
9 | end
10 |
11 | defmodule FooBad do
12 | import Agala.Conn
13 |
14 | def init(opts), do: opts
15 |
16 | def call_bad(conn, _opts) do
17 | %{conn | request: :foo}
18 | end
19 | end
20 |
21 | defmodule Bar do
22 | use Agala.Chain.Builder
23 |
24 | chain :bar
25 | chain :foobar
26 | chain FooGood
27 |
28 | def bar(conn, _opts) do
29 | IO.inspect "Calling BAR"
30 | IO.inspect conn
31 | %{conn | response: :bar}
32 | end
33 |
34 | def foobar(conn, _opts) do
35 | IO.inspect "Calling FOOBAR. Halting..."
36 | IO.inspect conn
37 | conn
38 | |> halt()
39 | end
40 | end
41 |
42 | defmodule ChainBuilderTest do
43 | use ExUnit.Case
44 |
45 | test "Simple chain" do
46 | assert %Agala.Conn{request: :foo} == FooGood.call(%Agala.Conn{}, [])
47 | end
48 |
49 | test "Compiletime fail" do
50 | assert_raise(ArgumentError, fn ->
51 | defmodule BarBad do
52 | use Agala.Chain.Builder
53 |
54 | chain FooBad
55 | end
56 | end)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/provider/provider.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Provider do
2 | @moduledoc """
3 | Behaviour that defines root structure for Agala provider
4 | """
5 |
6 | # @doc """
7 | # This function defines names for `Agala.Reciever` module for specific provider.
8 | # This adds some portion of flexibility for provider creators - the are not forced
9 | # to follow any naming convention.
10 |
11 | # ## Examples
12 |
13 | # iex> Agala.Provider.Vk.get_receiver
14 | # Agala.Provider.Vk.Receiver
15 | # iex> Agala.Provider.Telegram.get_receiver
16 | # Agala.Provider.Telegram.Receiver
17 | # """
18 | @callback get_bot(:poller | :plug | :handler) :: atom
19 |
20 | @typedoc """
21 | Provider is represented by it's module name
22 | """
23 | @type t :: atom()
24 |
25 | defmacro __using__(_opts) do
26 | quote location: :keep do
27 | @behaviour Agala.Provider
28 |
29 | def get_bot(:poller) do
30 | Module.concat(__MODULE__, Poller)
31 | end
32 |
33 | def get_bot(:plug) do
34 | Module.concat(__MODULE__, Plug)
35 | end
36 |
37 | def get_bot(:handler) do
38 | Module.concat(__MODULE__, Handler)
39 | end
40 |
41 | defoverridable get_bot: 1
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/extras/Providers.md:
--------------------------------------------------------------------------------
1 | ## Providers
2 |
3 | Provider is one of the main concepts of **Agala** framework. Different providers
4 | *provide* different incoming event sources for your application.
5 |
6 | All providers share same concepts of construction, but can have absolutely different
7 | underline logic. One can create his own provider and plug it into Agala platform or share
8 | between community. Or, you can use prepared provider in order to forget about
9 | event source protocols and deal only with business logic of incoming events.
10 |
11 | ### Propvider structure
12 |
13 | Basically, **provider package** can contain everything is needed to perform communication
14 | with third-parties. It doesn't have any *upper border*, it is limited only by common sence.
15 |
16 | At the same time, provider should follow some convention, and have some modules that implement
17 | special behaviours:
18 |
19 | * **Main module**: provider entry point. Should implement `Agala.Provider` behaviour.
20 | * 1 to 3 retreiving modules:
21 | * **Plug**, that follows `Agala.Bot.Plug` conventions
22 | * **Poller**, that follows `Agala.Bot.Poller` conventions
23 | * **Handler**. that follows `Agala.Bot.Handler` conventions
24 | * Optional **Helper** or **API** modules, that provide mechanisms to send response back to provider.
25 |
26 | ### Provider implimentation
27 |
28 | TODO
--------------------------------------------------------------------------------
/lib/bot/poller.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Bot.Poller do
2 | @moduledoc """
3 |
4 |
5 | ### Using
6 |
7 | Include `Agala.Bot.Poller` into your supervision tree
8 |
9 | ### Example
10 |
11 | Supervisor.start_link([
12 | MyApp.TelegramPoller
13 | ], options)
14 |
15 |
16 | ### Poller inside provider
17 |
18 | Provider's poller should be a GenServer realisation, that will take two params - Handler module and storage's pid
19 | """
20 |
21 | defmacro __using__(opts) do
22 | quote bind_quoted: [opts: opts] do
23 | @behaviour Agala.Bot.Poller
24 |
25 | {otp_app, provider, config} = Agala.Bot.Config.compile_config(:poller, __MODULE__, opts)
26 |
27 | @otp_app otp_app
28 | @provider provider
29 | @config config
30 |
31 | def config() do
32 | {:ok, config} = Agala.Bot.Supervisor.runtime_config(:poller, :dry_run, __MODULE__, @otp_app, @config)
33 | end
34 |
35 | def child_spec(opts) do
36 | %{
37 | id: __MODULE__,
38 | start: {__MODULE__, :start_link, [opts]},
39 | type: :supervisor
40 | }
41 | end
42 |
43 | def start_link(opts \\ []) do
44 | Agala.Bot.Supervisor.start_link(:poller, __MODULE__, @otp_app, @provider, @config)
45 | end
46 |
47 | def stop(pid, timeout \\ 5000) do
48 | Supervisor.stop(pid, :normal, timeout)
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/chain/chain.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Chain do
2 | @moduledoc """
3 | The Chain specification.
4 |
5 |
6 | There are two kind of Chains: function Chains and module Chains.
7 |
8 | #### Function Chains
9 |
10 | A function Chain is any function that receives a connection and a set of
11 | options and returns a connection. Its type signature must be:
12 | (Agala.Conn.t, Agala.Chain.opts) :: Agala.Conn.t
13 |
14 | #### Module Chains
15 |
16 | A module Chain is an extension of the function Chain. It is a module that must
17 | export:
18 | * a `call/2` function with the signature defined above
19 | * an `init/1` function which takes a set of options and initializes it.
20 | The result returned by `init/1` is passed as second argument to `call/2`. Note
21 | that `init/1` may be called during compilation and as such it must not return
22 | pids, ports or values that are not specific to the runtime.
23 | The API expected by a module Chain is defined as a behaviour by the
24 | `Agala.Chain` module (this module).
25 | ## Examples
26 | Here's an example of a function Chain:
27 | #TODO
28 | Here's an example of a module Chain:
29 | #TODO
30 | ## The Chain pipeline
31 | The `Agala.Chain.Builder` module provides conveniences for building Chain
32 | pipelines.
33 | """
34 |
35 | @type opts :: binary | tuple | atom | integer | float | [opts] | %{opts => opts}
36 |
37 | @callback init(opts :: opts) :: opts
38 | @callback call(conn :: Agala.Conn.t, opts) :: Agala.Conn.t
39 | end
40 |
--------------------------------------------------------------------------------
/test/unit/conn/conn_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Agala.ConnTest do
2 | use ExUnit.Case
3 |
4 | alias Agala.{Conn}
5 |
6 | setup do
7 | %{conn: %Agala.Conn{
8 | request: %{},
9 | response: nil,
10 | halted: false,
11 | request_bot_params: %Agala.BotParams{},
12 | responser: Test
13 | }}
14 | end
15 |
16 | describe "Agala.Conn :" do
17 | test ": access is working proper", %{conn: conn} do
18 | assert {:ok, %{}} = Access.fetch(conn, :request)
19 | assert {:ok, false} = Access.fetch(conn, :halted)
20 | assert {:ok, %Agala.BotParams{}} = Access.fetch(conn, :request_bot_params)
21 |
22 | assert %Agala.BotParams{} = Access.get(conn, :request_bot_params)
23 | assert nil == Access.get(conn, :foo)
24 |
25 | assert {false, %{halted: true}} = Access.get_and_update(conn, :halted, fn _ -> {false, true} end)
26 |
27 | assert {false, %{halted: false}} = Access.pop(conn, :halted)
28 | end
29 |
30 | test ": halt is working properly" do
31 | assert %{halted: true} = Conn.halt(%Agala.Conn{halted: false})
32 | end
33 |
34 | test ": send_to is working properly", %{conn: conn} do
35 | assert %{responser: Foo} = Conn.send_to(conn, Foo)
36 | end
37 |
38 | test ": assigns is working properly", %{conn: conn} do
39 | assert nil == conn.assigns[:hello]
40 | conn = Conn.assign(conn, :hello, "world")
41 | assert "world" == conn.assigns[:hello]
42 | end
43 |
44 | test ": put_private is working properly", %{conn: conn} do
45 | assert nil == conn.private[:hello]
46 | conn = Conn.put_private(conn, :hello, "world")
47 | assert "world" == conn.private[:hello]
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | [](https://hex.pm/packages/agala)
4 | [](https://hex.pm/packages/agala)
5 | [](https://hex.pm/packages/agala)
6 | [](https://travis-ci.org/agalaframework/agala)
7 | [](http://inch-ci.org/github/agalaframework/agala)
8 | [](https://coveralls.io/github/agalaframework/agala?branch=develop)
9 |
10 |
11 | Full-featured messaging bot framework.
12 |
13 | ## [Documentation](https://hexdocs.pm/agala/)
14 |
15 | All nessesary information, including tutorials, examples, guides and API documentation can be found here.
16 |
17 | ## Installation via Hex
18 |
19 | The package is [available in Hex](lttps://hex.pm/packages/agala), and can be installed as:
20 |
21 | 1. Add `agala` to your list of dependencies in `mix.exs`:
22 |
23 | ```elixir
24 | def deps do
25 | [{:agala, "~> 3.0"}]
26 | end
27 | ```
28 |
29 | 1. Ensure `agala` is started before your application:
30 |
31 | ```elixir
32 | def application do
33 | [applications: [:agala]]
34 | end
35 | ```
36 |
37 | ## Contributing
38 |
39 | 1. Fork it
40 | 2. Create your feature branch (git checkout -b my-new-feature)
41 | 3. Commit your changes (git commit -am 'Add some feature')
42 | 4. Push to the branch (git push origin my-new-feature)
43 | 5. Create new Pull Request
44 |
--------------------------------------------------------------------------------
/lib/bot/storage.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Bot.Storage do
2 | @moduledoc """
3 | This module represent permanent storage system for Agala bot.
4 |
5 | ### The problem
6 |
7 | Sometimes, some bot parts can shut down because of internal errors. It has no
8 | sense to handle this errors in *letitcrash* approach. Thus, bot should have
9 | the place to store some data that should not be lost during restarts.
10 |
11 | Of course, developer should implement his own storage for business logic, but
12 | `providers` can use this storage to save internal data.
13 |
14 | ### Implementation
15 |
16 | `Agala.Storage` has two methods: `set/3` and `get/2`.
17 | This methods are used to keep and retrieve data.
18 |
19 | If this storage is fundamental, it's lifecycle will be unlinked from `Agala.Bot`
20 | instance. But, you can implement optional `child_spec/1` method. In this case,
21 | the `Agala.Storage` module will be started inside `Agala.Bot` supervision tree.
22 | """
23 |
24 | @doc false
25 | def child_spec(opts) do
26 | case Keyword.get(opts, :name) do
27 | nil ->
28 | raise ArgumentError,
29 | "option :name is not specified in Agala.Storage call"
30 | name ->
31 | %{
32 | id: name,
33 | start: {__MODULE__, :start_link, [name]},
34 | type: :worker
35 | }
36 | end
37 | end
38 |
39 | @doc false
40 | @spec start_link(name :: atom()) :: Agent.on_start
41 | def start_link(name), do: Agent.start_link(&Map.new/0, name: name)
42 |
43 | @spec set(bot :: Agala.Bot.t(), key :: Map.key, value :: Map.value) ::
44 | {:ok, Map.value}
45 | def set(bot, key, value) do
46 | {
47 | Agent.update(Module.concat(bot, Storage), fn map -> Map.put(map, key, value) end),
48 | value
49 | }
50 | end
51 |
52 | @spec get(bot :: Agala.Bot.t(), key :: Map.key) :: any
53 | def get(bot, key) do
54 | Agent.get(Module.concat(bot, Storage), fn map -> Map.get(map, key) end)
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Agala.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :agala,
7 | version: "3.0.0",
8 | elixir: "~> 1.6",
9 | start_permanent: Mix.env() == :prod,
10 | description: description(),
11 | elixirc_paths: elixirc_paths(Mix.env()),
12 | package: package(),
13 | aliases: aliases(),
14 | test_coverage: [tool: ExCoveralls],
15 | preferred_cli_env: [
16 | coveralls: :test,
17 | "coveralls.detail": :test,
18 | "coveralls.post": :test,
19 | "coveralls.html": :test
20 | ],
21 | docs: docs(),
22 | deps: deps()
23 | ]
24 | end
25 |
26 | defp elixirc_paths(:test), do: ["lib", "test/support"]
27 | defp elixirc_paths(_), do: ["lib"]
28 |
29 | def application do
30 | [
31 | mod: {Agala, []},
32 | extra_applications: [:logger]
33 | ]
34 | end
35 |
36 | defp aliases do
37 | [
38 | test: "test --no-start"
39 | ]
40 | end
41 |
42 | defp deps do
43 | [
44 | # Dev and test dependecies
45 | {:ex_doc, "~> 0.18", only: :dev},
46 | {:inch_ex, "~> 0.5", only: [:dev, :test, :docs]},
47 | {:credo, "~> 0.8", only: [:dev, :test]},
48 | {:excoveralls, "~> 0.9", only: :test}
49 | ]
50 | end
51 |
52 | defp description do
53 | """
54 | Full featured events processing platform.
55 | """
56 | end
57 |
58 | defp docs do
59 | [
60 | main: "getting-started",
61 | logo: "extras/agala.svg.png",
62 | extras: [
63 | "extras/Getting Started.md",
64 | "extras/Bots.md",
65 | "extras/Providers.md",
66 | "extras/Usage.md",
67 | "extras/Handlers.md",
68 | "extras/General Configuration.md"
69 | ]
70 | ]
71 | end
72 |
73 | defp package do
74 | [
75 | maintainers: ["Dmitry Rubinstein"],
76 | licenses: ["MIT"],
77 | links: %{"GitHub" => "https://github.com/agalaframework/agala"},
78 | files: ~w(mix.exs README* CHANGELOG* lib)
79 | ]
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/bot/config.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Bot.Config do
2 | @doc """
3 | Retrieves the compile time configuration for the bot.
4 | """
5 | @spec compile_config(:poller | :plug | :handler, bot :: Agala.Bot.t(), opts :: Keyword.t()) ::
6 | {atom, Agala.Provider.t(), Keyword.t()}
7 | def compile_config(mode, bot, opts) when mode in [:poller, :plug] do
8 | otp_app = Keyword.fetch!(opts, :otp_app)
9 | config = Application.get_env(otp_app, bot, [])
10 | config = Keyword.merge(opts, config) |> Keyword.put(:bot, bot) |> Enum.into(%{})
11 |
12 | # Provider checking section
13 |
14 | provider = config[:provider]
15 |
16 | unless provider do
17 | raise ArgumentError,
18 | "missing :provider configuration in " <> "config #{inspect(otp_app)}, #{inspect(bot)}"
19 | end
20 |
21 | unless Code.ensure_compiled?(provider) do
22 | raise ArgumentError,
23 | "provider #{inspect(provider)} was not compiled, " <>
24 | "ensure it is correct and it is included as a project dependency"
25 | end
26 |
27 | unless Agala.Provider in Agala.Util.behaviours_list(provider) do
28 | raise ArgumentError,
29 | "provider #{inspect(provider)} does not implement Agala.Provider behaviour, " <>
30 | "ensure it is correct and it is included as a module in the project"
31 | end
32 |
33 | # Chain checking section
34 |
35 | chain = config[:chain]
36 |
37 | unless chain do
38 | raise ArgumentError,
39 | "missing :chain configuration in " <> "config #{inspect(otp_app)}, #{inspect(bot)}"
40 | end
41 |
42 | unless Code.ensure_compiled?(chain) do
43 | raise ArgumentError,
44 | "chain #{inspect(chain)} was not compiled, " <>
45 | "ensure it is correct and it is included as a module in the project"
46 | end
47 |
48 | unless Agala.Chain in Agala.Util.behaviours_list(chain) do
49 | raise ArgumentError,
50 | "chain #{inspect(chain)} does not implement Agala.Chain behaviour, " <>
51 | "ensure it is correct and it is included as a module in the project"
52 | end
53 |
54 | {otp_app, provider, config}
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/extras/logo/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
33 |
--------------------------------------------------------------------------------
/lib/backbone/mnesia.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Backbone.Mnesia do
2 | use GenServer
3 | require Logger
4 | @behaviour Agala.Backbone
5 |
6 | def start_link(args) do
7 | GenServer.start_link(__MODULE__, args, name: __MODULE__)
8 | end
9 |
10 | @impl GenServer
11 | def init(args) do
12 | :mnesia.start()
13 | {:ok, args}
14 | end
15 |
16 | # ### Agala.Backbone implimentation
17 | # @impl Agala.Backbone
18 | # def init() do
19 | # # Ensure all nodes are added
20 | # cluster_nodes = [Node.self() | Node.list()]
21 | # case :mnesia.change_config(:extra_db_nodes, cluster_nodes) do
22 | # {:ok, _} -> add_tables()
23 | # error -> error
24 | # end
25 | # end
26 |
27 | @impl Agala.Backbone
28 | def get_load(arg) do
29 | # TODO: This is stab
30 | {:ok, arg}
31 | end
32 |
33 | @impl Agala.Backbone
34 | def push(a, b, c) do
35 | # TODO: This is stab
36 | {:ok, {a, b, c}}
37 | end
38 |
39 | defp add_tables() do
40 | # Тут надо добавить какие-то таблицы
41 | Logger.debug("Adding some tables")
42 | {:ok, :mnesia_cluster_initialized}
43 | end
44 |
45 | defp create_table(table_name, opts) do
46 | current_node = Node.self()
47 | case :mnesia.create_table(table_name, opts) do
48 | {:atomic, :ok} ->
49 | Logger.debug(fn -> "#{inspect table_name} was successfully created" end)
50 | :ok
51 | {:aborted, {:already_exists, ^table_name}} ->
52 | # table already exists, trying to add table copy to current node
53 | create_table_copy(table_name)
54 | {:aborted, {:already_exists, ^table_name, ^current_node}} ->
55 | # table already exists, trying to add table copy to current node
56 | create_table_copy(table_name)
57 | error ->
58 | Logger.error(fn -> "Error while creating #{inspect table_name}: #{inspect error}" end)
59 | {:error, error}
60 | end
61 | end
62 |
63 | defp create_table_copy(table_name) do
64 | current_node = Node.self()
65 | # wait for table
66 | :mnesia.wait_for_tables([table_name], 10000)
67 | # add copy
68 | case :mnesia.add_table_copy(table_name, current_node, :ram_copies) do
69 | {:atomic, :ok} ->
70 | Logger.debug(fn -> "Copy of #{inspect table_name} was successfully added to current node" end)
71 | :ok
72 | {:aborted, {:already_exists, table_name}} ->
73 | Logger.debug(fn -> "Copy of #{inspect table_name} is already added to current node" end)
74 | :ok
75 | {:aborted, {:already_exists, table_name, current_node}} ->
76 | Logger.debug(fn -> "Copy of #{inspect table_name} is already added to current node" end)
77 | :ok
78 | {:aborted, reason} ->
79 | Logger.error(fn -> "Error while creating copy of #{inspect table_name}: #{inspect reason}" end)
80 | {:error, :reason}
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/lib/agala.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala do
2 | use Application
3 |
4 | alias Agala.Config
5 |
6 | def start(_start_type, _start_args) do
7 | # Make some configurations
8 | Agala.Backbone.supervisor()
9 | # |> Kernel.++(another_configuration_map)
10 | |> Supervisor.start_link(strategy: :one_for_one, name: Agala.Application)
11 | end
12 |
13 | ### Backbone direct calls
14 |
15 | @doc """
16 | This method is used to show bot's **receive <-> handle** load.
17 |
18 | * **Active Receivers** can use this information in order to stop retrieving new updates from third-parties.
19 | * **Passive Receivers** can use this information to stop serving for a moment until load will not decrease.
20 |
21 | Example:
22 |
23 | # For active receivers
24 |
25 | def get_updates() do
26 | # check if service is overloaded
27 | case Agala.Backbone.Foo.get_load(MyApp.MyBot) do
28 | {:ok, overload} when overload > 1000 ->
29 | # This server is overloaded
30 | # waiting a bit, to let handlers deal with overload
31 | :timer.sleep(10_000)
32 | download_updates()
33 | {:ok, normal} ->
34 | # We should not wait - load is normal
35 | download_updates()
36 | end
37 | end
38 |
39 | # For passive receivers
40 | def call(conn, opts) do
41 | # check if service is overloaded
42 | case Agala.Backbone.Foo.get_load(MyApp.MyBot) do
43 | {:ok, overload} when overload > 1000 ->
44 | # This server is overloaded
45 | # Stop serving
46 | send_500_http_error(conn)
47 | {:ok, normal} ->
48 | # We should not wait - load is normal
49 | proceed_update(conn)
50 | end
51 | end
52 | """
53 | @spec get_load(bot_name :: Agala.Bot.t()) :: {:ok, integer} | {:error, any()}
54 | def get_load(bot_name), do: Config.get_backbone().get_load(bot_name)
55 |
56 | @doc """
57 | # TODO: Docs and examples
58 | """
59 | def push(bot_name, cid, value), do: Config.get_backbone().push(bot_name, cid, value)
60 |
61 | @doc """
62 | # TODO: Docs and examples
63 | """
64 | def pull(bot_name), do: Config.get_backbone().pull(bot_name)
65 |
66 | ### Storage
67 |
68 | @doc """
69 | Sets given `value` under given `key` across bot's supervisor lifetime.
70 | Can be usefull to store some state across restarting handlers, responsers
71 | and receivers.
72 | """
73 | def set(bot_params, key, value) do
74 | Agala.Bot.Storage.set(bot_params.bot, key, value)
75 | end
76 |
77 | @doc """
78 | Gets the value, stored under the given `key` across bot's supervisor lifetime.
79 | Can be usefull to reveal some state across restarting handlers, responsers
80 | and receivers.
81 | """
82 | def get(bot_params, key) do
83 | Agala.Bot.Storage.get(bot_params.bot, key)
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/bot/common/poller.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Bot.Common.Poller do
2 | @type message :: any
3 | @moduledoc """
4 | You can use this behaviour for implementing your own **Agala.Bot.Poller**.
5 |
6 | This poller has simple generic idea:
7 |
8 | * In loop, it's geting new messages via `get_updates/2` callback
9 | * Then, these messages are sent into chain one after one.
10 |
11 | So, you actually can `use Agala.Bot.Common.Poller`, and implement only one method -
12 | `get_updates/2`, which will get new messages.
13 |
14 | ### Example
15 | # Simple random numbers generator
16 | defmodule Random.Poller do
17 | use Agala.Bot.Common.Poller
18 |
19 | def get_updates(bot_params) do
20 | {[:rand.uniform], bot_params}
21 | end
22 | end
23 | """
24 |
25 | @doc """
26 | TODO: Docs
27 | """
28 | @callback get_updates(bot_params :: Agala.BotParams.t()) :: {list(), Agala.BotParams.t()}
29 |
30 | @doc """
31 | This function is called inside `init/1` callback of a `GenServer` in order
32 | to bootstrap initial params for a bot.
33 | `:private` and `:common` params can be modified here
34 | """
35 | @callback bootstrap(bot_params :: Agala.BotParams.t()) :: {:ok, Agala.BotParams.t()} | {:error, any()}
36 |
37 | defmacro __using__(_) do
38 | quote location: :keep do
39 | use GenServer
40 | require Logger
41 | @behaviour Agala.Bot.Poller
42 |
43 | def child_spec(opts) do
44 | %{
45 | id: Module.concat(opts.bot, Poller),
46 | start: {__MODULE__, :start_link, [opts]},
47 | type: :worker
48 | }
49 | end
50 |
51 | @spec start_link(opts :: Map.t()) :: GenServer.on_start
52 | def start_link(opts) do
53 | bot_params = struct(Agala.BotParams, opts)
54 | GenServer.start_link(__MODULE__, bot_params, name: Module.concat(bot_params.bot, Poller))
55 | end
56 |
57 | @spec init(bot_params :: Agala.BotParams.t) :: {:ok, Agala.BotParams.t}
58 | def init(bot_params) do
59 | Logger.debug("Starting receiver with params:\n\t#{inspect bot_params}\r")
60 | Process.send(self(), :loop, [])
61 | bootstrap(bot_params)
62 | end
63 |
64 | @spec handle_info(:loop, bot_params :: Agala.BotParams.t) :: {:noreply, Agala.BotParams.t}
65 | def handle_info(:loop, bot_params) do
66 | Process.send(self(), :loop, [])
67 | # this callback will be call to asyncronosly push messages to handler
68 | {updates, new_bot_params} = get_updates(bot_params)
69 | updates
70 | |> Enum.each(fn event ->
71 | new_bot_params.chain.call(%Agala.Conn{request: event, request_bot_params: new_bot_params}, [])
72 | end)
73 | case get_in(new_bot_params, ([:common, :restart])) do
74 | true -> {:stop, :normal, new_bot_params}
75 | _ -> {:noreply, new_bot_params}
76 | end
77 | end
78 | def handle_info(_, state), do: {:noreply, state}
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/lib/bot_params.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.BotParams do
2 | @moduledoc """
3 | This module specified generic system for configuration all the variety of `Agala` bots.
4 | It helps to keep types of the params in consistence.
5 |
6 | Every `Agala` bot requires some common params, and also requires some specific params
7 | for only that bot type. Both of them can be specified in `BotParams` structure.
8 |
9 | `BotParams` will be piped throw all the processes of your bot's lifecycle, untill
10 | the message will be finaly sent to the external environment.
11 |
12 | So, the params are:
13 | * `otp_app` - name for bot defined application
14 | * `bot` - name of specific bot module.
15 | * `provider` - the name of the `provider` which will be used in this bot. You can
16 | read about them here.
17 | * `chain` - handler's pipline, which will work with incomming messages and do all
18 | the business logic. Thees modules you will create during your application development.
19 | You can read about handlers pipline here.
20 | * `provider_params` - params, that are needed only for specified `provider`. Agala
21 | framework does not define any structure for these params, you can find it in your
22 | `provider`s documentation.
23 | * `private` - sepcial place to put some additional information. These params can be
24 | precalculated by the internal processes of initiation for your provider, or to cache
25 | some data. In common situations you should not use it at all, until you are developing
26 | your own `provider`.
27 | * `common` - almost the same as private params, but they are the same
28 | across all possible providers. Read about them in the section below.
29 |
30 | #### Common params.
31 |
32 | Common params works the same as private params - store information for providers. But,
33 | they are **common** for each and every provider, and can touch not only `Agala.Provider`
34 | specific layer, but also `Agala` layer. You can use them inside your providers to control
35 | application flow:
36 |
37 | * `restart: true` will force `Agala.Bot.Receiver` process to restart in next cycle.
38 |
39 | """
40 |
41 | @typedoc """
42 | Type, representing `Agala.BotParams` struct.
43 | """
44 | @type t :: %Agala.BotParams{
45 | otp_app: atom,
46 | private: %{},
47 | common: %{},
48 | bot: atom,
49 | provider: atom,
50 | chain: atom,
51 | provider_params: Map.t
52 | }
53 |
54 | defstruct [
55 | otp_app: nil,
56 | private: %{},
57 | common: %{},
58 | bot: nil,
59 | provider: nil,
60 | chain: nil,
61 | provider_params: %{}
62 | ]
63 |
64 | @behaviour Access
65 | @doc false
66 | def fetch(bot_params, key) do
67 | Map.fetch(bot_params, key)
68 | end
69 |
70 | @doc false
71 | def get(structure, key, default \\ nil) do
72 | Map.get(structure, key, default)
73 | end
74 |
75 | @doc false
76 | def get_and_update(term, key, list) do
77 | Map.get_and_update(term, key, list)
78 | end
79 |
80 | @doc false
81 | def pop(term, key) do
82 | {get(term, key), term}
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
3 | "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
4 | "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
5 | "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"},
6 | "ex_doc": {:hex, :ex_doc, "0.18.4", "4406b8891cecf1352f49975c6d554e62e4341ceb41b9338949077b0d4a97b949", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
7 | "excoveralls": {:hex, :excoveralls, "0.9.1", "14fd20fac51ab98d8e79615814cc9811888d2d7b28e85aa90ff2e30dcf3191d6", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
8 | "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [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"},
9 | "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
10 | "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
11 | "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
12 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
13 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
14 | "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
15 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
16 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
17 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
18 | }
19 |
--------------------------------------------------------------------------------
/extras/Bots.md:
--------------------------------------------------------------------------------
1 | ## Bots
2 |
3 | **Bots** are main construction mechanism of **Agala** systems. You can define a **Bot**,
4 | start it inside your supervision tree or define it as a handler to your plugs,
5 | and all **API-connected** work will be done by itself.
6 |
7 | ## Long-polling receiving bots
8 |
9 | Let's for example look at **Telegram** long-polling receiving bot.
10 |
11 | It can be defined as custom module:
12 |
13 | ```elixir
14 | defmodule MyApp.TelegramReceiver do
15 | use Agala.Bot.Poller, [
16 | otp_app: :my_app,
17 | provider: Agala.Provider.Telegram,
18 | chain: MyApp.TelegramChain,
19 | provider_params: %Agala.Provider.Telegram.Poller.Params{
20 | token: "%TOKEN%",
21 | ...
22 | }
23 | ]
24 | end
25 | ```
26 |
27 | ## WebHooks receiving bots
28 |
29 | Let's for example look at **Telegram** WebHooks receiving bot.
30 |
31 | ```elixir
32 | defmodule MyApp.TelegramReceiver do
33 | use Agala.Bot.Plug, [
34 | otp_app: :my_app,
35 | provider: Agala.Provider.Telegram,
36 | chain: MyApp.TelegramChain,
37 | provider_params: %Agala.Provider.Telegram.Plug.Params{
38 | token: "%TOKEN%",
39 | ...
40 | }
41 | ]
42 | end
43 | ```
44 |
45 |
46 |
47 | ## Backbone handler bots
48 |
49 | ```elixir
50 | defmodule MyApp.TelegramHandler do
51 | use Agala.Bot.Handler, [
52 | id: MyApp.Telegram
53 | provider: Agala.Provider.Telegram,
54 | receiver: MyApp.TelegramReceiver,
55 | chain: MyApp.TelgramHandleChain,
56 | provider_params: %Agala.Provider.Telegram.Handler.Params{
57 | ...
58 | }
59 | ]
60 | end
61 | ```
62 |
63 | Rabbit:
64 | "agala.Elixir.MyApp.Telegram.in"
65 | "agala.Elixir.MyApp.Telegram.out"
66 |
67 | Rabbit:
68 |
69 | "agala.Elixir.MyApp.Telegram.rfc"
70 |
71 | "agala.Elixir.MyApp.Telegram.rfc.139" "sendPhoto"
72 |
73 |
74 |
75 | def handler(conn, _) do
76 |
77 | {:ok, xchtoto} = Telegram.get_name(id) :
78 | HTTPOSISOTN.get()
79 |
80 | {:ok, task} = Whatsapp.get_name(id, fn (response) -> end)
81 | {:ok, response} = Task.await(task)
82 |
83 | Task(
84 | AMQP.subscribe("agala.Elixir.MyApp.Telegram.rfc.139" "sendPhoto")
85 | recive do
86 | ok -> resp
87 | )
88 |
89 |
90 | end
91 |
92 |
93 |
94 | ### Compilation checks
95 |
96 | In order to provide the most fast response to something wrong in the configuration of the bot,
97 | some configuration params are checked during compilation time, and should be specified either
98 | in `Mix.Config` configuration files, or inside keyword, passed as argument to `use Agala.Bot._type_`,
99 | or inside `init/2` callback.
100 |
101 | Thus, if you want to read these configuration from environment, using for example `System.get_env`,
102 | these configurations should be specified inside your build environment.
103 |
104 | Meanwhile, configuration is just **checked** during compilation, not **compiled inside the program**.
105 | All configurations will be reread during `Agala.Bot` start process. So you can override
106 | it in your production environment.
107 |
108 | **BUT!** Facing problems with bot configuration can be an indicator of a bad code. The best ideaa will
109 | be to understand what is **runtime configuration** and what is **release configuration**,
110 | and refactor the code in respect of this understanding.
111 |
112 | Next variables are checked during compilation:
113 |
114 | * `:otp_app` - main config variable, that can be specified only as argument of `__using__` statement.
115 | This variable will be used to match configuration, specified in `Mix.Config`, with concrete `Agala.Bot`
116 | implementation. Compilation will rise, if this variable is not specified or wrong.
117 |
118 | * `:chain` - this variable specifies the pipeline to handle incoming events. It should be specified and
119 | implement `Agala.Chain` behaviour. In other case compilation will rise.
120 |
121 | * `:provider` - this variable specifies provider for entire bot. Compilation will rise if bot is not specified,
122 | or it's implemented wrong.
--------------------------------------------------------------------------------
/lib/bot/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Bot.Supervisor do
2 | use Supervisor
3 | # These default can be filled by some default parameters in the future
4 | @defaults []
5 |
6 | @doc """
7 | Retrieves the compile time configuration for the bot.
8 | """
9 | @spec compile_config(:poller | :plug | :handler, bot :: Agala.Bot.t(), opts :: Keyword.t()) ::
10 | {atom, Agala.Provider.t(), Keyword.t()}
11 | def compile_config(:poller, bot, opts) do
12 | otp_app = Keyword.fetch!(opts, :otp_app)
13 | config = Application.get_env(otp_app, bot, [])
14 | config = Keyword.merge(opts, config)
15 |
16 | # Provider checking section
17 |
18 | provider = opts[:provider] || config[:provider]
19 |
20 | unless provider do
21 | raise ArgumentError,
22 | "missing :provider configuration in " <> "config #{inspect(otp_app)}, #{inspect(bot)}"
23 | end
24 |
25 | unless Code.ensure_compiled?(provider) do
26 | raise ArgumentError,
27 | "provider #{inspect(provider)} was not compiled, " <>
28 | "ensure it is correct and it is included as a project dependency"
29 | end
30 |
31 | # Handler checking section
32 |
33 | chain = opts[:chain] || config[:chain]
34 |
35 | unless chain do
36 | raise ArgumentError,
37 | "missing :chain configuration in " <> "config #{inspect(otp_app)}, #{inspect(bot)}"
38 | end
39 |
40 | unless Code.ensure_compiled?(chain) do
41 | raise ArgumentError,
42 | "chain #{inspect(chain)} was not compiled, " <>
43 | "ensure it is correct and it is included as a module in the project"
44 | end
45 |
46 | unless Agala.Chain in Agala.Util.behaviours_list(chain) do
47 | raise ArgumentError,
48 | "chain #{inspect(chain)} does not implement Agala.Chain behaviour, " <>
49 | "ensure it is correct and it is included as a module in the project"
50 | end
51 |
52 | {otp_app, provider, config}
53 | end
54 |
55 | @doc """
56 | Retrieves the runtime configuration for the bot.
57 | """
58 | @spec runtime_config(
59 | mode :: :poller | :plug | :handler,
60 | type :: :dry_run | :supervisor,
61 | bot :: Agala.Bot.t(),
62 | otp_app :: atom(),
63 | opts :: Keyword.t()
64 | ) :: {:ok, Keyword.t()} | :ignore
65 | def runtime_config(:poller, type, bot, otp_app, opts) do
66 | if config = Application.get_env(otp_app, bot, []) do
67 | config =
68 | [otp_app: otp_app, bot: bot]
69 | |> Keyword.merge(@defaults)
70 | |> Keyword.merge(config)
71 | |> Enum.into(%{})
72 | |> Map.merge(opts)
73 |
74 | case bot_init(type, bot, config) do
75 | {:ok, config} -> {:ok, config}
76 | :ignore -> :ignore
77 | end
78 | else
79 | raise ArgumentError,
80 | "configuration for #{inspect(bot)} not specified in #{inspect(otp_app)} environment"
81 | end
82 | end
83 |
84 | ### Start section
85 | @spec start_link(
86 | mode :: :poller | :plug | :handler,
87 | bot :: Agala.Bot.t(),
88 | otp_app :: atom(),
89 | provider :: Agala.Provider.t(),
90 | opts :: Keyword.t()
91 | ) :: Supervisor.on_start()
92 | def start_link(:plug, bot, _, _, _) do
93 | raise RuntimeError,
94 | "Plugs should not be started as supervisors. Check out your #{inspect bot} plug"
95 | end
96 | def start_link(mode, bot, otp_app, provider, opts) do
97 | IO.inspect "Opts coming: #{inspect opts}"
98 | Supervisor.start_link(__MODULE__, {mode, bot, otp_app, provider, opts}, [name: bot])
99 | end
100 |
101 | @spec bot_init(type :: :dry_run | :supervisor, bot :: Agala.Bot.t(), config :: Keyword.t()) ::
102 | {:ok, Keyword.t()} | :ignore
103 | defp bot_init(type, bot, config) do
104 | if Code.ensure_loaded?(bot) and function_exported?(bot, :init, 2) do
105 | bot.init(type, config)
106 | else
107 | {:ok, config}
108 | end
109 | end
110 |
111 | ## Callbacks
112 |
113 | @spec init({
114 | mode :: :poller, :plug, :handler,
115 | bot :: Agala.Bot.t(),
116 | otp_app :: atom(),
117 | provider :: Agala.Provider.t(),
118 | opts :: Keyword.t()
119 | }) :: {:ok, tuple()} | :ignore
120 | def init({:poller, bot, otp_app, provider, opts}) do
121 | case runtime_config(:poller, :supervisor, bot, otp_app, opts) do
122 | {:ok, opts} ->
123 | children = [
124 | {Agala.Bot.Storage, name: Module.concat(bot, Storage)},
125 | {provider.get_bot(:poller), opts}
126 | ]
127 |
128 | Supervisor.init(children, strategy: :one_for_one)
129 | :ignore ->
130 | :ignore
131 | end
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/lib/backbone.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Backbone do
2 | @moduledoc """
3 | This behaviour specifies protocol, that should be implemented for each and every backbone,
4 | that can be used with `Agala` framework.
5 | """
6 |
7 | @typedoc """
8 | Backbone is represented by it's name as Atom
9 | """
10 | @type t :: atom()
11 |
12 | @doc """
13 | This function is given to configure the backbone.
14 | """
15 | @callback bake_backbone_config() :: :ok | no_return()
16 |
17 | @doc """
18 | This function is used to retrieve backbone's config
19 | """
20 | @callback get_config() :: any()
21 |
22 | @doc """
23 | This method is used to show bot's **receive <-> handle** load.
24 |
25 | * **Active Receivers** can use this information in order to stop retrieving new updates from third-parties.
26 | * **Passive Receivers** can use this information to stop serving for a moment until load will not decrease.
27 |
28 | Example:
29 |
30 | # For active receivers
31 |
32 | def get_updates() do
33 | # check if service is overloaded
34 | case Agala.Backbone.Foo.get_load(MyApp.MyBot) do
35 | {:ok, overload} when overload > 1000 ->
36 | # This server is overloaded
37 | # waiting a bit, to let handlers deal with overload
38 | :timer.sleep(10_000)
39 | download_updates()
40 | {:ok, normal} ->
41 | # We should not wait - load is normal
42 | download_updates()
43 | end
44 | end
45 |
46 | # For passive receivers
47 | def call(conn, opts) do
48 | # check if service is overloaded
49 | case Agala.Backbone.Foo.get_load(MyApp.MyBot) do
50 | {:ok, overload} when overload > 1000 ->
51 | # This server is overloaded
52 | # Stop serving
53 | send_500_http_error(conn)
54 | {:ok, normal} ->
55 | # We should not wait - load is normal
56 | proceed_update(conn)
57 | end
58 | end
59 | """
60 | @callback get_load(bot_name :: atom()) :: {:ok, integer} | {:error, any()}
61 |
62 | @doc """
63 | This method is uused to initialize bot. It should be probably used upon bot initialization
64 | """
65 | @callback init_bot(bot_name :: atom()) :: :ok | {:error, any()}
66 | @doc """
67 | This method is used to add new element to the end of queue, defined by `Agala.Bot` and queue's **CID**
68 | """
69 | @callback push(bot_name :: Agala.Bot.name(), cid :: any(), value :: any()) ::
70 | :ok | {:error, any()}
71 |
72 | @doc """
73 | This method is used to pull available element from the queue, defined by `Agala.Bot`
74 | """
75 | @callback pull(bot_name :: Agala.Bot.name()) ::
76 | {:ok, any()} | {:error, :empty} | {:error, any()}
77 |
78 | # @doc """
79 | # This method will subscribe caller process to get new events from the bot.
80 |
81 | # Messages will be of type `Agala.Conn.t()`.
82 | # """
83 | # @callback subscribe(bot_name :: Agala.Bot.name()) :: {:ok, any()} | {:error, any()}
84 |
85 | ### -------------------------------------------------------------------------------------------------------------------------------------------------
86 | ### API
87 | ### -------------------------------------------------------------------------------------------------------------------------------------------------
88 |
89 | @doc """
90 | Returns supervision config for specififed backbone.
91 |
92 | If the backbone is not specified - returns empty list
93 | """
94 | @spec supervisor() :: [atom()] | []
95 | def supervisor() do
96 | case bake_backbone_config() do
97 | {:ok, :empty} -> []
98 | {:ok, backbone} -> [backbone]
99 | end
100 | end
101 |
102 | @doc """
103 | This function will check backbone configuration.
104 |
105 | If everything is right - `ok tuple` with `Agala.Backbone` implementation module will be returned.
106 | If backbone is not specified -
107 |
108 | If it's not specified correct - function will rise.
109 | """
110 | @spec bake_backbone_config() :: {:ok, t} | {:ok, :empty} | no_return()
111 | def bake_backbone_config() do
112 | # Checking backbone
113 | case Application.get_env(:agala, :backbone, nil) do
114 | nil ->
115 | {:ok, :empty}
116 |
117 | backbone ->
118 | unless Code.ensure_loaded?(backbone) do
119 | raise ArgumentError,
120 | "backbone #{inspect(backbone)} was not compiled, " <>
121 | "ensure it is correct and it is included as a module in the project"
122 | end
123 |
124 | unless Agala.Backbone in Agala.Util.behaviours_list(backbone) do
125 | raise ArgumentError,
126 | "backbone #{inspect(backbone)} does not implement Agala.Backbone behaviour, " <>
127 | "ensure it is correct and it is included as a module in the project"
128 | end
129 |
130 | # All ok. Backing backbone specific config
131 | :ok = backbone.bake_backbone_config()
132 | {:ok, backbone}
133 | end
134 | end
135 | end
136 |
--------------------------------------------------------------------------------
/lib/conn/conn.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Conn do
2 | @moduledoc """
3 | The Agala connection.
4 |
5 | This module defines a `Agala.Conn` struct. This struct contains
6 | both request and response data.
7 |
8 | ## Request fields
9 |
10 |
11 | These fields contain request information:
12 |
13 | * `request` - request data structure. It's internal structure depends
14 | on provider type.
15 | """
16 |
17 | defstruct [
18 | assigns: %{},
19 | request: nil,
20 | response: nil,
21 | halted: false,
22 | private: %{},
23 | request_bot_params: %Agala.BotParams{},
24 | responser: nil
25 | ]
26 |
27 | @type t :: %Agala.Conn{
28 | assigns: Map.t,
29 | request: Map.t,
30 | response: Map.t,
31 | halted: boolean,
32 | private: Map.t,
33 | request_bot_params: Agala.BotParams.t,
34 | responser: String.t | Atom
35 | }
36 |
37 | @behaviour Access
38 | @doc false
39 | def fetch(bot_params, key) do
40 | Map.fetch(bot_params, key)
41 | end
42 |
43 | @doc false
44 | def get(structure, key, default \\ nil) do
45 | Map.get(structure, key, default)
46 | end
47 |
48 | @doc false
49 | def get_and_update(term, key, list) do
50 | Map.get_and_update(term, key, list)
51 | end
52 |
53 | @doc false
54 | def pop(term, key) do
55 | {get(term, key), term}
56 | end
57 |
58 | @doc """
59 | Halts the Agala.Chain pipeline by preventing further chains downstream from being
60 | invoked. See the docs for `Agala.Chain.Builder` for more information on halting a
61 | Chain pipeline.
62 | """
63 | @spec halt(t) :: t
64 | def halt(%Agala.Conn{} = conn) do
65 | %{conn | halted: true}
66 | end
67 |
68 | @doc """
69 | Specifies the name for the bot, which will send the response back
70 | to side APIs.
71 | """
72 | def send_to(%Agala.Conn{} = conn, name) do
73 | conn
74 | |> Map.put(:responser, name)
75 | end
76 |
77 | @doc """
78 | Assigns a value to a key in the connection
79 | ## Examples
80 | iex> conn.assigns[:hello]
81 | nil
82 | iex> conn = assign(conn, :hello, :world)
83 | iex> conn.assigns[:hello]
84 | :world
85 | """
86 | @spec assign(t, atom, term) :: t
87 | def assign(%Agala.Conn{assigns: assigns} = conn, key, value) when is_atom(key) do
88 | %{conn | assigns: Map.put(assigns, key, value)}
89 | end
90 |
91 | @doc """
92 | Starts a task to assign a value to a key in the connection.
93 | `await_assign/2` can be used to wait for the async task to complete and
94 | retrieve the resulting value.
95 | Behind the scenes, it uses `Task.async/1`.
96 | ## Examples
97 | iex> conn.assigns[:hello]
98 | nil
99 | iex> conn = async_assign(conn, :hello, fn -> :world end)
100 | iex> conn.assigns[:hello]
101 | %Task{...}
102 | """
103 | @spec async_assign(t, atom, (() -> term)) :: t
104 | def async_assign(%Agala.Conn{} = conn, key, fun) when is_atom(key) and is_function(fun, 0) do
105 | assign(conn, key, Task.async(fun))
106 | end
107 |
108 | @doc """
109 | Awaits the completion of an async assign.
110 | Returns a connection with the value resulting from the async assignment placed
111 | under `key` in the `:assigns` field.
112 | Behind the scenes, it uses `Task.await/2`.
113 | ## Examples
114 | iex> conn.assigns[:hello]
115 | nil
116 | iex> conn = async_assign(conn, :hello, fn -> :world end)
117 | iex> conn = await_assign(conn, :hello) # blocks until `conn.assigns[:hello]` is available
118 | iex> conn.assigns[:hello]
119 | :world
120 | """
121 | @spec await_assign(t, atom, timeout) :: t
122 | def await_assign(%Agala.Conn{} = conn, key, timeout \\ 5000) when is_atom(key) do
123 | task = Map.fetch!(conn.assigns, key)
124 | assign(conn, key, Task.await(task, timeout))
125 | end
126 |
127 | @doc """
128 | Assigns a new **private** key and value in the connection.
129 | This storage is meant to be used by libraries and frameworks to avoid writing
130 | to the user storage (the `:assigns` field). It is recommended for
131 | libraries/frameworks to prefix the keys with the library name.
132 | For example, if some plug needs to store a `:hello` key, it
133 | should do so as `:plug_hello`:
134 | iex> conn.private[:plug_hello]
135 | nil
136 | iex> conn = put_private(conn, :plug_hello, :world)
137 | iex> conn.private[:plug_hello]
138 | :world
139 | """
140 | @spec put_private(t, atom, term) :: t
141 | def put_private(%Agala.Conn{private: private} = conn, key, value) when is_atom(key) do
142 | %{conn | private: Map.put(private, key, value)}
143 | end
144 |
145 | @doc """
146 | Specifies the lambda function that will be called after the result of
147 | provider's respponse to the bot's response will appear.
148 | The lambda shuld have only one parameter - `Agala.Conn.t` for current connection.
149 | It'll have `request` with request to bot, `response` with response from bot, and
150 | `fallback` with response sending results.
151 | """
152 | def with_fallback(%Agala.Conn{} = conn, fallback_callback) do
153 | conn
154 | |> Map.put(:fallback, fallback_callback)
155 | end
156 | end
157 |
--------------------------------------------------------------------------------
/lib/chain/chain_builder.ex:
--------------------------------------------------------------------------------
1 | defmodule Agala.Chain.Builder do
2 | @type chain :: module | atom
3 |
4 | @doc false
5 | defmacro __using__(opts) do
6 | quote do
7 | @behaviour Agala.Chain
8 | @chain_builder_opts unquote(opts)
9 |
10 | def init(opts) do
11 | opts
12 | end
13 |
14 | def call(conn, opts) do
15 | chain_builder_call(conn, opts)
16 | end
17 |
18 | defoverridable [init: 1, call: 2]
19 |
20 | import Agala.Conn
21 | import Agala.Chain.Builder, only: [chain: 1, chain: 2]
22 |
23 | Module.register_attribute(__MODULE__, :chains, accumulate: true)
24 | @before_compile Agala.Chain.Builder
25 | end
26 | end
27 |
28 | @doc false
29 | defmacro __before_compile__(env) do
30 | chains = Module.get_attribute(env.module, :chains)
31 | builder_opts = Module.get_attribute(env.module, :chain_builder_opts)
32 |
33 | {conn, body} = Agala.Chain.Builder.compile(env, chains, builder_opts)
34 |
35 | quote do
36 | defp chain_builder_call(unquote(conn), _), do: unquote(body)
37 | end
38 | end
39 |
40 | @doc """
41 | A macro that stores a new chain. `opts` will be passed unchanged to the new
42 | chain.
43 |
44 | This macro doesn't add any guards when adding the new chain to the pipeline;
45 | for more information about adding chains with guards see `compile/1`.
46 |
47 | ## Examples
48 | chain Agala.Chain.Logger # chain module
49 | chain :foo, some_options: true # chain function
50 | """
51 | defmacro chain(chain, opts \\ []) do
52 | quote do
53 | @chains {unquote(chain), unquote(opts), true}
54 | end
55 | end
56 |
57 | @doc """
58 | Compiles a chain pipeline.
59 |
60 | Each element of the chain pipeline (according to the type signature of this
61 | function) has the form:
62 | ```
63 | {chain_name, options, guards}
64 | ```
65 |
66 | Note that this function expects a reversed pipeline (with the last chain that
67 | has to be called coming first in the pipeline).
68 |
69 | The function returns a tuple with the first element being a quoted reference
70 | to the connection and the second element being the compiled quoted pipeline.
71 |
72 | ## Examples
73 | Agala.Chain.Builder.compile(env, [
74 | {Agala.Chain.Logger, [], true}, # no guards, as added by the Agala.Chain.Builder.chain/2 macro
75 | {Agala.Chain.Head, [], quote(do: a when is_binary(a))}
76 | ], [])
77 | """
78 | @spec compile(Macro.Env.t, [{chain, Agala.Chain.opts, Macro.t}], Keyword.t) :: {Macro.t, Macro.t}
79 | def compile(env, pipeline, builder_opts) do
80 | conn = quote do: conn
81 | {conn, Enum.reduce(pipeline, conn, "e_chain(init_chain(&1), &2, env, builder_opts))}
82 | end
83 |
84 | # Initializes the options of a chain at compile time.
85 | defp init_chain({chain, opts, guards}) do
86 | case Atom.to_charlist(chain) do
87 | ~c"Elixir." ++ _ -> init_module_chain(chain, opts, guards)
88 | _ -> init_fun_chain(chain, opts, guards)
89 | end
90 | end
91 |
92 | defp init_module_chain(chain, opts, guards) do
93 | initialized_opts = chain.init(opts)
94 |
95 | if function_exported?(chain, :call, 2) do
96 | {:module, chain, initialized_opts, guards}
97 | else
98 | raise ArgumentError, message: "#{inspect chain} chain must implement call/2"
99 | end
100 | end
101 |
102 | defp init_fun_chain(chain, opts, guards) do
103 | {:function, chain, opts, guards}
104 | end
105 |
106 | # `acc` is a series of nested chain calls in the form of
107 | # chain3(chain2(chain1(conn))). `quote_chain` wraps a new chain around that series
108 | # of calls.
109 | defp quote_chain({chain_type, chain, opts, guards}, acc, env, builder_opts) do
110 | call = quote_chain_call(chain_type, chain, opts)
111 |
112 | error_message = case chain_type do
113 | :module -> "expected #{inspect chain}.call/2 to return a Agala.Conn"
114 | :function -> "expected #{chain}/2 to return a Agala.Conn"
115 | end <> ", all chains must receive a connection (conn) and return a connection"
116 |
117 | {fun, meta, [arg, [do: clauses]]} =
118 | quote do
119 | case unquote(compile_guards(call, guards)) do
120 | %Agala.Conn{halted: true} = conn ->
121 | unquote(log_halt(chain_type, chain, env, builder_opts))
122 | conn
123 | %Agala.Conn{} = conn ->
124 | unquote(acc)
125 | _ ->
126 | raise unquote(error_message)
127 | end
128 | end
129 |
130 | generated? = :erlang.system_info(:otp_release) >= '19'
131 |
132 | clauses =
133 | Enum.map(clauses, fn {:->, meta, args} ->
134 | if generated? do
135 | {:->, [generated: true] ++ meta, args}
136 | else
137 | {:->, Keyword.put(meta, :line, -1), args}
138 | end
139 | end)
140 |
141 | {fun, meta, [arg, [do: clauses]]}
142 | end
143 |
144 | defp quote_chain_call(:function, chain, opts) do
145 | quote do: unquote(chain)(conn, unquote(Macro.escape(opts)))
146 | end
147 |
148 | defp quote_chain_call(:module, chain, opts) do
149 | quote do: unquote(chain).call(conn, unquote(Macro.escape(opts)))
150 | end
151 |
152 | defp compile_guards(call, true) do
153 | call
154 | end
155 |
156 | defp compile_guards(call, guards) do
157 | quote do
158 | case true do
159 | true when unquote(guards) -> unquote(call)
160 | true -> conn
161 | end
162 | end
163 | end
164 |
165 | defp log_halt(chain_type, chain, env, builder_opts) do
166 | if level = builder_opts[:log_on_halt] do
167 | message = case chain_type do
168 | :module -> "#{inspect env.module} halted in #{inspect chain}.call/2"
169 | :function -> "#{inspect env.module} halted in #{inspect chain}/2"
170 | end
171 |
172 | quote do
173 | require Logger
174 | # Matching, to make Dialyzer happy on code executing Agala.Chain.Builder.compile/3
175 | _ = Logger.unquote(level)(unquote(message))
176 | end
177 | else
178 | nil
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------
/extras/logo/horizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
78 |
--------------------------------------------------------------------------------
/extras/agala.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
225 |
--------------------------------------------------------------------------------