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
@@ -0,0 +1,141 @@
1
+ require 'commute/core/util/event_emitter'
2
+
3
+ module Commute
4
+
5
+ # Internal: A Simple Evented stream modelled after node.js' stream.
6
+ # More info: http://nodejs.org/api/stream.html.
7
+ #
8
+ # When you create a stream, you can write anything to it. For every
9
+ # write, an event 'data' is emitted to the listeners (evented reading).
10
+ #
11
+ # When a stream is ended, an 'end' event is emitted and the stream is
12
+ # not writeable anymore.
13
+ #
14
+ module Stream
15
+ include EventEmitter
16
+
17
+ class Simple
18
+ include Stream
19
+ end
20
+
21
+ # Internal: Opens a new writeable stream.
22
+ def initialize *args
23
+ super *args
24
+ @writeable = true
25
+ end
26
+
27
+ # Internal: Writes data to the stream.
28
+ # Note: Silenty does not write if the stream is not writeable (see #writable?).
29
+ #
30
+ # data - A chunk of data to write to the stream.
31
+ #
32
+ # Returns the written data (nil if no data was written).
33
+ def write data
34
+ if writeable?
35
+ emit :data, data
36
+ data
37
+ end
38
+ end
39
+
40
+ # Internal: Checks if the stream is writeable.
41
+ #
42
+ # Returns true if the stream is writeable.
43
+ def writeable?
44
+ @writeable
45
+ end
46
+
47
+ # Internal: Ends the stream.
48
+ # Note: After this, the stream is not writeable anymore.
49
+ #
50
+ # data - Last chunk of data to send.
51
+ #
52
+ # Returns Nothing
53
+ def end data = nil
54
+ emit :data, data if data
55
+ @writeable = false
56
+ emit :end
57
+ nil
58
+ end
59
+
60
+ # Internal: Pipes the stream to another stream.
61
+ #
62
+ # Whenever data is written to the source stream,
63
+ # it gets written to the destination stream.
64
+ # When the source stream end, the destinations stream
65
+ # is ended as well.
66
+ #
67
+ # destination - The stream to pipe to.
68
+ # options - For future use.
69
+ #
70
+ # Returns Nothing.
71
+ def pipe destination, options = {}
72
+ on(:data) { |chunk| destination.write chunk }
73
+ on(:end) { destination.end }
74
+ nil
75
+ end
76
+
77
+ # Buffers data and emits the entire buffer.
78
+ # Calls << on the initial for every chunk in the stream.
79
+ #
80
+ # initial - The initial value of the buffer (default: []).
81
+ #
82
+ # Yields if done buffering.
83
+ # Returns a new buffered stream.
84
+ def buffer
85
+ inject [], &:<<
86
+ end
87
+
88
+ def >> sink
89
+ buffer.on(:data) { |buffer| sink.concat buffer }
90
+ end
91
+
92
+ def map &block
93
+ Simple.new.tap do |stream|
94
+ on(:data) { |chunk| stream.write block.call(chunk) }
95
+ on(:end) { stream.end }
96
+ end
97
+ end
98
+
99
+ def reject &block
100
+ Simple.new.tap do |stream|
101
+ on(:data) { |chunk| stream.write chunk unless block.call(chunk) }
102
+ on(:end) { stream.end }
103
+ end
104
+ end
105
+
106
+ def select &block
107
+ Simple.new.tap do |stream|
108
+ on(:data) { |chunk| stream.write chunk if block.call(chunk) }
109
+ on(:end) { stream.end }
110
+ end
111
+ end
112
+
113
+ def inject initial, &block
114
+ Simple.new.tap do |stream|
115
+ triggered = false
116
+ on(:data) do |chunk|
117
+ initial = block.call initial, chunk
118
+ triggered = true
119
+ end
120
+ on(:end) do
121
+ stream.write initial if triggered
122
+ stream.end
123
+ end
124
+ end
125
+ end
126
+
127
+ def take amount
128
+ Simple.new.tap do |stream|
129
+ count = 0
130
+ on(:data) do |chunk|
131
+ if count < amount
132
+ stream.write chunk
133
+ count += 1
134
+ else
135
+ stream.end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,88 @@
1
+ module Commute
2
+ module Extension
3
+
4
+ # Public: Defines standard Restful CRUD actions.
5
+ #
6
+ # create -> POST
7
+ # read -> GET
8
+ # update -> PUT
9
+ # destroy -> DELETE
10
+ #
11
+ # Note: Implement "one" and "list" methods to
12
+ # hook on different CRUD selectors.
13
+ #
14
+ # Examples:
15
+ #
16
+ # gist = gists.find(1).run!
17
+ # gist[:description] = "Testing CRUD"
18
+ # gists.update(gist).where(id: 1).run
19
+ # gists.destroy(1).run
20
+ #
21
+ # # Making use of contexts.
22
+ # handle = gists.where(id: 1)
23
+ # gist = handle.find.run!
24
+ # gist[:description] = "Testing CRUD"
25
+ # handle.update(gist).run
26
+ # handle.destroy
27
+ #
28
+ module Crud
29
+ METHODS = [:all, :find, :update, :create, :destroy].freeze
30
+
31
+ # Public: An Extended CRUD extension that uses PATCH for updates.
32
+ #
33
+ # create -> POST
34
+ # read -> GET
35
+ # update -> PATCH
36
+ # replace -> PUT
37
+ # destroy -> DELETE
38
+ #
39
+ module Extended
40
+ include Crud
41
+ METHODS = [:all, :find, :update, :replace, :create, :destroy].freeze
42
+
43
+ # Public: Replaces a resource
44
+ def replace body = nil
45
+ put.body(body).try :one
46
+ end
47
+
48
+ # Public: Updates a resource.
49
+ def update body = nil
50
+ patch.body(body).try :one
51
+ end
52
+ end
53
+
54
+ # Public: Get all resources.
55
+ def all
56
+ get.try :list
57
+ end
58
+
59
+ # Public: Find a resource using an id.
60
+ #
61
+ # id - The id of the resource (optional).
62
+ #
63
+ def find id = nil
64
+ with id: id
65
+ get.try :one
66
+ end
67
+
68
+ # Public: Updates a resource.
69
+ def update body = nil
70
+ put.body(body).try :one
71
+ end
72
+
73
+ # Public: Creates a resource.
74
+ def create body = nil
75
+ post.body(body).try :one
76
+ end
77
+
78
+ # Public: Destroys a resource.
79
+ #
80
+ # id - The id of the resource to destroy (optional).
81
+ #
82
+ def destroy id = nil
83
+ with id: id
84
+ delete.try :one
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,20 @@
1
+ module Commute
2
+ module Extension
3
+
4
+ module Param
5
+ METHOD = [:param, :params]
6
+
7
+ def param name, param_name = name
8
+ transform name do |request, value|
9
+ request.query[param_name] = value
10
+ end
11
+ end
12
+
13
+ def params names
14
+ names.each do |name|
15
+ self.param name
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ require 'uri'
2
+
3
+ module Commute
4
+ module Extension
5
+
6
+ # The url extensions provides an `url` method that pushes
7
+ # a transformation on the context that renders the required url
8
+ # based on a pattern.
9
+ #
10
+ # The pattern uses the standard ruby format specification. When
11
+ # not all variables are present, the url in sanitized in a
12
+ # way that it is valid again.
13
+ #
14
+ # Examples:
15
+ #
16
+ # class GistApi < Commute::Api
17
+ # include Commute::Extension::Url
18
+ #
19
+ # url 'https://api.github.com/gists/%{filter}'
20
+ #
21
+ # def user user = nil
22
+ # with(user: user)
23
+ # url('https://api.github.com/user/%{user}/gists').all
24
+ # end
25
+ # end
26
+ #
27
+ module Url
28
+ METHODS = [:url].freeze
29
+
30
+ # Public: Define the url pattern to use.
31
+ #
32
+ # pattern - The patter with variables between %{}.
33
+ #
34
+ # Returns self.
35
+ def url pattern
36
+ # Transform the context, filling in the url.
37
+ transform do |request, context|
38
+ # First, render the url pattern.
39
+ rendered = pattern.gsub(/%{([^}]*)}/) { |m| context[$1.to_sym] }
40
+ # Parse the rendered url using uri.
41
+ uri = URI rendered
42
+ # Substitute multiple / by one /.
43
+ uri.path.gsub! /\/+/, '/'
44
+ # Substiture /.format by .format
45
+ uri.path.sub! /\/\.([^\/]+)$/, '.\1'
46
+
47
+ # Assign the uri to the request.
48
+ request.uri = uri
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,3 @@
1
1
  module Commute
