frenetic 0.0.1.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|