From 1fd05dc7e4fcdc6382f5fb570b45e02b165b5b09 Mon Sep 17 00:00:00 2001 From: Anthony Leiro Date: Sun, 2 Jan 2022 01:22:28 +0300 Subject: [PATCH] Use response tuple (#41) * feat: take response tuple * chore: update readme * chore: fix error * chore: update readme --- CHANGELOG.md | 5 + README.md | 36 ++-- lib/ex_ussd.ex | 291 +++++++++++++++++++-------------- lib/ex_ussd/executer.ex | 128 +++++++++++---- lib/ex_ussd/op.ex | 1 - mix.exs | 2 +- test/ex_ussd/executer_test.exs | 4 +- test/ex_ussd/op_test.exs | 99 +++++------ 8 files changed, 346 insertions(+), 220 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0ddf8..1fd085f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,8 @@ ## v1.0.0 - 2021-10-02 - Add support for Zero Based menu list + +# v1.1.0 - 2022-01-01 + + - Deplicate set `error` on ExUssd.set/2 function + - use response tuple diff --git a/README.md b/README.md index c82faa2..0458a93 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ by adding `ex_ussd` to your list of dependencies in `mix.exs`: ```elixir defp deps do [ - {:ex_ussd, "~> 1.0.2"} + {:ex_ussd, "~> 1.1.0"} ] end ``` @@ -63,31 +63,37 @@ defmodule ApiWeb.HomeResolver do use ExUssd def ussd_init(menu, _payload) do - ExUssd.set(menu, title: "Enter your PIN") + {:ok, ExUssd.set(menu, title: "Enter your PIN")} end def ussd_callback(menu, payload, %{attempt: %{count: count}}) do if payload.text == "5555" do - menu - |> ExUssd.set(data: %{name: "John"}) # use payload `phone_number` to fetch the user from DB - |> ExUssd.set(resolve: &home_rc/2) + menu = + menu + |> ExUssd.set(data: %{name: "John"}) # use payload `phone_number` to fetch the user from DB + |> ExUssd.set(resolve: &home_rc/2) + {:ok, menu} else - ExUssd.set(menu, error: "Wrong PIN, #{2 - count} attempt left\n") + {:error, "Wrong PIN, #{2 - count} attempt left"} end end def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do - menu - |> ExUssd.set(title: "Account is locked, Dial *234# to reset your account") - |> ExUssd.set(should_close: true) + menu = + menu + |> ExUssd.set(title: "Account is locked, Dial *234# to reset your account") + |> ExUssd.set(should_close: true) + {:ok, menu} end def home_rc(%ExUssd{data: %{name: name}} = menu, _) do - menu - |> ExUssd.set(title: "Welcome #{name}!") - |> ExUssd.add(ExUssd.new(name: "option 1")) - |> ExUssd.add(ExUssd.new(name: "option 2")) - |> ExUssd.set(show_navigation: false) # hide navigation options + menu = + menu + |> ExUssd.set(title: "Welcome #{name}!") + |> ExUssd.add(ExUssd.new(name: "option 1")) + |> ExUssd.add(ExUssd.new(name: "option 2")) + |> ExUssd.set(show_navigation: false) # hide navigation options + {:ok, menu} end end ``` @@ -110,4 +116,4 @@ Auto-populated from: ## Licence -ExUssd is released under [Apache License 2.0](./LICENSE). +ExUssd is released under [Apache License 2.0](LICENSE). diff --git a/lib/ex_ussd.ex b/lib/ex_ussd.ex index c41f768..f24cfcd 100644 --- a/lib/ex_ussd.ex +++ b/lib/ex_ussd.ex @@ -88,7 +88,7 @@ defmodule ExUssd do iex> defmodule AppWeb.HomeResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> ExUssd.set(menu, title: "Enter your PIN") + ...> {:ok, ExUssd.set(menu, title: "Enter your PIN")} ...> end ...> end iex> # To simulate a user entering a PIN, you can use the `ExUssd.to_string/2` method. @@ -117,19 +117,21 @@ defmodule ExUssd do iex> defmodule AppWeb.PinResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> ExUssd.set(menu, title: "Enter your PIN") + ...> {:ok, ExUssd.set(menu, title: "Enter your PIN")} ...> end ...> def ussd_callback(menu, payload, _) do ...> if payload.text == "5555" do - ...> ExUssd.set(menu, resolve: &success_menu/2) + ...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)} ...> else - ...> ExUssd.set(menu, error: "Wrong PIN\\n") + ...> {:error, "Wrong PIN"} ...> end ...> end ...> def success_menu(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") - ...> |> ExUssd.set(should_close: true) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") + ...> |> ExUssd.set(should_close: true) + ...> {:ok, menu} ...> end ...> end iex> # To simulate a user entering correct PIN, you can use the `ExUssd.to_string/3` method. @@ -150,17 +152,19 @@ defmodule ExUssd do iex> defmodule AppWeb.PinResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> ExUssd.set(menu, title: "Enter your PIN") + ...> {:ok, ExUssd.set(menu, title: "Enter your PIN")} ...> end ...> def ussd_callback(menu, payload, _) do ...> if payload.text == "5555" do - ...> ExUssd.set(menu, resolve: &success_menu/2) + ...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)} ...> end ...> end ...> def success_menu(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") - ...> |> ExUssd.set(should_close: true) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") + ...> |> ExUssd.set(should_close: true) + ...> {:ok, menu} ...> end ...> end iex> # To simulate a user entering wrong PIN. @@ -177,21 +181,23 @@ defmodule ExUssd do iex> defmodule AppWeb.ProductResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "Product List, Enter 5555 for Offers") - ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Product List, Enter 5555 for Offers") + ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> {:ok, menu} ...> end ...> def ussd_callback(menu, payload, _) do ...> if payload.text == "5555" do - ...> ExUssd.set(menu, resolve: &product_offer/2) + ...> {:ok, ExUssd.set(menu, resolve: &product_offer/2)} ...> end ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") - ...> def product_offer(menu, _payload), do: menu |> ExUssd.set(title: "selected product offer") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} + ...> def product_offer(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product offer")} ...> end iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver) iex> # To simulate a user entering "5555" @@ -216,11 +222,13 @@ defmodule ExUssd do iex> defmodule AppWeb.ProductResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "Product List") - ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Product List") + ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> {:ok, menu} ...> end ...> def ussd_after_callback(%{error: true} = _menu, _payload, _metadata) do ...> # Use the gateway payload and metadata to capture user metrics on error @@ -228,9 +236,9 @@ defmodule ExUssd do ...> def ussd_after_callback(_menu, _payload, _metadata) do ...> # Use the gateway payload and metadata to capture user metrics before navigating to next menu ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} ...> end iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver) iex> # To simulate a user selecting option "1" @@ -248,24 +256,28 @@ defmodule ExUssd do iex> defmodule AppWeb.HomeResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> ExUssd.set(menu, title: "Enter your PIN") + ...> {:ok, ExUssd.set(menu, title: "Enter your PIN")} ...> end ...> def ussd_callback(menu, payload, _) do ...> if payload.text == "5555" do - ...> ExUssd.set(menu, resolve: &success_menu/2) + ...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)} ...> else - ...> ExUssd.set(menu, error: "Wrong PIN\\n") + ...> {:error, "Wrong PIN"} ...> end ...> end ...> def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do - ...> menu - ...> |> ExUssd.set(title: "Account is locked, you have entered the wrong PIN 3 times") - ...> |> ExUssd.set(should_close: true) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Account is locked, you have entered the wrong PIN 3 times") + ...> |> ExUssd.set(should_close: true) + ...> {:ok, menu} ...> end ...> def success_menu(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") - ...> |> ExUssd.set(should_close: true) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") + ...> |> ExUssd.set(should_close: true) + ...> {:ok, menu} ...> end ...> end iex> # To simulate a user entering wrong PIN 3 times. @@ -328,10 +340,12 @@ defmodule ExUssd do ## Example iex> resolve = fn menu, _payload -> - ...> menu - ...> |> ExUssd.set(title: "Menu title") - ...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &(ExUssd.set(&1, title: "option 1")))) - ...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &(ExUssd.set(&1, title: "option 2")))) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Menu title") + ...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &({:ok, ExUssd.set(&1, title: "option 1")}))) + ...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &({:ok, ExUssd.set(&1, title: "option 2")}))) + ...> {:ok, menu} ...> end iex> menu = ExUssd.new(name: "HOME", resolve: resolve) iex> ExUssd.to_string!(menu, []) @@ -357,9 +371,11 @@ defmodule ExUssd do ...> menus = Enum.map(locations, fn location -> ...> ExUssd.new(name: "Location " <> location, data: %{name: location}) ...> end) - ...> menu - ...> |> ExUssd.set(title: "Select Location") - ...> |> ExUssd.add(menus, resolve: &(ExUssd.set(&1, title: "Location " <> &1.data.name))) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Select Location") + ...> |> ExUssd.add(menus, resolve: &(ExUssd.set(&1, title: "Location " <> &1.data.name))) + ...> {:ok, menu} ...> end ...> end iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.LocationResolver) @@ -412,10 +428,12 @@ defmodule ExUssd do Example: iex> resolve = fn menu, _payload -> - ...> menu - ...> |> ExUssd.set(title: "Menu title") - ...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &(ExUssd.set(&1, title: "option 1")))) - ...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &(ExUssd.set(&1, title: "option 2")))) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Menu title") + ...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &({:ok, ExUssd.set(&1, title: "option 1")}))) + ...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &({:ok, ExUssd.set(&1, title: "option 2")}))) + ...> {:ok, menu} ...> end iex> menu = ExUssd.new(name: "HOME", resolve: resolve) iex> ExUssd.to_string!(menu, []) @@ -430,11 +448,13 @@ defmodule ExUssd do Example: iex> resolve = fn menu, _payload -> - ...> menu - ...> |> ExUssd.set(title: "Menu title") - ...> |> ExUssd.add(ExUssd.new(name: "offers", resolve: fn menu, _ -> ExUssd.set(menu, title: "offers") end)) - ...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &(ExUssd.set(&1, title: "option 1")))) - ...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &(ExUssd.set(&1, title: "option 2")))) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Menu title") + ...> |> ExUssd.add(ExUssd.new(name: "offers", resolve: fn menu, _ -> {:ok, ExUssd.set(menu, title: "offers")} end)) + ...> |> ExUssd.add(ExUssd.new(name: "option 1", resolve: &({:ok, ExUssd.set(&1, title: "option 1")}))) + ...> |> ExUssd.add(ExUssd.new(name: "option 2", resolve: &({:ok, ExUssd.set(&1, title: "option 2")}))) + ...> {:ok, menu} ...> end iex> menu = ExUssd.new(name: "HOME", is_zero_based: true, resolve: resolve) iex> ExUssd.to_string!(menu, []) @@ -456,22 +476,26 @@ defmodule ExUssd do ...> end iex> defmodule HomeResolver do ...> def home(%ExUssd{data: %{name: name}} = menu, _) do + ...> menu = ...> menu ...> |> ExUssd.set(title: "Welcome " <> name) ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> {:ok, menu} ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} ...> end iex> menu = ExUssd.new(fn menu, %{phone: phone} = _payload -> ...> user = User.get_user(phone) - ...> menu - ...> |> ExUssd.set(name: "Home") - ...> |> ExUssd.set(data: user) - ...> |> ExUssd.set(resolve: &HomeResolver.home/2) + ...> menu = + ...> menu + ...> |> ExUssd.set(name: "Home") + ...> |> ExUssd.set(data: user) + ...> |> ExUssd.set(resolve: &HomeResolver.home/2) + ...> {:ok, menu} ...> end) iex> ExUssd.to_string!(menu, [payload: %{text: "*544#", phone: "072000000"}]) "Welcome John\\n1:Product A\\n2:Product B\\n3:Product C" @@ -485,32 +509,38 @@ defmodule ExUssd do iex> defmodule HomeResolver do ...> def home(menu, %{phone: phone} = _payload) do ...> user = User.get_user(phone) - ...> menu - ...> |> ExUssd.set(title: "Welcome "<> user.name) - ...> |> ExUssd.set(data: user) - ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) - ...> |> ExUssd.add(ExUssd.new(&account/2)) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Welcome "<> user.name) + ...> |> ExUssd.set(data: user) + ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> |> ExUssd.add(ExUssd.new(&account/2)) + ...> {:ok, menu} ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} ...> def account(%{data: %{type: :personal, name: name}} = menu, _payload) do ...> # Should be stateless, don't put call functions with side effect (Insert to DB, fetch) ...> # Because it will be called every time the menu is rendered because the menu `:name` is dynamic ...> # See `ExUssd.new/2` for more details where `:name` is static. - ...> menu - ...> |> ExUssd.set(name: "Personal account") - ...> |> ExUssd.set(resolve: &(ExUssd.set(&1, title: "Personal account"))) + ...> menu = + ...> menu + ...> |> ExUssd.set(name: "Personal account") + ...> |> ExUssd.set(resolve: &({:ok, ExUssd.set(&1, title: "Personal account")})) + ...> {:ok, menu} ...> end ...> def account(%{data: %{type: :business, name: name}} = menu, _payload) do ...> # Should be stateless, don't put call functions with side effect (Insert to DB, fetch) ...> # Because it will be called every time the menu is rendered because the menu `:name` is dynamic ...> # See `ExUssd.new/2` for more details where `:name` is static. - ...> menu - ...> |> ExUssd.set(name: "Business account") - ...> |> ExUssd.set(resolve: &(ExUssd.set(&1, title: "Business account"))) + ...> menu = + ...> menu + ...> |> ExUssd.set(name: "Business account") + ...> |> ExUssd.set(resolve: &({:ok, ExUssd.set(&1, title: "Business account")})) + ...> {:ok, menu} ...> end ...> end iex> menu = ExUssd.new(name: "HOME", resolve: &HomeResolver.home/2) @@ -536,24 +566,26 @@ defmodule ExUssd do iex> defmodule HomeResolver do ...> def home(menu, %{phone: phone} = _payload) do ...> user = User.get_user(phone) - ...> menu - ...> |> ExUssd.set(title: "Welcome "<> user.name) - ...> |> ExUssd.set(data: user) - ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) - ...> |> ExUssd.add(ExUssd.new("Account", &account/2)) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Welcome "<> user.name) + ...> |> ExUssd.set(data: user) + ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> |> ExUssd.add(ExUssd.new("Account", &account/2)) + ...> {:ok, menu} ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} ...> def account(%{data: %{type: :personal}} = menu, _payload) do ...> # Get Personal account details, then set as data - ...> ExUssd.set(menu, resolve: &(ExUssd.set(&1, title: "Personal account"))) + ...> {:ok, ExUssd.set(menu, resolve: &({:ok, ExUssd.set(&1, title: "Personal account")}))} ...> end ...> def account(%{data: %{type: :business, name: name}} = menu, _payload) do ...> # Get Business account details, then set as data - ...> ExUssd.set(menu, resolve: &(ExUssd.set(&1, title: "Business account"))) + ...> {:ok, ExUssd.set(menu, resolve: &({:ok, ExUssd.set(&1, title: "Business account")}))} ...> end ...> end iex> menu = ExUssd.new(name: "HOME", resolve: &HomeResolver.home/2) @@ -591,7 +623,6 @@ defmodule ExUssd do - **`:delimiter`** Set's menu style delimiter. Default- `:` - **`:default_error`** Default error shown on invalid input - - **`:error`** Set custom error message - **`:name`** Sets the name of the menu - **`:nav`** Its used to set a new ExUssd Nav menu, see `ExUssd.Nav.new/1` @@ -615,15 +646,17 @@ defmodule ExUssd do iex> defmodule AppWeb.ProductResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "Product List") - ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Product List") + ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> {:ok, menu} ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} ...> end iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver) iex> # Simulate the first time user enters the menu @@ -640,15 +673,17 @@ defmodule ExUssd do iex> defmodule AppWeb.ProductResolver do ...> use ExUssd ...> def ussd_init(%ExUssd{data: %{user_name: user_name}} = menu, _) do - ...> menu - ...> |> ExUssd.set(title: "Welcome " <> user_name <> ", Select Product") - ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Welcome " <> user_name <> ", Select Product") + ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> {:ok, menu} ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} ...> end iex> menu = ExUssd.new(name: "HOME", resolve: AppWeb.ProductResolver) iex> # Simulate the first time user enters the menu @@ -666,17 +701,19 @@ defmodule ExUssd do iex> defmodule AppWeb.PinResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> ExUssd.set(menu, title: "Enter your PIN") + ...> {:ok, ExUssd.set(menu, title: "Enter your PIN")} ...> end ...> def ussd_callback(menu, payload, _) do ...> if payload.text == "5555" do - ...> ExUssd.set(menu, resolve: &success_menu/2) + ...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)} ...> end ...> end ...> def success_menu(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") - ...> |> ExUssd.set(should_close: true) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") + ...> |> ExUssd.set(should_close: true) + ...> {:ok, menu} ...> end ...> end iex> menu = ExUssd.new(name: "PIN", resolve: AppWeb.PinResolver) @@ -696,15 +733,17 @@ defmodule ExUssd do Example: iex> defmodule AppWeb.ProductResolver do ...> def products(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "Product List") - ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) - ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "Product List") + ...> |> ExUssd.add(ExUssd.new(name: "Product A", resolve: &product_a/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product B", resolve: &product_b/2)) + ...> |> ExUssd.add(ExUssd.new(name: "Product C", resolve: &product_c/2)) + ...> {:ok, menu} ...> end - ...> def product_a(menu, _payload), do: ExUssd.set(menu, title: "selected product a") - ...> def product_b(menu, _payload), do: ExUssd.set(menu, title: "selected product b") - ...> def product_c(menu, _payload), do: ExUssd.set(menu, title: "selected product c") + ...> def product_a(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product a")} + ...> def product_b(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product b")} + ...> def product_c(menu, _payload), do: {:ok, ExUssd.set(menu, title: "selected product c")} ...> end iex> menu = ExUssd.new(name: "HOME", resolve: &AppWeb.ProductResolver.products/2) iex> # Simulate the first time user enters the menu @@ -720,17 +759,19 @@ defmodule ExUssd do iex> defmodule AppWeb.PinResolver do ...> use ExUssd ...> def ussd_init(menu, _) do - ...> ExUssd.set(menu, title: "Enter your PIN") + ...> {:ok, ExUssd.set(menu, title: "Enter your PIN")} ...> end ...> def ussd_callback(menu, payload, _) do ...> if payload.text == "5555" do - ...> ExUssd.set(menu, resolve: &success_menu/2) + ...> {:ok, ExUssd.set(menu, resolve: &success_menu/2)} ...> end ...> end ...> def success_menu(menu, _) do - ...> menu - ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") - ...> |> ExUssd.set(should_close: true) + ...> menu = + ...> menu + ...> |> ExUssd.set(title: "You have Entered the Secret Number, 5555") + ...> |> ExUssd.set(should_close: true) + ...> {:ok, menu} ...> end ...> end iex> menu = ExUssd.new(name: "PIN", resolve: AppWeb.PinResolver) diff --git a/lib/ex_ussd/executer.ex b/lib/ex_ussd/executer.ex index 971e81e..bdcb324 100644 --- a/lib/ex_ussd/executer.ex +++ b/lib/ex_ussd/executer.ex @@ -14,8 +14,23 @@ defmodule ExUssd.Executer do def execute_navigate(%ExUssd{navigate: navigate} = menu, payload) when is_function(navigate) do case apply(navigate, [menu, payload]) do - %ExUssd{} = menu -> %{menu | navigate: nil} - _ -> menu + %ExUssd{} = menu -> + %{menu | navigate: nil} + + {:ok, menu} -> + menu + + {:error, message} when is_binary(message) -> + {:ok, %{menu | error: IO.iodata_to_binary([message, "\n"])}} + + {:error, message} -> + raise ArgumentError, "Expected a string, found #{inspect(message)}" + + nil -> + nil + + _ -> + menu end end @@ -30,7 +45,22 @@ defmodule ExUssd.Executer do def execute_init_callback(%ExUssd{resolve: resolve} = menu, payload) when is_function(resolve) do if is_function(resolve, 2) do - with %ExUssd{} = menu <- apply(resolve, [menu, payload]), do: {:ok, menu} + case apply(resolve, [menu, payload]) do + {:ok, %ExUssd{} = menu} -> + {:ok, menu} + + {:ok, menu} -> + raise ArgumentError, "Expected ExUssd struct, found #{inspect(menu)}" + + {:error, message} when is_binary(message) -> + {:ok, %{menu | error: IO.iodata_to_binary([message, "\n"])}} + + {:error, message} -> + raise ArgumentError, "Expected a string, found #{inspect(message)}" + + nil -> + nil + end else raise %BadArityError{function: resolve, args: [menu, payload]} end @@ -39,8 +69,22 @@ defmodule ExUssd.Executer do def execute_init_callback(%ExUssd{name: name, resolve: resolve} = menu, payload) when is_atom(resolve) do if function_exported?(resolve, :ussd_init, 2) do - with %ExUssd{} = menu <- apply(resolve, :ussd_init, [menu, payload]), - do: {:ok, menu} + case apply(resolve, :ussd_init, [menu, payload]) do + {:ok, %ExUssd{} = menu} -> + {:ok, menu} + + {:ok, menu} -> + raise ArgumentError, "Expected ExUssd struct, found #{inspect(menu)}" + + {:error, message} when is_binary(message) -> + {:ok, %{menu | error: IO.iodata_to_binary([message, "\n"])}} + + {:error, message} -> + raise ArgumentError, "Expected a string, found #{inspect(message)}" + + nil -> + nil + end else raise %ArgumentError{message: "resolve module for #{name} does not export ussd_init/2"} end @@ -94,24 +138,38 @@ defmodule ExUssd.Executer do ) try do - with %ExUssd{error: error} = current_menu <- - apply(resolve, :ussd_callback, [ - %{menu | resolve: nil, menu_list: []}, - payload, - metadata - ]) do - if is_bitstring(error) do + case apply(resolve, :ussd_callback, [ + %{menu | resolve: nil, menu_list: []}, + payload, + metadata + ]) do + {:ok, %ExUssd{} = response_menu} -> + build_response_menu(:ok, %{response_menu | error: nil}, menu, payload, opts) + |> get_next_menu(menu, payload, opts) + + {:ok, menu} -> + raise ArgumentError, "Expected ExUssd struct, found #{inspect(menu)}" + + {:error, message} when is_binary(message) -> if Keyword.get(opts, :state) do ExUssd.Registry.add_attempt(payload[:session_id], payload[:text]) end if Enum.empty?(menu_list) do - build_response_menu(:halt, current_menu, menu, payload, opts) + build_response_menu( + :halt, + %{menu | error: IO.iodata_to_binary([message, "\n"])}, + menu, + payload, + opts + ) end - else - build_response_menu(:ok, current_menu, menu, payload, opts) - |> get_next_menu(menu, payload, opts) - end + + {:error, message} -> + raise ArgumentError, "Expected a string, found #{inspect(message)}" + + nil -> + nil end rescue FunctionClauseError -> @@ -161,18 +219,32 @@ defmodule ExUssd.Executer do ) try do - with %ExUssd{error: error} = current_menu <- - apply(resolve, :ussd_after_callback, [ - %{menu | resolve: nil, menu_list: [], error: error_state}, - payload, - metadata - ]) do - if is_bitstring(error) do - build_response_menu(:halt, current_menu, menu, payload, opts) - else - build_response_menu(:ok, current_menu, menu, payload, opts) + case apply(resolve, :ussd_after_callback, [ + %{menu | resolve: nil, menu_list: [], error: error_state}, + payload, + metadata + ]) do + {:ok, %ExUssd{} = response_menu} -> + build_response_menu(:ok, %{response_menu | error: nil}, menu, payload, opts) |> get_next_menu(menu, payload, opts) - end + + {:ok, menu} -> + raise ArgumentError, "Expected ExUssd struct, found #{inspect(menu)}" + + {:error, message} when is_binary(message) -> + build_response_menu( + :halt, + %{menu | error: IO.iodata_to_binary([message, "\n"])}, + menu, + payload, + opts + ) + + {:error, message} -> + raise ArgumentError, "Expected a string, found #{inspect(message)}" + + nil -> + nil end rescue FunctionClauseError -> diff --git a/lib/ex_ussd/op.ex b/lib/ex_ussd/op.ex index ba0be67..26740ed 100644 --- a/lib/ex_ussd/op.ex +++ b/lib/ex_ussd/op.ex @@ -3,7 +3,6 @@ defmodule ExUssd.Op do alias ExUssd.{Display, Utils} @allowed_fields [ - :error, :title, :next, :previous, diff --git a/mix.exs b/mix.exs index 6a7b7bb..fed68a1 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule ExUssd.MixProject do use Mix.Project @source_url "https://github.com/beamkenya/ex_ussd.git" - @version "1.0.1" + @version "1.1.0" def project do [ diff --git a/test/ex_ussd/executer_test.exs b/test/ex_ussd/executer_test.exs index c20e7b0..91ff9b9 100644 --- a/test/ex_ussd/executer_test.exs +++ b/test/ex_ussd/executer_test.exs @@ -8,7 +8,7 @@ defmodule ExUssd.ExecuterTest do menu = ExUssd.new( name: Faker.Company.name(), - resolve: fn menu, _payload -> menu |> ExUssd.set(title: "Welcome") end + resolve: fn menu, _payload -> {:ok, ExUssd.set(menu, title: "Welcome")} end ) title = "Welcome" @@ -19,7 +19,7 @@ defmodule ExUssd.ExecuterTest do menu = ExUssd.new( name: Faker.Company.name(), - resolve: fn menu, _payload, _metadata -> menu |> ExUssd.set(title: "Welcome") end + resolve: fn menu, _payload, _metadata -> {:ok, ExUssd.set(menu, title: "Welcome")} end ) assert_raise BadArityError, fn -> Executer.execute_init_callback(menu, Map.new()) end diff --git a/test/ex_ussd/op_test.exs b/test/ex_ussd/op_test.exs index 9a7f539..acb8528 100644 --- a/test/ex_ussd/op_test.exs +++ b/test/ex_ussd/op_test.exs @@ -1,52 +1,53 @@ defmodule ExUssd.OpTest.Module do @moduledoc false def ussd_init(menu, _) do - menu - |> ExUssd.set(title: "Enter your PIN") + {:ok, ExUssd.set(menu, title: "Enter your PIN")} end def ussd_callback(menu, payload, _) do if payload.text == "5555" do - menu - |> ExUssd.set(title: "You have Entered the Secret Number, 5555") - |> ExUssd.set(should_close: true) + {:ok, + menu + |> ExUssd.set(title: "You have Entered the Secret Number, 5555") + |> ExUssd.set(should_close: true)} end end def simple(menu, _) do - menu - |> ExUssd.set(title: "Welcome") - |> ExUssd.add( - ExUssd.new( - name: "menu 1", - resolve: &simple/2 - ) - |> ExUssd.set(split: 3) - ) - |> ExUssd.add( - ExUssd.new( - name: "menu 2", - resolve: fn menu, _ -> ExUssd.set(menu, title: "menu 2") end - ) - ) - |> ExUssd.add( - ExUssd.new( - name: "menu 3", - resolve: fn menu, _ -> ExUssd.set(menu, title: "menu 3") end - ) - ) - |> ExUssd.add( - ExUssd.new( - name: "menu 4", - resolve: fn menu, _ -> ExUssd.set(menu, title: "menu 4") end - ) - ) - |> ExUssd.add( - ExUssd.new( - name: "menu 5", - resolve: fn menu, _ -> ExUssd.set(menu, title: "menu 5") end - ) - ) + {:ok, + menu + |> ExUssd.set(title: "Welcome") + |> ExUssd.add( + ExUssd.new( + name: "menu 1", + resolve: &simple/2 + ) + |> ExUssd.set(split: 3) + ) + |> ExUssd.add( + ExUssd.new( + name: "menu 2", + resolve: fn menu, _ -> {:ok, ExUssd.set(menu, title: "menu 2")} end + ) + ) + |> ExUssd.add( + ExUssd.new( + name: "menu 3", + resolve: fn menu, _ -> {:ok, ExUssd.set(menu, title: "menu 3")} end + ) + ) + |> ExUssd.add( + ExUssd.new( + name: "menu 4", + resolve: fn menu, _ -> {:ok, ExUssd.set(menu, title: "menu 4")} end + ) + ) + |> ExUssd.add( + ExUssd.new( + name: "menu 5", + resolve: fn menu, _ -> {:ok, ExUssd.set(menu, title: "menu 5")} end + ) + )} end end @@ -55,7 +56,7 @@ defmodule ExUssd.OpTest do use ExUnit.Case setup do - resolve = fn menu, _payload -> menu |> ExUssd.set(title: "Welcome") end + resolve = fn menu, _payload -> {:ok, ExUssd.set(menu, title: "Welcome")} end menu = ExUssd.new(name: Faker.Company.name(), resolve: resolve) @@ -295,27 +296,29 @@ defmodule ExUssd.OpTest do use ExUssd def ussd_init(menu, _) do - ExUssd.set(menu, title: "Enter your PIN") + {:ok, ExUssd.set(menu, title: "Enter your PIN")} end def ussd_callback(menu, payload, %{attempt: %{count: count}}) do if payload.text == "5555" do - ExUssd.set(menu, resolve: &success_menu/2) + {:ok, ExUssd.set(menu, resolve: &success_menu/2)} else - ExUssd.set(menu, error: "Wrong PIN, #{2 - count} attempt left\n") + {:error, "Wrong PIN, #{2 - count} attempt left"} end end def ussd_after_callback(%{error: true} = menu, _payload, %{attempt: %{count: 3}}) do - menu - |> ExUssd.set(title: "Account is locked, Dial *234# to reset your account") - |> ExUssd.set(should_close: true) + {:ok, + menu + |> ExUssd.set(title: "Account is locked, Dial *234# to reset your account") + |> ExUssd.set(should_close: true)} end def success_menu(menu, _) do - menu - |> ExUssd.set(title: "You have Entered the Secret Number, 5555") - |> ExUssd.set(should_close: true) + {:ok, + menu + |> ExUssd.set(title: "You have Entered the Secret Number, 5555") + |> ExUssd.set(should_close: true)} end end