2
- VERSION = "0.2.0.rc.2"
2
+ VERSION = "0.3.0.pre"
3
3
  end
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+ require 'commute/common/caching'
3
+
4
+ describe Commute::Common::Caching do
5
+
6
+ let(:cache) { stub }
7
+
8
+ let(:response) {
9
+ Commute::Http::Response.new(nil).tap { |response|
10
+ response.headers['ETag'] = 'A'
11
+ }
12
+ }
13
+
14
+ let(:body) {
15
+ { some: ['complex', 'structure'] }
16
+ }
17
+
18
+ let(:responder) {
19
+ proc { |router, request|
20
+ request.respond(response, status).tap { |response|
21
+ response.write body
22
+ response.end
23
+ }
24
+ }
25
+ }
26
+
27
+ let(:stack) {
28
+ Commute::Stack.new do |stack|
29
+ stack.sequence do |s|
30
+ s.append Commute::Common::Caching.new
31
+ s.append responder
32
+ end
33
+ end
34
+ }
35
+
36
+ let(:base) {
37
+ Commute::Context.new(stack).with(caching: {
38
+ cache: cache
39
+ }).context
40
+ }
41
+
42
+ describe 'a get request' do
43
+ let(:api) do
44
+ base.transform do |request|
45
+ request.method = :get
46
+ request.uri = URI('http://www.example.com')
47
+ end
48
+ end
49
+
50
+ describe 'nothing in the cache' do
51
+ before do
52
+ cache.stubs(:get).returns nil
53
+ end
54
+
55
+ it 'should fire the request' do
56
+ responder.expects(:call).once.with do |router, request|
57
+ request.http.method.must_equal :get
58
+ end
59
+ call
60
+ end
61
+
62
+ describe 'the response was succesful' do
63
+ let(:status) { Commute::Http::Status.new(200) }
64
+
65
+ it 'should store the response in the cache and return the response' do
66
+ cache.expects(:set).with('http://www.example.com', {
67
+ data: body,
68
+ etag: 'A'
69
+ })
70
+ call
71
+ @body.must_equal body
72
+ @status.success?.must_equal true
73
+ @status.cached.must_equal false
74
+ @status.http.wont_be_nil
75
+ end
76
+ end
77
+
78
+ describe 'the response was not successful' do
79
+ end
80
+ end
81
+
82
+ describe 'something in the cache' do
83
+ before do
84
+ cache.stubs(:get).with('http://www.example.com').returns \
85
+ data: body,
86
+ etag: 'A'
87
+ end
88
+
89
+ describe 'without validation' do
90
+ it 'should return a response with the cached data' do
91
+ responder.expects(:call).never
92
+ call
93
+ @body.must_equal body
94
+ @status.success?.must_equal true
95
+ @status.cached.must_equal true
96
+ @status.updated.must_equal false
97
+ @status.http.must_be_nil
98
+ end
99
+ end
100
+
101
+ describe 'with validation' do
102
+ let(:base) {
103
+ Commute::Context.new(stack).with(caching: {
104
+ cache: cache,
105
+ validate: true
106
+ }).context
107
+ }
108
+
109
+ it 'should verify the etag of the resource' do
110
+ responder.expects(:call).once.with do |router, request|
111
+ request.http.headers['If-None-Match'].must_equal 'A'
112
+ end
113
+ call
114
+ end
115
+
116
+ describe 'the resource was not modified' do
117
+ let(:status) { Commute::Http::Status.new(304) }
118
+
119
+ it 'it should respond without updating the cache' do
120
+ cache.expects(:set).never
121
+ call
122
+ @body.must_equal body
123
+ @status.success?.must_equal true
124
+ @status.cached.must_equal true
125
+ @status.updated.must_equal false
126
+ @status.http.wont_be_nil
127
+ end
128
+ end
129
+
130
+ describe 'the resource was modified' do
131
+ let(:status) { Commute::Http::Status.new(200) }
132
+
133
+ it 'it should respond and update the cache' do
134
+ cache.expects(:set).with('http://www.example.com', {
135
+ data: body,
136
+ etag: 'A'
137
+ })
138
+ call
139
+ @body.must_equal body
140
+ @status.success?.must_equal true
141
+ @status.cached.must_equal true
142
+ @status.updated.must_equal true
143
+ @status.http.wont_be_nil
144
+ end
145
+ end
146
+
147
+ describe 'the response was not succesful' do
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def call
156
+ @body, @status = api.run
157
+ end
158
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'commute/common/eventmachine'
3
+
4
+ describe Commute::Common::Eventmachine do
5
+
6
+ let(:adapter) { Commute::Common::Eventmachine.new }
7
+
8
+ let(:router) { Commute::Stack::Router.new [adapter].each }
9
+
10
+ before do
11
+ @stub = stub_request(:post, "http://www.example.com/").with \
12
+ headers: {
13
+ 'Accept-Encoding' => 'gzip, deflate'
14
+ },
15
+ body: 'Hello World!'
16
+
17
+ @done = proc {}
18
+ end
19
+
20
+ it 'should be able to make a successful request' do
21
+ @done.expects(:call).twice
22
+ EM.run do
23
+ # Stub the request.
24
+ @stub.to_return(status: 200, body: 'Hello!', headers: {})
25
+
26
+ # Create a Http Request.
27
+ http_request = Commute::Http::Request.new(nil).tap do |r|
28
+ r.uri = URI('http://www.example.com')
29
+ r.method = :post
30
+ end
31
+
32
+ # Execute the request.
33
+ request = router.call http_request do |response, status|
34
+ @done.call
35
+ status.success?.must_equal true
36
+ response.buffer.on(:data) do |body|
37
+ @done.call
38
+ body.join.must_equal 'Hello!'
39
+ end
40
+
41
+ EM.stop
42
+ end
43
+ request.write 'Hello'
44
+ request.write ' World!'
45
+ request.end
46
+ end
47
+ end
48
+
49
+ it 'should be able to handle a bad response' do
50
+ @done.expects(:call).once
51
+ EM.run do
52
+ # Stub the request.
53
+ @stub.to_return(status: 500, headers: {})
54
+
55
+ # Create a Http Request.
56
+ http_request = Commute::Http::Request.new(nil).tap do |r|
57
+ r.uri = URI('http://www.example.com')
58
+ r.method = :post
59
+ end
60
+
61
+ # Execute the request.
62
+ request = router.call http_request do |response, status|
63
+ @done.call
64
+ status.fail?.must_equal true
65
+ response.on(:data) { @done.call }
66
+
67
+ EM.stop
68
+ end
69
+ request.write 'Hello'
70
+ request.write ' World!'
71
+ request.end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+ require 'commute/common/typhoeus'
3
+
4
+ describe Commute::Common::Typhoeus do
5
+
6
+ let(:adapter) { Commute::Common::Typhoeus.new }
7
+
8
+ let(:router) { Commute::Stack::Router.new [adapter].each }
9
+
10
+ before do
11
+ @stub = stub_request(:post, "http://www.example.com/").with \
12
+ headers: {
13
+ 'User-Agent' => 'Typhoeus - https://github.com/typhoeus/typhoeus'
14
+ },
15
+ body: 'Hello World!'
16
+ end
17
+
18
+ it 'should be able to make a successful request' do
19
+ # Stub the request.
20
+ @stub.to_return(status: 200, body: 'Hello!', headers: {})
21
+
22
+ # Create a Http Request.
23
+ http_request = Commute::Http::Request.new(nil).tap do |r|
24
+ r.uri = URI('http://www.example.com')
25
+ r.method = :post
26
+ end
27
+
28
+ # Execute the request.
29
+ response, body, status = nil, [], nil
30
+ request = router.call http_request do |_response, _status|
31
+ _response >> body
32
+ response, status = _response, _status
33
+ end
34
+ request.write 'Hello'
35
+ request.write ' World!'
36
+ request.end
37
+
38
+ # Do some checks.
39
+ status.success?.must_equal true
40
+ body.join.must_equal 'Hello!'
41
+ end
42
+
43
+ it 'should be able to handle a bad response' do
44
+ # Stub the request.
45
+ @stub.to_return(status: 500, headers: {})
46
+
47
+ # Create a Http Request.
48
+ http_request = Commute::Http::Request.new(nil).tap do |r|
49
+ r.uri = URI('http://www.example.com')
50
+ r.method = :post
51
+ end
52
+
53
+ # Execute the request.
54
+ response, body, status = nil, [], nil
55
+ request = router.call http_request do |_response, _status|
56
+ _response >> body
57
+ response, status = _response, _status
58
+ end
59
+ request.write 'Hello'
60
+ request.write ' World!'
61
+ request.end
62
+
63
+ # Do some checks.
64
+ status.fail?.must_equal true
65
+ body.must_be_empty
66
+ end
67
+ end
@@ -48,14 +48,16 @@ describe Commute::Api do
48
48
  end
