frenetic 0.0.1 → 0.0.2.alpha1

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/README.md CHANGED
@@ -5,6 +5,122 @@
5
5
 
6
6
  An opinionated Ruby-based Hypermedia API (HAL+JSON) client.
7
7
 
8
+
9
+
10
+
11
+ ## About
12
+
13
+ fre&bull;net&bull;ic |frəˈnetik|<br/>
14
+ adjective<br/>
15
+ fast and energetic in a rather wild and uncontrolled way : *a frenetic pace of activity.*
16
+
17
+ So basically, this is a crazy way to interact with your Hypermedia HAL+JSON API.
18
+
19
+ Get it? *Hypermedia*?
20
+
21
+ *Hyper*?
22
+
23
+ ...
24
+
25
+ If you have not implemented a HAL+JSON API, then this will not work very well for you.
26
+
27
+
28
+
29
+
30
+ ## Opinions
31
+
32
+ Like I said, it is opinionated. It is so opinionated, it is probably the biggest
33
+ a-hole you've ever met.
34
+
35
+ Maybe in time, if you teach it, it will become more open-minded.
36
+
37
+
38
+
39
+ ### HAL+JSON Content Type
40
+
41
+ Frenetic expects all responses to be in [HAL+JSON][hal_json]. It chose that
42
+ standard because it is trying to make JSON API's respond in a predictable
43
+ manner, which it thinks is an awesome idea.
44
+
45
+
46
+
47
+ ### Authentication
48
+
49
+ Frenetic is going to try and use Basic Auth whether you like it or not. If
50
+ that is not required, nothing will probably happen. But its going to send the
51
+ header anyway.
52
+
53
+
54
+
55
+ ### API Description
56
+
57
+ The API's root URL must respond with a description, much like the
58
+ [Spire.io][spire.io] API. This is crucial in order for Frenetic to work. If
59
+ Frenetic doesn't know what the API contains, it can't parse any resource
60
+ responses.
61
+
62
+ It is expected that any subclasses of `Frenetic::Resource` will adhere to the
63
+ schema defined here.
64
+
65
+ Example:
66
+
67
+ ```js
68
+ {
69
+ "_links":{
70
+ "self":{"href":"/api/"},
71
+ "inkers":{"href":"/api/inkers"},
72
+ },
73
+ "_embedded":{
74
+ "schema":{
75
+ "_links":{
76
+ "self":{"href":"/api/schema"}
77
+ },
78
+ "order":{
79
+ "description":"A widget order",
80
+ "type":"object",
81
+ "properties":{
82
+ "id":{"type":"integer"},
83
+ "first_name":{"type":"string"},
84
+ "last_name":{"type":"string"},
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ ```
91
+
92
+ This response will be requested by Frenetic whenever a call to
93
+ `YourAPI.description` is made. The response is memoized so any future calls
94
+ will not trigger another API request.
95
+
96
+
97
+
98
+ ### API Resources
99
+
100
+ While HAL+JSON is awesome, not all implementations are perfect. Frenetic
101
+ assumes a HAL+JSON response as built by [Roar], which may not be in 100%
102
+ compliance.
103
+
104
+ Example:
105
+
106
+ ```js
107
+ {
108
+ "id":1,
109
+ "first_name":"Foo",
110
+ "last_name":"Bar",
111
+ "_links":{
112
+ "self":{"href":"/order/1"},
113
+ "next":{"href":"/order/2"}
114
+ }
115
+ }
116
+ ```
117
+
118
+ The problem here is that the entire response really should be wrapped in
119
+ `"_embedded"` and `"order"` keys.
120
+
121
+ So until that is fixed, Frenetic will continue to be pig headed and continue
122
+ to do the "wrong" thing.
123
+
8
124
  ## Installation
9
125
 
10
126
  Add this line to your application's Gemfile:
@@ -19,9 +135,128 @@ Or install it yourself as:
19
135
 
20
136
  $ gem install frenetic
21
137
 
138
+
139
+
140
+
22
141
  ## Usage
23
142
 
