whenner 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://secure.travis-ci.org/avdgaag/whenner.png?branch=master)](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:
|