Skip to content

Commit 0741d00

Browse files
authored
Move to pure / impure breakup (#165)
* proof of concept with dynamo * proof of concept with S3 * kinesis * lambda * SQS * green tests * remove unnecessary files * latest elixir travis * bug fix * dialyzer fixes * decode kinesis records * s3 parsers * correct tests for new parsers * delete_all_objects again * include actual function * green tests for rds and ec2 * dialyzer fixes * Don't do virtual style hosts per #168 * note about 0.5 branch * handle encoding lambda context
1 parent 000ff31 commit 0741d00

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+6376
-7157
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
language: elixir
22
sudo: false
3-
elixir: 1.2.0
3+
elixir: 1.2.5
44
notifications:
55
recipients:
66

README.md

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,45 @@ ExAws
22
=====
33
[![Build Status](https://travis-ci.org/CargoSense/ex_aws.svg?branch=master)](https://travis-ci.org/CargoSense/ex_aws)
44

5-
A flexible easy to use set of clients AWS APIs.
5+
A flexible easy to use set of AWS APIs.
6+
7+
## 1.0.0-beta0 Changes
8+
9+
The `v0.5` branch holds the legacy approach.
10+
11+
ExAws 1.0.0 takes a more data driven approach to querying APIs. The various functions
12+
that exist inside a service like `S3.list_objects` or `Dynamo.create_table` all
13+
return a struct which holds the information necessary to make that particular operation.
14+
15+
You then have 4 ways you can choose to execute that operation:
16+
17+
```elixir
18+
# Normal
19+
S3.list_buckets |> ExAw.request #=> {:ok, response}
20+
# With per request configuration overrides
21+
S3.list_buckets |> ExAw.request(config) #=> {:ok, response}
22+
23+
# Raise on error, return successful responses directly
24+
S3.list_buckets |> ExAw.request! #=> response
25+
S3.list_buckets |> ExAw.request!(config) #=> response
26+
```
27+
28+
Certain operations also support Elixir streams:
29+
30+
```elixir
31+
S3.list_objects("my-bucket") |> ExAws.stream! #=> #Function<13.52451638/2 in Stream.resource/3>
32+
S3.list_objects("my-bucket") |> ExAws.stream!(config) #=> #Function<13.52451638/2 in Stream.resource/3>
33+
```
34+
35+
The ability to return a stream is noticed in the function's documentation.
36+
37+
### Migration
38+
39+
This change greatly simplifies the ExAws code paths, and removes entirely the complex
40+
meta-programming pervasive to the original approach. However, it does constitute
41+
a breaking change for anyone who had a client with custom logic.
42+
43+
644

745
## Highlighted Features
846
- Easy configuration.
@@ -28,17 +66,17 @@ If you wish to use instance roles to obtain AWS access keys you will need to add
2866
```elixir
2967
def deps do
3068
[
31-
{:ex_aws, "~> 0.4.10"},
32-
{:poison, "~> 1.2"},
33-
{:httpoison, "~> 0.7"}
69+
{:ex_aws, "~> 1.0.0-beta0"},
70+
{:poison, "~> 2.0"},
71+
{:httpoison, "~> 0.8"}
3472
]
3573
end
3674
```
3775
Don't forget to add :httpoison to your applications list if that's in fact the http client you choose. `:ex_aws` must always be added to your applications list.
3876

3977
```elixir
4078
def application do
41-
[applications: [:ex_aws, :httpoison]]
79+
[applications: [:ex_aws, :httpoison, :poison]]
4280
end
4381
```
4482

lib/ex_aws.ex

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,65 @@ defmodule ExAws do
22
@moduledoc File.read!("#{__DIR__}/../README.md")
33
use Application
44

5+
@doc """
6+
Perform an AWS request
57
8+
First build an operation from one of the services, and then pass it to this
9+
function to perform it.
10+
11+
This function takes an optional second parameter of configuration overrides.
12+
This is useful if you want to have certain configuration changed on a per
13+
request basis.
14+
15+
## Examples
16+
17+
```
18+
ExAws.S3.list_buckets |> ExAws.request
19+
20+
ExAws.S3.list_buckets |> ExAws.request(region: "eu-west-1")
21+
22+
ExAws.Dynamo.get_object("users", "[email protected]") |> ExAws.request
23+
```
24+
25+
"""
26+
@spec request(ExAws.Operation.t) :: term
27+
@spec request(ExAws.Operation.t, Keyword.t) :: {:ok, term} | {:error, term}
28+
def request(op, config_overrides \\ []) do
29+
ExAws.Operation.perform(op, ExAws.Config.build(op.service, config_overrides))
30+
end
31+
32+
@doc """
33+
Build a stream
34+
"""
35+
@spec stream!(ExAws.Operation.t) :: Enumerable.t
36+
@spec stream!(ExAws.Operation.t, Keyword.t) :: Enumerable.t
37+
def stream!(op, config_overrides \\ []) do
38+
ExAws.Operation.stream!(op, ExAws.Config.build(op.service, config_overrides))
39+
end
40+
41+
@doc """
42+
Perform an AWS request, raise if it fails.
43+
44+
Same as `request/1,2` except it will either return the successful response from
45+
AWS or raise an exception.
46+
"""
47+
@spec request!(ExAws.Operation.t) :: term | no_return
48+
@spec request!(ExAws.Operation.t, Keyword.t) :: term | no_return
49+
def request!(op, config_overrides \\ []) do
50+
case request(op, config_overrides) do
51+
{:ok, result} ->
52+
result
53+
54+
error ->
55+
raise ExAws.Error, """
56+
ExAws Request Error!
57+
58+
#{inspect error}
59+
"""
60+
end
61+
end
62+
63+
@doc false
664
def start(_type, _args) do
765
import Supervisor.Spec, warn: false
866

lib/ex_aws/client.ex

Lines changed: 0 additions & 82 deletions
This file was deleted.

lib/ex_aws/config.ex

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule ExAws.Config do
22

33
@moduledoc false
44

5-
# Generates the configuration for a client.
5+
# Generates the configuration for a service.
66
# It starts with the defaults for a given environment
77
# and then merges in the common config from the ex_aws config root,
88
# and then finally any config specified for the particular service
@@ -12,72 +12,70 @@ defmodule ExAws.Config do
1212
:region, :security_token
1313
]
1414

15-
def build(client, opts \\ []) do
16-
config = client
17-
|> ExAws.Config.get
18-
|> Map.merge(Enum.into(opts, %{}))
15+
def build(service, opts \\ []) do
16+
overrides = Map.new(opts)
1917

20-
%{client | config: config}
18+
service
19+
|> ExAws.Config.get(overrides)
2120
|> retrieve_runtime_config
2221
|> parse_host_for_region
2322
end
2423

25-
def get(%{__struct__: client_module, service: service}) do
26-
config_root = client_module.config_root
27-
unless config_root, do: raise "A valid configuration root is required in your #{service} client"
24+
@doc """
25+
Builds a complete set of config for an operation.
2826
29-
defaults = ExAws.Config.Defaults.get
30-
config = config_root |> Keyword.get(service, [])
31-
common = defaults
32-
|> Keyword.merge(config_root)
33-
|> Keyword.take(@common_config)
27+
1) Defaults are pulled from `ExAws.Config.Defaults`
28+
2) Common values set via e.g `config :ex_aws` are merged in.
29+
3) Keys set on the individual service e.g `config :ex_aws, :s3` are merged in
30+
4) Finally, any configuration overrides are merged in
31+
"""
32+
def get(service, overrides) do
33+
defaults = ExAws.Config.Defaults.get(service)
34+
common_config = Application.get_all_env(:ex_aws) |> Map.new |> Map.take(@common_config)
35+
service_config = Application.get_env(:ex_aws, service, []) |> Map.new
3436

3537
defaults
36-
|> Keyword.get(service, [])
37-
|> Keyword.merge(common)
38-
|> Keyword.merge(config)
39-
|> Enum.into(%{})
38+
|> Map.merge(common_config)
39+
|> Map.merge(service_config)
40+
|> Map.merge(overrides)
4041
end
4142

42-
def retrieve_runtime_config(%{config: config} = client) do
43-
new_config = config
44-
|> Enum.reduce(%{}, fn
43+
def retrieve_runtime_config(config) do
44+
Enum.reduce(config, %{}, fn
4545
{:host, host}, config ->
46-
Map.put(config, :host, retrieve_runtime_value(host, client))
46+
Map.put(config, :host, retrieve_runtime_value(host, config))
4747
{k, v}, config ->
48-
case retrieve_runtime_value(v, client) do
48+
case retrieve_runtime_value(v, config) do
4949
%{} = result -> Map.merge(config, result)
5050
value -> Map.put(config, k, value)
5151
end
5252
end)
53-
54-
%{client | config: new_config}
5553
end
5654

5755
def retrieve_runtime_value({:system, env_key}, _) do
5856
System.get_env(env_key)
5957
end
60-
def retrieve_runtime_value(:instance_role, client) do
61-
client
58+
def retrieve_runtime_value(:instance_role, config) do
59+
config
6260
|> ExAws.Config.AuthCache.get
6361
|> Map.take([:access_key_id, :secret_access_key, :security_token])
6462
end
65-
def retrieve_runtime_value(values, client) when is_list(values) do
63+
def retrieve_runtime_value(values, config) when is_list(values) do
6664
values
67-
|> Stream.map(&retrieve_runtime_value(&1, client))
65+
|> Stream.map(&retrieve_runtime_value(&1, config))
6866
|> Enum.find(&(&1))
6967
end
7068
def retrieve_runtime_value(value, _), do: value
7169

72-
def parse_host_for_region(%{config: %{host: {stub, host}, region: region} = config} = client) do
73-
%{client | config: Map.put(config, :host, String.replace(host, stub, region))}
70+
def parse_host_for_region(%{host: {stub, host}, region: region} = config) do
71+
Map.put(config, :host, String.replace(host, stub, region))
7472
end
75-
def parse_host_for_region(%{config: %{host: map, region: region} = config} = client) when is_map(map) do
73+
def parse_host_for_region(%{host: map, region: region} = config) when is_map(map) do
7674
case Map.fetch(map, region) do
77-
{:ok, host} -> %{client | config: Map.put(config, :host, host)}
75+
{:ok, host} -> Map.put(config, :host, host)
7876
:error -> "A host for region #{region} was not found in host map #{inspect(map)}"
7977
end
8078
end
81-
def parse_host_for_region(client), do: client
79+
def parse_host_for_region(config), do: config
8280

8381
end

lib/ex_aws/config/auth_cache.ex

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ defmodule ExAws.Config.AuthCache do
99
GenServer.start_link(__MODULE__, :ok, opts)
1010
end
1111

12-
def get(client) do
12+
def get(config) do
1313
case :ets.lookup(__MODULE__, :aws_instance_auth) do
1414
[{:aws_instance_auth, auth_config}] -> auth_config
15-
[] -> GenServer.call(__MODULE__, {:refresh_config, client})
15+
[] -> GenServer.call(__MODULE__, {:refresh_config, config})
1616
end
1717
end
1818

@@ -23,20 +23,20 @@ defmodule ExAws.Config.AuthCache do
2323
{:ok, ets}
2424
end
2525

26-
def handle_call({:refresh_config, client}, _from, ets) do
27-
auth = refresh_config(client, ets)
26+
def handle_call({:refresh_config, config}, _from, ets) do
27+
auth = refresh_config(config, ets)
2828
{:reply, auth, ets}
2929
end
3030

31-
def handle_info({:refresh_config, client}, ets) do
32-
refresh_config(client, ets)
31+
def handle_info({:refresh_config, config}, ets) do
32+
refresh_config(config, ets)
3333
{:noreply, ets}
3434
end
3535

36-
def refresh_config(client, ets) do
37-
auth = ExAws.InstanceMeta.security_credentials(client)
36+
def refresh_config(config, ets) do
37+
auth = ExAws.InstanceMeta.security_credentials(config)
3838
:ets.insert(ets, {:aws_instance_auth, auth})
39-
Process.send_after(self(), {:refresh_config, client}, refresh_in(auth[:expiration]))
39+
Process.send_after(self(), {:refresh_config, config}, refresh_in(auth[:expiration]))
4040
auth
4141
end
4242

0 commit comments

Comments
 (0)