Skip to content

Commit

Permalink
Merge pull request #1 from danielberkompas/twiml-with-options
Browse files Browse the repository at this point in the history
Add "option" feature
  • Loading branch information
danielberkompas committed Apr 11, 2015
2 parents d8516c9 + 610e501 commit 25e49c2
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 15 deletions.
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
language: elixir
elixir:
- 1.0.3
- 1.0.4
otp_release:
- 17.4
before_script:
- export PATH=`pwd`/elixir/bin:$PATH
- export PLT_FILENAME=elixir-${TRAVIS_ELIXIR_VERSION}_$TRAVIS_OTP_RELEASE.plt
- export PLT_LOCATION=/home/travis/$PLT_FILENAME
- wget -O $PLT_LOCATION https://raw.github.com/danielberkompas/travis_elixir_plts/master/$PLT_FILENAME
- mix local.hex --force
- mix deps.get --only test
script:
- mix test
- dialyzer --no_check_plt --plt $PLT_LOCATION -Wno_match -Wno_return --no_native _build/test/lib/ex_twiml/ebin
after_script:
- MIX_ENV=docs mix deps.get
- MIX_ENV=docs mix inch.report
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ be a binary in this format:
The `twiml` macro simply returns a binary (or string), so you're not limited to
the pattern above. Just use `twiml` wherever you need a TwiML string.

### Options

If you are integrating `ExTwilio` into a state machine for handling your call
flows, you might find the `option` macro useful. It helps you dry up your code
by recording a user's option at the same time as you `Say` it to the user.

```elixir
extensions = ... # load from database

{opts, _xml} = twiml do
Enum.each extensions, fn ext ->
option ext.code, "Press #{ext.number} for #{ext.name}", [menu: :call_person], [voice: "woman"]
end
end

opts # => [{301, [menu: :call_person]}, ...]
```

