hyperion_http 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/indigo-biosystems/hyperion.svg)](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
|