commute 0.2.0.rc.2 → 0.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.todo +28 -12
  2. data/README.md +0 -1
  3. data/commute.gemspec +4 -5
  4. data/lib/commute/common/basic_auth.rb +10 -9
  5. data/lib/commute/common/caching.rb +208 -0
  6. data/lib/commute/common/chemicals.rb +47 -24
  7. data/lib/commute/common/eventmachine.rb +68 -0
  8. data/lib/commute/common/synchrony.rb +42 -0
  9. data/lib/commute/common/typhoeus.rb +64 -0
  10. data/lib/commute/core/api.rb +42 -29
  11. data/lib/commute/core/builder.rb +4 -15
  12. data/lib/commute/core/context.rb +156 -15
  13. data/lib/commute/core/http.rb +124 -0
  14. data/lib/commute/core/layer.rb +187 -0
  15. data/lib/commute/core/sequence.rb +83 -132
  16. data/lib/commute/core/stack.rb +63 -72
  17. data/lib/commute/core/status.rb +45 -0
  18. data/lib/commute/core/util/event_emitter.rb +58 -0
  19. data/lib/commute/core/util/path.rb +37 -0
  20. data/lib/commute/core/util/stream.rb +141 -0
  21. data/lib/commute/extensions/crud.rb +88 -0
  22. data/lib/commute/extensions/param.rb +20 -0
  23. data/lib/commute/extensions/url.rb +53 -0
  24. data/lib/commute/version.rb +1 -1
  25. data/spec/commute/common/caching_spec.rb +158 -0
  26. data/spec/commute/common/eventmachine_spec.rb +74 -0
  27. data/spec/commute/common/typhoeus_spec.rb +67 -0
  28. data/spec/commute/core/api_spec.rb +3 -1
  29. data/spec/commute/core/builder_spec.rb +8 -8
  30. data/spec/commute/core/http_spec.rb +39 -0
  31. data/spec/commute/core/layer_spec.rb +81 -0
  32. data/spec/commute/core/sequence_spec.rb +36 -150
  33. data/spec/commute/core/stack_spec.rb +33 -83
  34. data/spec/commute/core/util/event_emitter_spec.rb +35 -0
  35. data/spec/commute/core/util/path_spec.rb +29 -0
  36. data/spec/commute/core/util/stream_spec.rb +90 -0
  37. data/spec/commute/extensions/url_spec.rb +76 -0
  38. data/spec/spec_helper.rb +3 -1
  39. metadata +61 -48
  40. data/examples/gist_api.rb +0 -71
  41. data/examples/highrise_task_api.rb +0 -59
  42. data/examples/pastie_api.rb +0 -18
  43. data/lib/commute/aspects/caching.rb +0 -37
  44. data/lib/commute/aspects/crud.rb +0 -41
  45. data/lib/commute/aspects/pagination.rb +0 -16
  46. data/lib/commute/aspects/url.rb +0 -57
  47. data/lib/commute/common/cache.rb +0 -43
  48. data/lib/commute/common/conditional.rb +0 -27
  49. data/lib/commute/common/em-synchrony_adapter.rb +0 -29
  50. data/lib/commute/common/em_http_request_adapter.rb +0 -57
  51. data/lib/commute/common/typhoeus_adapter.rb +0 -40
  52. data/lib/commute/common/xml.rb +0 -7
  53. data/lib/commute/core/commuter.rb +0 -116
  54. data/lib/commute/core/processors/code_status_processor.rb +0 -40
  55. data/lib/commute/core/processors/hook.rb +0 -14
  56. data/lib/commute/core/processors/request_builder.rb +0 -26
  57. data/lib/commute/core/processors/sequencer.rb +0 -46
  58. data/lib/commute/core/request.rb +0 -58
  59. data/lib/commute/core/response.rb +0 -18
  60. data/spec/commute/aspects/caching_spec.rb +0 -12
  61. data/spec/commute/aspects/url_spec.rb +0 -61
  62. data/spec/commute/core/commuter_spec.rb +0 -64
  63. data/spec/commute/core/processors/code_status_processor_spec.rb +0 -5
  64. data/spec/commute/core/processors/hook_spec.rb +0 -25
  65. data/spec/commute/core/processors/request_builder_spec.rb +0 -25
  66. data/spec/commute/core/processors/sequencer_spec.rb +0 -33
