Skip to content

Commit 6300781

Browse files
brucebenwilson512
authored andcommitted
[WIP] SNS implementation (#40)
* Beginning of SNS implementation * ExAws.SNS.Client.publish defcallback * working requests for SNS * no need to rehash an empty string on every request * initial work towards a working publish impl * publish works now * working tests after master merge * update contributing md for passing travis ci * Use percent encoding for canonical query strings, per AWS docs
1 parent ec8f664 commit 6300781

File tree

10 files changed

+294
-17
lines changed

10 files changed

+294
-17
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Running the test suite for ex_aws requires a few things:
2121
"kinesis:ListStreams",
2222
"lambda:ListFunctions",
2323
"s3:ListAllMyBuckets",
24-
"sqs:ListQueues"
24+
"sns:ListTopics",
25+
"sqs:ListQueues",
2526
],
2627
"Resource": "*"
2728
}

lib/ex_aws/config/defaults.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ defmodule ExAws.Config.Defaults do
4343
host: {"$region", "sqs.$region.amazonaws.com"},
4444
region: "us-east-1",
4545
port: 80
46+
],
47+
sns: [
48+
host: "sns.us-east-1.amazonaws.com",
49+
scheme: "https://",
50+
region: "us-east-1"
4651
]
4752
]
4853
end

lib/ex_aws/sns.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
defmodule ExAws.SNS do
2+
use ExAws.SNS.Client
3+
4+
def config_root, do: Application.get_all_env(:ex_aws)
5+
end

lib/ex_aws/sns/client.ex

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
defmodule ExAws.SNS.Client do
2+
use Behaviour
3+
4+
@moduledoc """
5+
Defines an SNS Client
6+
7+
By default you can use ExAws.SNS
8+
9+
## Usage
10+
11+
```
12+
defmodule MyApp.SNS do
13+
use ExAws.SNS.Client, otp_app: :my_otp_app
14+
end
15+
```
16+
17+
In your config:
18+
19+
```
20+
config :my_otp_app, :ex_aws,
21+
sns: [], # SNS config goes here
22+
```
23+
24+
You can now use MyApp.SNS as the root module for the SNS API without
25+
needing to pass in a particular configuration. This enables different OTP
26+
apps to configure their AWS configurations separately.
27+
28+
The alignment with a particular OTP app while convenient is however entirely
29+
optional.
30+
31+
The following also works:
32+
33+
```
34+
defmodule MyApp.SNS do
35+
use ExAws.SNS.Client
36+
37+
def config_root do
38+
Application.get_all_env(:my_aws_config_root)
39+
end
40+
end
41+
```
42+
43+
ExAws now expects the config for that client to live under `:my_aws_config_root`:
44+
45+
```elixir
46+
config :my_aws_config_root
47+
sns: [] # SNS config goes here
48+
```
49+
50+
Default config values can be found in ExAws.Config.
51+
52+
## General notes
53+
54+
TODO
55+
56+
## Examples
57+
58+
TODO
59+
60+
http://docs.aws.amazon.com/sns/latest/APIReference/API_Operations.html
61+
"""
62+
63+
## Topics
64+
######################
65+
66+
@type topic_name :: binary
67+
@type topic_arn :: binary
68+
@type topic_attribute_name ::
69+
:policy |
70+
:display_name |
71+
:delivery_policy
72+
73+
@doc "List topics"
74+
defcallback list_topics() :: ExAws.Request.response_t
75+
defcallback list_topics(opts :: [next_token: binary]) :: ExAws.Request.response_t
76+
77+
@doc "Create topic"
78+
defcallback create_topic(topic_name :: topic_name) :: ExAws.Request.response_t
79+
80+
@doc "Get topic attributes"
81+
defcallback get_topic_attributes(topic_arn :: topic_arn) :: ExAws.Request.response_t
82+
83+
@doc "Set topic attributes"
84+
defcallback set_topic_attributes(attribute_name :: topic_attribute_name,
85+
attribute_value :: binary,
86+
topic_arn :: topic_arn) :: ExAws.Request.response_t
87+
88+
@doc "Delete topic"
89+
defcallback delete_topic(topic_arn :: topic_arn) :: ExAws.Request.response_t
90+
91+
## Publishing
92+
######################
93+
94+
@type message_attribute :: %{
95+
:name => binary,
96+
:data_type => :string | :number | :binary,
97+
:value => {:string, binary} | {:binary, binary}
98+
}
99+
@type publish_opts :: [
100+
{:message_attributes, [message_attribute]} |
101+
{:message_structure, :json} |
102+
{:subject, binary} |
103+
{:target_arn, binary} |
104+
{:topic_arn, binary}]
105+
106+
@doc """
107+
Publish message to a target/topic ARN
108+
109+
You must set either :target_arn or :topic_arn but not both via the options argument.
110+
111+
Do NOT assume that because your message is a JSON blob that you should set
112+
message_structure: to :json. This has a very specific meaning, please see
113+
http://docs.aws.amazon.com/sns/latest/api/API_Publish.html for details.
114+
"""
115+
defcallback publish(message :: binary, opts :: publish_opts) :: ExAws.Request.response_t
116+
117+
## Requests
118+
######################
119+
120+
@doc """
121+
Enables custom request handling.
122+
123+
By default this just forwards the request to the `ExAws.SNS.Request.request/3`.
124+
However, this can be overriden in your client to provide pre-request adjustments to headers, params, etc.
125+
"""
126+
defcallback request(client_struct :: %{}, data :: %{}, action :: atom)
127+
128+
@doc "Retrieves the root AWS config for this client"
129+
defcallback config_root() :: Keyword.t
130+
131+
defmacro __using__(opts) do
132+
boilerplate = __MODULE__
133+
|> ExAws.Client.generate_boilerplate(opts)
134+
135+
quote do
136+
defstruct config: nil, service: :sns
137+
138+
unquote(boilerplate)
139+
140+
@doc false
141+
def request(client, action, data) do
142+
ExAws.SNS.Request.request(client, action, data)
143+
end
144+
145+
defoverridable config_root: 0, request: 3
146+
end
147+
end
148+
149+
end

