commute 0.1.2 → 0.2.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.todo +15 -0
  2. data/Gemfile +5 -2
  3. data/commute.gemspec +2 -1
  4. data/examples/gist_api.rb +71 -0
  5. data/examples/highrise_task_api.rb +59 -0
  6. data/examples/pastie_api.rb +18 -0
  7. data/lib/commute/aspects/caching.rb +37 -0
  8. data/lib/commute/aspects/crud.rb +41 -0
  9. data/lib/commute/aspects/pagination.rb +16 -0
  10. data/lib/commute/aspects/url.rb +57 -0
  11. data/lib/commute/common/basic_auth.rb +20 -0
  12. data/lib/commute/common/cache.rb +43 -0
  13. data/lib/commute/common/chemicals.rb +39 -0
  14. data/lib/commute/common/conditional.rb +27 -0
  15. data/lib/commute/common/em-synchrony_adapter.rb +29 -0
  16. data/lib/commute/common/em_http_request_adapter.rb +57 -0
  17. data/lib/commute/common/json.rb +28 -0
  18. data/lib/commute/common/typhoeus_adapter.rb +40 -0
  19. data/lib/commute/common/xml.rb +7 -0
  20. data/lib/commute/configuration.rb +8 -0
  21. data/lib/commute/core/api.rb +116 -0
  22. data/lib/commute/core/builder.rb +261 -0
  23. data/lib/commute/core/commuter.rb +116 -0
  24. data/lib/commute/core/context.rb +63 -0
  25. data/lib/commute/core/processors/code_status_processor.rb +40 -0
  26. data/lib/commute/core/processors/hook.rb +14 -0
  27. data/lib/commute/core/processors/request_builder.rb +26 -0
  28. data/lib/commute/core/processors/sequencer.rb +46 -0
  29. data/lib/commute/core/request.rb +58 -0
  30. data/lib/commute/core/response.rb +18 -0
  31. data/lib/commute/core/sequence.rb +180 -0
  32. data/lib/commute/core/stack.rb +145 -0
  33. data/lib/commute/version.rb +1 -1
  34. data/lib/commute.rb +4 -2
  35. data/spec/commute/aspects/caching_spec.rb +12 -0
  36. data/spec/commute/aspects/url_spec.rb +61 -0
  37. data/spec/commute/core/api_spec.rb +70 -0
  38. data/spec/commute/core/builder_spec.rb +123 -0
  39. data/spec/commute/core/commuter_spec.rb +64 -0
  40. data/spec/commute/core/processors/code_status_processor_spec.rb +5 -0
  41. data/spec/commute/core/processors/hook_spec.rb +25 -0
  42. data/spec/commute/core/processors/request_builder_spec.rb +25 -0
  43. data/spec/commute/core/processors/sequencer_spec.rb +33 -0
  44. data/spec/commute/core/sequence_spec.rb +190 -0
  45. data/spec/commute/core/stack_spec.rb +96 -0
  46. data/spec/spec_helper.rb +2 -3
  47. metadata +73 -18
  48. data/lib/commute/adapters/typhoeus.rb +0 -13
  49. data/lib/commute/api.rb +0 -86
  50. data/lib/commute/context.rb +0 -154
  51. data/lib/commute/layer.rb +0 -16
  52. data/lib/commute/stack.rb +0 -104
  53. data/spec/commute/api_spec.rb +0 -97
  54. data/spec/commute/context_spec.rb +0 -140
  55. data/spec/commute/layer_spec.rb +0 -22
  56. data/spec/commute/stack_spec.rb +0 -125
