diff --git a/README.md b/README.md
index 6295141..dae49c0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[![Test](https://github.com/YodelTalk/twiml/actions/workflows/test.yml/badge.svg)](https://github.com/YodelTalk/twiml/actions/workflows/test.yml)
[![CI](https://github.com/YodelTalk/twiml/actions/workflows/ci.yml/badge.svg)](https://github.com/YodelTalk/twiml/actions/workflows/ci.yml)
-TwiML is a Elixir library to generate complex TwiML responses for Twilio in an
+TwiML is a Elixir library to generate complex TwiML documents for Twilio in an
elegant way.
diff --git a/lib/twiml.ex b/lib/twiml.ex
index 6a3f771..1b3049d 100644
--- a/lib/twiml.ex
+++ b/lib/twiml.ex
@@ -2,23 +2,40 @@ defmodule TwiML do
import TwiML.Camelize, only: [camelize: 2]
@external_resource "README.md"
- @moduledoc "README.md"
- |> File.read!()
- |> String.split("")
- |> Enum.fetch!(1)
+ @moduledoc """
+ Generate complex TwiML documents for Twilio in an elegant Elixir way.
+
+ > #### Note {: .warning}
+ >
+ > Please refer to the [official TwiML
+ > documentation](https://www.twilio.com/docs/voice/twiml) to verify that the
+ > TwiML verb actually supports content or the given attributes.
+
+ #{"README.md" |> File.read!() |> String.split("") |> Enum.fetch!(1)}
+ """
use TwiML.Magic,
# The nesting and duplication is intentionally as it improves comparing the
# verbs with the official TwiML at https://www.twilio.com/docs/voice/twiml
verbs: [
[:connect, :autopilot, :siprec, :stream, :virtual_agent],
- [:dial, :client, [:identity, :parameter], :conference, :number, :queue, :sim, :sip],
+ [
+ :dial,
+ :application,
+ :client,
+ [:identity, :parameter],
+ :conference,
+ :number,
+ :queue,
+ :sim,
+ :sip
+ ],
:enqueue,
:gather,
:hangup,
:leave,
:pause,
- [:pay, :prompt],
+ [:pay, :prompt, :parameter],
:play,
:record,
:redirect,
@@ -29,6 +46,31 @@ defmodule TwiML do
:stream
]
+ @typedoc """
+ A TwiML document contains one or more TwiML verbs. These verbs can have
+ attributes and can wrap nested TwiML verbs.
+ """
+ @type t :: [{atom(), keyword(), content() | t()}]
+
+ @typedoc """
+ Content which can be used within a TwiML verb. Refer to the `XmlBuilder`
+ documentation for more information.
+ """
+ @type content :: binary() | {:safe, binary()} | {:cdata, binary()} | {:iodata, binary()}
+
+ @doc """
+ Generates a XML document from the provided verbs and arguments. For the
+ supported `opts`, please refer to the documentation of
+ `XmlBuilder.generate/2`.
+
+ ## Examples
+
+ iex> TwiML.say("Hello world")
+ ...> |> TwiML.to_xml(format: :none)
+ ~s(Hello world)
+
+ """
+ @doc helper: true
def to_xml(verbs, opts \\ []) do
XmlBuilder.document(:Response, verbs)
|> XmlBuilder.generate(opts)
@@ -49,10 +91,31 @@ defmodule TwiML do
{verb, attrs, children}
end
+ @doc """
+ Generates a comment.
+
+ ## Examples
+
+ iex> TwiML.comment("Blocked because of insufficient funds")
+ ...> |> TwiML.to_xml(format: :none)
+ ~s(Blocked because of insufficient funds!-->)
+
+ """
+ @doc helper: true
def comment(text) do
[{"!--", [], text}]
end
+ @doc """
+ Appends a comment to the TwiML.
+
+ ## Examples
+
+ iex> TwiML.reject() |> TwiML.comment("Blocked because of insufficient funds") |> TwiML.to_xml(format: :none)
+ ~s(Blocked because of insufficient funds!-->)
+
+ """
+ @doc helper: true
def comment(verbs, text) do
verbs ++ [comment(text)]
end
diff --git a/lib/twiml/magic.ex b/lib/twiml/magic.ex
index 665e768..a96a6f5 100644
--- a/lib/twiml/magic.ex
+++ b/lib/twiml/magic.ex
@@ -12,10 +12,30 @@ defmodule TwiML.Magic do
def twiml_verb(verb) do
quote do
+ twiml = TwiML.Camelize.camelize(Atom.to_string(unquote(verb)))
+
+ @doc """
+ Generates an empty `<#{twiml}>` verb.
+ """
+ @spec unquote(verb)() :: TwiML.t()
def unquote(verb)() do
[build_verb(unquote(verb), [], [])]
end
+ @doc """
+ Generates a `<#{twiml}>` verb.
+
+ There are three supported usages:
+
+ - Wraps `arg` in `<#{twiml}>` if it's `t:TwiML.content/0`.
+ - Creates an empty `<#{twiml}>` with attributes for `arg` as a keyword
+ list.
+ - Appends an empty `<#{twiml}>` to `arg` if it's `t:TwiML.t/0`, enabling
+ verb chaining (refer to [Examples](#module-examples)).
+ """
+ @spec unquote(verb)(TwiML.content() | keyword() | TwiML.t()) :: TwiML.t()
+ def unquote(verb)(arg)
+
def unquote(verb)(content) when is_binary(content) do
[build_verb(unquote(verb), [], content)]
end
@@ -40,10 +60,30 @@ defmodule TwiML.Magic do
end
end
+ @doc """
+ Appends or generates a `<#{twiml}>` verb.
+
+ There are two supported usages:
+
+ - Appends `<#{twiml}>` to `verbs_or_content` if it's `t:TwiML.t/0`,
+ enabling verb chaining (see [Examples](#module-examples)). This verb
+ wraps `content_or_attrs` if it's `t:TwiML.content/0`, or includes
+ attributes if `content_or_attrs` is a keyword list.
+ - Generates `<#{twiml}>` with attributes if `content_or_attrs` is a
+ keyword list.
+ """
+ def unquote(verb)(verbs_or_content, content_or_attrs)
+
+ @spec unquote(verb)(TwiML.t(), keyword() | TwiML.content()) :: TwiML.t()
def unquote(verb)(verbs, attrs) when is_list(verbs) and is_list(attrs) do
verbs ++ [build_verb(unquote(verb), attrs, [])]
end
+ def unquote(verb)(verbs, content) when is_binary(content) do
+ verbs ++ [build_verb(unquote(verb), [], content)]
+ end
+
+ @spec unquote(verb)(TwiML.content(), keyword()) :: TwiML.t()
def unquote(verb)(content, attrs) when is_binary(content) do
[build_verb(unquote(verb), attrs, content)]
end
@@ -60,28 +100,60 @@ defmodule TwiML.Magic do
[build_verb(unquote(verb), attrs, content)]
end
- def unquote(verb)(verbs, content) when is_binary(content) do
- verbs ++ [build_verb(unquote(verb), [], content)]
- end
+ @doc """
+ Appends a `<#{twiml}>` verb with attributes.
+ Adds a `<#{twiml}>` verb to `verbs` using `attrs` as attributes,
+ facilitating verb chaining (see [Examples](#module-examples)).
+ """
+ @spec unquote(verb)(TwiML.t(), TwiML.content(), keyword()) :: TwiML.t()
def unquote(verb)(verbs, content, attrs)
when is_list(verbs) and is_binary(content) and is_list(attrs) do
verbs ++ [build_verb(unquote(verb), attrs, content)]
end
- # How many of the preceding XML elements should be put into this one as
- # children? By default :all will be put in.
- def unquote(String.to_atom("into_#{verb}"))(verbs, last_n_elements \\ :all)
+ @doc """
+ Wraps preceding TwiML verbs in a `<#{twiml}>` verb.
+
+ There are three supported usages:
+ - Uses `attrs_or_last_n_elements` as attributes to wrap all preceding verbs
+ in `<#{twiml}>` if it's a keyword list.
+ - Wraps all preceding verbs in `<#{twiml}>` if `attrs_or_last_n_elements` is
+ `:all`.
+ - Encloses the last `attrs_or_last_n_elements` verbs in `<#{twiml}>` if it's a
+ positive integer.
+ """
+ @spec unquote(String.to_atom("into_#{verb}"))(
+ TwiML.t(),
+ keyword() | :all | pos_integer()
+ ) :: TwiML.t()
+ def unquote(String.to_atom("into_#{verb}"))(verbs, attrs_or_last_n_elements \\ :all)
- def unquote(String.to_atom("into_#{verb}"))(verbs, last_n_elements)
- when is_integer(last_n_elements) or is_atom(last_n_elements) do
- unquote(String.to_atom("into_#{verb}"))(verbs, last_n_elements, [])
+ def unquote(String.to_atom("into_#{verb}"))(verbs, attrs_or_last_n_elements)
+ when is_integer(attrs_or_last_n_elements) or is_atom(attrs_or_last_n_elements) do
+ unquote(String.to_atom("into_#{verb}"))(verbs, attrs_or_last_n_elements, [])
end
def unquote(String.to_atom("into_#{verb}"))(verbs, attrs) when is_list(attrs) do
unquote(String.to_atom("into_#{verb}"))(verbs, :all, attrs)
end
+ @doc """
+ Wraps preceding TwiML verbs in a `<#{twiml}>` verb with attributes.
+
+ There are three supported usages:
+ - Wraps all preceding verbs in `<#{twiml}>` using `attrs` as attributes if
+ `last_n_elements` is `:all`.
+ - Encloses the last `last_n_elements` verbs in `<#{twiml}>` using `attrs`
+ as attribute if it's a positive integer.
+ """
+ @spec unquote(String.to_atom("into_#{verb}"))(
+ TwiML.t(),
+ :all | pos_integer(),
+ keyword()
+ ) :: TwiML.t()
+ def unquote(String.to_atom("into_#{verb}"))(verbs, last_n_elements, attrs)
+
def unquote(String.to_atom("into_#{verb}"))(verbs, :all, attrs) do
[build_verb(unquote(verb), attrs, [verbs])]
end
diff --git a/mix.exs b/mix.exs
index 0075788..eae13f6 100644
--- a/mix.exs
+++ b/mix.exs
@@ -22,7 +22,14 @@ defmodule Twiml.MixProject do
# Docs
name: "TwiML",
source_url: @url,
- docs: [main: "TwiML", source_ref: "v#{@version}"]
+ docs: [
+ main: "TwiML",
+ source_ref: "v#{@version}",
+ groups_for_docs: [
+ {:Types, &(&1[:kind] == :type)},
+ {:"TwiML Verbs", &(!&1[:helper])}
+ ]
+ ]
]
end
@@ -30,7 +37,7 @@ defmodule Twiml.MixProject do
defp deps do
[
{:xml_builder, "~> 2.2"},
- {:ex_doc, "~> 0.24", only: :docs}
+ {:ex_doc, "~> 0.31", only: :docs}
]
end
diff --git a/mix.lock b/mix.lock
index b335a40..cf1459d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,9 +1,9 @@
%{
- "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"},
- "ex_doc": {:hex, :ex_doc, "0.27.0", "5600033896bbcf754c8de49262ae305cf7717d964710c25f488f9c659c22a3c3", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "38c5d707aa317480e8ba7fa5fbfcb8028662135d448679e8ef6206c47d106aa0"},
- "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
- "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
- "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
- "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
+ "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"},
+ "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
+ "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"xml_builder": {:hex, :xml_builder, "2.2.0", "cc5f1eeefcfcde6e90a9b77fb6c490a20bc1b856a7010ce6396f6da9719cbbab", [:mix], [], "hexpm", "9d66d52fb917565d358166a4314078d39ef04d552904de96f8e73f68f64a62c9"},
}