lib/ex_aws/sns/impl.ex

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
defmodule ExAws.SNS.Impl do
2+
import ExAws.Utils, only: [camelize_key: 1, camelize_keys: 1]
3+
4+
@moduledoc false
5+
# Implementation of the AWS SNS API.
6+
#
7+
# See ExAws.SNS.Client for usage.
8+
9+
## Topics
10+
######################
11+
12+
def list_topics(client, opts \\ []) do
13+
opts = opts
14+
|> Enum.into(%{})
15+
|> camelize_keys
16+
17+
request(client, :list_topics, opts)
18+
end
19+
20+
def create_topic(client, topic_name) do
21+
request(client, :create_topic, %{"Name" => topic_name})
22+
end
23+
24+
def get_topic_attributes(client, topic_arn) do
25+
request(client, :get_topic_attributes, %{"TopicArn" => topic_arn})
26+
end
27+
28+
def set_topic_attributes(client, attribute_name, attribute_value, topic_arn) do
29+
request(client, :set_topic_attributes, %{
30+
"AttributeName" => attribute_name |> camelize_key,
31+
"AttributeValue" => attribute_value,
32+
"TopicArn" => topic_arn
33+
})
34+
end
35+
36+
def delete_topic(client, topic_arn) do
37+
request(client, :delete_topic, %{"TopicArn" => topic_arn})
38+
end
39+
40+
def publish(client, message, opts) do
41+
opts = opts |> Enum.into(%{})
42+
43+
message_attrs = opts
44+
|> Map.get(:message_attributes, [])
45+
|> build_message_attributes
46+
47+
params = opts
48+
|> Map.drop([:message_attributes])
49+
|> camelize_keys
50+
|> Map.put("Message", message)
51+
|> Map.merge(message_attrs)
52+
53+
request(client, :publish, params)
54+
end
55+
56+
defp build_message_attributes(attrs) do
57+
attrs
58+
|> Stream.with_index
59+
|> Enum.reduce(%{}, &build_message_attribute/2)
60+
end
61+
62+
def build_message_attribute({%{name: name, data_type: data_type, value: {value_type, value}}, i}, params) do
63+
param_root = "MessageAttribute.#{i}"
64+
value_type = value_type |> String.capitalize
65+
66+
params
67+
|> Map.put(param_root <> ".Name", name)
68+
|> Map.put(param_root <> ".Value.#{value_type}Value", value)
69+
|> Map.put(param_root <> ".Value.DataType", data_type)
70+
end
71+
72+
## Request
73+
######################
74+
75+
defp request(%{__struct__: client_module} = client, action, params) do
76+
client_module.request(client, action, params)
77+
end
78+
79+
end