@@ -54,7 +54,7 @@ module Commute
54
54
  end
55
55
 
56
56
  # Public: Adds logical parameters to the context.
57
- # Logocal parameters whose names match the id of a
57
+ # Logical parameters whose names match the id of a
58
58
  # layer, get passed to that layer as options.
59
59
  #
60
60
  # When a parameter is set to nil, it is excluded
@@ -188,7 +188,7 @@ module Commute
188
188
  end
189
189
 
190
190
  # Same as `transform` but removes all existing transformations for these dependencies.
191
- def transform! *dependencies, &transformation
191
+ def transform! name, *dependencies, &transformation
192
192
  end
193
193
 
194
194
  # Public: Used to alter the stack or one of its sequences.
@@ -233,19 +233,8 @@ module Commute
233
233
  end
234
234
  end
235
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 }
236
+ def body body
237
+ with! body: body
249
238
  end
250
239
 
251
240
  private
@@ -2,14 +2,61 @@ require 'forwardable'
2
2
 
3
3
  require 'commute/core/builder'
4
4
  require 'commute/core/stack'
5
- require 'commute/core/builder'
5
+ require 'commute/core/http'
6
6
 
7
+ # A context is a snapshot of a builder. It holds partial
8
+ # information on how to build and execute a request.
9
+ #
10
+ # This is:
11
+ # * A stack
12
+ # * Logical parameters
13
+ # * Transformation that transform logical parameters into request parameters
14
+ # * Disables (for skipping layers)
15
+ #
16
+ # When a context holds enough information to execute a request, you
17
+ # can execute it in 6 different ways (depending on your adapter):
18
+ #
19
+ # 1) Run Synchronously.
20
+ # paste, status[, response] = pastes.find(1).run
21
+ # pasted, status[, response] = api.create.run("Some paste")
22
+ # # OR
23
+ # pasted, status[, response] = api.put.one.run("Some paste")
24
+ #
25
+ # 2) Run Synchronously unsafe
26
+ # paste = pasted.find(1).run!
27
+ #
28
+ # 3) Run Asynchronously.
29
+ # pastes.find(1).run { |paste[, status[, response]]| ... }
30
+ #
31
+ # 4) Stream (down)
32
+ # pastes.all.stream { |response, status|
33
+ # # Received a response.
34
+ # response.on(:data) { |paste| ... }
35
+ # response.on(:end) { ... }
36
+ # }
37
+ #
38
+ # 5) Stream unsafe (down)
39
+ # pastes.all.stream! { |paste|
40
+ # }
41
+ #
42
+ # 6) The full treatment (up and downstreaming)
43
+ # request = pastes.all.request { |response, status|
44
+ # }
45
+ # request.write ...
46
+ # request.end
47
+ #
7
48
  module Commute
8
49
  class Context
9
50
  include Buildable
10
51
 
52
+ # Public: All information needed to build and execute a request.
11
53
  attr_reader :stack, :parameters, :transformations, :disables
12
54
 
55
+ # Internal: Creates a new context. A developer never has to call
56
+ # this method manually.
57
+ #
58
+ # It sets up a snapshot of a builder and freezes all information.
59
+ #
13
60
  def initialize stack = nil, parameters = {}, transformations = [], \
14
61
  disables = [], builder_class = Builder
15
62
  @stack = stack.freeze
@@ -19,32 +66,111 @@ module Commute
19
66
  @builder_class = builder_class
20
67
  end
21
68
 
69
+ # Public: Get a logical parameter.
70
+ #
71
+ # key - The key of the logical parameter.
72
+ #
73
+ # Returns the value of the logical parameter.
22
74
  def [] key
23
75
  @parameters[key]
24
76
  end
25
77
 
26
- # Public: Creates a new context, resetting all transformations.
78
+ # Public: Check if a layer is enabled.
27
79
  #
80
+ # name - The name of the layer in question.
81
+ #
82
+ # Returns `true` if the layer is enabled in the current context.
83
+ def enabled? name
84
+ !disables.include?(name)
85
+ end
86
+
87
+ # Internal: Creates a new context, resetting all transformations.
28
88
  def reset
89
+ self.class.new stack, parameters, [], disables, builder_class
29
90
  end
30
91
 
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
92
+ # Public: Down and Upstream any content (safely).
93
+ # The Full Treatment.
94
+ #
95
+ # Yields Layer Response and a status.
96
+ # Returns a Layer Request.
97
+ #
98
+ def request &on_response
99
+ @stack.call self.build, &on_response
37
100
  end