@@ -0,0 +1,116 @@
1
+ require 'commute/core/context'
2
+
3
+ require 'commute/core/processors/hook'
4
+ require 'commute/core/processors/request_builder'
5
+ require 'commute/core/processors/sequencer'
6
+ require 'commute/core/processors/code_status_processor'
7
+
8
+ module Commute
9
+
10
+ # Public: An API is a context that already provides some standard
11
+ # context alternations. The collection of these alternations
12
+ # form a client driver for a remote API.
13
+ #
14
+ # The way that a context
15
+ # is built in an api is exactly the same as you would built it
16
+ # outside of an API class.
17
+ #
18
+ # An API also provides some basic primitives to execute the
19
+ # transformed request of a context. This can be done through
20
+ # the `run` method.
21
+ #
22
+ # To help you build default contexts (that is a context that
23
+ # every API instance starts building from), a API class itself
24
+ # is also a builder. When a API class is instantiated, the
25
+ # base context is built from the current base builder state, and
26
+ # is used as a base builder for the API instance (A builder itself).
27
+ #
28
+ # Examples
29
+ #
30
+ # class PastieApi < Commute::Api
31
+ #
32
+ # using(:response_body) do
33
+ # append Downcaser.new
34
+ # end
35
+ #
36
+ # def get
37
+ # transform(:id) { |request, id|
38
+ # request.url = "http://pastie.org/pastes/#{id}/text"
39
+ # }.get
40
+ # end
41
+ # end
42
+ #
43
+ # gist = PastieApi.get(id: 5109586)
44
+ #
45
+ class Api < Builder
46
+ class << self
47
+ include Buildable
48
+
49
+ # The Builder methods should only be called in the Class itself.
50
+ # Otherwise, something like TestApi.with ... would modify
51
+ # the base builder, while we want it to create a new builder
52
+ # off the base one, with the provided parameters.
53
+ #
54
+ # Use TestApi.builder.with ... instead.
55
+ private *Builder::METHODS
56
+
57
+ # Internal: Memoized base builder.
58
+ def builder
59
+ @builder ||= Builder.new Context.new(Stack.new {})
60
+ end
61
+
62
+ # Internal: When an Api inherits another Api, it inherits its context.
63
+ def inherited klass
64
+ klass.instance_variable_set :@builder, Builder.new(builder.context)
65
+ end
66
+ end
67
+
68
+ # Internal: Forwards all methods on the class to the
69
+ # base context for this api.
70
+ #
71
+ # This is equivalent to calling TestApi.new.[method]
72
+ def self.method_missing method, *args, &block
73
+ self.new.send method, *args, &block
74
+ end
75
+
76
+ # Internal: A base stack that is used as a starting
77
+ # point for all apis. Includes a RequestBuilder
78
+ # and some convenient sequences for easy processor injection.
79
+ #
80
+ # Also provides basic on_request, on_response and
81
+ # on_complete hooks.
82
+ using do |stack, main|
83
+ main.append RequestBuilder.new
84
+ main.append Sequencer.new(:request), as: :request
85
+ main.append Sequencer.new(:execution), as: :execution
86
+ main.append Sequencer.new(:response), as: :response
87
+ main.append CodeStatusProcessor.new, as: :status
88
+ main.append Hook.new, as: :on_complete
89
+
90
+ stack.sequence(:execution) do
91
+ append Proc.new { |c|
92
+ Configuration.adapter.call c
93
+ }
94
+ end
95
+
96
+ # sequence(:request) do
97
+ # append Scope.new(Sequencer.new(:request_body), :body), as: :body
98
+ # end
99
+
100
+ # sequence(:response) do
101
+ # append Hook.new, as: :on_response
102
+ # append Scope.new(Sequencer.new(:response_body), :body), as: :body
103
+ # end
104
+ end
105
+
106
+ # Public: Creates a new Api Builder, starting to build from
107
+ # the base context by default. Only when an Api has been
108
+ # turned into a context and then into a builder again, an
109
+ # argument will be given here.
110
+ #
111
+ # Returns a new Api instance.
112
+ def initialize context = self.class.builder.context
113
+ super
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,261 @@
1
+ module Commute
2
+
3
+ # TODO comments
4
+ module Buildable
5
+
6
+ def self.included klass
7
+ klass.extend Forwardable
8
+ klass.def_delegators :builder, *Builder::METHODS
9
+ end
10
+
11
+ private
12
+
13
+ def builder
14
+ Builder.new self
15
+ end
16
+ end
17
+
18
+ # TODO comments
19
+ class Builder
20
+
21
+ METHODS = [:with, :without, :enable, :disable,
22
+ :hook, :transform, :using, :get, :put, :post, :patch, :delete].freeze
23
+
24
+ # Public: Initializes a new Builder from a context that
25
+ # starts to build a new context on top of it.
26
+ #
27
+ # Use 'context' to get the built context.
28
+ def initialize context
29
+ @stack = context.stack
30
+ @parameters = context.parameters
31
+ @transformations = context.transformations
32
+ @disables = context.disables
33
+ @cloned = []
34
+ end
35
+
36
+ # Internal: Get the context built with this builder.
37
+ #
38
+ # Returns a new Context.
39
+ def context
40
+ # Make new clones now.
41
+ @cloned = []
42
+ # Return the context.
43
+ Context.new @stack, @parameters, @transformations, @disables, self.class
44
+ end
45
+
46
+ # Internal: When a method is called on the builder that
47
+ # does not exist, we assume that the developer wanted to
48
+ # call a method on the already built context. So we return
49
+ # that built context and forward the method to it.
50
+ #
51
+ # Returns the return value from the method invoked on the built context.
52
+ def method_missing name, *args, &block
53
+ context.send name, *args, &block
54
+ end
55
+
56
+ # Public: Adds logical parameters to the context.
57
+ # Logocal parameters whose names match the id of a
58
+ # layer, get passed to that layer as options.
59
+ #
60
+ # When a parameter is set to nil, it is excluded
61
+ # from the update, use `without` instead.
62
+ #
63
+ # Examples
64
+ #
65
+ # people.all.with(max: 100)
66
+ # people.all.enable(:auth).with(auth: 'secret')
67
+ #
68
+ def with parameters
69
+ cloned(:@parameters)
70
+ parameters.delete_if { |k,v| v.nil? }
71
+ @parameters.merge!(parameters) do |key, o, n|
72
+ if o.kind_of? Array
73
+ o + n
74
+ elsif o.kind_of? Hash
75
+ o.merge! n
76
+ else
77
+ n
78
+ end
79
+ end
80
+ self
81
+ end
82
+ alias :for :with
83
+ alias :where :with
84
+
85
+ def with! parameters
86
+ cloned(:@parameters)
87
+ parameters.delete_if { |k,v| v.nil? }
88
+ # Do not check for collisions.
89
+ @parameters.merge!(parameters)
90
+ self
91
+ end
92
+
93
+ # Public: Removes logical parameters from the context.
94
+ #
95
+ # Examples
96
+ #
97
+ # people.all.without(:index, :max)
98
+ #
99
+ def without *parameters
100
+ cloned(:@parameters)
101
+ parameters.each { |parameter| @parameters.delete parameter }
102
+ self
103
+ end
104
+
105
+ # Public: Enables processors in the stack. Causes the request
106
+ # to be processed by these processors before executing it.
107
+ #
108
+ # Only needed if the processor was disabled before. Adding
109
+ # a processor to a sequence will auto-enable it.
110
+ #
111
+ # Examples
112
+ #
113
+ # api = people.all.disable(:format)
114
+ # # later
115
+ # api.enable(:format)
116
+ #
117
+ def enable *processors
118
+ cloned(:@disables)
119
+ processors.each { |processor| @disables.delete processor }
120
+ self
121
+ end
122
+
123
+ # Public: Disable layers in the stack. Causes the request
124
+ # not to be processed by these layers.
125
+ #
126
+ # Equivalent for context.without(:auth)
127
+ #
128
+ # Examples
129
+ #
130
+ # people.all.disable(:auth)
131
+ #
132
+ def disable *processors
133
+ cloned(:@disables)
134
+ @disables = @disables | processors
135
+ self
136
+ end
137
+
138
+ # Public: Adds a hook to the context, this hook will be
139
+ # triggered for all requests made based on this context.
140
+ #
141
+ # Block form equivalent for context.with(event: handler)
142
+ #
143
+ # The logic behind this is that the id of the hook processor
144
+ # is the name of the event. The parameter (handler) will
145
+ # be passed as options to the processor.
146
+ #
147
+ # event - The event to register the handler for.
148
+ # handler - The handler to be triggered when an event comes up.
149
+ #
150
+ def hook event, &handler
151
+ with(event => handler)
152
+ end
153
+
154
+ # Public: Adds a transformation to the context that instructs
155
+ # the context transformer how a request must be built from this context.
156
+ #
157
+ # Mostly used in API classes.
158
+ #
159
+ # dependencies - Logical parameters this transformation is based on (optional).
160
+ # transformation - The transformation logic.
161
+ #
162
+ # Yields
163
+ # No dependencies: the request and the context (if the arity permits it).
164
+ # Dependencies: The request and the dependency values (unless all dependency values are nil).
165
+ #
166
+ # Examples
167
+ #
168
+ # context.transform(:since) { |request, since| request.query[:since] = since }
169
+ # context.transform(:id) { |request, id| request.url = "http://pastie.com/#{id}"}
170
+ #
171
+ # context.transform { |request| request.url = 'http://example.com' }
172
+ #
173
+ # context.transform { |request, context|
174
+ # context[:user] ? "/users/#{self[:user]}/gists" : '/gists'
175
+ # }
176
+ #
177
+ def transform *dependencies, &transformation
178
+ cloned(:@transformations)
179
+ if dependencies.empty?
180
+ @transformations << transformation
181
+ else
182
+ @transformations << Proc.new do |request, context|
183
+ dependency_values = dependencies.map { |dep| context[dep] }
184
+ transformation.call request, *dependency_values unless dependency_values.all?(&:nil?)
185
+ end
186
+ end
187
+ self
188
+ end
189
+
190
+ # Same as `transform` but removes all existing transformations for these dependencies.
191
+ def transform! *dependencies, &transformation
192
+ end
193
+
194
+ # Public: Used to alter the stack or one of its sequences.
195
+ #
196
+ # When no name is given, the stack is altered.
197
+ #
198
+ # name - The name of the sequence that needs altering.
199
+ #
200
+ # Yields the stack or one of its sequences.
201
+ # When the block has no arguments, the block is evalled
202
+ # on the stack or its sequence.
203
+ #
204
+ # Examples
205
+ #
206
+ # using(:response_body) do
207
+ # append(Commute::Common::Json::Parse.new)
208
+ # end
209
+ #
210
+ # # Is actually the equivalent for
211
+ # using do
212
+ # sequence(:response_body) do
213
+ # append(Commute::Common::Json::Parse.new)
214
+ # end
215
+ # end
216
+ #
217
+ def using name = nil, &alter
218
+ cloned(:@stack)
219
+ if name
220
+ sequence = @stack.sequence(name)
221
+ sequence.alter &alter if alter
222
+ else
223
+ @stack.alter &alter
224
+ end
225
+ self
226
+ end
227
+
228
+ # Public: Configure the HTTP request method.
229
+ # Adds a request transformation configuring the request http method.
230
+ [:get, :put, :post, :patch, :delete].each do |http_method|
231
+ define_method http_method do
232
+ transform { |request| request.method = http_method }
233
+ end
234
+ end
235
+
236
+ # TODO
237
+ def limit max
238
+ with limit: max
239
+ end
240
+
241
+ # TODO
242
+ def offset index
243
+ with offset: index
244
+ end
245
+
246
+ # TODO
247
+ def order field, direction = :asc
248
+ with order: { field: field, direction: direction }
249
+ end
250
+
251
+ private
252
+
253
+ # Makes sure the given instance variable is cloned.
254
+ def cloned variable
255
+ if !@cloned.include? variable
256
+ @cloned << variable
257
+ instance_variable_set variable, instance_variable_get(variable).dup
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,116 @@
1
+ require 'commute/core/context'
2
+
3
+ module Commute
4
+
5
+ # Public: A Commuter is a package that holds a value
6
+ # associated to a context.
7
+ #
8
+ # A commuter can be tagged to make conditional
9
+ # processing possible.
10
+ #
11
+ class Commuter
12
+ attr_reader :context
13
+
14
+ # Public: Creates a new commuter
15
+ def initialize context, value = nil
16
+ @context = context
17
+ @value = value
18
+ @tags = []
19
+ end
20
+
21
+ # Public: Returns the inner value of the commuter.
22
+ def get
23
+ @value
24
+ end
25
+
26
+ # Public: Sets the inner value of the commuter.
27
+ #
28
+ # value - The new value.
29
+ def set value
30
+ @value = value
31
+ end
32
+
33
+ # Public: Changes the value of a commuter using a block.
34
+ # ONLY calls the block if the commuter value is not nil.
35
+ #
36
+ # Yields the value of the commuter.
37
+ #
38
+ # Returns nothing
39
+ def change
40
+ @value = yield @value unless @value.nil?
41
+ return nil
42
+ end
43
+
44
+ # Public: Get some parameters on how the commuter
45
+ # should be processed within a certain processor.
46
+ #
47
+ # processor_id - The id of the processor
48
+ #
49
+ # Returns whatever set in the context for this processor.
50
+ def parameters processor_id
51
+ self.context[processor_id]
52
+ end
53
+
54
+ # TODO
55
+ def disabled? processor_id
56
+ false
57
+ end
58
+
59
+ # TODO
60
+ def enabled? processor_id
61
+ !disabled?(processor_id)
62
+ end
63
+
64
+ # Public: Tags the commuter.
65
+ #
66
+ # tag - The tag to tag with (can be any object).
67
+ #
68
+ # Returns nothing.
69
+ def tag tag
70
+ @tags << tag
71
+ return nil
72
+ end
73
+
74
+ # Public: Checks if a commuter has a certain tag.
75
+ #
76
+ # tag - The tag to check presence of (can be any object).
77
+ #
78
+ # Returns true if the commuter is tagged with the given tag.
79
+ def tagged? tag
80
+ @tags.include? tag
81
+ end
82
+
83
+ # Public: Delays a commuter by giving it a block that
84
+ # must definitely be executed before the commute can continue.
85
+ #
86
+ # delay - The delay that must be executed.
87
+ #
88
+ # Returns nothing.
89
+ def delay &delay
90
+ @delay = delay
91
+ nil
92
+ end
93
+
94
+ def return
95
+ delay {}
96
+ end
97
+
98
+ # Public: check if a commuter is delayed.
99
+ def delayed?
100
+ !!@delay
101
+ end
102
+
103
+ # Public: Wait for a delay to be executed.
104
+ # And the do something when done.
105
+ #
106
+ # Yields when the delay has been executed.
107
+ #
108
+ # Returns nothing.
109
+ def wait &done
110
+ delay = @delay
111
+ @delay = nil
112
+ delay.call &done
113
+ return nil
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,63 @@
1
+ require 'forwardable'
2
+
3
+ require 'commute/core/builder'
4
+ require 'commute/core/stack'
5
+ require 'commute/core/builder'
6
+
7
+ module Commute
8
+ class Context
9
+ include Buildable
10
+
11
+ attr_reader :stack, :parameters, :transformations, :disables
12
+
13
+ def initialize stack = nil, parameters = {}, transformations = [], \
14
+ disables = [], builder_class = Builder
15
+ @stack = stack.freeze
16
+ @parameters = parameters.freeze || {}
17
+ @transformations = transformations.freeze || []
18
+ @disables = disables.freeze || {}
19
+ @builder_class = builder_class
20
+ end
21
+
22
+ def [] key
23
+ @parameters[key]
24
+ end
25
+
26
+ # Public: Creates a new context, resetting all transformations.
27
+ #
28
+ def reset
29
+ end
30
+
31
+ # Returns the
32
+ def run &on_complete
33
+ context = on_complete ? with(on_complete: on_complete) : self
34
+ commuter = Commuter.new context
35
+ @stack.call commuter
36
+ commuter.get
37
+ end
38
+
39
+ def run! *args, &block
40
+ result = self.run(*args, &block)
41
+ if result.kind_of?(Array)
42
+ result.first
43
+ else
44
+ result
45
+ end
46
+ end
47
+
48
+ def method_missing method, *args, &block
49
+ b = builder
50
+ if b.respond_to?(method)
51
+ b.send method, *args, &block
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def builder
60
+ @builder_class.new self
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,40 @@
1
+ require 'commute/core/commuter'
2
+
3
+ module Commute
4
+
5
+ # Internal: Looks at a response and decides if it was
6
+ # successful or not. This is the most basic one of
7
+ # those checkers. It simply checks if the response
8
+ # code is a 2xx code, when it is, the request was
9
+ # a success.
10
+ #
11
+ # This processor replaces the commuter's Response
12
+ #
13
+ class CodeStatusProcessor
14
+
15
+ class CodeStatus
16
+ @id = :status
17
+
18
+ attr_reader :code
19
+
20
+ def initialize code
21
+ @code = code
22
+ @success = (200..299).include? code
23
+ end
24
+
25
+ def success?
26
+ @success
27
+ end
28
+
29
+ def fail?
30
+ !@success
31
+ end
32
+ end
33
+
34
+ def call commuter
35
+ commuter.change do |response|
36
+ [response.body, CodeStatus.new(response.code)]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,14 @@
1
+ require 'commute/core/commuter'
2
+
3
+ module Commute
4
+
5
+ # Public: A Hook is a processor that yields the commuter value to a handler.
6
+ # The handler is given via the options for the processor (via its name).
7
+ #
8
+ class Hook
9
+
10
+ def call commuter, handler = nil
11
+ handler.call *commuter.get if handler
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ require 'commute/core/commuter'
2
+ require 'commute/core/request'
3
+
4
+ module Commute
5
+
6
+ # Internal: A RequestBuilder builds a request given a context.
7
+ #
8
+ # It basically call all transformations defined on the context,
9
+ # resulting in a request. The request is then passed as the new
10
+ # value of the commuter (still with a reference to the context).
11
+ #
12
+ class RequestBuilder
13
+ @id = :builder
14
+
15
+ def call commuter
16
+ # Create a new request.
17
+ request = Request.new
18
+ # Build the request using context transformations.
19
+ commuter.context.transformations.each do |t|
20
+ t.call request, commuter.context
21
+ end
22
+ # Set the request on the commuter.
23
+ commuter.set request
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ require 'commute/core/commuter'
2
+
3
+ module Commute
4
+
5
+ # Public: A sequencer is a processor that lazily fetches a sequence
6
+ # from the stack and executes it with the given commuter.
7
+ #
8
+ # This has the advantage that an either not ye existent sequence or
9
+ # a sequence that could be modified later, can be referenced from
10
+ # another sequence.
11
+ #
12
+ # Examples:
13
+ #
14
+ # Stack.new do |stack, main|
15
+ # # Defining a new sequence.
16
+ # response = stack.sequence(:response) do
17
+ # ...
18
+ # end
19
+ #
20
+ # # This would work but if we alter the response
21
+ # # sequence later on, it would still use this one.
22
+ # main.append response
23
+ #
24
+ # # Instead we do
25
+ # main.append Sequencer.new(:response)
26
+ # end
27
+ #
28
+ class Sequencer
29
+
30
+ # Public: Creates a new sequencer.
31
+ #
32
+ # name - The name of the sequence that needs to be called (lazily).
33
+ def initialize name
34
+ @name = name
35
+ end
36
+
37
+ def call commuter
38
+ # Get the stack from the processing context.
39
+ stack = commuter.context.stack
40
+ # Get the sequence we need to call.
41
+ sequence = stack.get(@name)
42
+ # Call the sequence if one was found.
43
+ sequence.call commuter if sequence
44
+ end
45
+ end
46
+ end