commute 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in commute.gemspec
4
+ gemspec
5
+
6
+ # Partially solves https://github.com/typhoeus/typhoeus/pull/171.
7
+ gem 'typhoeus', :git => 'https://github.com/challengee/typhoeus.git'
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'minitest' do
2
+ watch(%r|^spec/(.*)_spec\.rb|)
3
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
4
+ watch(%r|^spec/spec_helper\.rb|) { "spec" }
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Mattias Putman
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,262 @@
1
+ # Commute
2
+ [![Build Status](https://secure.travis-ci.org/challengee/commute.png)](http://travis-ci.org/challengee/commute)
3
+ [![Dependency Status](https://gemnasium.com/challengee/commute.png?travis)](https://gemnasium.com/challengee/commute)
4
+
5
+ Commute helps you to:
6
+
7
+ * Dynamically build HTTP requests for your favorite HTTP library (currently only Typhoeus).
8
+ * Easily process request and response bodies.
9
+ * Execute parallel HTTP requests.
10
+
11
+ ## Contexts and Api's
12
+
13
+ Almost everything in Commute is a context. Contexts represent all the information that Commute needs to build, execute and process your requests. This includes:
14
+
15
+ * **Request options**: url, body, url params, headers, ....
16
+ * **Custom options**: custom named parameters that can be translated in request options.
17
+ * **Stack options**: used to provide information on how request/response bodies are processed.
18
+ * **A Stack**: A sequence of layers that a request/response body is pushed through to be processed (these layers user the stack options).
19
+ * A reference to an **Api** that is responsible for executing the request.
20
+
21
+ Api's are nothing more than a set of predefined, named contexts. If you think about it, options and stack layers needed for certain Api calls can be seen as an extension as a context. So these named contexts (aka Api calls) take a context and extend it with the needed information.
22
+
23
+ ## Stacks
24
+
25
+ Stacks are responsible for processing request and response bodies. A stack consists of a request and response `sequence`. The response sequence is generally the inverse of the request sequence.
26
+
27
+ A sequence consists of an ordered list of layers. Each layer provides some logic to transform a body according to some options. For example a chemicals layer could parse or render an xml document based on a given template. This would look like this:
28
+
29
+ ```ruby
30
+ class Chemicals < Layer
31
+ def request body, template
32
+ template.render body
33
+ end
34
+
35
+ def response body, template
36
+ template.parse body
37
+ end
38
+ end
39
+ ```
40
+
41
+ When a context is built into a request. The body is ran through the request sequence. When the request is completed, the response body is ran through the response sequence.
42
+
43
+ ## Building Contexts
44
+
45
+ ### Extending Contexts 101
46
+
47
+ Commute is all about building partial contexts and extending them until enough information is present to build a real HTTP request. Basically it works like this:
48
+
49
+ ```ruby
50
+ # Assume there is an Api "GistApi".
51
+ # This would create a context containing a custom option 'user'
52
+ # and then one with a context-type header.
53
+ defunkt_api = GistApi.with(user: 'defunkt')
54
+ defunkt_xml_api = defunkt_api.with(headers: {
55
+ 'Content-Type' => 'application/xml'
56
+ })
57
+ ```
58
+
59
+ Any further context extensions or api calls on `defunkt_xml_api` would thus be in xml and for the user 'defunkt'.
60
+
61
+ For example, you could take an vanilla api, extend it to a context that contains needed authentication parameters, and pass that context through your app acting as an api that does not need authentication. Here lies the power of commute: A context will act as an api with some predefined options and behavior.
62
+
63
+ ### Default and raw contexts
64
+
65
+ Without you knowing, `GistApi.with` extended a special context of the `GistApi` called the `default` context. It contains options that every requests for this api will be needing. When you don't want this you can use the `raw` context.
66
+
67
+ Example:
68
+
69
+ ```ruby
70
+ class GistApi < Commute::Api
71
+ def default
72
+ # Extends the raw context with some options.
73
+ # Every request/response will be json.
74
+ raw.with headers: {
75
+ 'Content-Type' => 'application/json',
76
+ 'Accept' => 'application/json'
77
+ }
78
+ end
79
+ end
80
+
81
+ defunkt = GistApi.with(user: 'defunkt')
82
+ defunkt.options.inspect
83
+ # => { user: 'defunkt', headers: {
84
+ 'Content-Type' => 'application/json',
85
+ 'Accept' => 'application/json'
86
+ }}
87
+ ```
88
+
89
+ ### Making basic Api calls
90
+
91
+ Let's say we want to create an Api call that fetches all gists for a certain user. This call would extend the context by using the custom `:user` option and putting it in the url.
92
+
93
+ ```ruby
94
+ class GistApi < Commute::Api
95
+ def default
96
+ # Extends the raw context with some options.
97
+ # Every request/response will be json.
98
+ raw.with headers: {
99
+ 'Content-Type' => 'application/json',
100
+ 'Accept' => 'application/json'
101
+ }
102
+ end
103
+
104
+ def for_user context
105
+ context.with url: "https://api.github.com/users/#{context[:user]}/gists"
106
+ end
107
+ end
108
+
109
+ # A context for getting all gists from defunkt.
110
+ gists = GistApi.for_user user: 'defunkt'
111
+
112
+ # or, you can play with it. See the potential?
113
+ gists = GistApi.with(user: 'defunkt').for_user
114
+ ```
115
+
116
+ Internally, those last two methods of creating the gists context are the same. When calling an api method from a context, passed arguments are used to create a context to be passed to the api call. That is why the argument of an api call is always `context`. The first method is only a convenient shortcut.
117
+
118
+ ### A basic stack
119
+
120
+ If we would want to parse and encode json for every request and response we could create a layer like this:
121
+
122
+ ```ruby
123
+ class Json < Commute::Layer
124
+ # Enode on request.
125
+ def request body, options
126
+ Yajl::Encoder.encode body, options
127
+ end
128
+
129
+ # Decode on response.
130
+ def response body, options
131
+ Yajl::Parser.parse body, options
132
+ end
133
+ end
134
+ ```
135
+
136
+ And use it in our default context:
137
+
138
+ ```ruby
139
+ def default
140
+ # Extends the raw context with some options.
141
+ # Every request/response will be json.
142
+ raw.with headers: {
143
+ 'Content-Type' => 'application/json',
144
+ 'Accept' => 'application/json'
145
+ } do |stack|
146
+ stack << Json.new(:format)
147
+ end
148
+ end
149
+ ```
150
+
151
+ When adding a layer to a stack, you can give it a name. This can then later be used in stack altering or as a reference to pass options to the stack.
152
+
153
+ ### Extending Contexts 201
154
+
155
+ We only showed simple context extending using some extra parameters. As said, contexts also define behavior (How does a request/response body have to be processed?) using stacks.
156
+
157
+ On extending a context, the stack can be altered.
158
+
159
+ Lat's say that in the basic stack, we want to send raw json, but we still want the stack to automatically parse json for the response. We could create a new context that does this:
160
+
161
+ ```ruby
162
+ raw_requests = GistApi.with { |stack|
163
+ # Dump the format layer in the request sequence.
164
+ stack.request.without! :format
165
+ }
166
+ ```
167
+
168
+ Other stack altering methods are:
169
+
170
+ * `before!`: inserts a layer before a certain named layer.
171
+ * `after!`: inserts a layer after a certain named layer.
172
+
173
+ ### Passing options to the stack
174
+
175
+ Naming layers has the advantage that they can be used to pass options to the layer. In the case of the `Json` layer, we could pass options to `yajl`:
176
+
177
+ ```ruby
178
+ symbolized_api = GistApi.with(format: {
179
+ symbolize_keys: true
180
+ })
181
+
182
+ # Now all further extensions will return bodies with symbolized keys.
183
+ ```
184
+
185
+ ### Authorization
186
+
187
+ A authorization mechanism can be implemented for an api by implementing the `authorize(context)` method. This does not follow the standard context system because Authorization headers can be time sensitive. The Authorization header is computed just before the requests is actually fired.
188
+
189
+ ### Hooks
190
+
191
+ Commute provides a before (default) and after hook. They can be used to set up default context options/behavior or finishing up a context before it can be built.
192
+
193
+ We already showed how to provide `default` contexts. The after hook can be provided by implementing `after(context)`.
194
+
195
+ ## Building requests and executing them.
196
+
197
+ We now know how to build a context, it's time to turn those contexts into requests and firing them onto the internets.
198
+
199
+ ### Building requests.
200
+
201
+ When you build a request from a context, you receive a pure `Typhoeus::Request`. You can do with it what you want, the context system just acts as a builder solution. You can pass a block to the build method that gets called when the request completes. This block gets called with:
202
+
203
+ * The pure `Typhoeus::Response` as it was received from typhoeus.
204
+ * A transformed response body (went through the stack).
205
+
206
+ For example:
207
+
208
+ ```ruby
209
+ request = GistApi.for_user(user: 'defunkt').build do |response, gists|
210
+ puts gists.count
211
+ end
212
+ ```
213
+
214
+ ### Executing requests
215
+
216
+ Executing requests is done through the `rush` method on a context. It executes the given request, runs the response through the stack and call the callback block.
217
+
218
+ Building on the previous example that would go like this:
219
+
220
+ ```ruby
221
+ GistApi.rush request
222
+ # => 10
223
+ ```
224
+
225
+ Remember that `GistApi.rush` calls the rush method on the `default` context. It would be the same as calling:
226
+
227
+ ```ruby
228
+ GistApi.default.rush request
229
+ ```
230
+
231
+ The `rush` method can be called on every context, it will be router to the underlaying Api.
232
+
233
+ ### Queueing requests for parallel execution
234
+
235
+ Using the `commute` method you can queue requests for later execution. It simply works like this (Let's add some coolness for fun):
236
+
237
+ ```ruby
238
+ # Define a callback.
239
+ callback = Proc.new { |response, gists|
240
+ puts gists.count
241
+ }
242
+ # Build and queue some requests.
243
+ ['defunkt', 'challengee'].map { |user|
244
+ GistApi.commute GistApi.for_user(user: user).build
245
+ }
246
+ # Execute all requests in parallel
247
+ GistApi.rush
248
+ # => 10
249
+ # => 0
250
+ ```
251
+
252
+ ### Error handling
253
+
254
+ Commute does not provide any help with handling errors. This is done purely through the underlaying HTTP client that is used (here Typhoeus).
255
+
256
+ In the callback one can for example check if the requests has timed out by issueing `response.timed_out?`.
257
+
258
+ ## What's next?
259
+
260
+ * I would like an "adapter" system so that `Typhoeus` would just be an adapter.
261
+ * Adding support for `em-http-request`.
262
+ * Caching support (As a special adapter?).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new(:spec) do |t|
6
+ t.libs = ['lib', 'spec']
7
+ t.test_files = FileList['spec/**/*_spec.rb']
8
+ end
9
+
10
+ task :default => :spec
data/commute.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/commute/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.authors = ["Mattias Putman"]
6
+ s.email = ["mattias.putman@gmail.com"]
7
+ s.description = %q{Handling the HTTP commute like a boss}
8
+ s.summary = %q{Commute helps you to: 1) Dynamically build HTTP requests for your favorite HTTP library (currently only Typhoeus). 2) Easily process request and response bodies. 3) Execute parallel HTTP requests.}
9
+ s.homepage = "http://challengee.github.com/commute/"
10
+
11
+ s.files = `git ls-files`.split($\)
12
+ s.test_files = s.files.grep(%r{^(spec)/})
13
+ s.name = "commute"
14
+ s.require_paths = ["lib"]
15
+ s.version = Commute::VERSION
16
+
17
+ s.add_dependency 'typhoeus'
18
+
19
+ s.add_development_dependency 'rake'
20
+ s.add_development_dependency 'mocha'
21
+ s.add_development_dependency 'guard'
22
+ s.add_development_dependency 'guard-minitest'
23
+ s.add_development_dependency 'yard'
24
+ s.add_development_dependency 'simplecov'
25
+ s.add_development_dependency 'webmock'
26
+
27
+ # For the examples.
28
+ s.add_development_dependency 'yajl-ruby'
29
+ end
@@ -0,0 +1,13 @@
1
+ module Commute
2
+ module Typhoeus
3
+
4
+ class ContextualRequest < ::Typhoeus::Request
5
+ attr_reader :context
6
+
7
+ def initialize context, url, options = {}
8
+ @context = context
9
+ super(url, options)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,86 @@
1
+ require 'singleton'
2
+ require 'forwardable'
3
+
4
+ require 'typhoeus'
5
+
6
+ require 'commute/context'
7
+
8
+ module Commute
9
+
10
+ # An Api holds:
11
+ # * Contexts of defaults.
12
+ # * Named contexts (= Api calls)
13
+ #
14
+ # Every Api is a singleton, it contains no state, only some name configurations (contexts).
15
+ class Api
16
+ include Singleton
17
+
18
+ # @!attribute queue
19
+ # @return List of requests waiting for execution.
20
+ attr_reader :queue
21
+
22
+ class << self
23
+ extend Forwardable
24
+
25
+ def_delegators :instance, :default, :raw, :with
26
+
27
+ # Call missing methods on the default context.
28
+ def method_missing name, *args, &block
29
+ default.send name, *args, &block
30
+ end
31
+ end
32
+
33
+ # Initializes an Api with a parallel manager.
34
+ def initialize
35
+ @queue = []
36
+ @hydra = ::Typhoeus::Hydra.new
37
+ end
38
+
39
+ # A pretty standard starting point is the `default` context.
40
+ # An Api class can implement it to provide some default options and stack layers.
41
+ #
42
+ # @return [Context] A default context.
43
+ def default
44
+ @default ||= raw
45
+ end
46
+
47
+ # Get a raw context without any defaults.
48
+ #
49
+ # @return [Context] A raw context for this api.
50
+ def raw
51
+ @raw ||= Context.new self, {}, Stack.new
52
+ end
53
+
54
+ # Start scoping on this Api.
55
+ # Creates a context with provided options and stack.
56
+ #
57
+ # @return [Context] The created context.
58
+ def with options = {}, &stack_mod
59
+ default.with options, &stack_mod
60
+ end
61
+
62
+ # Queue a request for later parallel execution.
63
+ #
64
+ # @param request [Typhoeus::Request] The request to queue.
65
+ def commute request
66
+ @queue << request
67
+ end
68
+
69
+ # Executes all requests in the queue in parallel.
70
+ #
71
+ # @param request [Typhoeus::Request] Last request to add to the queue.
72
+ # Shortcut to commute and rush one request in one line.
73
+ def rush request = nil
74
+ commute request if request
75
+ # Authorize each request right before executing
76
+ @queue.each do |request|
77
+ request.headers['Authorization'] = authorize(request.context) if respond_to? :authorize
78
+ @hydra.queue request
79
+ end
80
+ # Clear the queue.
81
+ @queue.clear
82
+ # Run all requests.
83
+ @hydra.run
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,154 @@
1
+ require 'forwardable'
2
+
3
+ require 'commute/stack'
4
+ require 'commute/adapters/typhoeus'
5
+
6
+ module Commute
7
+
8
+ # A context represents all what can better define a HTTP request/response trip. This includes:
9
+ # * An url.
10
+ # * A partial request/response stack that defines request/response body transformations.
11
+ # * Partial options for the request, the stack or behavior of certain {Api} calls.
12
+ #
13
+ # Contexts work incrementally to provide a full context (all required options and behaviour)
14
+ # for a HTTP request/response trip.
15
+ #
16
+ # Every Context has notion of an underlying api to easily make requests.
17
+ #
18
+ # Contexts can be extended through {#with}. This way you can add options/override options,
19
+ # alter the stack or specify an url.
20
+ #
21
+ class Context
22
+ extend Forwardable
23
+
24
+ # @!attribute options
25
+ # @return The options of the context.
26
+ attr_accessor :options
27
+
28
+ # @!attribute stack
29
+ # @return The stack of the context.
30
+ attr_accessor :stack
31
+
32
+ # Create a new Context.
33
+ #
34
+ # @param api [Api] The underlying api.
35
+ # @param options [Hash] all kinds of options (for the request, api call and stack layers).
36
+ # @param stack [Stack] The request/response stack.
37
+ def initialize api, options, stack
38
+ @api = api
39
+ @options = options
40
+ @stack = stack
41
+ end
42
+
43
+ # @!method [](key)
44
+ # @param key [Symbol] The key to get from the options.
45
+ # @return [Object] The value associated with that key.
46
+ def_delegator :@options, :[], :[]
47
+
48
+ # @!method commute(request)
49
+ # Queues a request for later parallel execution.
50
+ # @param request [Typhoeus::Request] The request to queue for later execution.
51
+ def_delegator :@api, :commute, :commute
52
+
53
+ # @!method rush
54
+ # Executes all the requests in the queue of the api.
55
+ def_delegator :@api, :rush, :rush
56
+
57
+ # Create a new context from this context by providing new options and stack modifications.
58
+ #
59
+ # @param options [Hash] Options for the new {Context}.
60
+ #
61
+ # @yieldparam stack [Stack] The stack in this {Context}.
62
+ # @yieldreturn [Stack] The modified {Stack}.
63
+ def with options = {}
64
+ # Alter the stack if necessary
65
+ stack = @stack.clone
66
+ yield(stack) if block_given?
67
+ # Create a new context.
68
+ Context.new @api, mix(@options, options), stack
69
+ end
70
+
71
+ # Builds a Typhoeus::Request from the context.
72
+ #
73
+ # @todo This should work with some kind of Typhoeus adapter.
74
+ # @todo Actually the prepare method should only be called right before the request
75
+ # is sent. We do not take queueing into account here. Timestamps for authorization could
76
+ # expire. Can be solved by extending Typhoeus::Request with an UnpreparedRequest and
77
+ # preparing it right before it is sent.
78
+ #
79
+ # @yieldparam response [Typhoeus::Response] The response object as we got it from Typhoeus.
80
+ # @yieldparam result [Object] The result of the body after going through the {Stack}.
81
+ #
82
+ # @return [Typhoeus::Request] The built Typhoeus Request.
83
+ def build &callback
84
+ # Call after hook if necessary.
85
+ context = if @api.respond_to? :after
86
+ @api.after self
87
+ else
88
+ self
89
+ end
90
+ # Build the real context (with or without after hook).
91
+ context.build_without_after &callback
92
+ end
93
+
94
+ # Builds the context without calling the after hook.
95
+ #
96
+ # @private
97
+ def build_without_after &callback
98
+ # Run the request body through the stack.
99
+ body = self[:body]
100
+ raw_body = if body && body != ''
101
+ @stack.run_request body, options
102
+ else
103
+ body
104
+ end
105
+ # Build the Typhoeus Request.
106
+ options[:body] = raw_body
107
+ request = Typhoeus::ContextualRequest.new self, self[:url], options
108
+ # Attach an on_complete handler that wraps the callback.
109
+ request.on_complete do |response|
110
+ # Run the response body through the stack.
111
+ body = response.body
112
+ result = if body && body != '' && response.success?
113
+ @stack.run_response response.body, options
114
+ else
115
+ body
116
+ end
117
+ # Call the real commute callback.
118
+ callback.call response, result if callback
119
+ end
120
+ # Return the request.
121
+ request
122
+ end
123
+
124
+ # Any method missing should be a shortcut to an api call with the current context.
125
+ # The options and the block that are passed are used to create a new context.
126
+ # This context is then passed to the API call to return another new context.
127
+ def method_missing name, *args, &block
128
+ # Extract the options from the method arguments.
129
+ options = args.first || {}
130
+ # Create the new context.
131
+ context = with options, &block
132
+ # Call the Api. This returns a new context.
133
+ @api.send name, context
134
+ end
135
+
136
+ private
137
+
138
+ # Merge a 2-level deep hash of options.
139
+ #
140
+ # @param options [Hash] The base options hash.
141
+ # @param override_options [Hash] The options hash considered as more important.
142
+ #
143
+ # @return [Hash] A mixed hash.
144
+ def mix options, override_options
145
+ options.merge(override_options) { |key, old_value, new_value|
146
+ if old_value.kind_of? Hash
147
+ old_value.merge new_value
148
+ else
149
+ new_value
150
+ end
151
+ }
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,16 @@
1
+ module Commute
2
+
3
+ # @todo more docs and tests.
4
+ class Layer < Struct.new(:name)
5
+
6
+ # Default request forwards to the call method.
7
+ def request request, options
8
+ call request, options if respond_to? :call
9
+ end
10
+
11
+ # Default response forwards to the call method.
12
+ def response response, options
13
+ call response, options if respond_to? :call
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,104 @@
1
+ require 'forwardable'
2
+ require 'delegate'
3
+
4
+ require 'commute/layer'
5
+
6
+ module Commute
7
+
8
+ # A {Stack} is a sequence of layers that define a transfornation of a request/response body.
9
+ #
10
+ # Each {Layer} has a optional name. Options of the {Layer} can be passed in the
11
+ # {Context}'s options under the the equally named key.
12
+ #
13
+ # A Stack can both process a request and a response body (that can be either object,
14
+ # as long as it conforms with the first {Layer}).
15
+ class Stack
16
+ extend Forwardable
17
+
18
+ # Represents one way of the {Stack}
19
+ # Delegates methods to the underlying layer array.
20
+ class Sequence < SimpleDelegator
21
+
22
+ # Remove some layers from this sequence.
23
+ #
24
+ # @param names [<Symbol>] The names of the layers that must be removed.
25
+ def without! *names
26
+ reject! { |layer| names.include? layer.name }
27
+ end
28
+
29
+ # Insert a layer before another layer.
30
+ #
31
+ # @param reference [Symbol] The layer to insert before.
32
+ # @param insert [Layer] The layer to insert.
33
+ def before! reference, inserting
34
+ insert(index { |layer| layer.name == reference }, inserting)
35
+ end
36
+
37
+ # Insert a layer after another layer.
38
+ #
39
+ # @param reference [Symbol] The layer to insert after.
40
+ # @param insert [Layer] The layer to insert.
41
+ def after! reference, insert
42
+ insert((index { |layer| layer.name == reference }) + 1, insert)
43
+ end
44
+ end
45
+
46
+ # @!attribute request
47
+ # The request {Sequence} of the stack.
48
+ attr_accessor :request
49
+
50
+ # @!attribute response
51
+ # The response {Sequence} of the stack.
52
+ attr_accessor :response
53
+
54
+ # Create a new {Stack} with a few layers.
55
+ def initialize *layers
56
+ @request = Sequence.new layers
57
+ @response = Sequence.new layers.reverse
58
+ end
59
+
60
+ # @param layer [Layer] The layer to be added to the stack.
61
+ def << layer
62
+ @request << layer
63
+ @response.insert 0, layer
64
+ end
65
+
66
+ # Run a request body through the stack.
67
+ #
68
+ # @param body [Object] The request body.
69
+ # @param options [Hash] Options for the layers.
70
+ #
71
+ # @return [Object] the transformed body.
72
+ def run_request body, options = {}
73
+ @request.inject(body) do |body, layer|
74
+ # Run through layer to modfiy the request
75
+ layer.request body, options[layer.name] || {}
76
+ end
77
+ end
78
+
79
+ # Run a response body through the stack.
80
+ # We want the response to have the inverse manipulations
81
+ # so we invert the stack.
82
+ #
83
+ # @param body [Object] The request body.
84
+ # @param options [Hash] Options for the layers.
85
+ #
86
+ # @return [Object] the transformed body.
87
+ def run_response body, options = {}
88
+ @response.inject(body) do |body, layer|
89
+ # Run through layer to modfiy the request
90
+ layer.response body, options[layer.name] || {}
91
+ end
92
+ end
93
+
94
+ # Override the clone method to clone the layers.
95
+ #
96
+ # @return A duplicate of the stack.
97
+ def clone
98
+ stack = super
99
+ stack.request = stack.request.clone
100
+ stack.response = stack.response.clone
101
+ stack
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,3 @@
1
+ module Commute
2
+ VERSION = "0.1.2"
3
+ end
data/lib/commute.rb ADDED
@@ -0,0 +1,7 @@
1
+ require "commute/version"
2
+
3
+ require 'commute/api'
4
+
5
+ module Commute
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe Commute::Api do
4
+
5
+ class TestApi < Commute::Api
6
+ end
7
+
8
+ let(:default) { TestApi.with(user: 'mattias') }
9
+
10
+ let(:request1) { default.with(url: 'http://www.example.com', method: :get).build }
11
+ let(:request2) { default.with(url: 'http://www.example.com', method: :post).build }
12
+ let(:request3) { default.with(url: 'http://www.example.com', method: :delete).build }
13
+
14
+ after do
15
+ TestApi.instance.queue.clear
16
+ end
17
+
18
+ describe '#method_missing' do
19
+ before do
20
+ TestApi.stubs(:default).returns(default)
21
+ end
22
+
23
+ it 'should forward calls to the singleton with the default context' do
24
+ TestApi.instance.expects(:get).with { |c|
25
+ c.options.must_equal user: 'mattias'
26
+ }
27
+ TestApi.get
28
+ end
29
+
30
+ it 'should take no default context if the raw context is used' do
31
+ TestApi.instance.expects(:get).with { |c|
32
+ c.options.must_equal id: 1
33
+ }
34
+ TestApi.raw.get id: 1
35
+ end
36
+
37
+ it 'should scope on the default context' do
38
+ TestApi.instance.expects(:get).with { |c|
39
+ c.options.must_equal user: 'mattias', id: 1
40
+ }
41
+ TestApi.get id: 1
42
+ end
43
+ end
44
+
45
+ describe '#commute' do
46
+ it 'should add requests to the queue' do
47
+ default.commute request1
48
+ default.commute request2
49
+ TestApi.instance.queue.size.must_equal 2
50
+ end
51
+ end
52
+
53
+ describe '#rush' do
54
+ describe 'no authorize' do
55
+ before do
56
+ @stub_get = stub_request(:get, "http://www.example.com")
57
+ @stub_post = stub_request(:post, "http://www.example.com")
58
+ @stub_delete = stub_request(:delete, "http://www.example.com")
59
+
60
+ default.commute request1
61
+ default.commute request2
62
+ end
63
+
64
+ it 'should clear the queue' do
65
+ default.rush
66
+ TestApi.instance.queue.size.must_equal 0
67
+ end
68
+
69
+ it 'should run all requests' do
70
+ default.rush
71
+ assert_requested(@stub_get)
72
+ assert_requested(@stub_post)
73
+ end
74
+
75
+ it 'should make the additional requests given to rush' do
76
+ default.rush request3
77
+ assert_requested(@stub_get)
78
+ assert_requested(@stub_post)
79
+ assert_requested(@stub_delete)
80
+ end
81
+ end
82
+
83
+ describe 'authorize method provided' do
84
+ it 'should authorize if a methode is provided' do
85
+ request = default.with(url: 'http://www.example.com', method: :get).build
86
+ TestApi.instance.stubs(:authorize).returns 'Authorized'
87
+ stub_request(:get, "http://www.example.com")
88
+ # Actually make the request
89
+ default.rush request
90
+ # TestApi.instance.stubs(:authorize).returns('Authorized')
91
+ assert_requested(:get, "http://www.example.com", :times => 1) { |request|
92
+ request.headers['Authorization'] = 'Authorized'
93
+ }
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,140 @@
1
+ require 'spec_helper'
2
+ require 'webmock/minitest'
3
+
4
+ describe Commute::Context do
5
+
6
+ let(:context) {
7
+ Commute::Context.new api, options, stack
8
+ }
9
+
10
+ let(:stack) { Commute::Stack.new(stub, stub) }
11
+
12
+ let(:api) { stub }
13
+
14
+ let(:options) {
15
+ { body: 1, two: 2, params: { a: 'a' }}
16
+ }
17
+
18
+ describe '#with' do
19
+ it 'should keep its current options when the context is incremented' do
20
+ new_context = context.with body: 'one', params: { a: 'A', b: 'b' }
21
+ context.options.must_equal body: 1, two: 2, params: { a: 'a' }
22
+ new_context.options.must_equal body: 'one', two: 2, params: { a: 'A', b: 'b' }
23
+ end
24
+
25
+ it 'should not alter the current stack when the context is incremented' do
26
+ new_context = context.with do |stack|
27
+ stack << stub
28
+ end
29
+ context.stack.request.size.must_equal 2
30
+ new_context.stack.request.size.must_equal 3
31
+ end
32
+ end
33
+
34
+ describe '#[]' do
35
+ it 'should give the specified option' do
36
+ context[:body].must_equal 1
37
+ end
38
+ end
39
+
40
+ describe '#build' do
41
+ let(:stack) { stub_everything }
42
+
43
+ it 'should return a typhoeus request with the raw body' do
44
+ stack.expects(:run_request).with(1, options).returns 2
45
+ request = context.build
46
+ request.body.must_equal 2
47
+ end
48
+
49
+ describe 'when called with a callback' do
50
+ let(:options) {
51
+ {url: 'www.example.com', method: :get, body: 'sent'}
52
+ }
53
+
54
+ before do
55
+ # Stub the stack
56
+ stack.stubs(:run_request).with('sent', options).returns('sent transformed')
57
+ stack.stubs(:run_response).with('returned', options).returns('returned transformed')
58
+ end
59
+
60
+ describe 'when the requests succeeds with good status' do
61
+ it 'should call the callback with the original response and the transformed result' do
62
+ # Stub the http request.
63
+ stub_request(:get, 'www.example.com').with(body: 'sent transformed').to_return(body: 'returned')
64
+ # Build the request with a callback
65
+ callback = Proc.new {}
66
+ callback.expects(:call).with { |response, result|
67
+ response.body.must_equal 'returned'
68
+ result.must_equal 'returned transformed'
69
+ }
70
+ request = context.build &callback
71
+ # Run the request.
72
+ Typhoeus::Hydra.hydra.queue request
73
+ Typhoeus::Hydra.hydra.run
74
+ end
75
+ end
76
+
77
+ describe 'when the request succeeds with bad status' do
78
+ it 'should call the callback with the original response and no result' do
79
+ # Stub the http request.
80
+ stub_request(:get, 'www.example.com').with(body: 'sent transformed').to_return(\
81
+ status: [500, "Internal Server Error"])
82
+ # Response stack should never be triggered.
83
+ stack.expects(:run_response).never
84
+ # Build the request with a callback
85
+ callback = Proc.new {}
86
+ callback.expects(:call).with { |response, result|
87
+ response.code.must_equal 500
88
+ result.must_equal ''
89
+ }
90
+ request = context.build &callback
91
+ # Run the request.
92
+ Typhoeus::Hydra.hydra.queue request
93
+ Typhoeus::Hydra.hydra.run
94
+ end
95
+ end
96
+
97
+ describe 'when an after hook is configured' do
98
+ it 'should call it with the context' do
99
+ api.stubs(:after).with(context).returns(context.with(after: true))
100
+ request = context.build
101
+ request.context[:after].must_equal true
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ describe '#method_missing' do
108
+ it 'should call the api with an incremented context when only given options' do
109
+ api.expects(:test_api_call).with { |context|
110
+ context[:enhanced].must_equal true
111
+ }
112
+ context.test_api_call enhanced: true
113
+ end
114
+
115
+ it 'should call the api with an incremented context when given no options' do
116
+ api.expects(:test_api_call).with { |incr_context|
117
+ context.options.must_equal context.options
118
+ }
119
+ context.test_api_call
120
+ end
121
+
122
+ it 'should call the api with an incremented context when given options and a block' do
123
+ api.expects(:test_api_call).with { |context|
124
+ context[:enhanced].must_equal true
125
+ context.stack.request.size.must_equal 3
126
+ }
127
+ context.test_api_call enhanced: true do |stack|
128
+ stack << stub
129
+ end
130
+ end
131
+ end
132
+
133
+ it 'should forward commute and rush to the api' do
134
+ request = stub
135
+ api.expects(:commute).with(request)
136
+ api.expects(:rush)
137
+ context.commute request
138
+ context.rush
139
+ end
140
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Commute::Layer do
4
+
5
+ let(:layer) { Commute::Layer.new }
6
+
7
+ describe 'when there is no call method' do
8
+ it 'should do nothing' do
9
+ layer.request nil, nil
10
+ layer.response nil, nil
11
+ end
12
+ end
13
+
14
+ describe 'when there is a call methode' do
15
+ it 'should call it' do
16
+ request = stub
17
+ options = { test: 1 }
18
+ layer.expects(:call).with(request, options)
19
+ layer.request request, options
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe Commute::Stack do
4
+
5
+ let(:stack) { Commute::Stack.new }
6
+
7
+ let(:layer1) { stub.tap { |stub|
8
+ stub.stubs(:name).returns(:first)
9
+ }}
10
+
11
+ let(:layer2) { stub.tap { |stub|
12
+ stub.stubs(:name).returns(:second)
13
+ }}
14
+
15
+ let(:layer3) { stub.tap { |stub|
16
+ stub.stubs(:name).returns(:third)
17
+ }}
18
+
19
+ describe '#size' do
20
+ it 'should return the size of the stack' do
21
+ stack.request.size.must_equal 0
22
+ stack << stub
23
+ stack << stub
24
+ stack.request.size.must_equal 2
25
+ end
26
+ end
27
+
28
+ describe '#run_request' do
29
+ before do
30
+ stack << layer1
31
+ stack << layer2
32
+ end
33
+
34
+ it 'should run the body through all layers' do
35
+ layer1.expects(:request).with(1, {}).returns(2)
36
+ layer2.expects(:request).with(2, {}).returns(3)
37
+
38
+ stack.run_request(1).must_equal 3
39
+ end
40
+
41
+ it 'should route the options to the layers' do
42
+ layer1.expects(:request).with(1, 'options').returns(2)
43
+ layer2.expects(:request).with(2, user: 'defunkt')
44
+
45
+ stack.run_request 1, first: 'options', second: {
46
+ user: 'defunkt'
47
+ }
48
+ end
49
+ end
50
+
51
+ describe '#run_response' do
52
+ before do
53
+ stack << layer1
54
+ stack << layer2
55
+ end
56
+
57
+ it 'should run the body through all layers' do
58
+ layer2.expects(:response).with(1, {}).returns(2)
59
+ layer1.expects(:response).with(2, {}).returns(3)
60
+
61
+ stack.run_response(1).must_equal 3
62
+ end
63
+
64
+ it 'should route the options to the layers' do
65
+ layer2.expects(:response).with(1, user: 'defunkt').returns(2)
66
+ layer1.expects(:response).with(2, 'options')
67
+
68
+ stack.run_response 1, first: 'options', second: {
69
+ user: 'defunkt'
70
+ }
71
+ end
72
+ end
73
+
74
+ describe Commute::Stack::Sequence do
75
+
76
+ describe '#without!' do
77
+ before do
78
+ stack << layer1
79
+ stack << layer2
80
+ stack << layer3
81
+ end
82
+
83
+ it 'should remove the correct layers from the stack' do
84
+ stack.request.without! :second, :third
85
+ stack.request.size.must_equal 1
86
+ stack.response.size.must_equal 3
87
+ end
88
+ end
89
+
90
+ describe '#before!' do
91
+ before do
92
+ stack << layer1
93
+ stack << layer3
94
+ end
95
+
96
+ it 'should insert a layer before another one' do
97
+ stack.request.before! :third, layer2
98
+ stack.request[1].must_equal layer2
99
+ stack.request.size.must_equal 3
100
+ stack.response.size.must_equal 2
101
+ end
102
+ end
103
+
104
+ describe '#after!' do
105
+ before do
106
+ stack << layer1
107
+ stack << layer3
108
+ end
109
+
110
+ it 'should insert a layer after another one' do
111
+ stack.request.after! :first, layer2
112
+ stack.request[1].must_equal layer2
113
+ stack.request.size.must_equal 3
114
+ stack.response.size.must_equal 2
115
+ end
116
+
117
+ it 'should insert after the last one' do
118
+ stack.request.after! :third, layer2
119
+ stack.request.last.must_equal layer2
120
+ stack.request.size.must_equal 3
121
+ stack.response.size.must_equal 2
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,14 @@
1
+ require 'simplecov'
2
+
3
+ SimpleCov.command_name 'minitest'
4
+ SimpleCov.start
5
+
6
+ require 'minitest/autorun'
7
+ require 'minitest/spec'
8
+
9
+ require 'mocha'
10
+
11
+ require 'webmock'
12
+ require 'webmock/minitest'
13
+
14
+ require 'commute'
metadata ADDED
@@ -0,0 +1,217 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: commute
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mattias Putman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-18 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: typhoeus
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: mocha
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: guard
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: guard-minitest
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: yard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: simplecov
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: webmock
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: yajl-ruby
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ description: Handling the HTTP commute like a boss
159
+ email:
160
+ - mattias.putman@gmail.com
161
+ executables: []
162
+ extensions: []
163
+ extra_rdoc_files: []
164
+ files:
165
+ - .gitignore
166
+ - .travis.yml
167
+ - Gemfile
168
+ - Guardfile
169
+ - LICENSE
170
+ - README.md
171
+ - Rakefile
172
+ - commute.gemspec
173
+ - lib/commute.rb
174
+ - lib/commute/adapters/typhoeus.rb
175
+ - lib/commute/api.rb
176
+ - lib/commute/context.rb
177
+ - lib/commute/layer.rb
178
+ - lib/commute/stack.rb
179
+ - lib/commute/version.rb
180
+ - spec/commute/api_spec.rb
181
+ - spec/commute/context_spec.rb
182
+ - spec/commute/layer_spec.rb
183
+ - spec/commute/stack_spec.rb
184
+ - spec/spec_helper.rb
185
+ homepage: http://challengee.github.com/commute/
186
+ licenses: []
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ none: false
193
+ requirements:
194
+ - - ! '>='
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ required_rubygems_version: !ruby/object:Gem::Requirement
198
+ none: false
199
+ requirements:
200
+ - - ! '>='
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 1.8.19
206
+ signing_key:
207
+ specification_version: 3
208
+ summary: ! 'Commute helps you to: 1) Dynamically build HTTP requests for your favorite
209
+ HTTP library (currently only Typhoeus). 2) Easily process request and response bodies.
210
+ 3) Execute parallel HTTP requests.'
211
+ test_files:
212
+ - spec/commute/api_spec.rb
213
+ - spec/commute/context_spec.rb
214
+ - spec/commute/layer_spec.rb
215
+ - spec/commute/stack_spec.rb
216
+ - spec/spec_helper.rb
217
+ has_rdoc: