hyperion_http 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +4 -0
- data/.travis.yml +7 -0
- data/CHANGES.md +145 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +421 -0
- data/Rakefile +11 -0
- data/hyperion_http.gemspec +34 -0
- data/lib/hyperion/aux/bug_error.rb +2 -0
- data/lib/hyperion/aux/hash_ext.rb +5 -0
- data/lib/hyperion/aux/logger.rb +54 -0
- data/lib/hyperion/aux/typho.rb +9 -0
- data/lib/hyperion/aux/util.rb +18 -0
- data/lib/hyperion/aux/version.rb +3 -0
- data/lib/hyperion/formats.rb +69 -0
- data/lib/hyperion/headers.rb +43 -0
- data/lib/hyperion/hyperion.rb +79 -0
- data/lib/hyperion/requestor.rb +88 -0
- data/lib/hyperion/result_handling/dispatch_dsl.rb +67 -0
- data/lib/hyperion/result_handling/dispatching_hyperion_result.rb +10 -0
- data/lib/hyperion/result_handling/result_maker.rb +64 -0
- data/lib/hyperion/types/client_error_code.rb +9 -0
- data/lib/hyperion/types/client_error_detail.rb +46 -0
- data/lib/hyperion/types/client_error_response.rb +50 -0
- data/lib/hyperion/types/hyperion_error.rb +6 -0
- data/lib/hyperion/types/hyperion_result.rb +24 -0
- data/lib/hyperion/types/hyperion_status.rb +10 -0
- data/lib/hyperion/types/hyperion_uri.rb +97 -0
- data/lib/hyperion/types/payload_descriptor.rb +9 -0
- data/lib/hyperion/types/response_descriptor.rb +21 -0
- data/lib/hyperion/types/rest_route.rb +20 -0
- data/lib/hyperion.rb +15 -0
- data/lib/hyperion_test/fake.rb +64 -0
- data/lib/hyperion_test/fake_server/config.rb +36 -0
- data/lib/hyperion_test/fake_server/dispatcher.rb +74 -0
- data/lib/hyperion_test/fake_server/types.rb +7 -0
- data/lib/hyperion_test/fake_server.rb +54 -0
- data/lib/hyperion_test/spec_helper.rb +19 -0
- data/lib/hyperion_test/test_framework_hooks.rb +34 -0
- data/lib/hyperion_test.rb +2 -0
- data/spec/lib/hyperion/aux/util_spec.rb +29 -0
- data/spec/lib/hyperion/formats_spec.rb +84 -0
- data/spec/lib/hyperion/headers_spec.rb +61 -0
- data/spec/lib/hyperion/logger_spec.rb +60 -0
- data/spec/lib/hyperion/test_spec.rb +222 -0
- data/spec/lib/hyperion/types/client_error_response_spec.rb +52 -0
- data/spec/lib/hyperion/types/hyperion_result_spec.rb +17 -0
- data/spec/lib/hyperion/types/hyperion_uri_spec.rb +113 -0
- data/spec/lib/hyperion_spec.rb +187 -0
- data/spec/lib/superion_spec.rb +151 -0
- data/spec/lib/types_spec.rb +46 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/support/core_helpers.rb +5 -0
- metadata +280 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fe93d63e5710f95bd417f7281c7c3b96c267e625
|
4
|
+
data.tar.gz: 13daa226ae477357aacb51fa413f85f9967fed25
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 453822633702779a4fd790ef9527643865b42c8efb6ec3292dffa7cdb5e86457737062d106dfc978b3360c11aad670353f54f42b823858e9e26f4a6fbe0e760c
|
7
|
+
data.tar.gz: c5c618b3a65d8090964c9f0d36aafb050c2f75c8758a8100c2e873bd9e619b54a37db34e35a31b962ce20e9f9597fb6304ad3944cdb4d6dda15af69aabc31188
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGES.md
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
### 0.0.16
|
2
|
+
- Hyperion::request can now take a block an easily dispatch on response status, code, or other stuff
|
3
|
+
|
4
|
+
### 0.0.17
|
5
|
+
- If Hyperion.fake allow is passed a route, its block can now return an object instead of a rack-style response.
|
6
|
+
The object is serialized according to the route's response descriptor.
|
7
|
+
- Serialize the POST/PUT payload according to the route's payload descriptor.
|
8
|
+
|
9
|
+
### 0.0.18
|
10
|
+
- Return 404 instead of crashing when headers are the only thing preventing a faked route from matching.
|
11
|
+
|
12
|
+
### 0.0.19
|
13
|
+
- Log stubs and requests for debugging purposes.
|
14
|
+
|
15
|
+
### 0.0.20
|
16
|
+
- Added contracts to some public methods to provide more helpful error messages when passed invalid arguments.
|
17
|
+
|
18
|
+
### 0.0.25
|
19
|
+
- Allow payload descriptor to be nil (for things like DELETE).
|
20
|
+
|
21
|
+
### 0.0.26
|
22
|
+
- Fixed bug with Hyperion fake not defaulting the port to 80
|
23
|
+
|
24
|
+
### 0.0.27
|
25
|
+
- Use Rails.logger if present
|
26
|
+
|
27
|
+
### 0.0.28
|
28
|
+
- Use Rails.logger if present and not nil
|
29
|
+
|
30
|
+
### 0.0.29
|
31
|
+
- Pretty RestRoute.to_s
|
32
|
+
- Added the ability to match responses on an HTTP code range
|
33
|
+
|
34
|
+
### 0.0.30
|
35
|
+
- locked 'contracts' gem to version 0.5 due to possible incompatibility with ascent-web
|
36
|
+
|
37
|
+
### 0.0.31
|
38
|
+
- disabled method contracts due to apparent weirdness
|
39
|
+
- enable Oj's 'compat' mode to allow writing of both string- and symbol-keyed hashes
|
40
|
+
- write Time objects to JSON as ISO 8601 (Indigo convention)
|
41
|
+
|
42
|
+
### 0.0.32
|
43
|
+
- Include requested route on response object
|
44
|
+
|
45
|
+
### 0.0.33
|
46
|
+
- Pretty HyperionResult#to_s
|
47
|
+
|
48
|
+
### 0.0.34
|
49
|
+
- HyperionUri (models query params as a hash)
|
50
|
+
- Fixed logging bug where the logger would capture $stdout the first time it saw it
|
51
|
+
and always use that object instead of always using the current value of $stdout.
|
52
|
+
|
53
|
+
### 0.0.35
|
54
|
+
- Canonicalize HyperionUri#to_s output by sorting query param names to make route matching possible.
|
55
|
+
|
56
|
+
### 0.0.36
|
57
|
+
- Pretty ResponseDescriptor#to_s
|
58
|
+
- When using Hyperion.fake and a rack result is returned, the body is serialized as JSON
|
59
|
+
if it is not already a string.
|
60
|
+
- If parsing JSON fails, just return the unparsed JSON. (For 400s).
|
61
|
+
|
62
|
+
### 0.0.37
|
63
|
+
- Pass the HyperionResult to the `when` block, since it won't always be in lexical scope.
|
64
|
+
|
65
|
+
### 0.0.38
|
66
|
+
- Added superion
|
67
|
+
- Catch raised errors in handler predicates
|
68
|
+
- Read 400-level bodies as ClientErrorResponse
|
69
|
+
|
70
|
+
### 0.0.39
|
71
|
+
- Refactored to appease CodeClimate
|
72
|
+
|
73
|
+
### 0.0.40
|
74
|
+
- Moved mimic back to being a runtime dependency
|
75
|
+
|
76
|
+
### 0.0.41
|
77
|
+
- Fixed ClientErrorResponse#from_attrs
|
78
|
+
|
79
|
+
### 0.0.42
|
80
|
+
- Made ClientErrorResponse constructor interface less error-prone
|
81
|
+
- Always read client error response as JSON
|
82
|
+
|
83
|
+
### 0.0.43
|
84
|
+
- Raise an error if superion response falls through and no superion_fallthrough method is defined.
|
85
|
+
|
86
|
+
### 0.0.44
|
87
|
+
- Fixed "require" problems
|
88
|
+
|
89
|
+
### 0.0.45
|
90
|
+
- Support query params that are arrays
|
91
|
+
|
92
|
+
### 0.0.46
|
93
|
+
- Allow query to be nil (bugfix)
|
94
|
+
|
95
|
+
### 0.0.47
|
96
|
+
- Fixed slow POSTs > 1KB
|
97
|
+
|
98
|
+
### 0.0.48
|
99
|
+
- Fixed bug where superion's interface was too loose
|
100
|
+
- Do not require dispatch predicates to take an argument
|
101
|
+
- Log when requests complete
|
102
|
+
|
103
|
+
### 0.0.50
|
104
|
+
- Allow dispatching on client error code
|
105
|
+
|
106
|
+
### 0.0.52
|
107
|
+
- `ErrorInfo` -> `ClientErrorDetail`
|
108
|
+
- `ErrorInfo::Code` -> `ClientErrorCode`
|
109
|
+
- `HyperionResult::Status` -> `HyperionStatus`
|
110
|
+
- `ClientErrorResponse.new` signature changed
|
111
|
+
- superion was absorbed into hyperion. instead, `require 'hyperion'`
|
112
|
+
and `include Hyperion::Requestor`
|
113
|
+
- `superion_handler` -> `hyperion_handler`
|
114
|
+
- removed `superion_fallthrough`. it's only hit on 100s and 300s.
|
115
|
+
Custom handlers can already handle those if they want. An error is
|
116
|
+
raised if a 100 or 300 falls through.
|
117
|
+
- removed `Superion.missing`
|
118
|
+
|
119
|
+
### 0.0.53
|
120
|
+
- Fixed bug due to missing `require`
|
121
|
+
|
122
|
+
### 0.0.54
|
123
|
+
- abstractivator 0.0.27
|
124
|
+
|
125
|
+
### 0.0.55
|
126
|
+
- upgraded abstractivator for wrapped enum values
|
127
|
+
|
128
|
+
### 0.0.56
|
129
|
+
- another enum tweak
|
130
|
+
|
131
|
+
### 0.0.57
|
132
|
+
- json enum support from abstractivator
|
133
|
+
|
134
|
+
### 0.0.58
|
135
|
+
- Renamed `ClientErrorResponse#body` -> `ClientErrorResponse#content`
|
136
|
+
|
137
|
+
### 0.1.0
|
138
|
+
- Bumped version
|
139
|
+
|
140
|
+
### 0.1.1
|
141
|
+
- Attempted to solve problem in tests where clients hit an old server while
|
142
|
+
it's shutting down, resulting in a timeout.
|
143
|
+
|
144
|
+
### 0.1.2
|
145
|
+
- Fixed broken gemspec version for abstractivator
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Peter Winton
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,421 @@
|
|
1
|
+
# Hyperion
|
2
|
+
[](https://travis-ci.org/indigo-biosystems/hyperion)
|
3
|
+
|
4
|
+
Hyperion is a Ruby REST client that follows certain conventions
|
5
|
+
layered on top of HTTP. The conventions implement best practices
|
6
|
+
surrounding versioning, error reporting, and crafting an API for
|
7
|
+
consumption by third parties. Hyperion provides abstractions that
|
8
|
+
make it easy to follow these conventions consistently across
|
9
|
+
projects.
|
10
|
+
|
11
|
+
This document describes the conventions, then demonstrates how to
|
12
|
+
use the API, and finally shows how to test your client-side code.
|
13
|
+
|
14
|
+
## Conventions
|
15
|
+
|
16
|
+
The conventions are modeled after [GitHub's Developer API](https://developer.github.com/v3/).
|
17
|
+
|
18
|
+
### Versioning
|
19
|
+
|
20
|
+
According to best practices, hyperion uses both _resource_ versioning and
|
21
|
+
_message_ versioning.
|
22
|
+
|
23
|
+
#### Message versioning
|
24
|
+
|
25
|
+
A client sends a _request_ to the server, and the server returns a
|
26
|
+
_response_. The request and response each have a message type with a
|
27
|
+
well specified structure. When placing the request, the client
|
28
|
+
specifies the type of messages it expects back. For PUT, POST, and
|
29
|
+
PATCH requests, the client also specifies the message type of the
|
30
|
+
payload it is sending with the request.
|
31
|
+
|
32
|
+
A client might send a request with the header:
|
33
|
+
|
34
|
+
`Accept: application/vnd.indigobio-ascent.user-v1+json`
|
35
|
+
|
36
|
+
which indicates that the server must return an "ascent.user" message, version
|
37
|
+
1, formatted as JSON. If the server does not support this, it must
|
38
|
+
return a 400-level error (discussed below).
|
39
|
+
|
40
|
+
A client POSTing a new user also includes the header:
|
41
|
+
|
42
|
+
`Content-Type: application/json`
|
43
|
+
|
44
|
+
indicating that it is sending JSON. The server takes the Accept header
|
45
|
+
message type to be the type of the request payload.
|
46
|
+
<!--- ^ seems fishy -->
|
47
|
+
|
48
|
+
The message version is incremented when the message structure changes.
|
49
|
+
|
50
|
+
_Note: Message types are currently established via documentation. In
|
51
|
+
the future, it would be desirable for them to be declared precisely in
|
52
|
+
a [protobuf](https://github.com/google/protobuf)-like form, which would
|
53
|
+
allow for generated documentation, simpler serialization code,
|
54
|
+
automatic validation, and enhanced logging and diagnostics._
|
55
|
+
|
56
|
+
#### Resource versioning
|
57
|
+
|
58
|
+
Less frequently, the semantics of a given resource change. There are
|
59
|
+
several ways a server can route a particular resource version,
|
60
|
+
including:
|
61
|
+
|
62
|
+
- `/v2/` - incorporating the version in the URI
|
63
|
+
- `?v=2` - accepting the version as a query parameter
|
64
|
+
- `v2.archiver.indigobio.com` - incorporating the version in the hostname
|
65
|
+
- creating a differently named resource altogether
|
66
|
+
|
67
|
+
Hyperion does not expressly support any of these conventions, although
|
68
|
+
it may in the future.
|
69
|
+
|
70
|
+
|
71
|
+
### Client Errors
|
72
|
+
|
73
|
+
The server always returns 400-level errors as exactly 400; no
|
74
|
+
distinction is made in the HTTP response code. Instead, the server
|
75
|
+
returns a well-defined "client error response" structure that contains
|
76
|
+
|
77
|
+
- a human-oriented error message, and
|
78
|
+
- a machine-oriented list of "error detail" structures.
|
79
|
+
|
80
|
+
The _message_ describes the problem. The _error details_ provide
|
81
|
+
enough information to begin resolving the problem.
|
82
|
+
|
83
|
+
An error detail consists of:
|
84
|
+
|
85
|
+
- code (not to be confused with the HTTP status code, _e.g._, 200)
|
86
|
+
- reason
|
87
|
+
- resource
|
88
|
+
- field
|
89
|
+
- value
|
90
|
+
|
91
|
+
The _code_ is an enumeration value (_e.g._, "missing", "invalid",
|
92
|
+
"unsupported") which describes the type of problem with the request.
|
93
|
+
<!--- ^ consider renaming "code" to "problem" -->
|
94
|
+
|
95
|
+
The _reason_ explains why the problem occurred.
|
96
|
+
|
97
|
+
The problem is associated with a particular _resource_, and perhaps
|
98
|
+
more specifically with a particular _field_ and _value_ on that resource.
|
99
|
+
|
100
|
+
Each field is a string. Depending on the code, some fields may not
|
101
|
+
apply. Inapplicable fields are always present, with the empty string
|
102
|
+
as their value. This simplifies the code that deals with them.
|
103
|
+
|
104
|
+
|
105
|
+
### Server Errors
|
106
|
+
|
107
|
+
Server errors are returned as normal 500 responses, which may or may
|
108
|
+
not have a body. There is no special treatment.
|
109
|
+
|
110
|
+
|
111
|
+
## Using hyperion
|
112
|
+
|
113
|
+
Hyperion's API revolves around the idea of a _route_, which is a
|
114
|
+
combination of:
|
115
|
+
|
116
|
+
- HTTP method
|
117
|
+
- URI
|
118
|
+
- parameters that influence header generation and de/serialization
|
119
|
+
- `ResponseDescriptor` (what kind of data do we want back)
|
120
|
+
- influences the `Accept` header
|
121
|
+
- `PayloadDescriptor` (what kind of data is being PUT or POSTed)
|
122
|
+
- influences the `Content-Type` header
|
123
|
+
|
124
|
+
Hyperion automatically deserializes responses according to the
|
125
|
+
`ResponseDescriptor` and serializes the request payload according to
|
126
|
+
the `PayloadDescriptor`.
|
127
|
+
|
128
|
+
Hyperion provides a basic interface for requesting routes.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
require 'hyperion'
|
132
|
+
|
133
|
+
message_type = 'user'
|
134
|
+
version = 1
|
135
|
+
format = :json
|
136
|
+
route = RestRoute.new(:get, 'http://somesite.org/users/0', ResponseDescriptor.new(message_type, version, format))
|
137
|
+
user = Hyperion.request(route)
|
138
|
+
```
|
139
|
+
|
140
|
+
You can pass `request` a block, in which case the return value of the
|
141
|
+
block becomes the return value of `request`.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
user = Hyperion.request(route) do |result|
|
145
|
+
if result.status == HyperionResult::Status::SUCCESS
|
146
|
+
User.new(result.body)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
Production-quality error handling becomes hairy quickly, so hyperion
|
152
|
+
provides a mini DSL to make it easier.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
Hyperion.request(route) do |result|
|
156
|
+
result.when(HyperionResult::Status::SUCCESS) { User.new(result.body) }
|
157
|
+
result.when(400..499) { raise 'we screwed up' }
|
158
|
+
result.when(500..599) { raise 'they screwed up' }
|
159
|
+
result.when(evil) { exit(1) }
|
160
|
+
end
|
161
|
+
|
162
|
+
def evil
|
163
|
+
proc do |result|
|
164
|
+
result.body['things'].any?{|x| x['id'] == '666'}
|
165
|
+
end
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
Conditions are tested in order. When hyperion encounters the first
|
170
|
+
true condition, it executes the associated block, the value of which
|
171
|
+
becomes the return value of `request`.
|
172
|
+
|
173
|
+
A condition may be:
|
174
|
+
|
175
|
+
- a `HyperionResult::Status` enumeration member,
|
176
|
+
- an `ErrorInfo::Code` enumeration member,
|
177
|
+
- an HTTP code,
|
178
|
+
- a range of HTTP codes, or
|
179
|
+
- an arbitrary [predicate](http://en.wikipedia.org/wiki/Predicate_(mathematical_logic)).
|
180
|
+
|
181
|
+
To obviate guard logic in predicates, a predicate that raises an
|
182
|
+
exception is treated as a non-match. In the example above, if the body
|
183
|
+
didn't have a `'things'` key, then `.any?` would raise a
|
184
|
+
`NoMethodError`, interpreted as a non-match, and hyperion would move
|
185
|
+
on to the next predicate.
|
186
|
+
|
187
|
+
### Route classes
|
188
|
+
|
189
|
+
In practice, you don't want to litter your code with `RestRoute.new`
|
190
|
+
invocations. Here is a pattern for encapsulating the routes. It is the
|
191
|
+
client-side analog of `routes.rb` in Rails.
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class CrudRoutes
|
195
|
+
def initialize(resource)
|
196
|
+
@resource = resource
|
197
|
+
end
|
198
|
+
|
199
|
+
def read(id)
|
200
|
+
build(:get, id, response(message_type_for(@resource), 1, :json))
|
201
|
+
end
|
202
|
+
|
203
|
+
def create
|
204
|
+
build(:post, '', response(message_type_for(@resource), 1, :json), payload(:json))
|
205
|
+
end
|
206
|
+
|
207
|
+
...
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
You can easily imagine the rest of the routes and what the helper
|
212
|
+
methods `build`, `message_type_for`, `response`, and `payload` look like.
|
213
|
+
|
214
|
+
Use it like this:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
user_routes = CrudRoutes.new('users')
|
218
|
+
Hyperion.request(user_routes.create, body: {name: 'joe', email: 'joe@schmoe.com'})
|
219
|
+
# later...
|
220
|
+
joe = Hyperion.request(user_routes.read(joes_id))
|
221
|
+
```
|
222
|
+
|
223
|
+
A few notes:
|
224
|
+
|
225
|
+
`CrudRoutes` functions as an API spec, albeit a stripped down one.
|
226
|
+
Therefore, there is no need to write specs for it; the specs would
|
227
|
+
likely be harder to understand than the code itself. `CrudRoutes`
|
228
|
+
could be DRYed up more, but that would reduce its understandability
|
229
|
+
and create the need for specs.
|
230
|
+
|
231
|
+
See `AssaymaticRoutes` in ascent-web for the most complete example to
|
232
|
+
date.
|
233
|
+
|
234
|
+
|
235
|
+
### Configuration
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
# configure hyperion
|
239
|
+
Hyperion.configure do |config|
|
240
|
+
config.vendor_string = 'indigobio-ascent' # becomes part of the Accept header
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
|
245
|
+
## Superion
|
246
|
+
|
247
|
+
Superion layers more convenience onto Hyperion by helping dispatch the
|
248
|
+
response: internalizing the response and dealing with errors.
|
249
|
+
|
250
|
+
### Render and project
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
require 'superion'
|
254
|
+
|
255
|
+
class UserGateway
|
256
|
+
include Superion
|
257
|
+
|
258
|
+
def find(id)
|
259
|
+
route = RestRoute.new(:get, "http://somesite.org/users/#{id}")
|
260
|
+
user = request(route, render: as_user)
|
261
|
+
end
|
262
|
+
|
263
|
+
def as_user
|
264
|
+
proc do |hash|
|
265
|
+
User.new(hash)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
```
|
270
|
+
|
271
|
+
On success, the "render" proc has a chance to transform the body
|
272
|
+
(usually a `Hash`) into an internal representation (often an entity).
|
273
|
+
|
274
|
+
After rendering, a "project" proc (the block) has a chance to project
|
275
|
+
the rendered entity; for example, by choosing a subdocument or field.
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
def user_names
|
279
|
+
route = RestRoute.new(:get, "http://somesite.org/users")
|
280
|
+
request(route, render: as_users) do |users|
|
281
|
+
users.map(&:name)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
```
|
285
|
+
|
286
|
+
### Result dispatch
|
287
|
+
|
288
|
+
Superion has four levels of dispatching:
|
289
|
+
|
290
|
+
- _core_,
|
291
|
+
- _includer_, and
|
292
|
+
- _request_.
|
293
|
+
|
294
|
+
<!--- TODO: these terms could use improvement. -->
|
295
|
+
|
296
|
+
They are distinguished by their scope. The core handler is built into
|
297
|
+
superion. An includer handler affects all requests made by a
|
298
|
+
particular class. A request handler affects only a particular request.
|
299
|
+
|
300
|
+
When superion receives a response, it passes the result through the
|
301
|
+
request, includer, and core handlers, in that order. The first handler
|
302
|
+
to match wins, and no further handlers are tried. If no handler
|
303
|
+
matches, then superion raises a `HyperionError` error.
|
304
|
+
|
305
|
+
#### Core
|
306
|
+
|
307
|
+
The core handler handles the 200 success case and 400- and 500-level
|
308
|
+
errors. In the success case, the body is rendered and projected. In
|
309
|
+
the error cases, a `HyperionError` is raised. The message for 400s is
|
310
|
+
taken from the error response. The message for 500s contains the raw
|
311
|
+
string body. Specifically for 404, no body is available, so an error
|
312
|
+
indicating an unimplemented route is raised.
|
313
|
+
|
314
|
+
|
315
|
+
#### Includer
|
316
|
+
|
317
|
+
The includer handler is an optional method on the requesting class.
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
class UserGateway
|
321
|
+
include Superion
|
322
|
+
|
323
|
+
def find(id)
|
324
|
+
...
|
325
|
+
end
|
326
|
+
|
327
|
+
def superion_handler(result)
|
328
|
+
result.when(ErrorInfo::MISSING) { raise "The resource was not found: #{result.route}" }
|
329
|
+
result.when(HyperionResult::Status::SERVER_ERROR) { ... }
|
330
|
+
end
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
#### Request
|
335
|
+
|
336
|
+
The request handler provides a convenient way to specify a handler as
|
337
|
+
a `Hash` for an individual `request` call. If a `superion_handler`
|
338
|
+
looks like:
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
result.when(condition) { return_something }
|
342
|
+
```
|
343
|
+
|
344
|
+
then the equivalent request handler is
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
{ condition => proc { return_something } }
|
348
|
+
```
|
349
|
+
|
350
|
+
Pass it as the `also_handle` option:
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
request(route, render: as_user, also_handle: { condition => proc { return_something } })
|
354
|
+
```
|
355
|
+
|
356
|
+
## Testing
|
357
|
+
|
358
|
+
Hyperion includes methods to help test your client-side code.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
require 'hyperion_test'
|
362
|
+
|
363
|
+
list_route = RestRoute.new(:get, "http://somesite.org/users")
|
364
|
+
find_route = RestRoute.new(:get, "http://somesite.org/users/123")
|
365
|
+
|
366
|
+
# start a fake server
|
367
|
+
Hyperion.fake('http://somesite.org') do |svr|
|
368
|
+
svr.allow(list_route) { [User.new('123'), User.new('456')].as_json }
|
369
|
+
svr.allow(find_route) do |result|
|
370
|
+
User.new(result.body['id']).as_json
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# then place requests against it
|
375
|
+
users = Hyperion.request(list_route)
|
376
|
+
expect(users[0]['id']).to eql 123
|
377
|
+
expect(users[1]['id']).to eql 456
|
378
|
+
```
|
379
|
+
|
380
|
+
`Hyperion::fake` starts a real web server which responds to the
|
381
|
+
configured routes. It provides an easy way to exercise the entire
|
382
|
+
stack when running your tests.
|
383
|
+
|
384
|
+
For simpler cases:
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
list_route = RestRoute.new(:get, "http://somesite.org/users")
|
388
|
+
response = [User.new('123'), User.new('456')]
|
389
|
+
fake_route(list_route, response)
|
390
|
+
```
|
391
|
+
|
392
|
+
See the specs for details.
|
393
|
+
|
394
|
+
## Maintenance
|
395
|
+
|
396
|
+
When improving hyperion, increment the version in `version.rb` (Hyperion
|
397
|
+
uses [semantic versioning](http://semver.org)) and describe your
|
398
|
+
changes in `CHANGES.md`.
|
399
|
+
|
400
|
+
## Design decisions
|
401
|
+
|
402
|
+
Hyperion is backed by Typhoeus, which in turn is backed by libcurl.
|
403
|
+
Both are fully featured, have widespread adoption, and are actively
|
404
|
+
maintained. One particularly nice feature of Typhoeus is that it
|
405
|
+
provides an easy way to issue multiple requests in parallel, which
|
406
|
+
is important when you have a microservices architecture.
|
407
|
+
|
408
|
+
Our original plan was to have a second gem containing `Hyperion.fake`
|
409
|
+
and its dependencies. The problem is that you need to keep the two
|
410
|
+
gems in sync, which reduces agility. As a compromise, `Hyperion.fake`
|
411
|
+
still lives in the hyperion gem but is only loaded when you `require
|
412
|
+
'hyperion_test'`. Assuming no production code `require`s
|
413
|
+
`hyperion_test`, none of the test-related dependencies will be loaded
|
414
|
+
in a production system, although the gem dependencies will be part of
|
415
|
+
the production bundle.
|
416
|
+
|
417
|
+
# Parking Lot
|
418
|
+
|
419
|
+
- Consider making the set of supported formats/content-types extensible.
|
420
|
+
- Consider adding a configuration value to set the logger to use. If none
|
421
|
+
is provided, fall back on Rails.logger, and then a new `Logger`.
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hyperion/aux/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'hyperion_http'
|
8
|
+
spec.version = Hyperion::VERSION
|
9
|
+
spec.authors = ['Indigo BioAutomation, Inc.']
|
10
|
+
spec.email = ['pwinton@indigobio.com']
|
11
|
+
spec.summary = 'Ruby REST client'
|
12
|
+
spec.description = 'Ruby REST client for internal service architecture'
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.7'
|
22
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
+
spec.add_development_dependency 'yard'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
25
|
+
spec.add_development_dependency 'json_spec'
|
26
|
+
spec.add_development_dependency 'rspec_junit_formatter'
|
27
|
+
|
28
|
+
spec.add_runtime_dependency 'abstractivator', '~> 0.0'
|
29
|
+
spec.add_runtime_dependency 'activesupport'
|
30
|
+
spec.add_runtime_dependency 'immutable_struct', '~> 1.1'
|
31
|
+
spec.add_runtime_dependency 'oj', '~> 2.12'
|
32
|
+
spec.add_runtime_dependency 'typhoeus', '~> 0.7'
|
33
|
+
spec.add_runtime_dependency 'mimic', '~> 0.4.3'
|
34
|
+
end
|