commute 0.1.2 → 0.2.0.rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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