24
- Your guess is as good as mine. (*TODO*)
143
+
144
+
145
+ ### Client Initialization
146
+
147
+ ```ruby
148
+ MyAPI = Frenetic.new(
149
+ 'url' => 'https://api.yoursite.com',
150
+ 'username' => 'yourname',
151
+ 'password' => 'yourpassword',
152
+ 'headers' => {
153
+ 'accept' => 'application/vnd.yoursite-v1.hal+json'
154
+ # Optional
155
+ 'user-agent' => 'Your Site's API Client', # Optional custom User Agent, just 'cuz
156
+ }
157
+ )
158
+ ```
159
+
160
+
161
+ ### Response Caching
162
+
163
+ If configured to do so, Frenetic will autotmatically cache appropriate responses
164
+ through [Rack::Cache][rach_cache]. Only on-disk stores are supported right now.
165
+
166
+ Add the following `Rack::Cache` configuration options when initializing Frenetic:
167
+
168
+ ```ruby
169
+ MyAPI = Frenetic.new(
170
+ ...
171
+ 'cache' => {
172
+ 'metastore' => 'file:/path/to/where/you/want/to/store/files/meta',
173
+ 'entitystore' => 'file:/path/to/where/you/want/to/store/files/meta'
174
+ }
175
+ )
176
+ ```
177
+
178
+ The `cache` options are passed directly to `Rack::Cache`, so anything it
179
+ supports can be added to the Hash.
180
+
181
+
182
+
183
+ ### Making Requests
184
+
185
+ Once you have created a client instance, you are free to use it however you'd
186
+ like.
187
+
188
+ A Frenetic instance supports any HTTP verb that [Faraday][faraday] has
189
+ impletented. This includes GET, POST, PUT, PATCH, and DELETE.
190
+
191
+
192
+
193
+ #### Frenetic::Resource
194
+
195
+ An easier way to make requests for a resource is to have your model inherit from
196
+ `Frenetic::Resource`. This makes it a bit easier to encapsulate all of your
197
+ resource's API requests into one place.
198
+
199
+ ```ruby
200
+ class Order < Frenetic::Resource
201
+
202
+ api_client { MyAPI }
203
+
204
+ class << self
205
+ def find( id )
206
+ if response = api.get( api.description.links.order.href.gsub('{id}', id.to_s) ) and response.success?
207
+ self.new( response.body )
208
+ else
209
+ raise OrderNotFound, "No Order found for #{id}"
210
+ end
211
+ end
212
+ end
213
+ end
214
+ ```
215
+
216
+ The `api_client` class method merely tells `Frenetic::Resource` which API Client
217
+ instance to use. If you lazily instantiate your client, then you should pass a
218
+ block as demonstrated above.
219
+
220
+ Otherwise, you may pass by reference:
221
+
222
+ ```ruby
223
+ class Order < Frenetic::Resource
224
+ api_client MyAPI
225
+ end
226
+ ```
227
+
228
+ When your model is initialized, it will contain attribute readers for every
229
+ property defined in your API's schema or description. In theory, this allows an
230
+ API to add, remove, or change properties without the need to directly update
231
+ your model.
232
+
233
+
234
+
235
+ ### Interpretting Responses
236
+
237
+ Any response body returned by a Frenetic generated API call will be returned as
238
+ an OpenStruct-like object. This object responds to dot-notation as well as Hash
239
+ keys and is enumerable.
240
+
241
+ ```ruby
242
+ response.body.resources.orders.first
243
+ ```
244
+
245
+ or
246
+
247
+ ```ruby
248
+ response.body['_embedded']['orders'][0]
249
+ ```
250
+
251
+ For your convenience, certain HAL+JSON keys have been aliased by methods a bit
252
+ more readable:
253
+
254
+ * `_embedded` can be referenced as `resources`
255
+ * `_links` can be referenced as `links`
256
+ * `href` can be referenced as `url`
257
+
258
+
259
+
25
260
 
26
261
  ## Contributing
27
262
 
@@ -30,3 +265,9 @@ Your guess is as good as mine. (*TODO*)
30
265
  3. Commit your changes (`git commit -am 'Added some feature'`)
31
266
  4. Push to the branch (`git push origin my-new-feature`)
32
267
  5. Create new Pull Request
268
+
269
+ [hal_json]: http://stateless.co/hal_specification.html
270
+ [spire.io]: http://api.spire.io/
271
+ [roar]: https://github.com/apotonick/roar
272
+ [faraday]: https://github.com/technoweenie/faraday
273
+ [rack_cache]: https://github.com/rtomayko/rack-cache
@@ -15,14 +15,16 @@ 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.0.rc2'
19
- gem.add_dependency 'addressable', '~> 2.2.7'
20
- gem.add_dependency 'patron', '~> 0.4.18'
18
+ gem.add_dependency 'faraday', '~> 0.8.0.rc2'
19
+ gem.add_dependency 'faraday_middleware', '~> 0.8.6'
20
+ gem.add_dependency 'rack-cache', '~> 1.1'
21
+ gem.add_dependency 'addressable', '~> 2.2.7'
22
+ gem.add_dependency 'patron', '~> 0.4.18'
21
23
 
