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.
Files changed (43) hide show
  1. data/.gitignore +1 -0
  2. data/.irbrc +3 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +0 -1
  5. data/Gemfile +1 -3
  6. data/README.md +138 -125
  7. data/frenetic.gemspec +5 -6
  8. data/lib/frenetic.rb +31 -43
  9. data/lib/frenetic/concerns/collection_rest_methods.rb +13 -0
  10. data/lib/frenetic/concerns/configurable.rb +59 -0
  11. data/lib/frenetic/concerns/hal_linked.rb +59 -0
  12. data/lib/frenetic/concerns/member_rest_methods.rb +15 -0
  13. data/lib/frenetic/concerns/structured.rb +53 -0
  14. data/lib/frenetic/configuration.rb +40 -76
  15. data/lib/frenetic/middleware/hal_json.rb +23 -0
  16. data/lib/frenetic/resource.rb +77 -62
  17. data/lib/frenetic/resource_collection.rb +46 -0
  18. data/lib/frenetic/version.rb +2 -2
  19. data/spec/concerns/configurable_spec.rb +50 -0
  20. data/spec/concerns/hal_linked_spec.rb +116 -0
  21. data/spec/concerns/member_rest_methods_spec.rb +41 -0
  22. data/spec/concerns/structured_spec.rb +214 -0
  23. data/spec/configuration_spec.rb +99 -0
  24. data/spec/fixtures/test_api_requests.rb +88 -0
  25. data/spec/frenetic_spec.rb +137 -0
  26. data/spec/middleware/hal_json_spec.rb +83 -0
  27. data/spec/resource_collection_spec.rb +80 -0
  28. data/spec/resource_spec.rb +211 -0
  29. data/spec/spec_helper.rb +4 -13
  30. data/spec/support/rspec.rb +5 -0
  31. data/spec/support/webmock.rb +3 -0
  32. metadata +59 -57
  33. data/.rvmrc +0 -1
  34. data/lib/frenetic/hal_json.rb +0 -23
  35. data/lib/frenetic/hal_json/response_wrapper.rb +0 -43
  36. data/lib/recursive_open_struct.rb +0 -79
  37. data/spec/fixtures/vcr_cassettes/description_error_unauthorized.yml +0 -36
  38. data/spec/fixtures/vcr_cassettes/description_success.yml +0 -38
  39. data/spec/lib/frenetic/configuration_spec.rb +0 -189
  40. data/spec/lib/frenetic/hal_json/response_wrapper_spec.rb +0 -70
  41. data/spec/lib/frenetic/hal_json_spec.rb +0 -68
  42. data/spec/lib/frenetic/resource_spec.rb +0 -182
  43. data/spec/lib/frenetic_spec.rb +0 -129
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ spec/reports
16
16
  test/tmp
17
17
  test/version_tmp
18
18
  tmp
19
+ vendor
data/.irbrc ADDED
@@ -0,0 +1,3 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+ require 'awesome_print'
@@ -0,0 +1 @@
1
+ 1.9.3-p374
@@ -1,4 +1,3 @@
1
1
  rvm:
2
- - 1.9.2
3
2
  - 1.9.3
4
3
  script: bundle exec rspec --require spec_helper --order rand --color --format documentation
data/Gemfile CHANGED
@@ -1,7 +1,5 @@
1
1
  source 'https://rubygems.org'
2
+ ruby '1.9.3'
2
3
 
3
4
  # Specify your gem's dependencies in frenetic.gemspec
4
5
  gemspec
5
-
6
- gem 'awesome_print'
7
- gem 'fakefs', '~> 0.4.0', require: false
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&bull;net&bull;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. This is crucial in order for Frenetic to work. If
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
- It is expected that any subclasses of `Frenetic::Resource` will adhere to the
65
- schema defined here.
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":{"href":"/api/"},
73
- "orders":{"href":"/api/orders"},
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. The response is memoized so any future calls
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
- gem 'frenetic'
102
+ ## Configuring
106
103
 
107
- And then execute:
104
+ ### Client Initialization
108
105
 
109
- $ bundle
106
+ Initializing an API client is really easy:
110
107
 
111
- Or install it yourself as:
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
- $ gem install frenetic
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
- ## Usage
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
- ### Client Initialization
138
+ Or both...
123
139
 
124
140
  ```ruby
125
- MyAPI = Frenetic.new(
126
- # 'adapter' => :patron # Or some other Faraday-compatible Adapter. Defaults to `:net_http`
127
- 'url' => 'https://api.yoursite.com',
128
- 'username' => 'yourname',
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
- Symbol- or string-based keys work equally well.
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
- #### Sending API Keys
156
+ ```ruby
157
+ Frenetic.new( url:url, username:'user', password:'password' )
158
+ ```
141
159
 
142
- If the API you are consuming requires an API Key, you can provide that in the
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:'https://example.org', api_key:'abcde12345' )
163
+ Frenetic.new( url:url, app_id:'123abcSHA1', api_key:'bada55SHA1k3y' )
147
164
  ```
