Skip to content

Commit a9265ad

Browse files
committed
doc updates
1 parent 5f52d75 commit a9265ad

File tree

6 files changed

+52
-181
lines changed

6 files changed

+52
-181
lines changed

CONTRIBUTING.md

Lines changed: 12 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Contributing
22
============
33

4+
## Updates for 1.0
5+
6+
The organizational structure of ExAws has been greatly simplified as we move into 1.0. Please read this document carefully as it has changed.
7+
48
Contributions to ExAws are absolutely appreciated. For general bug fixes or other tweaks to existing code, a regular pull request is fine. For those who wish to add to the set of APIs supported by ExAws, please consult the rest of this document, as any PRs adding a service are expected to follow the structure defined herein.
59

610
## Running Tests
@@ -34,106 +38,18 @@ The test suite can be run with `AWS_ACCESS_KEY_ID=your-aws-access-key AWS_SECRET
3438

3539
## Organization
3640

37-
If you're the kind of person who learns best by example, it may help to read the Example section below first.
38-
39-
For a given service, the following basic files and modules exist:
40-
```
41-
lib/ex_aws/service_name.ex #=> ExAws.ServiceName
42-
lib/ex_aws/service_name/client.ex #=> ExAws.ServiceName.Client
43-
lib/ex_aws/service_name/impl.ex #=> ExAws.ServiceName.Impl
44-
lib/ex_aws/service_name/request.ex #=> ExAws.ServiceName.Request
45-
```
46-
47-
### ExAws.ServiceName.Request
48-
49-
Consists of a `request` function and any custom request logic required for a given API. This may include particular headers that service expects, url formatting, etc. It should not include the authorization header signing process, as this is handled by the ExAws.Request module. The request function ought to call `ExAws.Request.request/5`.
50-
51-
### ExAws.ServiceName.Impl
41+
ExAws 1.0.0 takes a more data driven approach to querying APIs. The various functions that exist inside a service like `S3.list_objects` or `Dynamo.create_table` all return a struct which holds the information necessary to make that particular operation. Creating a service module then is very easy, as you just need to create functions which return an operations struct, and you're done. If there is not yet an operations struct applicable to the desired service, creating one of those isn't too bad either. See the relevant sections below.
5242

53-
houses the functions that correspond to a particular action in the AWS service. Function names should correspond as closely as is reasonable to the AWS action they implement. All functions in this module (excluding any private helpers) MUST accept a client as the first argument, and call the client.request function with whatever relevant data exists for that action.
43+
Often the same struct is used across several services if those services have the same underlying request characteristics. For example Dynamo, Kinesis, and Lambda all use the JSON operation.
5444

55-
### ExAws.ServiceName.Client
45+
The `ExAws.Operation` protocol is implemented for each operation struct, giving us `perform` and `stream` functions. The `perform/2` function operations basically like the service specific `request` functions that existed pre 1.0. They take the operation struct and any configuration overrides, do any service specific steps that require configuration, and then call the `ExAws.request` module.
5646

57-
This module serves several rolls. The first is to hold all of the callbacks that must be implemented by a given client. The second is to define a __using__ macro that implements all of the aforementioned callbacks. Most of this is done automatically via macros in the ExAws.Client module. However, the client author is responsible for a request function that simply passes the arguments to the function in the Request module. This indirection exists so that users with custom clients can specify custom behaviour around a request by overriding this function in their client module.
58-
59-
Typespec for the callbacks ought to be fairly complete. See existing Clients for examples.
60-
61-
### ExAws.ServiceName
62-
Finally, the bare ExAws.ServiceName ought to simply consist of the following.
63-
```elixir
64-
defmodule ExAws.ServiceName do
65-
use ExAws.ServiceName.Client
66-
67-
def config_root, do: Application.get_all_env(:ex_aws)
68-
end
69-
```
70-
This produces a reified client for the service in question.
71-
72-
## Example
73-
To make all of this concrete, let's take a look at the `Dynamo.describe_table` function.
74-
75-
ExAws.Dynamo.Client specifies the callback
76-
77-
```elixir
78-
defcallback describe_table(name :: binary) :: ExAws.Request.response_t
79-
```
80-
81-
The `ExAws.Client` boilerplate generation functions generate functions like within the `__using__/1` macro
82-
```elixir
83-
def describe_table(name) do
84-
ExAws.Dynamo.Impl.describe_table(__MODULE__, name)
85-
end
86-
```
87-
88-
Now we hop over to the `ExAws.Dynamo.Impl` module where we actually format the request:
89-
```elixir
90-
def describe_table(client, name) do
91-
%{"TableName" => name}
92-
|> client.request(:describe_table)
93-
end
94-
```
95-
96-
The client author is responsible for the following.
97-
```elixir
98-
defmacro __using__(opts) do
99-
boilerplate = __MODULE__
100-
|> ExAws.Client.generate_boilerplate(opts)
101-
102-
quote do
103-
unquote(boilerplate)
104-
105-
@doc false
106-
def request(data, action) do
107-
ExAws.Dynamo.Request.request(__MODULE__, action, data)
108-
end
109-
110-
@doc false
111-
def service, do: :dynamodb
112-
113-
defoverridable config_root: 0, request: 2
114-
end
115-
end
116-
```
117-
118-
You're probably wondering, why are we going to the effort of calling client.request when all it does is just pass things along to ExAws.Dynamo.Request? Good question! This pattern bestows some very useful abilities upon custom clients. For example gives us the ability to create dummy clients that merely return the structured request instead of actually sending a request, a very useful ability for testing.
119-
120-
More importantly however, suppose had staging and production Dynamo tables such that you had a Users-staging and Users-production, and some STAGE environment variable to tell the app what stage it's in. Instead of the tedious and bug prone route of putting `"Users-#{System.get_env("STAGE")}" |> Dynamo.#desired_function` everywhere, you can just override the request function in a custom client. For example:
121-
122-
```elixir
123-
defmodule My.Dynamo do
124-
def request(client, action, %{"TableName" => table_name} = data) do
125-
data = %{data | "TableName" => "#{table_name}-#{System.get_env("STAGE")}"}
126-
127-
ExAws.Dynamo.Request.request(client, action, data)
128-
end
129-
def request(client, action, data), do: super(client, action, data)
130-
end
131-
```
47+
The `stream` function generally calls a function contained in the operation struct with the operation and config, returning a stream that can be later consumed.
13248