22
- gem.add_development_dependency 'guard-spork', '~> 0.6.0'
23
- gem.add_development_dependency 'guard-rspec', '~> 0.7.0'
24
- gem.add_development_dependency 'rspec', '~> 2.9.0'
25
- gem.add_development_dependency 'bourne', '~> 1.1.2'
26
- gem.add_development_dependency 'webmock', '~> 1.8.6'
27
- gem.add_development_dependency 'vcr', '~> 2.0.1'
24
+ gem.add_development_dependency 'guard-spork', '~> 0.6.0'
25
+ gem.add_development_dependency 'guard-rspec', '~> 0.7.0'
26
+ gem.add_development_dependency 'rspec', '~> 2.9.0'
27
+ gem.add_development_dependency 'bourne', '~> 1.1.2'
28
+ gem.add_development_dependency 'webmock', '~> 1.8.6'
29
+ gem.add_development_dependency 'vcr', '~> 2.0.1'
28
30
  end
@@ -1,5 +1,8 @@
1
1
  require 'addressable/uri'
2
+ require 'patron' # Needed to prevent https://github.com/technoweenie/faraday/issues/140
2
3
  require 'faraday'
4
+ require 'faraday_middleware'
5
+ require 'rack-cache'
3
6
 
4
7
  require "frenetic/configuration"
5
8
  require "frenetic/hal_json"
@@ -25,6 +28,11 @@ class Frenetic
25
28
  @connection = Faraday.new( config ) do |builder|
26
29
  builder.use HalJson
27
30
  builder.request :basic_auth, config[:username], config[:password]
31
+
32
+ if config[:cache]
33
+ builder.use FaradayMiddleware::RackCompatible, Rack::Cache::Context, config[:cache]
34
+ end
35
+
28
36
  builder.adapter :patron
29
37
  end
30
38
  end
@@ -15,11 +15,7 @@ class Frenetic
15
15
  config[:headers] ||= {}
16
16
  config[:request] ||= {}
17
17
 
18
- if config[:"content-type"]
19
- config[:headers][:accepts] = config[:"content-type"]
20
- else
21
- config[:headers][:accepts] = "application/hal+json"
22
- end
18
+ config[:headers][:accept] ||= "application/hal+json"
23
19
 
24
20
  # Copy the config into this Configuration instance.
25
21
  config.each { |k, v| self[k] = v }
@@ -27,6 +23,7 @@ class Frenetic
27
23
  super()
28
24
 
29
25
  configure_user_agent
26
+ configure_cache
30
27
 
31
28
  validate
32
29
  end
@@ -43,12 +40,35 @@ class Frenetic
43
40
  end
44
41
  end
45
42
 
43
+ def configure_cache
44
+ if self[:cache]
45
+ ignore_headers = (self[:cache][:ignore_headers] || '').split(' ')
46
+
47
+ self[:cache][:ignore_headers] = (ignore_headers + %w[Set-Cookie X-Content-Digest]).uniq
48
+ end
49
+ end
50
+
46
51
  def validate
47
52
  unless self[:url]
48
53
  raise ConfigurationError, "No API URL defined!"
49
54
  end
55
+ if self[:cache]
56
+ raise( ConfigurationError, "No cache :metastore defined!" ) if self[:cache][:metastore].to_s == ""
57
+ raise( ConfigurationError, "No cache :entitystore defined!" ) if self[:cache][:entitystore].to_s == ""
58
+ raise( ConfigurationError, "Required cache header filters are missing!" ) if missing_required_headers?
59
+ end
60
+ end
61
+
62
+ def missing_required_headers?
63
+ return true if self[:cache][:ignore_headers].empty?
64
+
65
+ header_set = self[:cache][:ignore_headers]
66
+ custom_headers = header_set - %w[Set-Cookie X-Content-Digest]
67
+
68
+ header_set == custom_headers
50
69
  end
51
70
 
71
+ # TODO: Is this even being used?
52
72
  def config_file
53
73
  config_path = File.join( 'config', 'frenetic.yml' )
54
74
 
@@ -1,3 +1,3 @@
1
1
  class Frenetic
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2.alpha1"
3
3
  end