148
165
 
149
- The value will be sent as the `:username` portion of the HTTP
150
- Basic Authentication header.
166
+ The `app_id` and `api_key` values are simply aliases to `username` and
167
+ `password`
151
168
 
152
- #### Sending API Keys with an App ID
169
+ ##### Token Auth
153
170
 
154
- If the API requires both an App ID or access token in addition to an API Key,
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:'https://example.org', app_id:'abcde12345', api_key:'mysecret' )
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
- ### Response Caching
182
+ *It is highly recommended that you turn this feature on!*
167
183
 
168
- If configured to do so, Frenetic will autotmatically cache appropriate responses
169
- through [Rack::Cache][rack_cache]. Only on-disk stores are supported right now.
184
+ ##### Rack::Cache
170
185
 
171
- Add the following `Rack::Cache` configuration options when initializing Frenetic:
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
- MyAPI = Frenetic.new(
175
- ...
176
- 'cache' => {
177
- 'metastore' => 'file:/path/to/where/you/want/to/store/files/meta',
178
- 'entitystore' => 'file:/path/to/where/you/want/to/store/files/meta'
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
- The `cache` options are passed directly to `Rack::Cache`, so anything it
184
- supports can be added to the Hash.
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
- Frenetic supports anything that Faraday does. You may specify additional
191
- middleware with the `use` method:
213
+ #### Faraday Middleware
214
+
215
+ Frenetic will yield its internal Faraday connection during initialization:
192
216
 
193
217
  ```ruby
194
- Frenetic.new( url:'http://example.org' ) do |config|
195
- config.use :instrumentation
196
- config.use MyMiddleware, { foo:123 }
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
- ### Making Requests
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
- #### Frenetic::Resource
246
+ An easier way to make requests for a resource is to create an object that
247
+ inherits from `Frenetic::Resource`.
213
248
 
214
- An easier way to make requests for a resource is to have your model inherit from
215
- `Frenetic::Resource`. This makes it a bit easier to encapsulate all of your
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
- class << self
224
- def find( id )
225
- if response = api.get( api.description.links.order.href.gsub('{id}', id.to_s) ) and response.success?
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 attribute readers for every
248
- property defined in your API's schema or description. In theory, this allows an
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
- [roar]: https://github.com/apotonick/roar
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/
@@ -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.1'
18
+ gem.add_dependency 'faraday', '~> 0.8.7'
19
19
  gem.add_dependency 'faraday_middleware', '~> 0.9.0'
20
- gem.add_dependency 'activesupport', '>= 2'
21
- gem.add_dependency 'rack-cache', '~> 1.1'
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 'patron', '~> 0.4.18'
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
@@ -1,66 +1,54 @@
1
- require 'addressable/uri'
1
+ require 'socket'
2
2
  require 'faraday'
3
3
  require 'faraday_middleware'
4
- require 'rack-cache'
5
4
 
6
- require "frenetic/configuration"
7
- require "frenetic/hal_json"
8
- require "frenetic/resource"
9
- require "frenetic/version"
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
- def_delegators :@connection, :get, :put, :post, :delete
19
-
20
- attr_reader :connection
21
- alias_method :conn, :connection
13
+ include Configurable
22
14
 
23
- def initialize( config = {} )
24
- config = Configuration.new( config )
15
+ def_delegators :connection, :get, :put, :post, :delete
25
16
 
26
- yield config if block_given?
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
- api_url = Addressable::URI.parse( config.url )
29
- @root_url = api_url.path
24
+ def connection
25
+ @connection ||= begin
26
+ validate_configuration!
30
27
 
31
- @connection = Faraday.new( config.to_hash ) do |builder|
32
- builder.use HalJson
28
+ Faraday.new( config ) do |builder|
29
+ configure_authentication builder
33
30
 
34
- config.middleware.each { |mw| builder.use(*mw) }
31
+ builder.response :hal_json
35
32
 
36
- builder.request :basic_auth, config.username, config.password
33
+ configure_caching builder
37
34
 
38
- builder.response :logger if config.response[:use_logger]
35
+ @builder_config.call( builder ) if @builder_config
39
36
 
40
- if config.cache.present?
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
- @description ||= load_description
45
+ if response = get( config.url.to_s ) and response.success?
46
+ response.body
47
+ end
50
48
  end
51
49
 
52
- # A naive approach to reloading a Frenetic instance for testing purpose.
53
- def reload!
54
- instance_variables.each { |var| instance_variable_set(var, nil) }
50
+ def schema
51
+ description['_embedded']['schema']
55
52
  end
56
53
 
57
- private
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