133-
And there we go. Now we can simply do `My.Dynamo.describe_table("Users")` and it will automatically handle the stage question for us\*\*
49+
## Creating a New Service
13450

135-
In any case, `ExAws.Dynamo.Request.request/3` is called by our client and handles dynamo specific headers, and to use the configuration associated with our client to build the URL to hit. We finally end up in `ExAws.Request.request/5` where the configuration for our client is used to retrieve AWS keys and so forth.
51+
In progress. Please see any of the existing services by way of example.
13652

137-
Lastly we have our `ExAws.Dynamo` module which uses the `ExAws.Dynamo.Client` to become a reified client.
53+
## Creating a New Operation
13854

139-
\*\**DISCLAIMER* This is NOT a replacement for or even in the same category as proper AWS security practices. The keys used by your staging and production instances ought to be different, with sufficiently restrictive security policies such that the staging keys can only touch *-staging tables and so on. This functionality exists to minimize bugs and boilerplate, not replace actual security practices.
55+
In progress. Please see any of the existing operations by way of example.

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ ExAws
44

55
A flexible easy to use set of AWS APIs.
66

7+
- `ExAws.Dynamo`
8+
- `ExAws.EC2`
9+
- `ExAws.Kinesis`
10+
- `ExAws.Lambda`
11+
- `ExAws.RDS`
12+
- `ExAws.S3`
13+
- `ExAws.SNS`
14+
- `ExAws.SQS`
15+
716
## 1.0.0-beta0 Changes
817