@@ -4,7 +4,9 @@ describe Frenetic::Configuration do
4
4
  { 'test' => {
5
5
  'url' => 'http://example.org',
6
6
  'api_key' => '1234567890',
7
- 'content-type' => content_type,
7
+ 'headers' => {
8
+ 'accept' => content_type,
9
+ },
8
10
  'request' => {
9
11
  'timeout' => 10000
10
12
  }
@@ -37,16 +39,16 @@ describe Frenetic::Configuration do
37
39
  subject[:headers][:user_agent].should =~ %r{Frenetic v.+; \S+$}
38
40
  end
39
41
 
40
- context "with a specified Content-Type" do
41
- it "should set an Accepts request header" do
42
- subject[:headers].should include(:accepts => 'application/vnd.frenetic-v1-hal+json')
42
+ context "with a specified Accept header" do
43
+ it "should set an Accept request header" do
44
+ subject[:headers].should include(:accept => 'application/vnd.frenetic-v1-hal+json')
43
45
  end
44
46
  end
45
- context "without a specified Content-Type" do
47
+ context "without a specified Accept header" do
46
48
  let(:content_type) { nil }
47
49
 
48
- it "should set an Accepts request header" do
49
- subject[:headers].should include(:accepts => 'application/hal+json')
50
+ it "should set an Accept request header" do
51
+ subject[:headers].should include(:accept => 'application/hal+json')
50
52
  end
51
53
  end
52
54
  end
@@ -63,11 +65,45 @@ describe Frenetic::Configuration do
63
65
  it { should be_a( Hash ) }
64
66
  it { should_not be_empty }
65
67
  it "should set an Accepts request header" do
66
- subject[:headers].should include(:accepts => 'application/hal+json')
68
+ subject[:headers].should include(:accept => 'application/hal+json')
67
69
  end
68
70
  it "should set a User Agent request header" do
69
71
  subject[:headers][:user_agent].should =~ %r{Frenetic v.+; \S+$}
70
72
  end
73
+
74
+ context "which includes incorrect cache settings" do
75
+ before { Frenetic::Configuration.any_instance.stubs(:configure_cache).returns(nil) }
76
+
77
+ it "should raise a configuration error for a missing :metastore" do
78
+ expect {
79
+ Frenetic::Configuration.new('url' => 'http://example.org', 'cache' => {} )
80
+ }.to raise_error(
81
+ Frenetic::Configuration::ConfigurationError, "No cache :metastore defined!"
82
+ )
83
+ end
84
+
85
+ it "should raise a configuration error for a missing :entitystore" do
86
+ expect {
87
+ Frenetic::Configuration.new('url' => 'http://example.org', 'cache' => { 'metastore' => 'foo' } )
88
+ }.to raise_error(
89
+ Frenetic::Configuration::ConfigurationError, "No cache :entitystore defined!"
90
+ )
91
+ end
92
+
93
+ it "should raise a configuration error for missing required header filters" do
94
+ cache_cfg = {
95
+ 'metastore' => 'foo',
96
+ 'entitystore' => 'bar',
97
+ 'ignore_headers' => ['baz'] # `configure_cache` method is skipped to create a bad state
98
+ }
99
+
100
+ expect {
101
+ Frenetic::Configuration.new('url' => 'http://example.org', 'cache' => cache_cfg )
102
+ }.to raise_error(
103
+ Frenetic::Configuration::ConfigurationError, "Required cache header filters are missing!"
104
+ )
105
+ end
106
+ end
71
107
  end
72
108
  end
73
109
  end
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frenetic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 0.0.2.alpha1
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Derek Lindahl
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-11 00:00:00.000000000Z
12
+ date: 2012-04-18 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
16
- requirement: &2157361220 !ruby/object:Gem::Requirement
16
+ requirement: &2156329760 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,32 @@ dependencies:
21
21
  version: 0.8.0.rc2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *2157361220
24
+ version_requirements: *2156329760
25
+ - !ruby/object:Gem::Dependency
26
+ name: faraday_middleware
27
+ requirement: &2156328980 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.6
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2156328980
36
+ - !ruby/object:Gem::Dependency
37
+ name: rack-cache
38
+ requirement: &2156328220 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.1'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *2156328220
25
47
  - !ruby/object:Gem::Dependency
26
48
  name: addressable
27
- requirement: &2157360720 !ruby/object:Gem::Requirement
49
+ requirement: &2156327400 !ruby/object:Gem::Requirement
28
50
  none: false
29
51
  requirements:
30
52
  - - ~>
@@ -32,10 +54,10 @@ dependencies:
32
54
  version: 2.2.7
33
55
  type: :runtime
34
56
  prerelease: false
35
- version_requirements: *2157360720
57
+ version_requirements: *2156327400
36
58
  - !ruby/object:Gem::Dependency
37
59
  name: patron
38
- requirement: &2157360240 !ruby/object:Gem::Requirement
60
+ requirement: &2156326620 !ruby/object:Gem::Requirement
39
61
  none: false
40
62
  requirements:
41
63
  - - ~>
@@ -43,10 +65,10 @@ dependencies:
43
65
  version: 0.4.18
44
66
  type: :runtime
45
67
  prerelease: false
46
- version_requirements: *2157360240
68
+ version_requirements: *2156326620
47
69
  - !ruby/object:Gem::Dependency
48
70
  name: guard-spork
49
- requirement: &2157359780 !ruby/object:Gem::Requirement
71
+ requirement: &2156325900 !ruby/object:Gem::Requirement
50
72
  none: false
51
73
  requirements:
52
74
  - - ~>
@@ -54,10 +76,10 @@ dependencies:
54
76
  version: 0.6.0
55
77
  type: :development
56
78
  prerelease: false
57
- version_requirements: *2157359780
79
+ version_requirements: *2156325900
58
80
  - !ruby/object:Gem::Dependency
59
81
  name: guard-rspec
60
- requirement: &2157359320 !ruby/object:Gem::Requirement
82
+ requirement: &2156325160 !ruby/object:Gem::Requirement
61
83
  none: false
62
84
  requirements:
63
85
  - - ~>
@@ -65,10 +87,10 @@ dependencies:
65
87
  version: 0.7.0
66
88
  type: :development
67
89
  prerelease: false
68
- version_requirements: *2157359320
90
+ version_requirements: *2156325160
69
91
  - !ruby/object:Gem::Dependency
70
92
  name: rspec
71
- requirement: &2157358860 !ruby/object:Gem::Requirement
93
+ requirement: &2156324300 !ruby/object:Gem::Requirement
72
94
  none: false
73
95
  requirements:
74
96
  - - ~>
@@ -76,10 +98,10 @@ dependencies:
76
98
  version: 2.9.0
77
99
  type: :development
78
100
  prerelease: false
79
- version_requirements: *2157358860
101
+ version_requirements: *2156324300
80
102
  - !ruby/object:Gem::Dependency
81
103
  name: bourne
82
- requirement: &2157358400 !ruby/object:Gem::Requirement
104
+ requirement: &2156322980 !ruby/object:Gem::Requirement
83
105
  none: false
84
106
  requirements:
85
107
  - - ~>
@@ -87,10 +109,10 @@ dependencies:
87
109
  version: 1.1.2
88
110
  type: :development
89
111
  prerelease: false
90
- version_requirements: *2157358400
112
+ version_requirements: *2156322980
91
113
  - !ruby/object:Gem::Dependency
92
114
  name: webmock
93
- requirement: &2157357940 !ruby/object:Gem::Requirement
115
+ requirement: &2156322340 !ruby/object:Gem::Requirement
94
116
  none: false
95
117
  requirements:
96
118
  - - ~>
@@ -98,10 +120,10 @@ dependencies:
98
120
  version: 1.8.6
99
121
  type: :development
100
122
  prerelease: false
101
- version_requirements: *2157357940
123
+ version_requirements: *2156322340
102
124
  - !ruby/object:Gem::Dependency
103
125
  name: vcr
104
- requirement: &2157357480 !ruby/object:Gem::Requirement
126
+ requirement: &2156321440 !ruby/object:Gem::Requirement
105
127
  none: false
106
128
  requirements:
107
129
  - - ~>
@@ -109,7 +131,7 @@ dependencies:
109
131
  version: 2.0.1
110
132
  type: :development
111
133
  prerelease: false
112
- version_requirements: *2157357480
134
+ version_requirements: *2156321440
113
135
  description: An opinionated Ruby-based Hypermedia API client.
114
136
  email:
115
137
  - dlindahl@customink.com
@@ -155,9 +177,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
155
177
  required_rubygems_version: !ruby/object:Gem::Requirement
156
178
  none: false
157
179
  requirements:
158
- - - ! '>='
180
+ - - ! '>'
159
181
  - !ruby/object:Gem::Version
160
- version: '0'
182
+ version: 1.3.1
161
183
  requirements: []
162
184
  rubyforge_project:
163
185
  rubygems_version: 1.8.10