49
49
 
50
50
  it 'should create a new Api instance when a method is called on the class' do
51
+ api_klass.builder[:id].must_equal 1
51
52
  context = api_klass.with(id: 2).all.context
52
53
  context.parameters.must_equal id: 2, text: 'go', help: true
54
+ api_klass.builder[:id].must_equal 1
53
55
  end
54
56
 
55
57
  it 'should deal with inheritance' do
56
58
  context = TestInheritance.new.context
57
59
  context.parameters[:id].must_equal 2
58
- context.stack.sequence.processors.size.must_be :>=, 2
60
+ context.stack.sequence.size.must_be :>=, 2
59
61
  end
60
62
 
61
63
  describe '#new' do
@@ -1,12 +1,12 @@
1
1
  require 'spec_helper'
2
2
  require 'commute/core/context'
3
- require 'commute/core/request'
3
+ require 'commute/core/http'
4
4
 
5
5
  describe Commute::Builder do
6
6
 
7
7
  let(:parameters) {{}}
8
- let(:stack) { Commute::Stack.new { |stack, main|
9
- main.append Proc.new {}
8
+ let(:stack) { Commute::Stack.new {
9
+ sequence.append Proc.new {}
10
10
  }}
11
11
  let(:transformations) {[]}
12
12
  let(:disables) {[]}
@@ -15,7 +15,7 @@ describe Commute::Builder do
15
15
 
16
16
  let(:builder) { Commute::Builder.new context }
17
17
 
18
- let(:request) { Commute::Request.new }
18
+ let(:request) { Commute::Http::Request.new context }
19
19
 
20
20
  describe '#with' do
21
21
  it 'should add parameters to the context' do
@@ -82,18 +82,18 @@ describe Commute::Builder do
82
82
 
83
83
  it 'should be able to add a dynamic transform without dependencies' do
84
84
  context = builder.with(id: 1).transform { |request, context|
85
- request.path = "/#{context[:id]}"
85
+ request.uri.path = "/#{context[:id]}"
86
86
  }.context
87
87
  context.transformations.first.call(request, context)
88
- request.path.must_equal '/1'
88
+ request.uri.path.must_equal '/1'
89
89
  end
90
90
 
91
91
  it 'should be able to add a dynamic transform with dependencies' do
92
92
  context = builder.with(id: 1).transform(:id) { |request, id|
93
- request.path = "/#{id}"
93
+ request.uri.path = "/#{id}"
94
94
  }.context
95
95
  context.transformations.first.call(request, context)
96
- request.path.must_equal '/1'
96
+ request.uri.path.must_equal '/1'
97
97
  end
98
98
 
99
99
  it 'should never call the transformation when its dependencies are all nil' do