Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Only possible with Elixir - ubots Case Study

Only possible with Elixir - ubots Case Study

How to build four successful products with a tiny team of two working part-time after-hours? The answer is: Elixir.

- What are Slack Bots and why you should care
- How we decided on our architecture and what were the consequences
- How we've built a new framework using old ideas
- How we've built an end-to-end testing framework for complex interactions

31bfdacd5334a8c414b25fd66a40a092?s=128

Tymon Tobolski

May 07, 2021
Tweet

More Decks by Tymon Tobolski

Other Decks in Programming

Transcript

  1. None
  2. Only possible with Elixir UBOTS Case Study

  3. My name is Tymon Tobolski. I'm the one behind tesla

    HTTP library. Co-founder of UBOTS - Useful bots for Slack @teamon @ GitHub @iteamon @ Twitter
  4. Ruby ➡ Elixir (ASM, C, PHP, Java, Scala, Haskell, JavaScript,

    Go, Infra, ...)
  5. How to build something exclusively with Elixir?

  6. None
  7. None
  8. UI = JSON ‑ ‑ ‑ ! =

  9. None
  10. Adam ! Development @ Ubots Head of Integrations @ Recruitee

    Kamil Marketing @ Ubots SRE @ Cabify
  11. Why we succeeded?

  12. Why we succeeded? We didn't quit our jobs We didn't

    move to Silicon Valley We didn't take VC money
  13. Why we succeeded? We did charge $$$ from day one

    We chose Elixir
  14. Developer Productivity

  15. Elixir is Extremely Stable

  16. Seamless Updates 1.9 ➡ 1.10 ➡ 1.11

  17. Best Tooling mix, hex, dialyzer, formatter phoenix live reload, mix

    test.watch ElixirLS
  18. !! != One Git repository One OTP application One Erlang

    release One Docker image + Two Developers ------------------------------ One Majestic Monolith
  19. Top-Level Modules lib !"" core/ !"" core.ex !"" core_web/ !""

    core_web.ex Core CoreWeb lib !"" bdgt/ !"" bdgt.ex !"" lunch/ !"" lunch.ex Bdgt Lunch lib !"" queue/ !"" queue.ex !"" td/ #"" td.ex Queue Td
  20. This must be slow lib/ 21.000 LOC $ time mix

    compile Compiling 277 files (.ex) real 0m34.552s test/ 10.000 LOC $ mix test Finished in 17.1 seconds 18 doctests, 483 tests
  21. the BORING stack

  22. Old Patterns New "Framework"

  23. HTTP POST /events content-type: application/json { "payload": { "user_id": "U123",

    "team_id": "T456" "command": "/help" } } --- POST /events content-type: text/form-urlencoded user_id=U123&team_id=T456&command=/help Core.Bot %Context{ user_id: "U123", team_id: "T456", responses: [...] ... } # Actions %Command{ command: "/queue", text: "help" } %Button{value: "join"} %Shortcut{...} ...
  24. Ubots Slack "Framework" Web App Slack App Plug.Conn Context Struct

    params Action Structs Plug Middleware Phoenix.Router (Pattern Matching) Phoenix.Controller Controller Phoenix.View View
  25. Pure Functional Code

  26. View (pure function) defmodule Queue.Views.ModalConfig do def render(queue) do %{

    type: "modal", blocks: [...] } end end
  27. Impure Functional Code (side effects are cool)

  28. Middleware (impure function) defmodule Core.Bot.Middleware.Logger do def call(ctx, action) do

    Logger.info(...) ctx end end
  29. Pattern Matching

  30. Pattern Matching in Actions ( ) def build(%{ "command" =>

    command, "text" => text }) do %Command{command: command, text: text} end def build(%{ "type" => "shortcut", "callback_id" => callback_id }) do %Shortcut{callback_id: callback_id} end def build(%{ "type" => "view_submission", "view" => %{ "callback_id" => callback_id, "external_id" => external_id, "state" => %{"values" => values} } }) do %Submit{ callback_id: callback_id, external_id: decode_external_id(external_id), values: decode_values(values) } end
  31. Pattern Matching in Controllers defmodule Queue.Controllers.Main do use Core.Bot.Controller def

    call(ctx, %Button{action_id: "button_join"}), do: # ... def call(ctx, %Command{text: "join"}), do: # ... def call(ctx, %Button{action_id: "button_leave"}), do: # ... def call(ctx, %Command{text: "leave"}), do: # ...
  32. Macros (just some drops)

  33. Macros for Controller Pipelines defmodule Queue do use Core.Bot.Controller plug

    Core.Bot.Middleware.Logger plug Core.Bot.Middleware.Ignorables plug Core.Bot.Middleware.Duplicates plug Core.Bot.Middleware.Support plug Queue.Controllers.Main plug Queue.Controllers.Home plug Queue.Controllers.Workflows # ...
  34. Macros for DRY defmodule Core.Bot.Actions do use Core.Bot.ActionsBuilder defaction(Button, action_id:

    binary, value: binary ) defaction(Datepicker, action_id: binary, action_ts: binary, block_id: binary, initial_date: binary | nil, selected_date: binary )
  35. Processes & OTP

  36. Phoenix, Oban, ... got your back

  37. Unleashing the full potential setup do [ alice: Slack.user(), dev:

    Slack.channel() ] end test "/queue", %{alice: alice, dev: dev} do alice |> go_to_channel(dev) |> command("/queue") |> click_button("Join") assert visible?(dev, "People in queue") end
  38. Unleashing the full potential

  39. Refactoring

  40. Lax.Server (Single GenSever) defmodule Lax.Server do use GenServer # ...

    def handle_call({:get_channel, channel_id}, from, state) do handle_reply(Lax.State.get_channel(state, channel_id), from) end def handle_call({:access, action}, from, state) do handle_reply(Lax.Access.on(action, state), from) end def handle_call({:ui, action}, from, state) do handle_reply(Lax.UI.on(action, state), from) end def handle_call({:outgoing, ...}) do handle_reply(Lax.API.on(method, body, state), from) end # ... end ... The chery on top of all elixir features is the amazing community
  41. Friendly Community & Smart People

  42. Is this really only possible with Elixir?

  43. 10 out of 10 boxes say: YES! ✅ Tooling ✅

    Stability ✅ Readability ✅ Productivity ✅ Developer Experience ✅ Rich ecosystem ✅ Performance ✅ Scalability ✅ Community ✅ Great choice for business
  44. ! Q & A ! Visit ubots.xyz for Useful Slack

    Bots ! Sign up at moonhome.io for the Future of Remote Development ! Follow @iteamon on Twitter to stay up-to-date
  45. None