38
101
 
39
- def run! *args, &block
40
- result = self.run(*args, &block)
41
- if result.kind_of?(Array)
42
- result.first
43
- else
44
- result
102
+ # Public: Downstream any remote content (safely).
103
+ #
104
+ # body - Optional body to send with the request.
105
+ #
106
+ # Yields a Layer Response and a status.
107
+ # Returns Nothing.
108
+ #
109
+ def stream body = self[:body], &on_response
110
+ self.request(&on_response).tap do |request|
111
+ request.write body if body
112
+ request.end
45
113
  end
114
+ # Return Nothing.
115
+ nil
46
116
  end
47
117
 
118
+ # Public: Downstream any remote content (unsafely).
119
+ #
120
+ # body - Optional body to send with the request.
121
+ #
122
+ # Yields for every chunk of body/result streamed.
123
+ # Returns Nothing.
124
+ #
125
+ def stream! body = self[:body], &on_data
126
+ self.stream do |response, status|
127
+ response.on :end, &on_data
128
+ end
129
+ # Return Nothing.
130
+ nil
131
+ end
132
+
133
+ # Public: Run (a)synchronously, only returns if data is fully
134
+ # received. Direct return only possible with synchronous adapter.
135
+ #
136
+ # body - Optional body to send with the request.
137
+ #
138
+ # Yields the buffered data, status and response. (sync)
139
+ # Returns the buffered data, status and response. (sync/async)
140
+ #
141
+ def run body = self[:body], &on_result
142
+ # Temprary result variable in this scope.
143
+ result, status, response = nil, nil, nil
144
+ # Stream the response.
145
+ self.stream(body) do |_response, _status|
146
+ # Set status.
147
+ status, response = _status, _response
148
+ # Buffer all data in result.
149
+ response.once(:data) { |_result| result = _result }
150
+ # Call on_result in the end.
151
+ response.on(:end) do
152
+ on_result.call result, status, response if on_result
153
+ end
154
+ end
155
+ # Also just return the result.
156
+ return result, status, response
157
+ end
158
+
159
+ # Public: Run synchronously unsafe.
160
+ # Only returns data when fully buffered, without a status.
161
+ #
162
+ # body - Optional body to send with the request.
163
+ #
164
+ # Returns the buffered data.
165
+ #
166
+ def run! body = self[:body]
167
+ return self.run(body).first
168
+ end
169
+
170
+ # Internal: Methods not defined on Context trigger
171
+ # the creation of a builder.
172
+ #
173
+ # The method is then forwarded to the builder.
48
174
  def method_missing method, *args, &block
49
175
  b = builder
50
176
  if b.respond_to?(method)
@@ -54,8 +180,23 @@ module Commute
54
180
  end
55
181
  end
56
182
 
57
- private
183
+ protected
58
184
 
185
+ # Internal: Build a Http Request using context transformations.
186
+ #
187
+ def build
188
+ # Create a new request.
189
+ http_request = Http::Request.new(self)
190
+ # Build the request using context transformations.
191
+ @transformations.each do |t|
192
+ t.call http_request, self
193
+ end
194
+ # Return the http request.
195
+ http_request
196
+ end
197
+
198
+ # Internal: Creates a new Builder for this context.
199
+ #
59
200
  def builder
60
201
  @builder_class.new self
61
202
  end
