frenetic 0.0.12 → 0.0.20.alpha.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/.irbrc +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +0 -1
- data/Gemfile +1 -3
- data/README.md +138 -125
- data/frenetic.gemspec +5 -6
- data/lib/frenetic.rb +31 -43
- data/lib/frenetic/concerns/collection_rest_methods.rb +13 -0
- data/lib/frenetic/concerns/configurable.rb +59 -0
- data/lib/frenetic/concerns/hal_linked.rb +59 -0
- data/lib/frenetic/concerns/member_rest_methods.rb +15 -0
- data/lib/frenetic/concerns/structured.rb +53 -0
- data/lib/frenetic/configuration.rb +40 -76
- data/lib/frenetic/middleware/hal_json.rb +23 -0
- data/lib/frenetic/resource.rb +77 -62
- data/lib/frenetic/resource_collection.rb +46 -0
- data/lib/frenetic/version.rb +2 -2
- data/spec/concerns/configurable_spec.rb +50 -0
- data/spec/concerns/hal_linked_spec.rb +116 -0
- data/spec/concerns/member_rest_methods_spec.rb +41 -0
- data/spec/concerns/structured_spec.rb +214 -0
- data/spec/configuration_spec.rb +99 -0
- data/spec/fixtures/test_api_requests.rb +88 -0
- data/spec/frenetic_spec.rb +137 -0
- data/spec/middleware/hal_json_spec.rb +83 -0
- data/spec/resource_collection_spec.rb +80 -0
- data/spec/resource_spec.rb +211 -0
- data/spec/spec_helper.rb +4 -13
- data/spec/support/rspec.rb +5 -0
- data/spec/support/webmock.rb +3 -0
- metadata +59 -57
- data/.rvmrc +0 -1
- data/lib/frenetic/hal_json.rb +0 -23
- data/lib/frenetic/hal_json/response_wrapper.rb +0 -43
- data/lib/recursive_open_struct.rb +0 -79
- data/spec/fixtures/vcr_cassettes/description_error_unauthorized.yml +0 -36
- data/spec/fixtures/vcr_cassettes/description_success.yml +0 -38
- data/spec/lib/frenetic/configuration_spec.rb +0 -189
- data/spec/lib/frenetic/hal_json/response_wrapper_spec.rb +0 -70
- data/spec/lib/frenetic/hal_json_spec.rb +0 -68
- data/spec/lib/frenetic/resource_spec.rb +0 -182
- data/spec/lib/frenetic_spec.rb +0 -129
data/.gitignore
CHANGED
data/.irbrc
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p374
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,6 @@ An opinionated Ruby-based Hypermedia API (HAL+JSON) client.
|
|
9
9
|
|
10
10
|
|
11
11
|
|
12
|
-
|
13
12
|
## About
|
14
13
|
|
15
14
|
fre•net•ic |frəˈnetik|<br/>
|
@@ -29,6 +28,7 @@ If you have not implemented a HAL+JSON API, then this will not work very well fo
|
|
29
28
|
|
30
29
|
|
31
30
|
|
31
|
+
|
32
32
|
## Opinions
|
33
33
|
|
34
34
|
Like I said, it is opinionated. It is so opinionated, it is probably the biggest
|
@@ -37,7 +37,6 @@ a-hole you've ever met.
|
|
37
37
|
Maybe in time, if you teach it, it will become more open-minded.
|
38
38
|
|
39
39
|
|
40
|
-
|
41
40
|
### HAL+JSON Content Type
|
42
41
|
|
43
42
|
Frenetic expects all responses to be in [HAL+JSON][hal_json]. It chose that
|
@@ -45,45 +44,43 @@ standard because it is trying to make JSON API's respond in a predictable
|
|
45
44
|
manner, which it thinks is an awesome idea.
|
46
45
|
|
47
46
|
|
48
|
-
|
49
|
-
### Authentication
|
50
|
-
|
51
|
-
Frenetic is going to try and use Basic Auth whether you like it or not. If
|
52
|
-
that is not required, nothing will probably happen. But its going to send the
|
53
|
-
header anyway.
|
54
|
-
|
55
|
-
|
56
|
-
|
57
47
|
### API Description
|
58
48
|
|
59
49
|
The API's root URL must respond with a description, much like the
|
60
|
-
[Spire.io][spire.io] API.
|
61
|
-
Frenetic doesn't know what the API contains, it can't parse any resource
|
62
|
-
responses.
|
50
|
+
[Spire.io][spire.io] API.
|
63
51
|
|
64
|
-
|
65
|
-
|
52
|
+
This is crucial in order for Frenetic to work. If Frenetic doesn't know what
|
53
|
+
the API contains, it can't navigate around it or parse any of it's responses.
|
66
54
|
|
67
|
-
Example
|
55
|
+
**Example:**
|
68
56
|
|
69
57
|
```js
|
58
|
+
// GET http://example.com/api
|
70
59
|
{
|
71
|
-
"_links":{
|
72
|
-
"self":{
|
73
|
-
|
60
|
+
"_links": {
|
61
|
+
"self": {
|
62
|
+
"href": "/api/"
|
63
|
+
},
|
64
|
+
"orders": {
|
65
|
+
"href":"/api/orders"
|
66
|
+
},
|
67
|
+
"order": {
|
68
|
+
"href": "/api/orders/{id}",
|
69
|
+
"templated": true
|
70
|
+
}
|
74
71
|
},
|
75
|
-
"_embedded":{
|
76
|
-
"schema":{
|
77
|
-
"_links":{
|
78
|
-
"self":{"href":"/api/schema"}
|
72
|
+
"_embedded": {
|
73
|
+
"schema": {
|
74
|
+
"_links": {
|
75
|
+
"self": { "href":"/api/schema" }
|
79
76
|
},
|
80
|
-
"order":{
|
81
|
-
"description":"A widget order",
|
82
|
-
"type":"object",
|
83
|
-
"properties":{
|
84
|
-
"id":{"type":"integer"},
|
85
|
-
"first_name":{"type":"string"},
|
86
|
-
"last_name":{"type":"string"},
|
77
|
+
"order": {
|
78
|
+
"description": "A widget order",
|
79
|
+
"type": "object",
|
80
|
+
"properties": {
|
81
|
+
"id": { "type":"integer" },
|
82
|
+
"first_name": { "type":"string" },
|
83
|
+
"last_name": { "type":"string" },
|
87
84
|
}
|
88
85
|
}
|
89
86
|
}
|
@@ -92,114 +89,144 @@ Example:
|
|
92
89
|
```
|
93
90
|
|
94
91
|
This response will be requested by Frenetic whenever a call to
|
95
|
-
`YourAPI.description` is made.
|
96
|
-
will not trigger another API request.
|
92
|
+
`YourAPI.description` is made.
|
97
93
|
|
94
|
+
**Note:** It is highly advised that you implement some sort of caching in both
|
95
|
+
your API server as well as your API client. Refer to the [Caching][caching] section for
|
96
|
+
more information.
|
98
97
|
|
99
98
|
|
100
99
|
|
101
|
-
## Installation
|
102
100
|
|
103
|
-
Add this line to your application's Gemfile:
|
104
101
|
|
105
|
-
|
102
|
+
## Configuring
|
106
103
|
|
107
|
-
|
104
|
+
### Client Initialization
|
108
105
|
|
109
|
-
|
106
|
+
Initializing an API client is really easy:
|
110
107
|
|
111
|
-
|
108
|
+
```ruby
|
109
|
+
class MyApiClient
|
110
|
+
# Arbitrary example
|
111
|
+
def self.api
|
112
|
+
@api ||= Frenetic.new( url:'http://example.com/api' )
|
113
|
+
end
|
114
|
+
end
|
115
|
+
```
|
112
116
|
|
113
|
-
|
117
|
+
At the bare minimum, Frenetic only needs to know what the URL of your API is.
|
114
118
|
|
115
119
|
|
120
|
+
### Configuring
|
116
121
|
|
122
|
+
Configuring Frenetic can be done during instantiation:
|
117
123
|
|
118
|
-
|
124
|
+
```ruby
|
125
|
+
Frenetic.new( url:'http://example.com', api_token:'123bada55k3y' )
|
126
|
+
```
|
119
127
|
|
128
|
+
Or with a block:
|
120
129
|
|
130
|
+
```ruby
|
131
|
+
f = Frenetic.new
|
132
|
+
f.configure do |cfg|
|
133
|
+
cfg.url = 'http://example.com'
|
134
|
+
cfg.api_token = '123bada55key'
|
135
|
+
end
|
136
|
+
```
|
121
137
|
|
122
|
-
|
138
|
+
Or both...
|
123
139
|
|
124
140
|
```ruby
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
'password' => 'yourpassword',
|
130
|
-
'headers' => {
|
131
|
-
'accept' => 'application/vnd.yoursite-v1.hal+json'
|
132
|
-
# Optional
|
133
|
-
'user-agent' => 'Your Site's API Client', # Optional custom User Agent, just 'cuz
|
134
|
-
}
|
135
|
-
)
|
141
|
+
f = Frenetic.new( url:'http://example.com' )
|
142
|
+
f.configure do |cfg|
|
143
|
+
cfg.api_token = '123bada55key'
|
144
|
+
end
|
136
145
|
```
|
137
146
|
|
138
|
-
|
147
|
+
#### Authentication
|
148
|
+
|
149
|
+
Frenetic supports both Basic Auth and Tokens Auth via the appropriate Faraday
|
150
|
+
middleware.
|
151
|
+
|
152
|
+
##### Basic Auth
|
153
|
+
|
154
|
+
To use Basic Auth, simply configure Frenetic with a `username` and `password`:
|
139
155
|
|
140
|
-
|
156
|
+
```ruby
|
157
|
+
Frenetic.new( url:url, username:'user', password:'password' )
|
158
|
+
```
|
141
159
|
|
142
|
-
If
|
143
|
-
config hash:
|
160
|
+
If your API uses an App ID and API Key pair, you can pass those as well:
|
144
161
|
|
145
162
|
```ruby
|
146
|
-
Frenetic.new( url:'
|
163
|
+
Frenetic.new( url:url, app_id:'123abcSHA1', api_key:'bada55SHA1k3y' )
|
147
164
|
```
|
148
165
|
|
149
|
-
The
|
150
|
-
|
166
|
+
The `app_id` and `api_key` values are simply aliases to `username` and
|
167
|
+
`password`
|
151
168
|
|
152
|
-
|
169
|
+
##### Token Auth
|
153
170
|
|
154
|
-
|
155
|
-
you can provide that in the config hash as well:
|
171
|
+
To use Token Auth, simply configure Frenetic with your token:
|
156
172
|
|
157
173
|
```ruby
|
158
|
-
Frenetic.new( url:
|
174
|
+
Frenetic.new( url:url, api_token:'bada55SHA1t0k3n' )
|
159
175
|
```
|
160
176
|
|
161
|
-
The App ID will be sent as the `:username` and the API Key will be sent as the
|
162
|
-
password portion of the HTTP Basic Authentication header.
|
163
177
|
|
178
|
+
#### Response Caching
|
164
179
|
|
180
|
+
If configured to do so, Frenetic will autotmatically cache API responses.
|
165
181
|
|
166
|
-
|
182
|
+
*It is highly recommended that you turn this feature on!*
|
167
183
|
|
168
|
-
|
169
|
-
through [Rack::Cache][rack_cache]. Only on-disk stores are supported right now.
|
184
|
+
##### Rack::Cache
|
170
185
|
|
171
|
-
|
186
|
+
```ruby
|
187
|
+
Frenetic.new( url:url, cache: :rack )
|
188
|
+
```
|
189
|
+
|
190
|
+
Passing in a cache option of `:rack` will cause Frenetic to use Faraday's
|
191
|
+
`Rack::Cache` middleware with a set of sane default configuration options.
|
192
|
+
|
193
|
+
If you wish to provide your own configuration options:
|
172
194
|
|
173
195
|
```ruby
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
)
|
196
|
+
Frenetic.new({
|
197
|
+
url: url,
|
198
|
+
cache: {
|
199
|
+
metastore: 'file:tmp/rack/meta',
|
200
|
+
entitystore: 'file:tmp/rack/body',
|
201
|
+
ignore_headers: %w{Authorization Set-Cookie X-Content-Digest}
|
202
|
+
}})
|
181
203
|
```
|
182
204
|
|
183
|
-
|
184
|
-
|
205
|
+
Any key/value pair contained in the `cache` hash will be passed directly onto
|
206
|
+
the Rack::Cache middleware.
|
185
207
|
|
208
|
+
##### Memcached
|
186
209
|
|
210
|
+
**TODO**
|
187
211
|
|
188
|
-
# Middleware
|
189
212
|
|
190
|
-
|
191
|
-
|
213
|
+
#### Faraday Middleware
|
214
|
+
|
215
|
+
Frenetic will yield its internal Faraday connection during initialization:
|
192
216
|
|
193
217
|
```ruby
|
194
|
-
Frenetic.new( url:
|
195
|
-
|
196
|
-
|
218
|
+
Frenetic.new( url:url ) do |builder|
|
219
|
+
# `builder` is the Faraday Connection instance with which you can
|
220
|
+
# add additional Faraday Middlewares or tweak the configuration.
|
197
221
|
end
|
198
222
|
```
|
199
223
|
|
224
|
+
You can then use the `builder` object as you see fit.
|
225
|
+
|
226
|
+
|
200
227
|
|
201
228
|
|
202
|
-
|
229
|
+
## Usage
|
203
230
|
|
204
231
|
Once you have created a client instance, you are free to use it however you'd
|
205
232
|
like.
|
@@ -207,12 +234,20 @@ like.
|
|
207
234
|
A Frenetic instance supports any HTTP verb that [Faraday][faraday] has
|
208
235
|
impletented. This includes GET, POST, PUT, PATCH, and DELETE.
|
209
236
|
|
237
|
+
```ruby
|
238
|
+
api = Frenetic.new( url:url )
|
239
|
+
|
240
|
+
api.get '/my_things/1'
|
241
|
+
# { 'id' => 1, 'name' => 'My Thing', '_links' => { 'self' { 'href' => '/api/my_things/1' } } }
|
242
|
+
```
|
210
243
|
|
244
|
+
### Frenetic::Resource
|
211
245
|
|
212
|
-
|
246
|
+
An easier way to make requests for a resource is to create an object that
|
247
|
+
inherits from `Frenetic::Resource`.
|
213
248
|
|
214
|
-
|
215
|
-
|
249
|
+
Not only does `Frenetic::Resource` handle the parsing of the raw API response
|
250
|
+
into a Ruby object, but it also makes it a bit easier to encapsulate all of your
|
216
251
|
resource's API requests into one place.
|
217
252
|
|
218
253
|
```ruby
|
@@ -220,14 +255,9 @@ class Order < Frenetic::Resource
|
|
220
255
|
|
221
256
|
api_client { MyAPI }
|
222
257
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
self.new( response.body )
|
227
|
-
else
|
228
|
-
raise OrderNotFound, "No Order found for #{id}"
|
229
|
-
end
|
230
|
-
end
|
258
|
+
# TODO: Write a better example for this.
|
259
|
+
def self.find_all_by_name( name )
|
260
|
+
api.get( search_url(name) ) and response.success?
|
231
261
|
end
|
232
262
|
end
|
233
263
|
```
|
@@ -244,35 +274,14 @@ class Order < Frenetic::Resource
|
|
244
274
|
end
|
245
275
|
```
|
246
276
|
|
247
|
-
When your model is initialized, it will contain
|
248
|
-
property defined in your API's schema
|
249
|
-
API to add, remove, or change properties without the need to directly update
|
250
|
-
your model.
|
251
|
-
|
277
|
+
When your model is initialized, it will contain getter methods for every
|
278
|
+
property defined in your API's schema/description.
|
252
279
|
|
280
|
+
Each time a request is made for a resource, Frenetic checks the API to see if
|
281
|
+
the schema has changed. If so, it will redefine the the getter methods available
|
282
|
+
on your Class. This is what Hypermedia APIs are all about, a loose coupling
|
283
|
+
between client and server.
|
253
284
|
|
254
|
-
### Interpretting Responses
|
255
|
-
|
256
|
-
Any response body returned by a Frenetic generated API call will be returned as
|
257
|
-
an OpenStruct-like object. This object responds to dot-notation as well as Hash
|
258
|
-
keys and is enumerable.
|
259
|
-
|
260
|
-
```ruby
|
261
|
-
response.body.resources.orders.first
|
262
|
-
```
|
263
|
-
|
264
|
-
or
|
265
|
-
|
266
|
-
```ruby
|
267
|
-
response.body['_embedded']['orders'][0]
|
268
|
-
```
|
269
|
-
|
270
|
-
For your convenience, certain HAL+JSON keys have been aliased by methods to
|
271
|
-
make your code a bit more readable:
|
272
|
-
|
273
|
-
* `_embedded` can be referenced as `resources`
|
274
|
-
* `_links` can be referenced as `links`
|
275
|
-
* `href` can be referenced as `url`
|
276
285
|
|
277
286
|
|
278
287
|
|
@@ -285,8 +294,12 @@ make your code a bit more readable:
|
|
285
294
|
4. Push to the branch (`git push origin my-new-feature`)
|
286
295
|
5. Create new Pull Request
|
287
296
|
|
297
|
+
I would love to hear how other people are using this (if at all) and am open to
|
298
|
+
ideas on how to support other Hypermedia formats like [Collection+JSON][coll_json].
|
299
|
+
|
288
300
|
[hal_json]: http://stateless.co/hal_specification.html
|
289
301
|
[spire.io]: http://api.spire.io/
|
290
|
-
[
|
302
|
+
[caching]: #response-caching
|
291
303
|
[faraday]: https://github.com/technoweenie/faraday
|
292
304
|
[rack_cache]: https://github.com/rtomayko/rack-cache
|
305
|
+
[coll_json]: http://amundsen.com/media-types/collection/
|
data/frenetic.gemspec
CHANGED
@@ -15,14 +15,13 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Frenetic::VERSION
|
17
17
|
|
18
|
-
gem.add_dependency 'faraday', '~> 0.8.
|
18
|
+
gem.add_dependency 'faraday', '~> 0.8.7'
|
19
19
|
gem.add_dependency 'faraday_middleware', '~> 0.9.0'
|
20
|
-
gem.add_dependency 'activesupport', '>=
|
21
|
-
gem.add_dependency '
|
22
|
-
gem.add_dependency 'addressable', '~> 2.2'
|
20
|
+
gem.add_dependency 'activesupport', '>= 3'
|
21
|
+
gem.add_dependency 'addressable', '~> 2.3.4'
|
23
22
|
|
24
|
-
gem.add_development_dependency '
|
23
|
+
gem.add_development_dependency 'awesome_print'
|
25
24
|
gem.add_development_dependency 'rspec', '~> 2.13.0'
|
25
|
+
gem.add_development_dependency 'rack-cache', '~> 1.2'
|
26
26
|
gem.add_development_dependency 'webmock', '~> 1.11.0'
|
27
|
-
gem.add_development_dependency 'vcr', '~> 2.4.0'
|
28
27
|
end
|
data/lib/frenetic.rb
CHANGED
@@ -1,66 +1,54 @@
|
|
1
|
-
require '
|
1
|
+
require 'socket'
|
2
2
|
require 'faraday'
|
3
3
|
require 'faraday_middleware'
|
4
|
-
require 'rack-cache'
|
5
4
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
5
|
+
require 'frenetic/concerns/configurable'
|
6
|
+
require 'frenetic/middleware/hal_json'
|
7
|
+
require 'frenetic/resource'
|
8
|
+
require 'frenetic/resource_collection'
|
9
|
+
require 'frenetic/version'
|
10
10
|
|
11
11
|
class Frenetic
|
12
|
-
Error = Class.new(StandardError)
|
13
|
-
ConfigurationError = Class.new(Error)
|
14
|
-
MissingAPIReference = Class.new(Error)
|
15
|
-
InvalidAPIDescription = Class.new(Error)
|
16
|
-
|
17
12
|
extend Forwardable
|
18
|
-
|
19
|
-
|
20
|
-
attr_reader :connection
|
21
|
-
alias_method :conn, :connection
|
13
|
+
include Configurable
|
22
14
|
|
23
|
-
|
24
|
-
config = Configuration.new( config )
|
15
|
+
def_delegators :connection, :get, :put, :post, :delete
|
25
16
|
|
26
|
-
|
17
|
+
Error = Class.new(StandardError)
|
18
|
+
ConfigError = Class.new(Error)
|
19
|
+
ClientError = Class.new(Error)
|
20
|
+
ServerError = Class.new(Error)
|
21
|
+
ParsingError = Class.new(Error)
|
22
|
+
HypermediaError = Class.new(Error)
|
27
23
|
|
28
|
-
|
29
|
-
@
|
24
|
+
def connection
|
25
|
+
@connection ||= begin
|
26
|
+
validate_configuration!
|
30
27
|
|
31
|
-
|
32
|
-
|
28
|
+
Faraday.new( config ) do |builder|
|
29
|
+
configure_authentication builder
|
33
30
|
|
34
|
-
|
31
|
+
builder.response :hal_json
|
35
32
|
|
36
|
-
|
33
|
+
configure_caching builder
|
37
34
|
|
38
|
-
|
35
|
+
@builder_config.call( builder ) if @builder_config
|
39
36
|
|
40
|
-
|
41
|
-
builder.use FaradayMiddleware::RackCompatible, Rack::Cache::Context, config.cache
|
37
|
+
builder.adapter config.adapter
|
42
38
|
end
|
43
|
-
|
44
|
-
builder.adapter config.adapter || :net_http
|
45
39
|
end
|
46
40
|
end
|
47
41
|
|
42
|
+
# It is highly advised that the server responds with some cache headers and
|
43
|
+
# the API Client is configured to use a Faraday caching strategy
|
48
44
|
def description
|
49
|
-
|
45
|
+
if response = get( config.url.to_s ) and response.success?
|
46
|
+
response.body
|
47
|
+
end
|
50
48
|
end
|
51
49
|
|
52
|
-
|
53
|
-
|
54
|
-
instance_variables.each { |var| instance_variable_set(var, nil) }
|
50
|
+
def schema
|
51
|
+
description['_embedded']['schema']
|
55
52
|
end
|
56
53
|
|
57
|
-
|
58
|
-
|
59
|
-
def load_description
|
60
|
-
if response = get( @root_url ) and response.success?
|
61
|
-
response.body
|
62
|
-
else
|
63
|
-
raise InvalidAPIDescription, "Status code #{response.status} encountered."
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
54
|
+
end
|