The `option` macro is really just an enhanced `say` macro under the hood, so the
twiml generated by the above will look like this:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="woman">
Press 301 for Engineering Department
</Say>
</Response>
```

## Supported Verbs and Nouns
See the [Twilio Documentation](https://www.twilio.com/docs/api/twiml) for a
complete list of verbs supported by Twilio. ExTwiml has built in macros for the
Expand Down
55 changes: 40 additions & 15 deletions lib/ex_twiml.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,23 @@ defmodule ExTwiml do
# The buffer's state is a list of XML fragments. New fragments are
# inserted by other macros. Finally, all the fragments are joined
# together in a string.
{:ok, var!(buffer, Twiml)} = start_buffer([header])
{:ok, var!(buffer, Twiml)} = start_buffer([header])
{:ok, var!(options, Twiml)} = start_buffer([])

# Wrap the whole block in a <Response> tag
tag :response do
unquote(block)
end

result = render(var!(buffer, Twiml)) # Convert buffer to string
:ok = stop_buffer(var!(buffer, Twiml)) # Kill the Agent
result
xml = render(var!(buffer, Twiml)) # Convert buffer to string
opts = get_buffer var!(options, Twiml)
:ok = stop_buffer(var!(buffer, Twiml)) # Kill the Agent

if length(opts) > 0 do
{opts, xml}
else
xml
end
end
end

Expand Down Expand Up @@ -177,34 +184,52 @@ defmodule ExTwiml do
end
"""
defmacro unquote(verb)(string \\ [], options \\ [])
defmacro unquote(verb)(string, options) when is_binary(string) do
defmacro unquote(verb)(string_or_options, options) do
current_verb = unquote(verb)

quote do
tag unquote(current_verb), unquote(options) do
text unquote(string)
{expanded, _} = Code.eval_quoted(string_or_options, Map.get(__CALLER__, :vars), __ENV__)

if is_binary(expanded) do
quote do
tag unquote(current_verb), unquote(options) do
text unquote(string_or_options)
end
end
else
quote do
put_buffer var!(buffer, Twiml), opening_tag(unquote(current_verb), " /", unquote(string_or_options))
end
end
end

defmacro unquote(verb)(options, _ignore) do
current_verb = unquote(verb)
end

quote do
put_buffer var!(buffer, Twiml), opening_tag(unquote(current_verb), " /", unquote(options))
end
@doc """
Add an option to the output.
"""
defmacro option(pattern, text, menu_options \\ [], verb_options \\ []) do
quote do
put_buffer var!(options, Twiml), {unquote(pattern), unquote(menu_options)}
say unquote(text), unquote(verb_options)
end
end

@doc "Start an Agent to store the TwiML buffer prior to rendering."
@doc "Start an Agent to store a given buffer state."
@spec start_buffer(list) :: {:ok, pid}
def start_buffer(state), do: Agent.start_link(fn -> state end)

@doc "Stop a buffer."
@spec stop_buffer(pid) :: atom
def stop_buffer(buff), do: Agent.stop(buff)

@doc "Update the buffer by pushing a new tag onto the beginning."
@spec put_buffer(pid, any) :: atom
def put_buffer(buff, content), do: Agent.update(buff, &[content | &1])

@doc "Get the current state of a buffer."
@spec get_buffer(pid) :: list
def get_buffer(buff), do: Agent.get(buff, &(&1)) |> Enum.reverse

@doc "Render the contents of the buffer into a string."
@spec render(pid) :: String.t
def render(buff), do: Agent.get(buff, &(&1)) |> Enum.reverse |> Enum.join
end
5 changes: 5 additions & 0 deletions lib/ex_twiml/utilities.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule ExTwiml.Utilities do
...> opening_tag "say", " /", option_1: "value"
"<Say option1=\\"value\\" />"
"""
@spec opening_tag(atom, String.t, list) :: String.t
def opening_tag(tag_name, close, options \\ []) do
"<#{capitalize(tag_name)}#{xml_attributes(options)}#{close}>"
end
Expand All @@ -33,6 +34,7 @@ defmodule ExTwiml.Utilities do
...> closing_tag("say")
"</Say>"
"""
@spec closing_tag(atom) :: String.t
def closing_tag(tag_name) do
"</#{capitalize(tag_name)}>"
end
Expand All @@ -48,6 +50,7 @@ defmodule ExTwiml.Utilities do
...> capitalize("string")
"String"
"""
@spec capitalize(atom) :: String.t
def capitalize(atom) do
String.capitalize to_string(atom)
end
Expand All @@ -64,6 +67,7 @@ defmodule ExTwiml.Utilities do
...> xml_attributes([digits: 1, finish_on_key: "#"])
" digits=\\"1\\" finishOnKey=\\"#\\""
"""
@spec xml_attributes(list) :: String.t
def xml_attributes(attrs) do
for {key, val} <- attrs, into: "", do: " #{to_camel_case key}=\"#{val}\""
end
Expand All @@ -77,6 +81,7 @@ defmodule ExTwiml.Utilities do
...> to_camel_case("finish_on_key")
"finishOnKey"
"""
@spec to_camel_case(String.t) :: String.t
def to_camel_case(string) do
to_string(string)
|> String.split("_")
Expand Down
15 changes: 15 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ defmodule ExTwiml.Mixfile do
version: "1.0.0",
elixir: "~> 1.0",
deps: deps,
dialyzer: [
plt_file: "#{System.get_env("HOME")}/#{plt_filename}",
flags: ["--no_native", "-Wno_match", "-Wno_return"]
],
package: package]
end

Expand Down Expand Up @@ -43,4 +47,15 @@ defmodule ExTwiml.Mixfile do
}
]
end

defp plt_filename do
"elixir-#{System.version}_#{otp_release}.plt"
end

defp otp_release do
case System.get_env("TRAVIS_OTP_RELEASE") do
nil -> :erlang.system_info(:otp_release)
release -> release
end
end
end
18 changes: 18 additions & 0 deletions test/ex_twiml_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,24 @@ defmodule ExTwimlTest do
assert_twiml markup, "<Media>https://demo.twilio.com/owl.png</Media>"
end

test "can render with options" do
{opts, _twiml} = twiml do
option 1, "hello there!", [menu: :other_menu], [voice: "woman"]
end

assert opts == [{1, [menu: :other_menu]}]
end

test ".twiml can include Enum loops" do
{opts, _xml} = twiml do
Enum.each 1..3, fn(n) ->
option n, "Press #{n}"
end
end

assert opts == [{1, []}, {2, []}, {3, []}]
end

defp assert_twiml(lhs, rhs) do
assert lhs == "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response>#{rhs}</Response>"
end
Expand Down

0 comments on commit 25e49c2

Please sign in to comment.