@@ -0,0 +1,124 @@
1
+ require 'uri'
2
+
3
+ require 'commute/core/status'
4
+
5
+ module Commute
6
+
7
+ # Collection of model classes associated with Http.
8
+ module Http
9
+
10
+ # Public: Status that derives its ok status from a given
11
+ # Http status code.
12
+ #
13
+ class Status < Commute::Status
14
+ attr_reader :code
15
+
16
+ # Public: Creates a new Http Status.
17
+ #
18
+ # code - Http Status code.
19
+ # error - Optional error details (Can be anything).
20
+ #
21
+ def initialize code, error = nil
22
+ super !(400..599).include?(code)
23
+ @code = code
24
+ end
25
+
26
+ def not_modified?
27
+ @code == 304
28
+ end
29
+ end
30
+
31
+ # Public: A Http connection to a host/port with a certain protocol.
32
+ class Connection
33
+
34
+ # Public: Uri of the host (containing scheme, host and port).
35
+ attr_reader :uri
36
+ end
37
+
38
+ # Public: A Http Request.
39
+ class Request
40
+
41
+ # Public: The context in which the Http Request is made.
42
+ attr_reader :context
43
+
44
+ # Public: Uri of EATS (Everything after the slash).
45
+ attr_accessor :uri
46
+
47
+ # Public: Url parameters (Hash).
48
+ attr_accessor :query
49
+ alias :params :query
50
+ alias :params= :query=
51
+
52
+ # Public: The HTTP Method of the request (Symbol).
53
+ # Mostly :get, :post, :put, :patch, :delete
54
+ attr_accessor :method
55
+
56
+ # Public: Request headers.
57
+ attr_accessor :headers
58
+
59
+ # Public: Optional connection associated with this request.
60
+ attr_reader :connection
61
+
62
+ # Public: Initializes a new Http Request with some default values.
63
+ #
64
+ # context - The context in which the Http Request is made.
65
+ #
66
+ def initialize context
67
+ @context = context
68
+ @uri = URI::Generic.build path: '/'
69
+ @headers = {}
70
+ @query = {}
71
+ @method = :get
72
+ end
73
+
74
+ # Public: Getting the Http method.
75
+ #
76
+ # Overload the default reader method for overloading
77
+ # of Object#method.
78
+ #
79
+ def method name = nil
80
+ name.nil? ? @method : super(name)
81
+ end
82
+
83
+ # Public: Get the URI of the Request.
84
+ #
85
+ # Override the default reader method for consistency
86
+ # with the query setter method on Request.
87
+ # We first merge the query string into the URI before
88
+ # returning it.
89
+ #
90
+ def uri
91
+ @uri.query = URI.encode_www_form_component( \
92
+ URI.encode_www_form(self.query)
93
+ ) if !self.query.empty?
94
+ @uri
95
+ end
96
+ end
97
+
98
+ # Public: An Http Response.
99
+ class Response
100
+
101
+ # Public: The request responsible for this response.
102
+ attr_accessor :request
103
+
104
+ # Public: Http Response code.
105
+ attr_accessor :code
106
+
107
+ # Public: Request headers.
108
+ attr_accessor :headers
109
+
110
+ # Public: Initializes a new Http Response with a reference Request.
111
+ #
112
+ # request - The request responsible for this response.
113
+ #
114
+ def initialize request
115
+ @request = request
116
+ @headers = {}
117
+ end
118
+
119
+ def etag
120
+ headers['ETag']
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,187 @@
1
+ require 'commute/core/util/event_emitter'
2
+ require 'commute/core/util/stream'
3
+
4
+ module Commute
5
+
6
+ # Internal: A Layer is one processing piece in a commute stack.
7
+ #
8
+ # A layer acts as a mini in-process server that takes its own
9
+ # requests (Layer::Request) and issues its own requests (to the
10
+ # next layer in the stack). Therefor implementing a layer
11
+ # gives you alot of rope to work with.
12
+ #
13
+ # Think of layers as seperate network components that call
14
+ # each other in a russian-doll model with the adapter as
15
+ # the most inner doll.
16
+ #
17
+ # The Layer requests and response are only for communication
18
+ # between the layers, they carry the real http requests
19
+ # and responses (access at Request#http and Response#http).
20
+ #
21
+ # The Spec for a layer is documented hereafter.
22
+ #
23
+ # A layer implements a call method that takes a router, request
24
+ # and some options from the context of that request
25
+ # meant for the layer.
26
+ #
27
+ # The router is a the router that a connects all layers of
28
+ # stack needed for this request.
29
+ #
30
+ # Take a simple layer that does not call the next layer,
31
+ # but just echoes all that is sent to it.
32
+ #
33
+ # def call router, request, options = {
34
+ # # Respond to the request right away.
35
+ # response = request.respond Response.new, Status.ok
36
+ # # Echo all data.
37
+ # request.on(:data) do |chunk|
38
+ # response.write chunk
39
+ # end
40
+ # # When the request ends, we also have no use anymore.
41
+ # request.on(:end) { response.end }
42
+ # end
43
+ #
44
+ # Sidenote: We could also have written
45
+ #
46
+ # def call router, request, options = {}
47
+ # # Respond to the request right away.
48
+ # response = request.respond Response.new
49
+ # # Connect the incoming stream with the outgoing stream.
50
+ # request.pipe response
51
+ # end
52
+ #
53
+ # A bit more complex example that shows the entire spec
54
+ # is a layer does nothing but two-way forwarding (short with piping).
55
+ #
56
+ # def call router, request, options = {}
57
+ # # Forward the request to the next layer.
58
+ # proxy_request = router.call request.http do |response, status|
59
+ # proxy_response = request.respond response.http, status
60
+ # response.pipe proxy_response
61
+ # end
62
+ #
63
+ # request.pipe proxy_request
64
+ # end
65
+ #
66
+ class Layer
67
+
68
+ # Internal: Base class for Request and Response.
69
+ # Includes some modules by default.
70
+ class Communication
71
+ include EventEmitter
72
+ include Stream
73
+
74
+ attr_reader :http
75
+
76
+ # Internal: Creates a new Request/Response.
77
+ #
78
+ # http - Underlaying http Request/Response.
79
+ #
80
+ def initialize http
81
+ super()
82
+ @http = http
83
+ end
84
+ end
85
+
86
+ # Public: An inter-Layer Request.
87
+ #
88
+ # Includes EventEmitter to listen for responses, data and request end.
89
+ # Includes Stream for stream-uploading.
90
+ #
91
+ class Request < Communication
92
+
93
+ # Public: Respond to a Request using a Http Response.
94
+ # The http response the gets wrapped in a Layer Response.
95
+ #
96
+ # http - Http Response for this Request.
97
+ # status - Any status indications for the Response.
98
+ #
99
+ # Yields the Layer response and ends the response thereafter
100
+ # if a block is given.
101
+ #
102
+ # Returns the Layer Response.
103
+ def respond http, status, &lifecycle
104
+ Response.new(http).tap do |response|
105
+ emit :response, response, status
106
+ if block_given?
107
+ lifecycle.call response
108
+ response.end
109
+ end
110
+ end
111
+ end
112
+
113
+ # Public: Returns a responder for this request.
114
+ #
115
+ # A Responder will automatically handle a response
116
+ # from a layer and use it to respond to this request.
117
+ #
118
+ # It will pipe all data from the response to the response
119
+ # for this request.
120
+ #
121
+ # Returns a block that responds to this request.
122
+ def responder
123
+ proc do |response, status|
124
+ response.pipe self.respond(response.http, status)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Public: An inter-Layer Response.
130
+ #
131
+ # Includes EventEmitter to listen for data and response end.
132
+ # Includes Stream for downloading data in chunks.
133
+ #
134
+ class Response < Communication
135
+ end
136
+
137
+ # Internal: Name of the Layer.
138
+ attr_reader :name
139
+
140
+ # Internal: Creates a new Layer.
141
+ #
142
+ # callable - Object with Layer logics that responds to #call.
143
+ # name - Name of the Layer (optional).
144
+ #
145
+ def initialize callable, name = nil
146
+ @name = name \
147
+ || callable.class.instance_variable_get(:@name) \
148
+ || (callable.respond_to?(:name) && callable.name)
149
+ @callable = callable
150
+ end
151
+
152
+ # Internal: Call the Layer with options fetched out of the context
153
+ # using the name of the Layer.
154
+ #
155
+ # For example, when you add a Layer to a sequence and name it auth.
156
+ # And when you then call `with auth: { key: 'secret'}` on your
157
+ # builder, `key: 'secret' will be passed as options to the Layer.
158
+ #
159
+ # router - Path of connected Layers.
160
+ # request - Request issued for this Layer.
161
+ #
162
+ # Returns Nothing
163
+ def call router, request, options = {}
164
+ @callable.call router, request, request.http.context[@name]
165
+ nil
166
+ end
167
+
168
+ # Internal: Checks if this Layer has to be called in a certain context.
169
+ #
170
+ # context - The context the Layer is called in.
171
+ #
172
+ # Returns true if the Layer has to be called.
173
+ def callable? context
174
+ !@callable.nil? && context.enabled?(name)
175
+ end
176
+
177
+ # Internal: Two layers are equal if there callables are equal.
178
+ def == layer
179
+ self.callable == layer.callable
180
+ end
181
+
182
+ protected
183
+
184
+ # Internal: The #call object where the logic of this Layer resides.
185
+ attr_reader :callable
186
+ end
187
+ end