├── 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 |

agala

2 | 3 | [![Hex.pm](https://img.shields.io/hexpm/v/agala.svg)](https://hex.pm/packages/agala) 4 | [![Hex.pm](https://img.shields.io/hexpm/dt/agala.svg)](https://hex.pm/packages/agala) 5 | [![Hex.pm](https://img.shields.io/hexpm/l/agala.svg)](https://hex.pm/packages/agala) 6 | [![Travis](https://travis-ci.org/agalaframework/agala.svg?branch=develop)](https://travis-ci.org/agalaframework/agala) 7 | [![Inline docs](http://inch-ci.org/github/agalaframework/agala.svg)](http://inch-ci.org/github/agalaframework/agala) 8 | [![Coverage Status](https://coveralls.io/repos/github/agalaframework/agala/badge.svg?branch=develop)](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 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 21 | 23 | 24 | 26 | 28 | 30 | 31 | 32 | 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 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 22 | 24 | 25 | 27 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 62 | 63 | 64 | 65 | 66 | 67 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /extras/agala.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 34 | 37 | 41 | 45 | 46 | 56 | 67 | 68 | 94 | 96 | 97 | 99 | image/svg+xml 100 | 102 | 103 | 104 | 105 | 106 | 111 | 113 | 119 | 125 | 128 | 130 | 132 | 137 | 141 | 146 | 147 | 148 | 150 | 152 | 154 | 156 | 158 | 160 | 162 | 164 | 166 | 168 | 170 | 172 | 174 | 176 | 178 | 179 | 182 | 184 | 190 | 191 | 193 | 195 | 197 | 199 | 201 | 203 | 205 | 207 | 209 | 211 | 213 | 215 | 217 | 219 | 221 | 222 | 223 | 224 | 225 | --------------------------------------------------------------------------------