frenetic 0.0.1.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rspec
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use ruby-1.9.2-p290@frenetic > /dev/null
@@ -0,0 +1,4 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
4
+ script: bundle exec rspec --require spec_helper --order rand --color --format documentation
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in frenetic.gemspec
4
+ gemspec
5
+
6
+ gem "awesome_print"
7
+ gem "fakefs", '~> 0.4.0', :require => false
@@ -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.
@@ -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&bull;net&bull;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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -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
@@ -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,3 @@
1
+ class Frenetic
2
+ VERSION = "0.0.1.alpha1"
3
+ 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
@@ -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