whenner 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +6 -0
- data/.travis.yml +4 -0
- data/.yardopts +8 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +30 -0
- data/HISTORY.md +6 -0
- data/LICENSE +20 -0
- data/README.md +119 -0
- data/Rakefile +15 -0
- data/lib/whenner/callback.rb +55 -0
- data/lib/whenner/conversions.rb +17 -0
- data/lib/whenner/deferred.rb +173 -0
- data/lib/whenner/promise.rb +44 -0
- data/lib/whenner/version.rb +3 -0
- data/lib/whenner.rb +63 -0
- data/spec/whenner/conversions_spec.rb +19 -0
- data/spec/whenner/deferred_spec.rb +141 -0
- data/spec/whenner/promise_spec.rb +51 -0
- data/spec/whenner_spec.rb +39 -0
- data/whenner.gemspec +42 -0
- metadata +131 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9a4aa8af9fb47aa98f75a11bb2433eacf417ae87
|
4
|
+
data.tar.gz: 69d6cbba0ed8f7549028e185618321b31be8a1a0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 33fe745591cd305f62158756d6de021c3bdb82b1501c73826a9e6a06010db311c4bfbf1f4234ea09680af99d3cea32391a0cfabf0e8b4f33f229ac65601f40b9
|
7
|
+
data.tar.gz: 97794e75339f30bf8de24b1325d51674fe96ee6a55a0e421ff17a54fdcc1e0b02fd97a21c0e1fa2ac28a0cb0b72dc17712fe55c45a424159043ba35f51708117
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
whenner (0.1.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.2.5)
|
10
|
+
kramdown (1.3.0)
|
11
|
+
rake (10.1.0)
|
12
|
+
rspec (2.14.1)
|
13
|
+
rspec-core (~> 2.14.0)
|
14
|
+
rspec-expectations (~> 2.14.0)
|
15
|
+
rspec-mocks (~> 2.14.0)
|
16
|
+
rspec-core (2.14.7)
|
17
|
+
rspec-expectations (2.14.4)
|
18
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
19
|
+
rspec-mocks (2.14.4)
|
20
|
+
yard (0.8.7.3)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
kramdown
|
27
|
+
rake
|
28
|
+
rspec
|
29
|
+
whenner!
|
30
|
+
yard
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (C) 2013 Arjan van der Gaag
|
2
|
+
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
6
|
+
the Software without restriction, including without limitation the rights to
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
9
|
+
so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
12
|
+
copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Whenner [](http://travis-ci.org/avdgaag/whenner)
|
2
|
+
|
3
|
+
## Introduction
|
4
|
+
|
5
|
+
A promise represents the eventual result of an asynchronous operation. The
|
6
|
+
primary way of interacting with a promise is through its `done` and `fail`
|
7
|
+
methods, which registers callbacks to receive either a promise’s eventual value
|
8
|
+
or the reason why the promise cannot be fulfilled.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Whenner is distributed as a Ruby gem, which should be installed on most Macs and
|
13
|
+
Linux systems. Once you have ensured you have a working installation of Ruby
|
14
|
+
and Ruby gems, install the gem as follows from the command line:
|
15
|
+
|
16
|
+
$ gem install whenner
|
17
|
+
|
18
|
+
## Usage
|
19
|
+
|
20
|
+
Whenner provides two basic methods to use deferreds:
|
21
|
+
|
22
|
+
* `Whenner.defer` to create a new deferred object and return its promise. In the
|
23
|
+
block to the method you can fulfill or reject the deferred.
|
24
|
+
* `Whenner.when` to convert one or more arguments into promises, combining them
|
25
|
+
into a single new promise that you can attach callbacks to.
|
26
|
+
|
27
|
+
Deferred objects can give you a promise that, at some point in the future, will
|
28
|
+
resolve to either a fulfilled or rejected state. When that happens, appropriate
|
29
|
+
callbacks are called. You can attach such callbacks on a deferred or promise
|
30
|
+
using three methods:
|
31
|
+
|
32
|
+
* `done` to register blocks to be called when the promise is fulfilled;
|
33
|
+
* `fail` to register blocks to be called when the promise is rejected;
|
34
|
+
* `always` to register blocks to be called when the promise is resolved (either
|
35
|
+
fulfilled or rejected);
|
36
|
+
|
37
|
+
Here's an example of making three asynchronous HTTP requests, waiting for them
|
38
|
+
all to finish and acting on their results:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
42
|
+
require 'whenner'
|
43
|
+
require 'uri'
|
44
|
+
require 'net/http'
|
45
|
+
|
46
|
+
include Whenner
|
47
|
+
|
48
|
+
def async_get(uri)
|
49
|
+
defer do |f|
|
50
|
+
thread = Thread.new do
|
51
|
+
response = Net::HTTP.get_response(URI(uri))
|
52
|
+
if response.code =~ /^2/
|
53
|
+
f.fulfill response.body
|
54
|
+
else
|
55
|
+
f.reject response.message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
at_exit { thread.join }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
cnn = async_get('http://edition.cnn.com')
|
63
|
+
nytimes = async_get('http://www.nytimes.com')
|
64
|
+
google = async_get('http://www.google.nl')
|
65
|
+
|
66
|
+
Whenner.when(cnn, google, nytimes).done do |results|
|
67
|
+
results.map { |str| str[/<title>(.+)<\/title>/, 1] }
|
68
|
+
end.done do |titles|
|
69
|
+
puts "Success: #{titles.inspect}"
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
As methods in Ruby can only take a single block, Whenner does not support a
|
74
|
+
`then` method yet, that would combine the `done` and `fail` methods. This might
|
75
|
+
be implementing in the future using something like this:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
defer { async_get('http://google.com') }.then do |on|
|
79
|
+
on.done { puts 'Success!' }
|
80
|
+
on.fail { puts 'Success!' }
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
### Documentation
|
85
|
+
|
86
|
+
See the inline [API
|
87
|
+
docs](http://rubydoc.info/github/avdgaag/whenner/master/frames) for more
|
88
|
+
information.
|
89
|
+
|
90
|
+
## Other
|
91
|
+
|
92
|
+
### Note on Patches/Pull Requests
|
93
|
+
|
94
|
+
1. Fork the project.
|
95
|
+
2. Make your feature addition or bug fix.
|
96
|
+
3. Add tests for it. This is important so I don't break it in a future version
|
97
|
+
unintentionally.
|
98
|
+
4. Commit, do not mess with rakefile, version, or history. (if you want to have
|
99
|
+
your own version, that is fine but bump version in a commit by itself I can
|
100
|
+
ignore when I pull)
|
101
|
+
5. Send me a pull request. Bonus points for topic branches.
|
102
|
+
|
103
|
+
### Issues
|
104
|
+
|
105
|
+
Please report any issues, defects or suggestions in the [Github issue
|
106
|
+
tracker](https://github.com/avdgaag/whenner/issues).
|
107
|
+
|
108
|
+
### What has changed?
|
109
|
+
|
110
|
+
See the [HISTORY](https://github.com/avdgaag/whenner/blob/master/HISTORY.md) file
|
111
|
+
for a detailed changelog.
|
112
|
+
|
113
|
+
### Credits
|
114
|
+
|
115
|
+
Created by: Arjan van der Gaag
|
116
|
+
URL: [http://arjanvandergaag.nl](http://arjanvandergaag.nl)
|
117
|
+
Project homepage: [http://avdgaag.github.com/whenner](http://avdgaag.github.com/whenner)
|
118
|
+
Date: april 2012
|
119
|
+
License: [MIT-license](https://github.com/avdgaag/whenner/blob/master/LICENSE) (same as Ruby)
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require 'bundler'
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
Bundler.setup
|
5
|
+
|
6
|
+
desc 'Default: run specs.'
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
desc 'Run specs'
|
11
|
+
RSpec::Core::RakeTask.new
|
12
|
+
|
13
|
+
require 'yard'
|
14
|
+
desc 'Generate API docs'
|
15
|
+
YARD::Rake::YardocTask.new
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Whenner
|
2
|
+
# A Callback is used internally by {Deferred} to store its callbacks.
|
3
|
+
# It provides the same `call` interface as regular blocks, but this
|
4
|
+
# will always return a promise for the block's return value.
|
5
|
+
#
|
6
|
+
# When the block in question returns a regular object, a new deferred for
|
7
|
+
# that object is created and immediately fulfilled. When the block raises an
|
8
|
+
# exception, the returned promise is rejected with that exception. When the
|
9
|
+
# block returns a promise itself, the returned deferred will mimic that
|
10
|
+
# promise -- as if that promise is what actually was returned.
|
11
|
+
class Callback
|
12
|
+
# A callable object, usually a Ruby block.
|
13
|
+
#
|
14
|
+
# @return [#call]
|
15
|
+
attr_reader :block
|
16
|
+
|
17
|
+
# @return [Deferred] the deferred object representing the block's return
|
18
|
+
# value.
|
19
|
+
attr_reader :deferred
|
20
|
+
|
21
|
+
def initialize(block)
|
22
|
+
@block = block
|
23
|
+
@deferred = Deferred.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Run the block, passing it any given arguments, and return a promise
|
27
|
+
# for its return value.
|
28
|
+
#
|
29
|
+
# @return [Promise]
|
30
|
+
def call(*args)
|
31
|
+
update_deferred(*args)
|
32
|
+
deferred.promise
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Promise] for this callback's {#deferred}.
|
36
|
+
# @see #deferred
|
37
|
+
def promise
|
38
|
+
deferred.promise
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def update_deferred(*args)
|
44
|
+
retval = block.call(*args)
|
45
|
+
if retval.kind_of?(Promise)
|
46
|
+
retval.done { |arg| deferred.fulfill(arg) }
|
47
|
+
retval.fail { |arg| deferred.reject(arg) }
|
48
|
+
else
|
49
|
+
deferred.fulfill(retval)
|
50
|
+
end
|
51
|
+
rescue => e
|
52
|
+
deferred.reject(e)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Whenner
|
2
|
+
module Conversions
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# Convert any object to a promise. When the object in question responds to
|
6
|
+
# `to_promise`, the result of that method will be returned. If not, a new
|
7
|
+
# deferred object is created and immediately fulfilled with the given
|
8
|
+
# object.
|
9
|
+
#
|
10
|
+
# @param [Object] obj
|
11
|
+
# @return [Promise]
|
12
|
+
def Promise(obj)
|
13
|
+
return obj.to_promise if obj.respond_to?(:to_promise)
|
14
|
+
Deferred.new.fulfill(obj)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module Whenner
|
2
|
+
# A deferred object is an operation that will eventually resolve to a result
|
3
|
+
# value. A deferred can be in three possible states:
|
4
|
+
#
|
5
|
+
# * Pending: it has been created but not yet resolved.
|
6
|
+
# * Fulfilled: it has been successfully resolved.
|
7
|
+
# * Rejected: it has been unsuccessfully resolved.
|
8
|
+
#
|
9
|
+
# A deferred might transition from pending to fulfilled or rejected, but it
|
10
|
+
# will not transition again once resolved (resolved can be either fulfilled
|
11
|
+
# or rejected).
|
12
|
+
#
|
13
|
+
# When a deferred does resolve, it will trigger any applicable callbacks. You
|
14
|
+
# can stack on callbacks on a deferred object before it has been resolved and
|
15
|
+
# they will be called later. When you register callbacks on an already
|
16
|
+
# resolved deferred, the callback will be called immediately. Note that a
|
17
|
+
# callback will only be run once.
|
18
|
+
#
|
19
|
+
# When a callback is in the fulfilled state, it has a value that represents
|
20
|
+
# its eventual outcome. When it is rejected, it has a reason.
|
21
|
+
class Deferred
|
22
|
+
# @return [Promise] a promise for this deferred
|
23
|
+
attr_reader :promise
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@promise = Promise.new(self)
|
27
|
+
@state = :pending
|
28
|
+
@fulfilled_callbacks = []
|
29
|
+
@rejected_callbacks = []
|
30
|
+
@always_callbacks = []
|
31
|
+
end
|
32
|
+
|
33
|
+
# The value the deferred was resolved with.
|
34
|
+
#
|
35
|
+
# @raise [UnresolvedError] when the deferred is still pending
|
36
|
+
def value
|
37
|
+
raise UnresolvedError unless resolved?
|
38
|
+
@value
|
39
|
+
end
|
40
|
+
|
41
|
+
# The reason the deferred was rejected.
|
42
|
+
#
|
43
|
+
# @raise [UnresolvedError] when the deferred is still pending
|
44
|
+
def reason
|
45
|
+
raise UnresolvedError unless resolved?
|
46
|
+
@reason
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Boolean] whether the deferred has not been resolved yet.
|
50
|
+
def pending?
|
51
|
+
state == :pending
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Boolean] whether the deferred was successfully resolved.
|
55
|
+
def fulfilled?
|
56
|
+
state == :fulfilled
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Boolean] whether the deferred was rejected.
|
60
|
+
def rejected?
|
61
|
+
state == :rejected
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Boolean] whether the deferred was either fulfilled or rejected.
|
65
|
+
def resolved?
|
66
|
+
fulfilled? || rejected?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Fulfill this promise with an optional value. The value will be stored in
|
70
|
+
# the deferred and passed along to any registered `done` callbacks.
|
71
|
+
#
|
72
|
+
# When fulfilling a deferred twice, nothing happens.
|
73
|
+
#
|
74
|
+
# @raise [CannotTransitionError] when it was already fulfilled.
|
75
|
+
# @return [Deferred] self
|
76
|
+
def fulfill(value = nil)
|
77
|
+
raise CannotTransitionError if rejected?
|
78
|
+
return if fulfilled?
|
79
|
+
unless resolved?
|
80
|
+
self.value = value
|
81
|
+
resolve_to(:fulfilled)
|
82
|
+
end
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
# Reject this promise with an optional reason. The reason will be stored in
|
87
|
+
# the deferred and passed along to any registered `fail` callbacks.
|
88
|
+
#
|
89
|
+
# When rejecting a deferred twice, nothing happens.
|
90
|
+
#
|
91
|
+
# @raise [CannotTransitionError] when it was already fulfilled.
|
92
|
+
# @return [Deferred] self
|
93
|
+
def reject(reason = nil)
|
94
|
+
raise CannotTransitionError if fulfilled?
|
95
|
+
return if rejected?
|
96
|
+
unless resolved?
|
97
|
+
self.reason = reason
|
98
|
+
resolve_to(:rejected)
|
99
|
+
end
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [Promise]
|
104
|
+
def to_promise
|
105
|
+
promise
|
106
|
+
end
|
107
|
+
|
108
|
+
# Register a callback to be run when the deferred is fulfilled.
|
109
|
+
#
|
110
|
+
# @yieldparam [Object] value
|
111
|
+
# @return [Promise] a new promise representing the return value
|
112
|
+
# of the callback, or -- when that return value is a promise itself
|
113
|
+
# -- a promise mimicking that promise.
|
114
|
+
def done(&block)
|
115
|
+
cb = Callback.new(block)
|
116
|
+
fulfilled_callbacks << cb
|
117
|
+
cb.call(*callback_response) if fulfilled?
|
118
|
+
cb.promise
|
119
|
+
end
|
120
|
+
|
121
|
+
# Register a callback to be run when the deferred is rejected.
|
122
|
+
#
|
123
|
+
# @yieldparam [Object] reason
|
124
|
+
# @return [Promise] a new promise representing the return value
|
125
|
+
# of the callback, or -- when that return value is a promise itself
|
126
|
+
# -- a promise mimicking that promise.
|
127
|
+
def fail(&block)
|
128
|
+
cb = Callback.new(block)
|
129
|
+
rejected_callbacks << cb
|
130
|
+
cb.call(*callback_response) if rejected?
|
131
|
+
cb.promise
|
132
|
+
end
|
133
|
+
|
134
|
+
# Register a callback to be run when the deferred is resolved.
|
135
|
+
#
|
136
|
+
# @yieldparam [Object] value
|
137
|
+
# @yieldparam [Object] reason
|
138
|
+
# @return [Promise] a new promise representing the return value
|
139
|
+
# of the callback, or -- when that return value is a promise itself
|
140
|
+
# -- a promise mimicking that promise.
|
141
|
+
def always(&block)
|
142
|
+
cb = Callback.new(block)
|
143
|
+
always_callbacks << cb
|
144
|
+
cb.call(*callback_response) if resolved?
|
145
|
+
cb.promise
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
attr_accessor :state
|
151
|
+
attr_writer :value, :reason
|
152
|
+
attr_reader :fulfilled_callbacks, :rejected_callbacks, :always_callbacks
|
153
|
+
|
154
|
+
def resolve_to(state)
|
155
|
+
self.state = state
|
156
|
+
flush
|
157
|
+
end
|
158
|
+
|
159
|
+
def result_callbacks
|
160
|
+
fulfilled? ? fulfilled_callbacks : rejected_callbacks
|
161
|
+
end
|
162
|
+
|
163
|
+
def callback_response
|
164
|
+
fulfilled? ? value : reason
|
165
|
+
end
|
166
|
+
|
167
|
+
def flush
|
168
|
+
(result_callbacks + always_callbacks).each do |cb|
|
169
|
+
cb.call(callback_response)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Whenner
|
2
|
+
# A promise represents the public face of a {Deferred} object. You can use it
|
3
|
+
# to add more callbacks to the deferred or inspect its state -- but you
|
4
|
+
# cannot resolve it.
|
5
|
+
#
|
6
|
+
# The methods and attributes of the promise are basically forwarded to the
|
7
|
+
# deferred.
|
8
|
+
class Promise
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# @!attribute [r] fulfilled?
|
12
|
+
# @return [Boolean] whether the deferred was successfully resolved.
|
13
|
+
# @!attribute [r] resolved?
|
14
|
+
# @return [Boolean] whether the deferred was either fulfilled or rejected.
|
15
|
+
# @!attribute [r] rejected?
|
16
|
+
# @return [Boolean] whether the deferred was rejected.
|
17
|
+
# @!attribute [r] pending?
|
18
|
+
# @return [Boolean] whether the deferred has not been resolved yet.
|
19
|
+
# @!attribute [r] reason
|
20
|
+
# @return [Object] the reason for the deferred to be rejected.
|
21
|
+
# @!attribute [r] value
|
22
|
+
# @return [Object] the value that the deferred was fulfilled with.
|
23
|
+
# @!method fail(&block)
|
24
|
+
# Register a callback to fire when the deferred is rejected.
|
25
|
+
# @return [Promise] a new promise for the return value of the block.
|
26
|
+
# @!method done(&block)
|
27
|
+
# Register a callback to fire when the deferred is fulfilled.
|
28
|
+
# @return [Promise] a new promise for the return value of the block.
|
29
|
+
# @!method always(&block)
|
30
|
+
# Register a callback to fire when the deferred is resolved.
|
31
|
+
# @return [Promise] a new promise for the return value of the block.
|
32
|
+
def_delegators :@deferred, *%i[
|
33
|
+
reason value pending? fulfilled? resolved? rejected? fail done always
|
34
|
+
]
|
35
|
+
|
36
|
+
def initialize(deferred)
|
37
|
+
@deferred = deferred
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_promise
|
41
|
+
self
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/whenner.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'whenner/version'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require 'whenner/conversions'
|
5
|
+
require 'whenner/callback'
|
6
|
+
require 'whenner/deferred'
|
7
|
+
require 'whenner/promise'
|
8
|
+
|
9
|
+
module Whenner
|
10
|
+
# Generic root exception for the Whenner library. Any other custom
|
11
|
+
# exceptions inherit from WhennerError.
|
12
|
+
class WhennerError < StandardError; end
|
13
|
+
|
14
|
+
# Custom exception raised when trying to access a deferred's value or
|
15
|
+
# reason before it is resolved.
|
16
|
+
#
|
17
|
+
# @see WhennerError
|
18
|
+
class UnresolvedError < WhennerError; end
|
19
|
+
|
20
|
+
# Custom exception raised when trying to transition an already resolved
|
21
|
+
# deferred.
|
22
|
+
#
|
23
|
+
# @see WhennerError
|
24
|
+
class CannotTransitionError < WhennerError; end
|
25
|
+
|
26
|
+
module_function
|
27
|
+
|
28
|
+
# Create a new deferred, resolve it in the block and get its promise back.
|
29
|
+
#
|
30
|
+
# @yieldparam [Deferred] deferred
|
31
|
+
# @return [Promise]
|
32
|
+
def defer
|
33
|
+
deferred = Deferred.new
|
34
|
+
yield deferred
|
35
|
+
deferred.promise
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a new deferred based that will resolve if/when the given promises
|
39
|
+
# are resolved. Use to combine multiple promises into a single deferred
|
40
|
+
# object.
|
41
|
+
#
|
42
|
+
# When all the given promises are fulfilled, the resulting promise from
|
43
|
+
# `when` if fulfilled with an array of all the values. When one of the given
|
44
|
+
# promises is rejected, the resulting promise is rejected with that reason.
|
45
|
+
#
|
46
|
+
# @param [Object] promises
|
47
|
+
# @return [Promise]
|
48
|
+
def when(*promises)
|
49
|
+
defer do |d|
|
50
|
+
promises.each_with_object([]) do |promise, values|
|
51
|
+
Conversions.Promise(promise).tap do |p|
|
52
|
+
p.done do |value|
|
53
|
+
values << value
|
54
|
+
d.fulfill(values) if values.size == promises.size
|
55
|
+
end
|
56
|
+
p.fail do |reason|
|
57
|
+
d.reject(reason)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Whenner
|
2
|
+
describe '#Promise' do
|
3
|
+
it 'returns a promise itself' do
|
4
|
+
promise = Whenner::Deferred.new.promise
|
5
|
+
expect(Whenner::Conversions.Promise(promise)).to be(promise)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'returns a promise for a deferred' do
|
9
|
+
deferred = Whenner::Deferred.new
|
10
|
+
expect(Whenner::Conversions.Promise(deferred)).to be(deferred.promise)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns a resolved promise for an object' do
|
14
|
+
promise = Whenner::Conversions.Promise('foo')
|
15
|
+
expect(promise.value).to eql('foo')
|
16
|
+
expect(promise).to be_fulfilled
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Whenner
|
2
|
+
describe Deferred do
|
3
|
+
it 'is pending by default' do
|
4
|
+
expect(subject).to be_pending
|
5
|
+
end
|
6
|
+
|
7
|
+
context 'when pending' do
|
8
|
+
it 'can be fulfilled' do
|
9
|
+
subject.fulfill
|
10
|
+
expect(subject).to be_fulfilled
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can be rejected' do
|
14
|
+
subject.reject
|
15
|
+
expect(subject).to be_rejected
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'has no value' do
|
19
|
+
expect { subject.value }.to raise_error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when fulfilled' do
|
24
|
+
subject do
|
25
|
+
described_class.new.tap { |d| d.fulfill }
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'cannot transition to other states' do
|
29
|
+
expect { subject.reject }.to raise_error
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'has a value' do
|
33
|
+
expect(subject.value).to be_nil
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'when rejected' do
|
38
|
+
subject do
|
39
|
+
described_class.new.tap { |d| d.reject(:foo) }
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'cannot transition to other states' do
|
43
|
+
expect { subject.fulfill }.to raise_error
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'has a reason' do
|
47
|
+
expect(subject.reason).to eql(:foo)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'creates a promise' do
|
52
|
+
expect(subject.promise).to be_kind_of(Promise)
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'callbacks' do
|
56
|
+
it 'calls fulfillment callbacks when fulfilled' do
|
57
|
+
resolved = false
|
58
|
+
subject.done { resolved = true }
|
59
|
+
expect { subject.fulfill(:a) }.to change { resolved }.to(true)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'calls rejection callbacks when rejected' do
|
63
|
+
resolved = false
|
64
|
+
subject.fail { resolved = true }
|
65
|
+
expect { subject.reject(:a) }.to change { resolved }.to(true)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'calls always callbacks when resolved' do
|
69
|
+
resolved = false
|
70
|
+
subject.always { resolved = true }
|
71
|
+
expect { subject.fulfill(:a) }.to change { resolved }.to(true)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'passes fulfillment callbacks the value' do
|
75
|
+
resolved = nil
|
76
|
+
subject.done { |arg| resolved = arg }
|
77
|
+
expect { subject.fulfill(:a) }.to change { resolved }.to(:a)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'passes rejection callbacks the reason' do
|
81
|
+
resolved = nil
|
82
|
+
subject.done { |arg| resolved = arg }
|
83
|
+
expect { subject.fulfill(:a) }.to change { resolved }.to(:a)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'passes the value and reason to always callbacks' do
|
87
|
+
subject.always do |value, reason|
|
88
|
+
expect(value).to eql(:a)
|
89
|
+
expect(reason).to be_nil
|
90
|
+
end
|
91
|
+
subject.fulfill(:a)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'calls callbacks only once' do
|
95
|
+
called = 0
|
96
|
+
subject.done { called += 1 }
|
97
|
+
subject.fulfill(:a)
|
98
|
+
subject.fulfill(:b)
|
99
|
+
expect(called).to eql(1)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'runs callbacks in order' do
|
103
|
+
output = ''
|
104
|
+
subject.done { output << 'a' }
|
105
|
+
subject.done { output << 'b' }
|
106
|
+
subject.fulfill
|
107
|
+
expect(output).to eql('ab')
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'returns a new promise that fulfills to the value' do
|
111
|
+
new_promise = subject.done { 'foo' }
|
112
|
+
subject.fulfill
|
113
|
+
expect(new_promise.value).to eql('foo')
|
114
|
+
expect(new_promise).to be_fulfilled
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'returns a new promise that rejects to the reason' do
|
118
|
+
new_promise = subject.fail { 'foo' }
|
119
|
+
subject.reject
|
120
|
+
expect(new_promise.value).to eql('foo')
|
121
|
+
expect(new_promise).to be_fulfilled
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'returns a new promise that mimics a promise value' do
|
125
|
+
d = Deferred.new
|
126
|
+
new_promise = subject.done { d.promise }
|
127
|
+
subject.fulfill(:b)
|
128
|
+
d.fulfill(:a)
|
129
|
+
expect(new_promise.value).to eql(:a)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'is rejected on exception' do
|
133
|
+
called = false
|
134
|
+
promise = subject.done { raise 'arg' }
|
135
|
+
promise.fail { |e| called = e }
|
136
|
+
subject.fulfill
|
137
|
+
expect(called).to be_a(RuntimeError)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Whenner
|
2
|
+
describe Promise do
|
3
|
+
let(:deferred) { Deferred.new }
|
4
|
+
subject(:promise) { deferred.promise }
|
5
|
+
|
6
|
+
it 'converts into a promise as itself' do
|
7
|
+
expect(promise.to_promise).to be(promise)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'callbacks' do
|
11
|
+
it 'can add done callbacks to the deferred' do
|
12
|
+
expect { promise.done { :a } }.to change { deferred.send(:fulfilled_callbacks).size }.by(1)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can add fail callbacks to the deferred' do
|
16
|
+
expect { promise.fail { :a } }.to change { deferred.send(:rejected_callbacks).size }.by(1)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'can add always callbacks to the deferred' do
|
20
|
+
expect { promise.always { :a } }.to change { deferred.send(:always_callbacks).size }.by(1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'when the deferred is fulfilled' do
|
25
|
+
let(:deferred) { Deferred.new.fulfill(:a) }
|
26
|
+
|
27
|
+
it 'has a value' do
|
28
|
+
expect(promise.value).to eql(:a)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'knows its state' do
|
32
|
+
expect(promise).to be_resolved
|
33
|
+
expect(promise).to be_fulfilled
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
context 'when the deferred is rejected' do
|
39
|
+
let(:deferred) { Deferred.new.reject(:a) }
|
40
|
+
|
41
|
+
it 'has a reason' do
|
42
|
+
expect(promise.reason).to eql(:a)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'knows its state' do
|
46
|
+
expect(promise).to be_resolved
|
47
|
+
expect(promise).to be_rejected
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
describe Whenner do
|
2
|
+
describe '#defer' do
|
3
|
+
it 'returns a new promise' do
|
4
|
+
promise = Whenner.defer { 'bla' }
|
5
|
+
expect(promise).to be_kind_of(Whenner::Promise)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'yields a deferred' do
|
9
|
+
expect { |b| Whenner.defer(&b) }.to yield_with_args(Whenner::Deferred)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#when' do
|
14
|
+
let(:deferred1) { Whenner::Deferred.new }
|
15
|
+
let(:deferred2) { Whenner::Deferred.new }
|
16
|
+
let!(:promise) { Whenner.when(deferred1.promise, deferred2.promise) }
|
17
|
+
|
18
|
+
it 'returns a promise' do
|
19
|
+
expect(Whenner.when).to be_kind_of(Whenner::Promise)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'resolves when all given promises are resolved' do
|
23
|
+
expect { deferred1.fulfill }.not_to change { promise.resolved? }.from(false)
|
24
|
+
expect { deferred2.fulfill }.to change { promise.resolved? }.to(true)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'fulfills with all values' do
|
28
|
+
deferred1.fulfill :a
|
29
|
+
deferred2.fulfill :b
|
30
|
+
expect(promise.value).to eql([:a, :b])
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'rejects with the first reason' do
|
34
|
+
deferred1.reject :a
|
35
|
+
deferred2.reject :b
|
36
|
+
expect(promise.reason).to eql(:a)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/whenner.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/whenner/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
# Metadata
|
6
|
+
s.name = 'whenner'
|
7
|
+
s.version = Whenner::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['Arjan van der Gaag']
|
10
|
+
s.email = %q{arjan@arjanvandergaag.nl}
|
11
|
+
s.description = %q{A simple promises implementation in Ruby.}
|
12
|
+
s.homepage = %q{http://avdgaag.github.com/whenner}
|
13
|
+
s.summary = <<-EOS
|
14
|
+
A promise represents the eventual result of an asynchronous operation. The
|
15
|
+
primary way of interacting with a promise is through its `done` and `fail`
|
16
|
+
methods, which registers callbacks to receive either a promise’s eventual value
|
17
|
+
or the reason why the promise cannot be fulfilled.
|
18
|
+
EOS
|
19
|
+
|
20
|
+
# Files
|
21
|
+
s.files = `git ls-files`.split("
|
22
|
+
")
|
23
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("
|
24
|
+
")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("
|
26
|
+
").map{ |f| File.basename(f) }
|
27
|
+
s.require_paths = ["lib"]
|
28
|
+
|
29
|
+
# Rdoc
|
30
|
+
s.rdoc_options = ['--charset=UTF-8']
|
31
|
+
s.extra_rdoc_files = [
|
32
|
+
'LICENSE',
|
33
|
+
'README.md',
|
34
|
+
'HISTORY.md'
|
35
|
+
]
|
36
|
+
|
37
|
+
# Dependencies
|
38
|
+
s.add_development_dependency 'kramdown'
|
39
|
+
s.add_development_dependency 'yard'
|
40
|
+
s.add_development_dependency 'rspec'
|
41
|
+
s.add_development_dependency 'rake'
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: whenner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arjan van der Gaag
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-12-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kramdown
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: A simple promises implementation in Ruby.
|
70
|
+
email: arjan@arjanvandergaag.nl
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files:
|
74
|
+
- LICENSE
|
75
|
+
- README.md
|
76
|
+
- HISTORY.md
|
77
|
+
files:
|
78
|
+
- .gitignore
|
79
|
+
- .rspec
|
80
|
+
- .travis.yml
|
81
|
+
- .yardopts
|
82
|
+
- Gemfile
|
83
|
+
- Gemfile.lock
|
84
|
+
- HISTORY.md
|
85
|
+
- LICENSE
|
86
|
+
- README.md
|
87
|
+
- Rakefile
|
88
|
+
- lib/whenner.rb
|
89
|
+
- lib/whenner/callback.rb
|
90
|
+
- lib/whenner/conversions.rb
|
91
|
+
- lib/whenner/deferred.rb
|
92
|
+
- lib/whenner/promise.rb
|
93
|
+
- lib/whenner/version.rb
|
94
|
+
- spec/whenner/conversions_spec.rb
|
95
|
+
- spec/whenner/deferred_spec.rb
|
96
|
+
- spec/whenner/promise_spec.rb
|
97
|
+
- spec/whenner_spec.rb
|
98
|
+
- whenner.gemspec
|
99
|
+
homepage: http://avdgaag.github.com/whenner
|
100
|
+
licenses: []
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options:
|
104
|
+
- --charset=UTF-8
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 2.1.11
|
120
|
+
signing_key:
|
121
|
+
specification_version: 4
|
122
|
+
summary: A promise represents the eventual result of an asynchronous operation. The
|
123
|
+
primary way of interacting with a promise is through its `done` and `fail` methods,
|
124
|
+
which registers callbacks to receive either a promise’s eventual value or the reason
|
125
|
+
why the promise cannot be fulfilled.
|
126
|
+
test_files:
|
127
|
+
- spec/whenner/conversions_spec.rb
|
128
|
+
- spec/whenner/deferred_spec.rb
|
129
|
+
- spec/whenner/promise_spec.rb
|
130
|
+
- spec/whenner_spec.rb
|
131
|
+
has_rdoc:
|