lib/ex_aws/sns/request.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule ExAws.SNS.Request do
2+
@moduledoc false
3+
# SNS specific request logic.
4+
5+
@empty_body_hash ExAws.Auth.Utils.hash_sha256("")
6+
7+
def request(client, action, params) do
8+
query = params
9+
|> Map.put("Action", Mix.Utils.camelize(Atom.to_string(action)))
10+
|> URI.encode_query
11+
|> String.replace("+", "%20")
12+
|> String.replace("%7E", "~")
13+
14+
headers = [
15+
{"x-amz-content-sha256", @empty_body_hash}
16+
]
17+
18+
ExAws.Request.request(:post, client.config |> url(query), "", headers, client)
19+
end
20+
21+
defp url(%{scheme: scheme, host: host}, query) do
22+
[scheme, host, "/?", query]
23+
|> IO.iodata_to_binary
24+
end
25+
end

lib/ex_aws/utils.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ defmodule ExAws.Utils do
3232
end
3333
end
3434

35-
defp camelize_key(key) when is_atom(key) do
35+
def camelize_key(key) when is_atom(key) do
3636
key
3737
|> Atom.to_string
3838
|> camelize
3939
end
4040

41-
defp camelize_key(key) when is_binary(key) do
41+
def camelize_key(key) when is_binary(key) do
4242
key |> camelize
4343
end
4444

mix.lock

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
%{"certifi": {:hex, :certifi, "0.4.0"},
2-
"earmark": {:hex, :earmark, "0.2.1"},
3-
"ex_doc": {:hex, :ex_doc, "0.11.4"},
4-
"hackney": {:hex, :hackney, "1.6.0"},
5-
"httpoison": {:hex, :httpoison, "0.8.3"},
6-
"httpotion": {:hex, :httpotion, "2.0.0"},
1+
%{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
2+
"earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []},
3+
"ex_doc": {:hex, :ex_doc, "0.11.4", "a064bdb720594c3745b94709b17ffb834fd858b4e0c1f48f37c0d92700759e02", [:mix], [{:earmark, "~> 0.1.17 or ~> 0.2", [hex: :earmark, optional: true]}]},
4+
"hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]},
5+
"httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]},
6+
"httpotion": {:hex, :httpotion, "2.0.0", "f9d19c482854adae8a8a6e4c828948a0165242d2233089085de183f92e81cb01", [:mix], []},
77
"ibrowse": {:git, "https://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]},
8-
"idna": {:hex, :idna, "1.2.0"},
9-
"jsx": {:hex, :jsx, "2.5.3"},
10-
"metrics": {:hex, :metrics, "1.0.1"},
11-
"mimerl": {:hex, :mimerl, "1.0.2"},
12-
"mixunit": {:hex, :mixunit, "0.9.2"},
13-
"poison": {:hex, :poison, "1.2.1"},
14-
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0"},
8+
"idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
9+
"jsx": {:hex, :jsx, "2.5.3", "4fd156ddd08aeea2a9e537fff31d93a7c57b017076996c5b3c35a0728b97de02", [:mix, :rebar], [{:mixunit, "~> 0.9.1", [hex: :mixunit, optional: false]}]},
10+
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
11+
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
12+
"mixunit": {:hex, :mixunit, "0.9.2", "4ace17bd1998c8853db5df4e081ec9bcf2b7b5ab04683a18b7ee2aad4c6320d0", [:mix], []},
13+
"poison": {:hex, :poison, "1.2.1", "a9e550f224bdff3a10d7f0651a72998b13098ab08e74f9c5737fc3c009c149f4", [:mix], []},
14+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []},
1515
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"},
16-
"sweet_xml": {:hex, :sweet_xml, "0.5.0"}}
16+
"sweet_xml": {:hex, :sweet_xml, "0.5.0", "d8f1b98527fbe829e9e4b3ad7a71f91ecc84e2e4ccd6c0c8a89b8e12cdcd7837", [:mix], []}}

test/default_helper.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ defmodule Test.S3 do
1818
def config_root, do: Application.get_all_env(:ex_aws)
1919
end
2020

21+
defmodule Test.SNS do
22+
use ExAws.SNS.Client
23+
24+
def config_root, do: Application.get_all_env(:ex_aws)
25+
end
26+
2127
## Other
2228

2329
defmodule Test.JSONCodec do
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule ExAws.SNS.IntegrationTest do
2+
use ExUnit.Case, async: true
3+
4+
test "basic sanity check" do
5+
assert {:error, {:http_error, 400, _}} = ExAws.SNS.list_topics(next_token: "foo")
6+
end
7+
end

0 commit comments

Comments
 (0)