frenetic 0.0.12 → 0.0.20.alpha.0
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.
- 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
|