├── VERSION
├── test
├── test_helper.exs
├── phoenix_alexa_test.exs
└── phoenix_alexa
│ ├── validate_application_id_test.exs
│ ├── request_test.exs
│ ├── response_test.exs
│ └── phoenix_alexa_controller_test.exs
├── .gitignore
├── lib
├── request
│ ├── application.ex
│ ├── intent.ex
│ ├── user.ex
│ ├── launch_request.ex
│ ├── session_ended_request.ex
│ ├── request.ex
│ ├── intent_request.ex
│ ├── generic_request.ex
│ └── session.ex
├── response
│ ├── ssml_output_speech.ex
│ ├── text_output_speech.ex
│ ├── simple_card.ex
│ ├── link_account_card.ex
│ ├── standard_card.ex
│ └── response.ex
├── validate_application_id_plug.ex
└── phoenix_alexa_controller.ex
├── .travis.yml
├── mix.exs
├── LICENSE
└── README.md
/VERSION:
--------------------------------------------------------------------------------
1 | 0.2.0
2 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/rel
2 | **/_build
3 | /deps
4 | erl_crash.dump
5 | *.ez
6 |
--------------------------------------------------------------------------------
/test/phoenix_alexa_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Test do
2 | use ExUnit.Case
3 |
4 | end
--------------------------------------------------------------------------------
/lib/request/application.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Application do
2 |
3 | defstruct applicationId: ""
4 | end
--------------------------------------------------------------------------------
/lib/request/intent.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Intent do
2 |
3 | defstruct name: nil,
4 | slots: %{}
5 | end
--------------------------------------------------------------------------------
/lib/request/user.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.User do
2 |
3 | defstruct userId: "",
4 | accessToken: ""
5 | end
--------------------------------------------------------------------------------
/lib/response/ssml_output_speech.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.SsmlOutputSpeech do
2 |
3 | defstruct type: "SSML",
4 | ssml: nil
5 | end
--------------------------------------------------------------------------------
/lib/response/text_output_speech.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.TextOutputSpeech do
2 |
3 | defstruct type: "PlainText",
4 | text: nil
5 | end
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir: 1.2.1
3 | otp_release:
4 | - 18.0
5 | sudo: false
6 | before_script:
7 | - mix deps.get --only test
8 | script:
9 | - mix test
10 |
--------------------------------------------------------------------------------
/lib/request/launch_request.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.LaunchRequest do
2 |
3 | defstruct type: "LaunchRequest",
4 | requestId: "",
5 | timestamp: ""
6 |
7 | end
--------------------------------------------------------------------------------
/lib/request/session_ended_request.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.SessionEndedRequest do
2 |
3 | defstruct type: "SessionEndedRequest",
4 | requestId: nil,
5 | timestamp: nil,
6 | reason: nil
7 |
8 | end
--------------------------------------------------------------------------------
/lib/request/request.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Request do
2 |
3 | alias PhoenixAlexa.{Session, GenericRequest}
4 |
5 | defstruct version: "",
6 | session: %Session{},
7 | request: %GenericRequest{}
8 |
9 | end
--------------------------------------------------------------------------------
/lib/request/intent_request.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.IntentRequest do
2 | alias PhoenixAlexa.Intent
3 |
4 | defstruct type: "IntentRequest",
5 | requestId: "",
6 | timestamp: "",
7 | intent: %Intent{}
8 | end
--------------------------------------------------------------------------------
/lib/request/generic_request.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.GenericRequest do
2 | alias PhoenixAlexa.Intent
3 |
4 | defstruct type: nil,
5 | requestId: nil,
6 | timestamp: nil,
7 | reason: nil,
8 | intent: %Intent{}
9 | end
--------------------------------------------------------------------------------
/lib/request/session.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Session do
2 |
3 | alias PhoenixAlexa.{Application, User}
4 |
5 | defstruct new: false,
6 | sessionId: "",
7 | application: %Application{},
8 | attributes: nil,
9 | user: %User{}
10 | end
--------------------------------------------------------------------------------
/lib/response/simple_card.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.SimpleCard do
2 | alias PhoenixAlexa.SimpleCard
3 |
4 | defstruct type: "Simple",
5 | title: "",
6 | content: ""
7 |
8 | def set_title(card, title) do
9 | %SimpleCard{card | title: title}
10 | end
11 | def set_content(card, content) do
12 | %SimpleCard{card | content: content}
13 | end
14 | end
--------------------------------------------------------------------------------
/lib/response/link_account_card.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.LinkAccountCard do
2 | alias PhoenixAlexa.LinkAccountCard
3 |
4 | defstruct type: "LinkAccount",
5 | title: "",
6 | content: ""
7 |
8 | def set_title(card, title) do
9 | %LinkAccountCard{card | title: title}
10 | end
11 | def set_content(card, content) do
12 | %LinkAccountCard{card | content: content}
13 | end
14 | end
--------------------------------------------------------------------------------
/lib/validate_application_id_plug.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.ValidateApplicationId do
2 | import Plug.Conn
3 |
4 | def init(applicationId), do: applicationId
5 |
6 | def call(conn, applicationId) do
7 | case conn.body_params["session"]["application"]["applicationId"] do
8 | ^applicationId ->
9 | conn
10 | _ ->
11 | conn
12 | |> Plug.Conn.send_resp(400, ~s({"error": "Invalid application"}))
13 | |> halt
14 | end
15 | end
16 | end
--------------------------------------------------------------------------------
/lib/response/standard_card.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.StandardCard do
2 | alias PhoenixAlexa.StandardCard
3 |
4 | defstruct type: "Standard",
5 | title: "",
6 | text: ""
7 |
8 | def set_title(card, title) do
9 | %StandardCard{card | title: title}
10 | end
11 | def set_text(card, text) do
12 | %StandardCard{card | text: text}
13 | end
14 | def set_small_image_url(card, image_url) do
15 | image = Map.get(card, :image) || %{}
16 | image = Map.put(image, :smallImageUrl, image_url)
17 | Map.put(card, :image, image)
18 | end
19 | def set_large_image_url(card, image_url) do
20 | image = Map.get(card, :image) || %{}
21 | image = Map.put(image, :largeImageUrl, image_url)
22 | Map.put(card, :image, image)
23 | end
24 | end
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Mixfile do
2 | use Mix.Project
3 |
4 | @version File.read!("VERSION") |> String.strip
5 |
6 | def project do
7 | [app: :phoenix_alexa,
8 | version: @version,
9 | elixir: "~> 1.2",
10 | description: "Alexa library for Phoenix",
11 | deps: deps,
12 | package: package,
13 | consolidate_protocols: Mix.env != :test]
14 | end
15 |
16 | def application do
17 | [applications: []]
18 | end
19 |
20 | defp deps do
21 | [{:poison, "~> 2.0"},
22 | {:plug, "~> 1.1"},
23 | ]
24 | end
25 |
26 | defp package do
27 | [files: ~w(lib test mix.exs README.md LICENSE VERSION),
28 | maintainers: ["Gabi Zuniga"],
29 | licenses: ["MIT"],
30 | links: %{"GitHub" => "https://github.com/gabiz/phoenix_alexa"}]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Gabi Zuniga
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/test/phoenix_alexa/validate_application_id_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.ValidateApplicationIdTest do
2 | use ExUnit.Case
3 | use Plug.Test
4 |
5 | alias PhoenixAlexa.ValidateApplicationId
6 |
7 | test "valid application id" do
8 | conn = conn(:post, "/alexa", "%{}")
9 | |> Map.put(:body_params, %{"session" => %{"application" => %{"applicationId" => "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"}}})
10 | options = ValidateApplicationId.init("amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a")
11 | assert conn == ValidateApplicationId.call(conn, options)
12 | end
13 |
14 | test "invalid application id" do
15 | conn = conn(:post, "/alexa", "%{}")
16 | |> Map.put(:body_params, %{"session" => %{"application" => %{"applicationId" => "__invalid_application_id__"}}})
17 | options = ValidateApplicationId.init("amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a")
18 | conn = ValidateApplicationId.call(conn, options)
19 | assert conn.state == :sent
20 | assert conn.status == 400
21 | assert conn.resp_body == ~s({"error": "Invalid application"})
22 | assert conn.halted
23 | end
24 | end
--------------------------------------------------------------------------------
/lib/phoenix_alexa_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Controller do
2 |
3 | defmacro __using__(method) do
4 | quote do
5 | import PhoenixAlexa.{Controller, Response}
6 | alias PhoenixAlexa.{Request, Response, TextOutputSpeech, SsmlOutputSpeech}
7 | alias PhoenixAlexa.{SimpleCard, StandardCard, LinkAccountCard}
8 |
9 | def set_response(conn, status \\ 200, response) do
10 | conn
11 | |> Plug.Conn.put_resp_content_type("application/json")
12 | |> Plug.Conn.resp(status, Poison.encode!(response))
13 | end
14 |
15 | def handle_request(conn, request) do
16 | case request.request.type do
17 | "LaunchRequest" ->
18 | launch_request(conn, request)
19 | "IntentRequest" ->
20 | intent_request(conn, request.request.intent.name, request)
21 | "SessionEndedRequest" ->
22 | session_ended_request(conn, request)
23 | |> set_response(%{})
24 | end
25 | |> Plug.Conn.send_resp()
26 | end
27 |
28 | def unquote(method)(conn, params) do
29 | case Poison.Decode.decode(params, as: %PhoenixAlexa.Request{}) do
30 | %PhoenixAlexa.Request{} = request -> handle_request(conn, request)
31 | _ ->
32 | conn
33 | |> Plug.Conn.put_resp_content_type("application/json")
34 | |> Plug.Conn.send_resp(500, Poison.encode!(%{error: "Internal Error"}))
35 | end
36 |
37 | end
38 |
39 | def launch_request(conn, _request) do
40 | conn |> set_response(%Response{})
41 | end
42 |
43 | def session_ended_request(conn, request) do
44 | conn
45 | end
46 |
47 | def intent_request(conn, _, request) do
48 | conn |> set_response(%Response{})
49 | end
50 |
51 | defoverridable [launch_request: 2, intent_request: 3, session_ended_request: 2]
52 |
53 | end
54 | end
55 |
56 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Alexa library for Phoenix
2 |
3 | [](https://travis-ci.org/gabiz/phoenix_alexa)
4 |
5 | ## Usage
6 |
7 | Add phoenix_alexa as a dependency in your `mix.exs` file.
8 |
9 | ```elixir
10 | def deps do
11 | [ { :phoenix_alexa, "~> 0.2.0" } ]
12 | end
13 | ```
14 |
15 | Update a route with a post request into your alexa controller.
16 |
17 | ```elixir
18 | scope "/", HelloAlexa do
19 | pipe_through :api
20 |
21 | post "/", AlexaController, :post
22 | end
23 |
24 | ```
25 |
26 | In the controller add a use statement for `PhoenixAlexa.Controller` and define functions for `launch_request`, `session_end_request` and `intent_request` as follows:
27 |
28 | ```elixir
29 |
30 | defmodule HelloPhoenixAlexa.AlexaController do
31 | use HelloPhoenixAlexa.Web, :controller
32 | use PhoenixAlexa.Controller, :post # param should match route name
33 |
34 | def launch_request(conn, request) do
35 | response = %Response{}
36 | |> set_output_speech(%TextOutputSpeech{text: "Welcome to the Horoscope."})
37 |
38 | conn
39 | |> set_response(response)
40 | end
41 |
42 | def session_end_request(conn, request) do
43 | conn
44 | end
45 |
46 | def intent_request(conn, "GetHoroscope", request) do
47 | response = case request.request.intent.slots["Sign"]["value"] do
48 | "Libra" ->
49 | card = %SimpleCard{}
50 | |> set_title("Get Horoscope")
51 | |> set_content("You are going to have an unexpected event today.")
52 |
53 | %Response{}
54 | |> set_output_speech(%TextOutputSpeech{text: "You are going to have an unexpected event today."})
55 | |> set_card(card)
56 | |> set_session_attributes(%{my_key: "my_data"})
57 | |> set_should_end_session(true)
58 | _ ->
59 | %Response{}
60 | |> set_output_speech(%TextOutputSpeech{text: "You are going to meet an interesting person."})
61 | |> set_should_end_session(true)
62 | end
63 |
64 | conn |> set_response(response)
65 | end
66 | end
67 |
68 | ```
69 |
70 | To authenticate that the request corresponds to your Alexa application add the ValidateApplicationId plug to your router as follows:
71 |
72 | ```elixir
73 | pipeline :api do
74 | plug :accepts, ["json"]
75 | plug ValidateApplicationId, "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"
76 | end
77 | ```
78 |
--------------------------------------------------------------------------------
/lib/response/response.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.Response do
2 | alias PhoenixAlexa.{Response, TextOutputSpeech, SsmlOutputSpeech}
3 | alias PhoenixAlexa.{SimpleCard, StandardCard, LinkAccountCard}
4 |
5 | defstruct version: "1.0",
6 | sessionAttributes: %{},
7 | response: %{
8 | shouldEndSession: false
9 | }
10 |
11 | # def session_attributes(response) do
12 | # response[:sessionAttributes]
13 | # end
14 |
15 | def set_session_attributes(response, sessionAttributes) do
16 | # response = response || %{}
17 | # Map.put(response, :sessionAttributes, sessionAttributes)
18 | %Response{response | sessionAttributes: sessionAttributes}
19 | end
20 |
21 | def set_output_speech(response, %TextOutputSpeech{} = outputspeech) do
22 | %Response{response | response: (response.response |> Map.put(:outputSpeech, outputspeech))}
23 | end
24 | def set_output_speech(response, %SsmlOutputSpeech{} = outputspeech) do
25 | %Response{response | response: (response.response |> Map.put(:outputSpeech, outputspeech))}
26 | end
27 |
28 | def set_card(response, %SimpleCard{} = card) do
29 | %Response{response | response: (response.response |> Map.put(:card, card))}
30 | end
31 | def set_card(response, %StandardCard{} = card) do
32 | %Response{response | response: (response.response |> Map.put(:card, card))}
33 | end
34 | def set_card(response, %LinkAccountCard{} = card) do
35 | %Response{response | response: (response.response |> Map.put(:card, card))}
36 | end
37 |
38 | def set_reprompt(response, %TextOutputSpeech{} = outputspeech) do
39 | %Response{response | response: (response.response |> Map.put(:reprompt, %{outputSpeech: outputspeech}))}
40 | end
41 | def set_reprompt(response, %SsmlOutputSpeech{} = outputspeech) do
42 | %Response{response | response: (response.response |> Map.put(:reprompt, %{outputSpeech: outputspeech}))}
43 | end
44 |
45 | def set_should_end_session(response, shouldEndSession) do
46 | %Response{response | response: %{response.response | shouldEndSession: shouldEndSession}}
47 | end
48 |
49 | # Card helpers
50 |
51 | def set_title(%SimpleCard{} = card, title) do
52 | SimpleCard.set_title(card, title)
53 | end
54 | def set_title(%StandardCard{} = card, title) do
55 | StandardCard.set_title(card, title)
56 | end
57 | def set_title(%LinkAccountCard{} = card, title) do
58 | LinkAccountCard.set_title(card, title)
59 | end
60 |
61 | def set_content(%SimpleCard{} = card, content) do
62 | SimpleCard.set_content(card, content)
63 | end
64 | def set_content(%LinkAccountCard{} = card, content) do
65 | LinkAccountCard.set_content(card, content)
66 | end
67 |
68 | def set_text(%StandardCard{} = card, text) do
69 | StandardCard.set_text(card, text)
70 | end
71 |
72 | def set_small_image_url(%StandardCard{} = card, image_url) do
73 | StandardCard.set_small_image_url(card, image_url)
74 | end
75 |
76 | def set_large_image_url(%StandardCard{} = card, image_url) do
77 | StandardCard.set_large_image_url(card, image_url)
78 | end
79 |
80 | end
--------------------------------------------------------------------------------
/test/phoenix_alexa/request_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.RequestTest do
2 | use ExUnit.Case
3 |
4 | alias PhoenixAlexa.{Session, IntentRequest}
5 |
6 | test "session" do
7 | json_session = """
8 | {
9 | "new": false,
10 | "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000",
11 | "application": {
12 | "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
13 | },
14 | "attributes": {
15 | "supportedHoroscopePeriods": {
16 | "daily": true,
17 | "weekly": false,
18 | "monthly": false
19 | }
20 | },
21 | "user": {
22 | "userId": "amzn1.account.AM3B00000000000000000000000"
23 | }
24 | }
25 | """
26 | session = Poison.decode!(json_session, as: %Session{})
27 |
28 | assert session.new == false
29 | assert session.sessionId == "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000"
30 | assert session.application.applicationId == "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
31 | assert session.attributes == %{
32 | "supportedHoroscopePeriods" => %{
33 | "daily" => true,
34 | "weekly" => false,
35 | "monthly" => false
36 | }
37 | }
38 | assert session.user.userId == "amzn1.account.AM3B00000000000000000000000"
39 | end
40 |
41 | test "request" do
42 | json_request = """
43 | {
44 | "type": "IntentRequest",
45 | "requestId": "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000",
46 | "timestamp": "2015-05-13T12:34:56Z",
47 | "intent": {
48 | "name": "GetZodiacHoroscopeIntent",
49 | "slots": {
50 | "ZodiacSign": {
51 | "name": "ZodiacSign",
52 | "value": "virgo"
53 | }
54 | }
55 | }
56 | }
57 | """
58 |
59 | request = Poison.decode!(json_request, as: %IntentRequest{})
60 |
61 | assert request.type == "IntentRequest"
62 | assert request.requestId == "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000"
63 | assert request.timestamp == "2015-05-13T12:34:56Z"
64 | assert request.intent.name == "GetZodiacHoroscopeIntent"
65 | assert request.intent.slots["ZodiacSign"]["name"] == "ZodiacSign"
66 | assert request.intent.slots["ZodiacSign"]["value"] == "virgo"
67 | end
68 |
69 | end
70 |
71 |
72 |
73 | # {
74 | # "version": "1.0",
75 | # "session": {
76 | # "new": false,
77 | # "sessionId": "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000",
78 | # "application": {
79 | # "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
80 | # },
81 | # "attributes": {
82 | # "supportedHoroscopePeriods": {
83 | # "daily": true,
84 | # "weekly": false,
85 | # "monthly": false
86 | # }
87 | # },
88 | # "user": {
89 | # "userId": "amzn1.account.AM3B00000000000000000000000"
90 | # }
91 | # },
92 | # "request": {
93 | # "type": "IntentRequest",
94 | # "requestId": " amzn1.echo-api.request.0000000-0000-0000-0000-00000000000",
95 | # "timestamp": "2015-05-13T12:34:56Z",
96 | # "intent": {
97 | # "name": "GetZodiacHoroscopeIntent",
98 | # "slots": {
99 | # "ZodiacSign": {
100 | # "name": "ZodiacSign",
101 | # "value": "virgo"
102 | # }
103 | # }
104 | # }
105 | # }
106 | # }
--------------------------------------------------------------------------------
/test/phoenix_alexa/response_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.ResponseTest do
2 | use ExUnit.Case
3 | alias PhoenixAlexa.{Response, TextOutputSpeech, SsmlOutputSpeech}
4 | alias PhoenixAlexa.{SimpleCard, StandardCard, LinkAccountCard}
5 | import PhoenixAlexa.Response
6 |
7 | test "empty response" do
8 | response = %Response{}
9 | |> Poison.encode!
10 | |> Poison.decode!
11 | assert response == %{
12 | "response" => %{
13 | "shouldEndSession" => false
14 | },
15 | "sessionAttributes" => %{},
16 | "version" => "1.0"
17 | }
18 | end
19 |
20 | test "set_output_speech with TextOutputSpeech" do
21 | response = %Response{}
22 | |> set_output_speech(%TextOutputSpeech{text: "Say hello"})
23 | |> Poison.encode!
24 | |> Poison.decode!
25 | assert response == %{
26 | "response" => %{
27 | "outputSpeech" => %{
28 | "text" => "Say hello",
29 | "type" => "PlainText"
30 | },
31 | "shouldEndSession" => false
32 | },
33 | "sessionAttributes" => %{},
34 | "version" => "1.0"
35 | }
36 | end
37 |
38 | test "set_output_speech with SsmlOutputSpeech" do
39 | response = %Response{}
40 | |> set_output_speech(%SsmlOutputSpeech{ssml: "This output speech uses SSML."})
41 | |> Poison.encode!
42 | |> Poison.decode!
43 | assert response == %{
44 | "response" => %{
45 | "outputSpeech" => %{
46 | "ssml" => "This output speech uses SSML.",
47 | "type" => "SSML"
48 | },
49 | "shouldEndSession" => false
50 | },
51 | "sessionAttributes" => %{},
52 | "version" => "1.0"
53 | }
54 | end
55 |
56 | test "set_card with SimpleCard" do
57 | card = %SimpleCard{}
58 | |> set_title("Title")
59 | |> set_content("Content")
60 | response = %Response{}
61 | |> set_card(card)
62 | |> Poison.encode!
63 | |> Poison.decode!
64 | assert response == %{
65 | "response" => %{
66 | "card" => %{
67 | "content" => "Content",
68 | "title" => "Title",
69 | "type" => "Simple"
70 | },
71 | "shouldEndSession" => false
72 | },
73 | "sessionAttributes" => %{},
74 | "version" => "1.0"
75 | }
76 | end
77 |
78 | test "set_card with StandardCard" do
79 | card = %StandardCard{}
80 | |> set_title("Title")
81 | |> set_text("Text")
82 | |> set_small_image_url("http://small_image_url")
83 | |> set_large_image_url("http://large_image_url")
84 | response = %Response{}
85 | |> set_card(card)
86 | |> Poison.encode!
87 | |> Poison.decode!
88 | assert response == %{
89 | "response" => %{
90 | "card" => %{
91 | "image" => %{
92 | "smallImageUrl" => "http://small_image_url",
93 | "largeImageUrl" => "http://large_image_url"
94 | },
95 | "text" => "Text",
96 | "title" => "Title",
97 | "type" => "Standard"
98 | },
99 | "shouldEndSession" => false
100 | },
101 | "sessionAttributes" => %{},
102 | "version" => "1.0"
103 | }
104 | end
105 |
106 | test "set_card with StandardCard no images" do
107 | card = %StandardCard{}
108 | |> set_title("Title")
109 | |> set_text("Text")
110 | response = %Response{}
111 | |> set_card(card)
112 | |> Poison.encode!
113 | |> Poison.decode!
114 | assert response == %{
115 | "response" => %{
116 | "card" => %{
117 | "text" => "Text",
118 | "title" => "Title",
119 | "type" => "Standard"
120 | },
121 | "shouldEndSession" => false
122 | },
123 | "sessionAttributes" => %{},
124 | "version" => "1.0"
125 | }
126 | end
127 |
128 | test "set_card with LinkAccountCard" do
129 | card = %LinkAccountCard{}
130 | |> set_title("Title")
131 | |> set_content("Content")
132 | response = %Response{}
133 | |> set_card(card)
134 | |> Poison.encode!
135 | |> Poison.decode!
136 | assert response == %{
137 | "response" => %{
138 | "card" => %{
139 | "content" => "Content",
140 | "title" => "Title",
141 | "type" => "LinkAccount"
142 | },
143 | "shouldEndSession" => false
144 | },
145 | "sessionAttributes" => %{},
146 | "version" => "1.0"
147 | }
148 | end
149 |
150 | test "set_reprompt with TextOutputSpeech" do
151 | response = %Response{}
152 | |> set_reprompt(%TextOutputSpeech{text: "Say hello"})
153 | |> Poison.encode!
154 | |> Poison.decode!
155 | assert response == %{
156 | "response" => %{
157 | "reprompt" => %{
158 | "outputSpeech" => %{
159 | "text" => "Say hello",
160 | "type" => "PlainText"
161 | },
162 | },
163 | "shouldEndSession" => false
164 | },
165 | "sessionAttributes" => %{},
166 | "version" => "1.0"
167 | }
168 | end
169 |
170 | test "set_reprompt with SsmlOutputSpeech" do
171 | response = %Response{}
172 | |> set_reprompt(%SsmlOutputSpeech{ssml: "This output speech uses SSML."})
173 | |> Poison.encode!
174 | |> Poison.decode!
175 | assert response == %{
176 | "response" => %{
177 | "reprompt" => %{
178 | "outputSpeech" => %{
179 | "ssml" => "This output speech uses SSML.",
180 | "type" => "SSML"
181 | },
182 | },
183 | "shouldEndSession" => false
184 | },
185 | "sessionAttributes" => %{},
186 | "version" => "1.0"
187 | }
188 | end
189 |
190 | test "set_should_end_session" do
191 | response = %Response{}
192 | |> set_should_end_session(true)
193 | |> Poison.encode!
194 | |> Poison.decode!
195 | assert response == %{
196 | "response" => %{
197 | "shouldEndSession" => true},
198 | "sessionAttributes" => %{},
199 | "version" => "1.0"
200 | }
201 | end
202 | end
--------------------------------------------------------------------------------
/test/phoenix_alexa/phoenix_alexa_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixAlexa.ControllerTest do
2 | use ExUnit.Case
3 | use Plug.Test
4 |
5 | defmodule HoroscopeController do
6 | use PhoenixAlexa.Controller, :post
7 |
8 | def launch_request(conn, request) do
9 | assert request.version == "1.0"
10 | assert request.session.new == false
11 | assert request.session.sessionId == "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f"
12 | assert request.session.application.applicationId == "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"
13 | assert request.session.attributes == %{}
14 | assert request.session.user.userId == "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI"
15 | assert request.request.type == "LaunchRequest"
16 | assert request.request.requestId == "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd"
17 | assert request.request.timestamp == "2016-07-07T00:45:08Z"
18 |
19 | response = %Response{}
20 | |> set_output_speech(%TextOutputSpeech{text: "Welcome to the Horoscope."})
21 |
22 | conn
23 | |> set_response(response)
24 | end
25 |
26 | def session_ended_request(conn, request) do
27 | assert request.version == "1.0"
28 | assert request.session.new == false
29 | assert request.session.sessionId == "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f"
30 | assert request.session.application.applicationId == "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"
31 | assert request.session.attributes == %{}
32 | assert request.session.user.userId == "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI"
33 | assert request.request.type == "SessionEndedRequest"
34 | assert request.request.requestId == "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd"
35 | assert request.request.timestamp == "2016-07-07T00:45:08Z"
36 | assert request.request.reason == "USER_INITIATED"
37 |
38 | conn
39 | end
40 |
41 | def intent_request(conn, "GetHoroscope", request) do
42 | assert request.version == "1.0"
43 | assert request.session.new == false
44 | assert request.session.sessionId == "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f"
45 | assert request.session.application.applicationId == "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"
46 | assert request.session.attributes == %{}
47 | assert request.session.user.userId == "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI"
48 | assert request.request.type == "IntentRequest"
49 | assert request.request.requestId == "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd"
50 | assert request.request.timestamp == "2016-07-07T00:45:08Z"
51 | assert request.request.intent.name == "GetHoroscope"
52 | assert request.request.intent.slots["Sign"]["name"] == "Sign"
53 | assert request.request.intent.slots["Sign"]["value"] == "Libra"
54 | assert request.request.intent.slots["Date"]["name"] == "Date"
55 | assert request.request.intent.slots["Date"]["value"] == nil
56 |
57 | card = %SimpleCard{}
58 | |> SimpleCard.set_title("Get Horoscope")
59 | |> SimpleCard.set_content("You are going to have an unexpected event today.")
60 |
61 | response = %Response{}
62 | |> set_output_speech(%TextOutputSpeech{text: "You are going to have an unexpected event today."})
63 | |> set_card(card)
64 | |> set_session_attributes(%{my_key: "my_data"})
65 | |> set_should_end_session(true)
66 |
67 | conn |> set_response(response)
68 | end
69 |
70 | end
71 |
72 | test "LaunchRequest" do
73 | json = """
74 | {
75 | "session": {
76 | "sessionId": "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f",
77 | "application": {
78 | "applicationId": "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"
79 | },
80 | "attributes": {},
81 | "user": {
82 | "userId": "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI"
83 | },
84 | "new": false
85 | },
86 | "request": {
87 | "type": "LaunchRequest",
88 | "requestId": "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd",
89 | "timestamp": "2016-07-07T00:45:08Z"
90 | },
91 | "version": "1.0"
92 | }
93 | """
94 |
95 | conn = conn(:post, "/alexa", "#{json}")
96 |
97 | conn = HoroscopeController.post(conn, Poison.decode!(json))
98 | response = Poison.decode!(conn.resp_body)
99 |
100 | expected_response = %{
101 | "version" => "1.0",
102 | "sessionAttributes" => %{},
103 | "response" => %{
104 | "outputSpeech" => %{
105 | "type" => "PlainText",
106 | "text" => "Welcome to the Horoscope."
107 | },
108 | "shouldEndSession" => false
109 | }
110 | }
111 |
112 | assert conn.status == 200
113 | assert response == expected_response
114 | end
115 |
116 | test "IntentRequest" do
117 | json = """
118 | {
119 | "session": {
120 | "sessionId": "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f",
121 | "application": {
122 | "applicationId": "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"
123 | },
124 | "attributes": {},
125 | "user": {
126 | "userId": "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI"
127 | },
128 | "new": false
129 | },
130 | "request": {
131 | "type": "IntentRequest",
132 | "requestId": "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd",
133 | "timestamp": "2016-07-07T00:45:08Z",
134 | "intent": {
135 | "name": "GetHoroscope",
136 | "slots": {
137 | "Sign": {
138 | "name": "Sign",
139 | "value": "Libra"
140 | },
141 | "Date": {
142 | "name": "Date"
143 | }
144 | }
145 | },
146 | "locale": "en-US"
147 | },
148 | "version": "1.0"
149 | }
150 | """
151 |
152 | conn = conn(:post, "/alexa", "#{json}")
153 |
154 | conn = HoroscopeController.post(conn, Poison.decode!(json))
155 |
156 | response = Poison.decode!(conn.resp_body)
157 |
158 | expected_response = %{
159 | "version" => "1.0",
160 | "response" => %{
161 | "card" => %{
162 | "type" => "Simple",
163 | "title" => "Get Horoscope",
164 | "content" => "You are going to have an unexpected event today."
165 | },
166 | "outputSpeech" => %{
167 | "type" => "PlainText",
168 | "text" => "You are going to have an unexpected event today."
169 | },
170 | "shouldEndSession" => true
171 | },
172 | "sessionAttributes" => %{"my_key" => "my_data"}
173 | }
174 |
175 | assert conn.status == 200
176 | assert response == expected_response
177 | end
178 |
179 | test "SessionEndedRequest" do
180 | json = """
181 | {
182 | "session": {
183 | "sessionId": "SessionId.80ef8951-172e-4f02-ace8-a7ec847e2d9f",
184 | "application": {
185 | "applicationId": "amzn1.echo-sdk-ams.app.05dcb1a4-cb45-46c5-a30e-bb3033a0770a"
186 | },
187 | "attributes": {},
188 | "user": {
189 | "userId": "amzn1.ask.account.AFP3ZWPOS2BGJR7OWJZ3DHPKMOMNWY4AY66FUR7ILBWANIHQN73QH3G5PC2FJVGDIDA7MY54GGNRGM4SVPKTT3K53SLI232MEFI77TZN7W6LISNFZTTFDSPCLX6OB4ISJDVJB6QZO3XC74US6CH5DQXYCVOODNTUFNI5JNSUWSBDVMWB7JXPVX43P4EUIMHTPZHNRHZDUDENZVI"
190 | },
191 | "new": false
192 | },
193 | "request": {
194 | "type": "SessionEndedRequest",
195 | "requestId": "EdwRequestId.27142539-8af4-430c-8f22-411cfab269bd",
196 | "timestamp": "2016-07-07T00:45:08Z",
197 | "reason": "USER_INITIATED"
198 | },
199 | "version": "1.0"
200 | }
201 | """
202 |
203 | conn = conn(:post, "/alexa", "#{json}")
204 |
205 | conn = HoroscopeController.post(conn, Poison.decode!(json))
206 | response = Poison.decode!(conn.resp_body)
207 |
208 | expected_response = %{
209 | }
210 |
211 | assert conn.status == 200
212 | assert response == expected_response
213 | end
214 |
215 |
216 |
217 | end
218 |
219 |
--------------------------------------------------------------------------------