commute 0.2.0.rc.2 → 0.3.0.pre

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 (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