defmodule ExTwiml do import ExTwiml.Utilities @moduledoc """ Contains macros to make generating TwiML from Elixir far easier and more efficient. Just `import` the module and go! ## Examples import Twiml twiml do play "/assets/welcome.mp3" gather digits: 1 do say "For more menus, please press 1.", voice: "woman" say "To speak with a real person, please press 2.", voice: "woman" end end Produces the following `string`: /assets/welcome.mp3 For more menus, please press 1. To speak with a real person, please press 2. You'd then need to render this string to the browser. """ @nested_verbs [ :gather, :dial, :message ] @verbs [ :say, :number, :play, :sms, :sip, :client, :conference, :queue, :enqueue, :leave, :hangup, :reject, :pause, :record, :redirect, :body, :media ] @doc """ Start creating a TwiML document. """ defmacro twiml(do: block) do quote do header = "" # Create an Agent to store the buffer results, using var! to allow us to # continue to be able to update the buffer through multiple macros. # # 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!(options, Twiml)} = start_buffer([]) # Wrap the whole block in a tag tag :response do unquote(block) end 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 @doc """ Use this macro to generate a tag not yet supported by this Twiml library. Note that you'll also need to use the `text` macro to include text within this tag. ## Examples tag :mms, to: "1112223333", from: "2223334444" do text "How are you doing?" end Will produce the following Twiml: How are you doing? """ defmacro tag(name, options \\ [], do: inner) do quote do put_buffer var!(buffer, Twiml), opening_tag(unquote(name), "", unquote(options)) unquote(inner) put_buffer var!(buffer, Twiml), closing_tag(unquote(name)) end end @doc """ Adds whatever text is given to the current Twiml buffer, unmodified. As a result, this macro is really only useful when nested inside one of the other macros provided by this module. """ defmacro text(string) do quote do put_buffer var!(buffer, Twiml), to_string(unquote(string)) end end # Generates a macro for each verb that allows nesting of other verbs beneath # it. These macros can be used like this: # # dial from: "1112223333" do # number "2223334444" # number "3334445555" # end # # Which will produce this Twiml: # # # 2223334444 # 3334445555 # Enum.each @nested_verbs, fn verb -> @doc """ Implements the `<#{capitalize verb}>` verb. Other verbs can be nested under this verb, as shown in the examples. See Twilio's official docs for the <#{capitalize verb}> verb here: ## Examples twiml do #{verb} do number "1112223333" end end """ defmacro unquote(verb)(options, do: inner) do current_verb = unquote(verb) quote do tag unquote(current_verb), unquote(options) do unquote(inner) end end end end # Generates macros for each verb that doesn't support nesting. # # Two macros are defined for each verb. The first allows the verb to be called # with a string argument, like so: # # verb "Some string", option: "value" # # Producing the following Twiml: # # Some string # # The second macro allows the verb to be called in this form: # # verb option: "value" # # This macro will produce a self-closing XML tag, including the attributes. # # Enum.each @verbs, fn verb -> @doc """ Implements the `<#{capitalize verb}>` verb. No other verbs can be nested under this verb. See Twilio's official docs for the <#{capitalize verb}> verb here: ## Examples twiml do #{verb} "Some text here", option1: "val", option2: "val" end twiml do #{verb} option1: "val", option2: "val" end """ defmacro unquote(verb)(string \\ [], options \\ []) defmacro unquote(verb)(string_or_options, options) do case string_or_options do string when is_binary(string) -> compile_string_macro(unquote(verb), options, string) {atom, _, _} when is_atom(atom) -> compile_string_macro(unquote(verb), options, string_or_options) _ -> compile_nested_macro(unquote(verb), string_or_options) end end end defp compile_string_macro(verb, options, string) do quote do tag unquote(verb), unquote(options) do text unquote(string) end end end defp compile_nested_macro(verb, options) do quote do put_buffer var!(buffer, Twiml), opening_tag(unquote(verb), " /", unquote(options)) end 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 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