frenetic 0.0.1.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/.gitignore +18 -0
- data/.rvmrc +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +7 -0
- data/Guardfile +15 -0
- data/LICENSE +22 -0
- data/README.md +223 -0
- data/Rakefile +2 -0
- data/frenetic.gemspec +28 -0
- data/lib/frenetic.rb +49 -0
- data/lib/frenetic/configuration.rb +82 -0
- data/lib/frenetic/hal_json.rb +23 -0
- data/lib/frenetic/hal_json/response_wrapper.rb +43 -0
- data/lib/frenetic/resource.rb +68 -0
- data/lib/frenetic/version.rb +3 -0
- data/lib/recursive_open_struct.rb +79 -0
- data/spec/fixtures/vcr_cassettes/description_success.yml +38 -0
- data/spec/lib/frenetic/configuration_spec.rb +77 -0
- data/spec/lib/frenetic/hal_json/response_wrapper_spec.rb +70 -0
- data/spec/lib/frenetic/hal_json_spec.rb +70 -0
- data/spec/lib/frenetic/resource_spec.rb +102 -0
- data/spec/lib/frenetic_spec.rb +66 -0
- data/spec/spec_helper.rb +31 -0
- metadata +175 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create use ruby-1.9.2-p290@frenetic > /dev/null
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# More info at https://github.com/guard/guard#readme
|
2
|
+
|
3
|
+
guard 'rspec', :notification => false do
|
4
|
+
watch(%r{^spec/.+_spec\.rb$})
|
5
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
6
|
+
watch('spec/spec_helper.rb') { "spec" }
|
7
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
8
|
+
end
|
9
|
+
|
10
|
+
guard 'spork', :rspec_env => { 'RAILS_ENV' => 'test' } do
|
11
|
+
watch('config/environment.rb')
|
12
|
+
watch('Gemfile')
|
13
|
+
watch('Gemfile.lock')
|
14
|
+
watch('spec/spec_helper.rb') { :rspec }
|
15
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Derek Lindahl
|
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,223 @@
|
|
1
|
+
# Frenetic [![Build Status][travis_status]][travis]
|
2
|
+
|
3
|
+
[travis_status]: https://secure.travis-ci.org/dlindahl/frenetic.png
|
4
|
+
[travis]: http://travis-ci.org/dlindahl/frenetic
|
5
|
+
|
6
|
+
An opinionated Ruby-based Hypermedia API (HAL+JSON) client.
|
7
|
+
|
8
|
+
## About
|
9
|
+
|
10
|
+
fre•net•ic |frəˈnetik|<br/>
|
11
|
+
adjective<br/>
|
12
|
+
fast and energetic in a rather wild and uncontrolled way : *a frenetic pace of activity.*
|
13
|
+
|
14
|
+
So basically, this is a crazy way to interact with your Hypermedia HAL+JSON API.
|
15
|
+
|
16
|
+
Get it? *Hypermedia*?
|
17
|
+
|
18
|
+
*Hyper*?
|
19
|
+
|
20
|
+
...
|
21
|
+
|
22
|
+
If you have not implemented a HAL+JSON API, then this will not work very well for you.
|
23
|
+
|
24
|
+
## Opinions
|
25
|
+
|
26
|
+
Like I said, it is opinionated. It is so opinionated, it is probably the biggest
|
27
|
+
a-hole you've ever met.
|
28
|
+
|
29
|
+
Maybe in time, if you teach it, it will become more open-minded.
|
30
|
+
|
31
|
+
### HAL+JSON Content Type
|
32
|
+
|
33
|
+
Frenetic expects all responses to be in [HAL+JSON][hal_json]. It chose that
|
34
|
+
standard because it is trying to make JSON API's respond in a predictable
|
35
|
+
manner, which it thinks is an awesome idea.
|
36
|
+
|
37
|
+
### Authentication
|
38
|
+
|
39
|
+
Frenetic is going to try and use Basic Auth whether you like it or not. If
|
40
|
+
that is not required, nothing will probably happen. But its going to send the
|
41
|
+
header anyway.
|
42
|
+
|
43
|
+
### API Description
|
44
|
+
|
45
|
+
The API's root URL must respond with a description, much like the
|
46
|
+
[Spire.io][spire.io] API. This is crucial in order for Frenetic to work. If
|
47
|
+
Frenetic doesn't know what the API contains, it can't parse any resource
|
48
|
+
responses.
|
49
|
+
|
50
|
+
It is expected that any subclasses of `Frenetic::Resource` will adhere to the
|
51
|
+
schema defined here.
|
52
|
+
|
53
|
+
Example:
|
54
|
+
|
55
|
+
```js
|
56
|
+
{
|
57
|
+
"_links":{
|
58
|
+
"self":{"href":"/api/"},
|
59
|
+
"inkers":{"href":"/api/inkers"},
|
60
|
+
},
|
61
|
+
"_embedded":{
|
62
|
+
"schema":{
|
63
|
+
"_links":{
|
64
|
+
"self":{"href":"/api/schema"}
|
65
|
+
},
|
66
|
+
"order":{
|
67
|
+
"description":"A widget order",
|
68
|
+
"type":"object",
|
69
|
+
"properties":{
|
70
|
+
"id":{"type":"integer"},
|
71
|
+
"first_name":{"type":"string"},
|
72
|
+
"last_name":{"type":"string"},
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
```
|
79
|
+
|
80
|
+
This response will be requested by Frenetic whenever a call to
|
81
|
+
`YourAPI.description` is made. The response is memoized so any future calls
|
82
|
+
will not trigger another API request.
|
83
|
+
|
84
|
+
### API Resources
|
85
|
+
|
86
|
+
While HAL+JSON is awesome, not all implementations are perfect. Frenetic
|
87
|
+
assumes a HAL+JSON response as built by [Roar], which may not be in 100%
|
88
|
+
compliance.
|
89
|
+
|
90
|
+
Example:
|
91
|
+
|
92
|
+
```js
|
93
|
+
{
|
94
|
+
"id":1,
|
95
|
+
"first_name":"Foo",
|
96
|
+
"last_name":"Bar",
|
97
|
+
"_links":{
|
98
|
+
"self":{"href":"/order/1"},
|
99
|
+
"next":{"href":"/order/2"}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
```
|
103
|
+
|
104
|
+
The problem here is that the entire response really should be wrapped in
|
105
|
+
`"_embedded"` and `"order"` keys.
|
106
|
+
|
107
|
+
So until that is fixed, Frenetic will continue to be pig headed and continue
|
108
|
+
to do the "wrong" thing.
|
109
|
+
|
110
|
+
## Installation
|
111
|
+
|
112
|
+
Add this line to your application's Gemfile:
|
113
|
+
|
114
|
+
gem 'frenetic'
|
115
|
+
|
116
|
+
And then execute:
|
117
|
+
|
118
|
+
$ bundle
|
119
|
+
|
120
|
+
Or install it yourself as:
|
121
|
+
|
122
|
+
$ gem install frenetic
|
123
|
+
|
124
|
+
## Usage
|
125
|
+
|
126
|
+
### Client Initialization
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
MyAPI = Frenetic.new(
|
130
|
+
'url' => 'https://api.yoursite.com',
|
131
|
+
'username' => 'yourname',
|
132
|
+
'password' => 'yourpassword',
|
133
|
+
'headers' => {
|
134
|
+
'accept' => 'application/vnd.yoursite-v1.hal+json'
|
135
|
+
# Optional
|
136
|
+
'user-agent' => 'Your Site's API Client', # Optional custom User Agent, just 'cuz
|
137
|
+
}
|
138
|
+
)
|
139
|
+
```
|
140
|
+
|
141
|
+
### Making Requests
|
142
|
+
|
143
|
+
Once you have created a client instance, you are free to use it however you'd
|
144
|
+
like.
|
145
|
+
|
146
|
+
A Frenetic instance supports any HTTP verb that [Faraday][faraday] has
|
147
|
+
impletented. This includes GET, POST, PUT, PATCH, and DELETE.
|
148
|
+
|
149
|
+
#### Frenetic::Resource
|
150
|
+
|
151
|
+
An easier way to make requests for a resource is to have your model inherit from
|
152
|
+
`Frenetic::Resource`. This makes it a bit easier to encapsulate all of your
|
153
|
+
resource's API requests into one place.
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class Order < Frenetic::Resource
|
157
|
+
|
158
|
+
api_client { MyAPI }
|
159
|
+
|
160
|
+
class << self
|
161
|
+
def find( id )
|
162
|
+
if response = api.get( api.description.links.order.href.gsub('{id}', id.to_s) ) and response.success?
|
163
|
+
self.new( response.body )
|
164
|
+
else
|
165
|
+
raise OrderNotFound, "No Order found for #{id}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
The `api_client` class method merely tells `Frenetic::Resource` which API Client
|
173
|
+
instance to use. If you lazily instantiate your client, then you should pass a
|
174
|
+
block as demonstrated above.
|
175
|
+
|
176
|
+
Otherwise, you may pass by reference:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class Order < Frenetic::Resource
|
180
|
+
api_client MyAPI
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
When your model is initialized, it will contain attribute readers for every
|
185
|
+
property defined in your API's schema or description. In theory, this allows an
|
186
|
+
API to add, remove, or change properties without the need to directly update
|
187
|
+
your model.
|
188
|
+
|
189
|
+
### Interpretting Responses
|
190
|
+
|
191
|
+
Any response body returned by a Frenetic generated API call will be returned as
|
192
|
+
an OpenStruct-like object. This object responds to dot-notation as well as Hash
|
193
|
+
keys and is enumerable.
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
response.body.resources.orders.first
|
197
|
+
```
|
198
|
+
|
199
|
+
or
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
response.body['_embedded']['orders'][0]
|
203
|
+
```
|
204
|
+
|
205
|
+
For your convenience, certain HAL+JSON keys have been aliased by methods a bit
|
206
|
+
more readable:
|
207
|
+
|
208
|
+
* `_embedded` can be referenced as `resources`
|
209
|
+
* `_links` can be referenced as `links`
|
210
|
+
* `href` can be referenced as `url`
|
211
|
+
|
212
|
+
## Contributing
|
213
|
+
|
214
|
+
1. Fork it
|
215
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
216
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
217
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
218
|
+
5. Create new Pull Request
|
219
|
+
|
220
|
+
[hal_json]: http://stateless.co/hal_specification.html
|
221
|
+
[spire.io]: http://api.spire.io/
|
222
|
+
[roar]: https://github.com/apotonick/roar
|
223
|
+
[faraday]: https://github.com/technoweenie/faraday
|
data/Rakefile
ADDED
data/frenetic.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/frenetic/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Derek Lindahl"]
|
6
|
+
gem.email = ["dlindahl@customink.com"]
|
7
|
+
gem.description = %q{An opinionated Ruby-based Hypermedia API client.}
|
8
|
+
gem.summary = %q{Here lies a Ruby-based Hypermedia API client that expects HAL+JSON and makes a lot of assumptions about your API.}
|
9
|
+
gem.homepage = "http://dlindahl.github.com/frenetic/"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "frenetic"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Frenetic::VERSION
|
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'
|
21
|
+
|
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'
|
28
|
+
end
|
data/lib/frenetic.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'patron' # Needed to prevent https://github.com/technoweenie/faraday/issues/140
|
3
|
+
require 'faraday'
|
4
|
+
|
5
|
+
require "frenetic/configuration"
|
6
|
+
require "frenetic/hal_json"
|
7
|
+
require "frenetic/resource"
|
8
|
+
require "frenetic/version"
|
9
|
+
|
10
|
+
class Frenetic
|
11
|
+
|
12
|
+
class MissingAPIReference < StandardError; end
|
13
|
+
|
14
|
+
extend Forwardable
|
15
|
+
def_delegators :@connection, :get, :put, :post, :delete
|
16
|
+
|
17
|
+
attr_reader :connection
|
18
|
+
alias_method :conn, :connection
|
19
|
+
|
20
|
+
def initialize( config = {} )
|
21
|
+
config = Configuration.new( config )
|
22
|
+
|
23
|
+
api_url = Addressable::URI.parse( config[:url] )
|
24
|
+
@root_url = api_url.path
|
25
|
+
|
26
|
+
@connection = Faraday.new( config ) do |builder|
|
27
|
+
builder.use HalJson
|
28
|
+
builder.request :basic_auth, config[:username], config[:password]
|
29
|
+
builder.adapter :patron
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def description
|
34
|
+
@description ||= load_description
|
35
|
+
end
|
36
|
+
|
37
|
+
# A naive approach to reloading a Frenetic instance for testing purpose.
|
38
|
+
def reload!
|
39
|
+
instance_variables.each { |var| instance_variable_set(var, nil) }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def load_description
|
45
|
+
if response = get( @root_url ) and response.success?
|
46
|
+
response.body
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
class Frenetic
|
4
|
+
class Configuration < Hash
|
5
|
+
|
6
|
+
class ConfigurationError < StandardError; end
|
7
|
+
|
8
|
+
# TODO: This is in desperate need of .with_indifferent_access...
|
9
|
+
# TODO: "content-type" should probably be within a "headers" key
|
10
|
+
def initialize( custom_config = {} )
|
11
|
+
config = config_file.merge custom_config
|
12
|
+
config = symbolize_keys config
|
13
|
+
|
14
|
+
config[:username] = config[:api_key] if config[:api_key]
|
15
|
+
config[:headers] ||= {}
|
16
|
+
config[:request] ||= {}
|
17
|
+
|
18
|
+
config[:headers][:accept] ||= "application/hal+json"
|
19
|
+
|
20
|
+
# Copy the config into this Configuration instance.
|
21
|
+
config.each { |k, v| self[k] = v }
|
22
|
+
|
23
|
+
super()
|
24
|
+
|
25
|
+
configure_user_agent
|
26
|
+
|
27
|
+
validate
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def configure_user_agent
|
33
|
+
frenetic_ua = "Frenetic v#{Frenetic::VERSION}; #{Socket.gethostname}"
|
34
|
+
|
35
|
+
if self[:headers][:user_agent]
|
36
|
+
self[:headers][:user_agent] << " (#{frenetic_ua})"
|
37
|
+
else
|
38
|
+
self[:headers][:user_agent] = frenetic_ua
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate
|
43
|
+
unless self[:url]
|
44
|
+
raise ConfigurationError, "No API URL defined!"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def config_file
|
49
|
+
config_path = File.join( 'config', 'frenetic.yml' )
|
50
|
+
|
51
|
+
if File.exists? config_path
|
52
|
+
config = YAML.load_file( config_path )
|
53
|
+
env = ENV['RAILS_ENV'] || ENV['RACK_ENV']
|
54
|
+
|
55
|
+
if config and config.has_key? env
|
56
|
+
config[env]
|
57
|
+
else
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
else
|
61
|
+
{}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def symbolize_keys( arg )
|
66
|
+
case arg
|
67
|
+
when Array
|
68
|
+
arg.map { |elem| symbolize_keys elem }
|
69
|
+
when Hash
|
70
|
+
Hash[
|
71
|
+
arg.map { |key, value|
|
72
|
+
k = key.is_a?(String) ? key.to_sym : key
|
73
|
+
v = symbolize_keys value
|
74
|
+
[k,v]
|
75
|
+
}]
|
76
|
+
else
|
77
|
+
arg
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'recursive_open_struct'
|
3
|
+
require 'frenetic/hal_json/response_wrapper'
|
4
|
+
|
5
|
+
class Frenetic
|
6
|
+
|
7
|
+
class HalJson < Faraday::Middleware
|
8
|
+
def call( env )
|
9
|
+
@app.call(env).on_complete { |env| on_complete(env) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_complete( env )
|
13
|
+
if success? env
|
14
|
+
env[:body] = ResponseWrapper.new( JSON.parse(env[:body]) )
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def success?( env )
|
19
|
+
(200..201) === env[:status]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Frenetic
|
2
|
+
class HalJson < Faraday::Middleware
|
3
|
+
# TODO: The API for this differs greatly from the `inspect` output.
|
4
|
+
# Perhaps the Hash keys should be normalized and then aliased back to the original keys?
|
5
|
+
class ResponseWrapper < RecursiveOpenStruct
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def []( key )
|
9
|
+
self.send(key)
|
10
|
+
end
|
11
|
+
|
12
|
+
def members
|
13
|
+
methods(false).grep(%r{_as_a_hash}).map { |m| m[0...-10] }
|
14
|
+
end
|
15
|
+
alias_method :keys, :members
|
16
|
+
|
17
|
+
def each
|
18
|
+
members.each do |method|
|
19
|
+
yield method, send(method)
|
20
|
+
end
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
# Do not define setters
|
27
|
+
def define_setter( * ); end
|
28
|
+
|
29
|
+
def define_getter( method_name, hash_key )
|
30
|
+
method_name = case method_name
|
31
|
+
when :_embedded then :resources
|
32
|
+
when :_links then :links
|
33
|
+
when :href then :url
|
34
|
+
else method_name
|
35
|
+
end
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
class Frenetic
|
2
|
+
class Resource
|
3
|
+
|
4
|
+
attr_reader :links
|
5
|
+
|
6
|
+
def initialize( attributes = {} )
|
7
|
+
if attributes.is_a? Hash
|
8
|
+
load attributes.keys, attributes
|
9
|
+
|
10
|
+
@links = []
|
11
|
+
else
|
12
|
+
load self.class.schema, attributes
|
13
|
+
load attributes.resources.members, attributes.resources
|
14
|
+
|
15
|
+
@links = attributes.links
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Attempts to retrieve the Resource Schema from the API based on the name
|
20
|
+
# of the subclass.
|
21
|
+
class << self
|
22
|
+
def api_client( client = nil )
|
23
|
+
metaclass.instance_eval do
|
24
|
+
define_method :api do
|
25
|
+
block_given? ? yield : client
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def schema
|
31
|
+
if self.respond_to? :api
|
32
|
+
class_name = self.to_s.split('::').last.downcase
|
33
|
+
|
34
|
+
api.description.resources.schema.send(class_name).properties
|
35
|
+
else
|
36
|
+
raise MissingAPIReference,
|
37
|
+
"This Resource needs a class accessor defined as " +
|
38
|
+
"`.api` that references an instance of Frenetic."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def metaclass
|
45
|
+
metaclass = class << self; self; end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def metaclass
|
52
|
+
metaclass = class << self; self; end
|
53
|
+
end
|
54
|
+
|
55
|
+
def load( keys, attributes )
|
56
|
+
keys.each do |key|
|
57
|
+
instance_variable_set "@#{key}", attributes[key.to_s]
|
58
|
+
|
59
|
+
metaclass.class_eval do
|
60
|
+
define_method key do
|
61
|
+
instance_variable_get( "@#{key}")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Stolen from https://github.com/aetherknight/recursive-open-struct/blob/master/lib/recursive_open_struct.rb
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
class RecursiveOpenStruct < OpenStruct
|
5
|
+
|
6
|
+
def new_ostruct_member(name)
|
7
|
+
name = name.to_sym
|
8
|
+
unless self.respond_to?(name)
|
9
|
+
class << self; self; end.class_eval { define_accessors name, name }
|
10
|
+
end
|
11
|
+
name
|
12
|
+
end
|
13
|
+
|
14
|
+
def keys
|
15
|
+
@table.keys
|
16
|
+
end
|
17
|
+
|
18
|
+
###
|
19
|
+
|
20
|
+
def self.define_accessors( *args )
|
21
|
+
define_getter *args
|
22
|
+
define_setter *args
|
23
|
+
define_getter_as_a_hash *args
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.define_getter( method_name, hash_key )
|
27
|
+
define_method( method_name ) do
|
28
|
+
v = @table[hash_key]
|
29
|
+
v.is_a?(Hash) ? self.class.new(v) : v
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.define_setter( method_name, hash_key )
|
34
|
+
define_method("#{method_name}=") { |x| modifiable[hash_key] = x }
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.define_getter_as_a_hash( method_name, hash_key )
|
38
|
+
define_method("#{method_name}_as_a_hash") { @table[hash_key] }
|
39
|
+
end
|
40
|
+
|
41
|
+
###
|
42
|
+
|
43
|
+
def debug_inspect(indent_level = 0, recursion_limit = 12)
|
44
|
+
display_recursive_open_struct(@table, indent_level, recursion_limit)
|
45
|
+
end
|
46
|
+
|
47
|
+
def display_recursive_open_struct(ostrct_or_hash, indent_level, recursion_limit)
|
48
|
+
|
49
|
+
if recursion_limit <= 0 then
|
50
|
+
# protection against recursive structure (like in the tests)
|
51
|
+
puts ' '*indent_level + '(recursion limit reached)'
|
52
|
+
else
|
53
|
+
#puts ostrct_or_hash.inspect
|
54
|
+
if ostrct_or_hash.is_a?(self.class) then
|
55
|
+
ostrct_or_hash = ostrct_or_hash.marshal_dump
|
56
|
+
end
|
57
|
+
|
58
|
+
# We'll display the key values like this : key = value
|
59
|
+
# to align display, we look for the maximum key length of the data that will be displayed
|
60
|
+
# (everything except hashes)
|
61
|
+
data_indent = ostrct_or_hash
|
62
|
+
.reject { |k, v| v.is_a?(self.class) || v.is_a?(Hash) }
|
63
|
+
.max {|a,b| a[0].to_s.length <=> b[0].to_s.length}[0].length
|
64
|
+
# puts "max length = #{data_indent}"
|
65
|
+
|
66
|
+
ostrct_or_hash.each do |key, value|
|
67
|
+
if (value.is_a?(self.class) || value.is_a?(Hash)) then
|
68
|
+
puts ' '*indent_level + key.to_s + '.'
|
69
|
+
display_recursive_open_struct(value, indent_level + 1, recursion_limit - 1)
|
70
|
+
else
|
71
|
+
puts ' '*indent_level + key.to_s + ' '*(data_indent - key.to_s.length) + ' = ' + value.inspect
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: http://example.org:5447/api
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
string: ''
|
9
|
+
headers:
|
10
|
+
Accept-Encoding:
|
11
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
12
|
+
Accept:
|
13
|
+
- ! '*/*'
|
14
|
+
User-Agent:
|
15
|
+
- Ruby
|
16
|
+
response:
|
17
|
+
status:
|
18
|
+
code: 200
|
19
|
+
message: ! 'OK '
|
20
|
+
headers:
|
21
|
+
Content-Type:
|
22
|
+
- application/json
|
23
|
+
Content-Length:
|
24
|
+
- '620'
|
25
|
+
Server:
|
26
|
+
- WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
|
27
|
+
Date:
|
28
|
+
- Sun, 08 Apr 2012 02:09:53 GMT
|
29
|
+
Connection:
|
30
|
+
- Keep-Alive
|
31
|
+
body:
|
32
|
+
encoding: US-ASCII
|
33
|
+
string: ! '{"_links":{"self":{"href":"/api/"},"inkers":{"href":"/api/inkers"},"inker":{"href":"/api/inker/{id}"},"sessions":{"href":"/api/sessions"}},"_embedded":{"schema":{"_links":{"self":{"href":"/api/schema"}},"mediaType":"application/vnd.customink-inker_directory-v1+json","inker":{"description":"A
|
34
|
+
CustomInk employee","type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"city":{"type":"string"},"images":{"type":"array","items":{"type":"object"}}}},"session":{"description":"A
|
35
|
+
gatewat for generating an authenticated session.","type":"object","properties":{"st-ticket-FOO":"string"}}}}}'
|
36
|
+
http_version: !!null
|
37
|
+
recorded_at: Sun, 08 Apr 2012 02:09:53 GMT
|
38
|
+
recorded_with: VCR 2.0.1
|
@@ -0,0 +1,77 @@
|
|
1
|
+
describe Frenetic::Configuration do
|
2
|
+
let(:content_type) { 'application/vnd.frenetic-v1-hal+json' }
|
3
|
+
let(:yaml_config) {
|
4
|
+
{ 'test' => {
|
5
|
+
'url' => 'http://example.org',
|
6
|
+
'api_key' => '1234567890',
|
7
|
+
'headers' => {
|
8
|
+
'accept' => content_type,
|
9
|
+
},
|
10
|
+
'request' => {
|
11
|
+
'timeout' => 10000
|
12
|
+
}
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|
16
|
+
let(:config) { Frenetic::Configuration.new( unknown: :option ) }
|
17
|
+
|
18
|
+
subject { config }
|
19
|
+
|
20
|
+
describe ".configuration" do
|
21
|
+
include FakeFS::SpecHelpers
|
22
|
+
|
23
|
+
context "with a proper config YAML" do
|
24
|
+
before do
|
25
|
+
FileUtils.mkdir_p("config")
|
26
|
+
File.open( 'config/frenetic.yml', 'w') do |f|
|
27
|
+
f.write( YAML::dump(yaml_config) )
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it { should include(:username) }
|
32
|
+
it { should include(:url) }
|
33
|
+
|
34
|
+
it { should_not include(:unknown => 'option')}
|
35
|
+
it "should set default request options" do
|
36
|
+
subject[:request][:timeout].should == 10000
|
37
|
+
end
|
38
|
+
it "should set a User Agent request header" do
|
39
|
+
subject[:headers][:user_agent].should =~ %r{Frenetic v.+; \S+$}
|
40
|
+
end
|
41
|
+
|
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')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
context "without a specified Accept header" do
|
48
|
+
let(:content_type) { nil }
|
49
|
+
|
50
|
+
it "should set an Accept request header" do
|
51
|
+
subject[:headers].should include(:accept => 'application/hal+json')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "with no config YAML" do
|
57
|
+
context "and no passed options" do
|
58
|
+
it "should raise a configuration error" do
|
59
|
+
expect { Frenetic::Configuration.new }.to raise_error( Frenetic::Configuration::ConfigurationError )
|
60
|
+
end
|
61
|
+
end
|
62
|
+
context "and passed options" do
|
63
|
+
let(:config) { Frenetic::Configuration.new( 'url' => 'http://example.org' ) }
|
64
|
+
|
65
|
+
it { should be_a( Hash ) }
|
66
|
+
it { should_not be_empty }
|
67
|
+
it "should set an Accepts request header" do
|
68
|
+
subject[:headers].should include(:accept => 'application/hal+json')
|
69
|
+
end
|
70
|
+
it "should set a User Agent request header" do
|
71
|
+
subject[:headers][:user_agent].should =~ %r{Frenetic v.+; \S+$}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
describe Frenetic::HalJson::ResponseWrapper do
|
2
|
+
let(:properties) do
|
3
|
+
{ 'a' => 1, 'b' => 2 }
|
4
|
+
end
|
5
|
+
let(:wrapper) { Frenetic::HalJson::ResponseWrapper.new( properties ) }
|
6
|
+
|
7
|
+
subject { wrapper }
|
8
|
+
|
9
|
+
describe "#members" do
|
10
|
+
subject { wrapper.members }
|
11
|
+
|
12
|
+
its(:size) { should == 2 }
|
13
|
+
its(:first) { should == 'a' }
|
14
|
+
its(:last) { should == 'b' }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#keys" do
|
18
|
+
subject { wrapper.keys }
|
19
|
+
|
20
|
+
it { should == wrapper.members }
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#each" do
|
24
|
+
before do
|
25
|
+
@items = []
|
26
|
+
wrapper.each do |*args|
|
27
|
+
@items << args
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it { should be_a Frenetic::HalJson::ResponseWrapper }
|
32
|
+
it "should iterate over each getter" do
|
33
|
+
@items.should == [ ['a',1], ['b',2] ]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe ".define_setter" do
|
38
|
+
subject { wrapper.methods(false) }
|
39
|
+
|
40
|
+
it "should not create setters" do
|
41
|
+
subject.none? { |name| name.to_s =~ %r{=} }.should be_true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe ".define_getter" do
|
46
|
+
context "with a :_links property" do
|
47
|
+
let(:properties) { { '_links' => 'foo' } }
|
48
|
+
|
49
|
+
it "should create a :links property" do
|
50
|
+
wrapper.links.should == 'foo'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "with a :_embedded property" do
|
55
|
+
let(:properties) { { '_embedded' => 'foo' } }
|
56
|
+
|
57
|
+
it "should create a :resources property" do
|
58
|
+
wrapper.resources.should == 'foo'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with a :_embedded property" do
|
63
|
+
let(:properties) { { 'href' => 'foo' } }
|
64
|
+
|
65
|
+
it "should create a :url property" do
|
66
|
+
wrapper.url.should == 'foo'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
describe Frenetic::HalJson do
|
2
|
+
let(:hal_json) { Frenetic::HalJson.new }
|
3
|
+
let(:app_callbacks_stub) do
|
4
|
+
stub('FaradayCallbackStubs').tap do |cb_stub|
|
5
|
+
cb_stub.stubs('on_complete').yields env
|
6
|
+
end
|
7
|
+
end
|
8
|
+
let(:app_stub) do
|
9
|
+
stub('FaradayAppStub').tap do |app_stub|
|
10
|
+
app_stub.stubs(call: app_callbacks_stub)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
before { hal_json.instance_variable_set("@app", app_stub) }
|
15
|
+
|
16
|
+
subject { hal_json }
|
17
|
+
|
18
|
+
describe "#call" do
|
19
|
+
let(:env) { Hash.new(:status => 200) }
|
20
|
+
|
21
|
+
before do
|
22
|
+
hal_json.stubs(:on_complete)
|
23
|
+
|
24
|
+
hal_json.call(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should execute the on_complete callback" do
|
28
|
+
hal_json.should have_received(:on_complete).with(env)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#on_complete" do
|
33
|
+
context "with a successful response" do
|
34
|
+
let(:env) do
|
35
|
+
{
|
36
|
+
:status => 200,
|
37
|
+
:body => JSON.generate({
|
38
|
+
'_links' => {}
|
39
|
+
})
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
before { hal_json.on_complete(env) }
|
44
|
+
|
45
|
+
it "should parse the HAL+JSON response" do
|
46
|
+
env[:body].should be_a( Frenetic::HalJson::ResponseWrapper )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#success?" do
|
52
|
+
subject { hal_json.success?( env ) }
|
53
|
+
|
54
|
+
context "with a 200 OK response" do
|
55
|
+
let(:env) { {:status => 200 } }
|
56
|
+
|
57
|
+
it { should be_true }
|
58
|
+
end
|
59
|
+
context "with a 201 Created response" do
|
60
|
+
let(:env) { {:status => 201 } }
|
61
|
+
|
62
|
+
it { should be_true }
|
63
|
+
end
|
64
|
+
context "with a 204 No Content" do
|
65
|
+
let(:env) { {:status => 204 } }
|
66
|
+
|
67
|
+
it { should be_false }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
describe Frenetic::Resource do
|
2
|
+
|
3
|
+
@client = Frenetic.new('url' => 'http://example.org')
|
4
|
+
|
5
|
+
let(:resource) { Frenetic::Resource.new }
|
6
|
+
let(:description_stub) do
|
7
|
+
Frenetic::HalJson::ResponseWrapper.new('resources' => { 'schema' => { 'resource' =>
|
8
|
+
{ 'properties' => { 'foo' => 2 } }
|
9
|
+
} } )
|
10
|
+
end
|
11
|
+
|
12
|
+
subject { resource }
|
13
|
+
|
14
|
+
context "created from a Hash" do
|
15
|
+
let(:resource) { Frenetic::Resource.new( foo: 'bar' ) }
|
16
|
+
|
17
|
+
it { should respond_to(:foo) }
|
18
|
+
its(:links) { should be_empty }
|
19
|
+
end
|
20
|
+
|
21
|
+
context "created from a HAL-JSON response" do
|
22
|
+
let(:api_response) do
|
23
|
+
Frenetic::HalJson::ResponseWrapper.new(
|
24
|
+
'_links' => {
|
25
|
+
'_self' => { '_href' => 'bar' }
|
26
|
+
},
|
27
|
+
'foo' => 1,
|
28
|
+
'bar' => 2,
|
29
|
+
'_embedded' => {
|
30
|
+
'baz' => 'resource'
|
31
|
+
}
|
32
|
+
)
|
33
|
+
end
|
34
|
+
let(:resource_a) { Frenetic::Resource.new( api_response ) }
|
35
|
+
let(:resource_b) { Frenetic::Resource.new }
|
36
|
+
|
37
|
+
before do
|
38
|
+
@client.stubs(:description).returns( description_stub )
|
39
|
+
|
40
|
+
Frenetic::Resource.stubs(:api).returns( @client )
|
41
|
+
|
42
|
+
resource_a and resource_b
|
43
|
+
end
|
44
|
+
|
45
|
+
context "initialized with data" do
|
46
|
+
subject { resource_a }
|
47
|
+
|
48
|
+
its(:foo) { should == 1 }
|
49
|
+
it { should_not respond_to(:bar) }
|
50
|
+
its(:links) { should_not be_empty }
|
51
|
+
end
|
52
|
+
context "intiailized in sequence without data" do
|
53
|
+
subject { resource_b }
|
54
|
+
|
55
|
+
it { should_not respond_to(:foo) }
|
56
|
+
it { should_not respond_to(:bar) }
|
57
|
+
its(:links) { should be_empty }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe ".api_client" do
|
62
|
+
context "with a block" do
|
63
|
+
before { resource.class.api_client { @client } }
|
64
|
+
|
65
|
+
it "should reference the defined API client" do
|
66
|
+
subject.class.api.should == @client
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "with an argument" do
|
71
|
+
before { resource.class.api_client @client }
|
72
|
+
|
73
|
+
it "should reference the defined API client" do
|
74
|
+
subject.class.api.should == @client
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe ".schema" do
|
80
|
+
subject { resource.class.schema }
|
81
|
+
|
82
|
+
context "with a defined API Client" do
|
83
|
+
before do
|
84
|
+
@client.stubs(:description).returns( description_stub )
|
85
|
+
|
86
|
+
resource.class.api_client @client
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should return the schema for the specific resource" do
|
90
|
+
subject.should == description_stub.resources.schema.resource.properties
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context "without a defined API Client" do
|
95
|
+
before { Frenetic::Resource.stubs(:respond_to?).with(:api).returns( false ) }
|
96
|
+
|
97
|
+
it "should raise an error" do
|
98
|
+
expect { subject }.to raise_error(Frenetic::MissingAPIReference)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
describe Frenetic do
|
2
|
+
let(:client) { Frenetic.new }
|
3
|
+
let(:config) { {
|
4
|
+
:url => 'http://example.org:5447/api',
|
5
|
+
:api_key => '1234567890',
|
6
|
+
:version => 'v1'
|
7
|
+
} }
|
8
|
+
|
9
|
+
before { Frenetic::Configuration.stubs(:new).returns(config) }
|
10
|
+
|
11
|
+
subject { client }
|
12
|
+
|
13
|
+
it { should respond_to(:description) }
|
14
|
+
it { should respond_to(:connection) }
|
15
|
+
it { should respond_to(:conn) }
|
16
|
+
it { should respond_to(:get) }
|
17
|
+
it { should respond_to(:put) }
|
18
|
+
it { should respond_to(:post) }
|
19
|
+
it { should respond_to(:delete) }
|
20
|
+
|
21
|
+
its(:connection) { should be_a(Faraday::Connection) }
|
22
|
+
|
23
|
+
describe "#connection" do
|
24
|
+
before do
|
25
|
+
faraday_stub = Faraday.new
|
26
|
+
Faraday.stubs(:new).returns( faraday_stub )
|
27
|
+
|
28
|
+
client
|
29
|
+
end
|
30
|
+
|
31
|
+
subject { client.connection }
|
32
|
+
|
33
|
+
it { should be_a(Faraday::Connection) }
|
34
|
+
|
35
|
+
it "should be created" do
|
36
|
+
Faraday.should have_received(:new).with() { |config|
|
37
|
+
config.has_key? :url
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#description" do
|
43
|
+
subject do
|
44
|
+
VCR.use_cassette('description_success') do
|
45
|
+
client.description
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it { should be_a( Frenetic::HalJson::ResponseWrapper ) }
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#reload!" do
|
53
|
+
before do
|
54
|
+
VCR.use_cassette('description_success') do
|
55
|
+
client.description
|
56
|
+
end
|
57
|
+
|
58
|
+
client.reload!
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not have any non-Nil instance variables" do
|
62
|
+
client.instance_variables.none? { |s| client.instance_variable_get(s).nil? }.should be_false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spork'
|
2
|
+
ENV['RAILS_ENV'] ||= 'test'
|
3
|
+
|
4
|
+
Spork.prefork do
|
5
|
+
# Loading more in this block will cause your tests to run faster. However,
|
6
|
+
# if you change any configuration or code from libraries loaded here, you'll
|
7
|
+
# need to restart spork for it take effect.
|
8
|
+
|
9
|
+
require 'rspec'
|
10
|
+
require 'fakefs/spec_helpers'
|
11
|
+
require 'awesome_print'
|
12
|
+
require 'bourne'
|
13
|
+
require 'vcr'
|
14
|
+
|
15
|
+
RSpec.configure do |config|
|
16
|
+
config.filter_run :focus => true
|
17
|
+
config.run_all_when_everything_filtered = true
|
18
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
19
|
+
config.mock_with :mocha
|
20
|
+
end
|
21
|
+
|
22
|
+
VCR.configure do |c|
|
23
|
+
c.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
|
24
|
+
c.hook_into :webmock
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Spork.each_run do
|
29
|
+
# This code will be run each time you run your specs.
|
30
|
+
require 'frenetic'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: frenetic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.alpha1
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Derek Lindahl
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-16 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: faraday
|
16
|
+
requirement: &2156831400 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.8.0.rc2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2156831400
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: addressable
|
27
|
+
requirement: &2156830640 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.2.7
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2156830640
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: patron
|
38
|
+
requirement: &2156829960 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.4.18
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2156829960
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: guard-spork
|
49
|
+
requirement: &2156829240 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.6.0
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2156829240
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: guard-rspec
|
60
|
+
requirement: &2156828580 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 0.7.0
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2156828580
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: &2156827880 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 2.9.0
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *2156827880
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: bourne
|
82
|
+
requirement: &2156827200 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 1.1.2
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *2156827200
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: webmock
|
93
|
+
requirement: &2156826480 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ~>
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 1.8.6
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *2156826480
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: vcr
|
104
|
+
requirement: &2156825880 !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 2.0.1
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: *2156825880
|
113
|
+
description: An opinionated Ruby-based Hypermedia API client.
|
114
|
+
email:
|
115
|
+
- dlindahl@customink.com
|
116
|
+
executables: []
|
117
|
+
extensions: []
|
118
|
+
extra_rdoc_files: []
|
119
|
+
files:
|
120
|
+
- .gitignore
|
121
|
+
- .rvmrc
|
122
|
+
- .travis.yml
|
123
|
+
- Gemfile
|
124
|
+
- Guardfile
|
125
|
+
- LICENSE
|
126
|
+
- README.md
|
127
|
+
- Rakefile
|
128
|
+
- frenetic.gemspec
|
129
|
+
- lib/frenetic.rb
|
130
|
+
- lib/frenetic/configuration.rb
|
131
|
+
- lib/frenetic/hal_json.rb
|
132
|
+
- lib/frenetic/hal_json/response_wrapper.rb
|
133
|
+
- lib/frenetic/resource.rb
|
134
|
+
- lib/frenetic/version.rb
|
135
|
+
- lib/recursive_open_struct.rb
|
136
|
+
- spec/fixtures/vcr_cassettes/description_success.yml
|
137
|
+
- spec/lib/frenetic/configuration_spec.rb
|
138
|
+
- spec/lib/frenetic/hal_json/response_wrapper_spec.rb
|
139
|
+
- spec/lib/frenetic/hal_json_spec.rb
|
140
|
+
- spec/lib/frenetic/resource_spec.rb
|
141
|
+
- spec/lib/frenetic_spec.rb
|
142
|
+
- spec/spec_helper.rb
|
143
|
+
homepage: http://dlindahl.github.com/frenetic/
|
144
|
+
licenses: []
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
none: false
|
151
|
+
requirements:
|
152
|
+
- - ! '>='
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ! '>'
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: 1.3.1
|
161
|
+
requirements: []
|
162
|
+
rubyforge_project:
|
163
|
+
rubygems_version: 1.8.10
|
164
|
+
signing_key:
|
165
|
+
specification_version: 3
|
166
|
+
summary: Here lies a Ruby-based Hypermedia API client that expects HAL+JSON and makes
|
167
|
+
a lot of assumptions about your API.
|
168
|
+
test_files:
|
169
|
+
- spec/fixtures/vcr_cassettes/description_success.yml
|
170
|
+
- spec/lib/frenetic/configuration_spec.rb
|
171
|
+
- spec/lib/frenetic/hal_json/response_wrapper_spec.rb
|
172
|
+
- spec/lib/frenetic/hal_json_spec.rb
|
173
|
+
- spec/lib/frenetic/resource_spec.rb
|
174
|
+
- spec/lib/frenetic_spec.rb
|
175
|
+
- spec/spec_helper.rb
|