918
The `v0.5` branch holds the legacy approach.
@@ -57,7 +66,6 @@ a breaking change for anyone who had a client with custom logic.
5766
- Elixir protocols allow easy customization of Dynamo encoding / decoding.
5867
- `mix kinesis.tail your-stream-name` task for easily watching the contents of a kinesis stream.
5968
- Simple. ExAws aims to provide a clear and consistent elixir wrapping around AWS APIs, not abstract them away entirely. For every action in a given AWS API there is a corresponding function within the appropriate module. Higher level abstractions like the aforementioned streams are in addition to and not instead of basic API calls.
60-
- Erlang user? Easily configure erlang friendly module names like `ex_aws_s3` instead of `'Elixir.ExAws.S3'`
6169

6270
## Getting started
6371

lib/ex_aws.ex

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,6 @@ defmodule ExAws do
2929
ExAws.Operation.perform(op, ExAws.Config.new(op.service, config_overrides))
3030
end
3131

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.new(op.service, config_overrides))
39-
end
40-
4132
@doc """
4233
Perform an AWS request, raise if it fails.
4334
@@ -60,6 +51,20 @@ defmodule ExAws do
6051
end
6152
end
6253

54+
@doc """
55+
Return a stream for the AWS resource.
56+
57+
## Examples
58+
```
59+
ExAws.S3.list_objects("my-bucket") |> ExAws.stream!
60+
```
61+
"""
62+
@spec stream!(ExAws.Operation.t) :: Enumerable.t
63+
@spec stream!(ExAws.Operation.t, Keyword.t) :: Enumerable.t
64+
def stream!(op, config_overrides \\ []) do
65+
ExAws.Operation.stream!(op, ExAws.Config.new(op.service, config_overrides))
66+
end
67+
6368
@doc false
6469
def start(_type, _args) do
6570
import Supervisor.Spec, warn: false

lib/ex_aws/dynamo.ex

Lines changed: 5 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
defmodule ExAws.Dynamo do
22
@moduledoc """
3-
Defines a Dynamo Client
4-
5-
By default you can use ExAws.Dynamo
3+
Operations on the AWS Dynamo service.
64
75
NOTE: When Mix.env in [:test, :dev] dynamo clients will run by default against
86
Dynamodb local.
@@ -19,59 +17,20 @@ defmodule ExAws.Dynamo do
1917
# Create a users table with a primary key of email [String]
2018
# and 1 unit of read and write capacity
2119
Dynamo.create_table("Users", "email", %{email: :string}, 1, 1)
20+
|> ExAws.request!
2221
2322
user = %User{email: "[email protected]", name: "Bubba", age: 23, admin: false}
2423
# Save the user
25-
Dynamo.put_item("Users", user)
24+
Dynamo.put_item("Users", user) |> ExAws.request!
2625
2726
# Retrieve the user by email and decode it as a User struct.
2827
result = Dynamo.get_item!("Users", %{email: user.email})
28+
|> ExAws.request!
2929
|> Dynamo.Decoder.decode(as: User)
3030
3131
assert user == result
3232
```
3333
34-
## Customization
35-
If you want more than one client you can define your own as follows. If you don't need more
36-
than one or plan on customizing the request process it's generally easier to just use
37-
and configure the ExAws.Dynamo client.
38-
```
39-
defmodule MyApp.Dynamo do
40-
use ExAws.Dynamo.Client, otp_app: :my_otp_app
41-
end
42-
```
43-
44-
In your config
45-
```
46-
config :my_otp_app, :ex_aws,
47-
dynamodb: [], # Dynamo config goes here
48-
```
49-
50-
You can now use MyApp.Dynamo as the root module for the Dynamo api without needing
51-
to pass in a particular configuration.
52-
This enables different otp apps to configure their AWS configuration separately.
53-
54-
The alignment with a particular OTP app while convenient is however entirely optional.
55-
The following also works:
56-
57-
```
58-
defmodule MyApp.Dynamo do
59-
use ExAws.Dynamo.Client
60-
61-
def config_root do
62-
Application.get_all_env(:my_aws_config_root)
63-
end
64-
end
65-
```
66-
ExAws now expects the config for that dynamo client to live under
67-
68-
```elixir
69-
config :my_aws_config_root
70-
dynamodb: [] # Dynamo config goes here
71-
```
72-
73-
Default config values can be found in ExAws.Config.
74-
7534
## General notes
7635
All options are handled as underscored atoms instead of camelcased binaries as specified
7736
in the Dynamo API. IE `IndexName` would be `:index_name`. Anywhere in the API that requires
@@ -98,31 +57,6 @@ defmodule ExAws.Dynamo do
9857
```
9958
Alternatively, if what's being encoded is a struct, you're always free to implement ExAws.Dynamo.Encodable for that struct.
10059
101-
## Examples
102-
103-
```elixir
104-
defmodule User do
105-
@derive [ExAws.Dynamo.Encodable]
106-
defstruct [:email, :name, :age, :admin]
107-
end
108-
109-
alias ExAws.Dynamo
110-
111-
# Create a users table with a primary key of email [String]
112-
# and 1 unit of read and write capacity
113-
Dynamo.create_table("Users", "email", %{email: :string}, 1, 1)
114-
115-
user = %User{email: "[email protected]", name: "Bubba", age: 23, admin: false}
116-
# Save the user
117-
Dynamo.put_item("Users", user)
118-
119-
# Retrieve the user by email and decode it as a User struct.
120-
result = Dynamo.get_item!("Users", %{email: user.email})
121-
|> Dynamo.Decoder.decode(as: User)
122-
123-
assert user == result
124-
```
125-
12660
http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Operations.html
12761
"""
12862

@@ -415,8 +349,7 @@ defmodule ExAws.Dynamo do
415349
def batch_get_item(data, opts \\ []) do
416350
request_items = data
417351
|> Enum.reduce(%{}, fn {table_name, table_query}, query ->
418-
keys = table_query
419-
|> Dict.get(:keys)
352+
keys = table_query[:keys]
420353
|> Enum.map(&encode_values/1)
421354

422355
dynamized_table_query = table_query

lib/ex_aws/operation/json.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
defmodule ExAws.Operation.JSON do
22
@moduledoc """
3-
Datastructure representing an operation on a JSON based AWS service
3+
Datastructure representing an operation on a JSON based AWS service.
4+
5+
This module is generally not used directly, but rather is constructed by one
6+
of the relevant AWS services.
47
58
These include:
69
- DynamoDB
710
- Kinesis
811
- Lambda (Rest style)
12+
13+
JSON services are generally pretty simple. You just need to populate the `data`
14+
attribute with whatever request body parameters need converted to JSON, and set
15+
any service specific headers.
16+
17+
The `before_request`
918
"""
1019

1120
defstruct [

mix.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule ExAws.Mixfile do
66
version: "1.0.0-beta1",
77
elixir: "~> 1.0",
88
elixirc_paths: elixirc_paths(Mix.env),
9-
description: "AWS client. Currently supports Dynamo, Kinesis, Lambda, S3, SQS, RDS, EC2",
9+
description: "AWS client. Currently supports Dynamo, EC2, Kinesis, Lambda, RDS, S3, SNS, SQS",
1010
name: "ExAws",
1111
source_url: "https://github.com/cargosense/ex_aws",
1212
package: package,
@@ -39,7 +39,7 @@ defmodule ExAws.Mixfile do
3939
end
4040

4141
defp package do
42-
[description: "AWS client. Currently supports Dynamo, Kinesis, Lambda, S3",
42+
[description: "AWS client. Currently supports Dynamo, EC2, Kinesis, Lambda, RDS, S3, SNS, SQS",
4343
files: ["lib", "config", "mix.exs", "README*"],
4444
maintainers: ["Ben Wilson"],
4545
licenses: ["MIT"],

0 commit comments

Comments
 (0)