|
1 | 1 | Contributing
|
2 | 2 | ============
|
3 | 3 |
|
| 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 | + |
4 | 8 | 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.
|
5 | 9 |
|
6 | 10 | ## Running Tests
|
@@ -34,106 +38,18 @@ The test suite can be run with `AWS_ACCESS_KEY_ID=your-aws-access-key AWS_SECRET
|
34 | 38 |
|
35 | 39 | ## Organization
|
36 | 40 |
|
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. |
52 | 42 |
|
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. |
54 | 44 |
|
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. |
56 | 46 |
|
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. |
132 | 48 |
|
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 |
134 | 50 |
|
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. |
136 | 52 |
|
137 |
| -Lastly we have our `ExAws.Dynamo` module which uses the `ExAws.Dynamo.Client` to become a reified client. |
| 53 | +## Creating a New Operation |
138 | 54 |
|
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. |
0 commit comments