pure_promise 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.travis.yml +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +172 -0
- data/Rakefile +6 -0
- data/lib/pure_promise.rb +135 -0
- data/lib/pure_promise/callback.rb +19 -0
- data/lib/pure_promise/coercer.rb +59 -0
- data/pure_promise.gemspec +23 -0
- data/spec/pure_promise/callback_spec.rb +26 -0
- data/spec/pure_promise/coercer_spec.rb +118 -0
- data/spec/pure_promise_spec.rb +445 -0
- data/spec/spec_helper.rb +67 -0
- data/spec/support/helper_macros.rb +37 -0
- data/spec/support/matchers.rb +47 -0
- data/spec/support/thenable.rb +44 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3a89eec90f03fb3f8769db7778d3996f489e2390
|
4
|
+
data.tar.gz: 992aad1bb58162a030f5e0a11b2e5f378e5d4065
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 20942612cb7d93992b621d962b01c8380d7805657e8d6004cb3f00965342e26b1f36e6f6c37558abd945a6b32c713d13ab9bb74df5f551b14b8e3e10943fb437
|
7
|
+
data.tar.gz: c5b903a596095ce5524f04f313fa5c6f6fe033776c528e7f99c169211880b2a51f60887d211873723d62e382e3772ef9776794d93bcc750907f04fd6b1cd7c97
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.9
|
4
|
+
- 2.0
|
5
|
+
- 2.1
|
6
|
+
- jruby
|
7
|
+
- rbx-2.1
|
8
|
+
- rbx-2.2
|
9
|
+
- ruby-head
|
10
|
+
- jruby-head
|
11
|
+
matrix:
|
12
|
+
allow_failures:
|
13
|
+
- rvm: ruby-head
|
14
|
+
- rvm: jruby-head
|
15
|
+
fast_finish: true
|
16
|
+
before_install:
|
17
|
+
- gem install bundler
|
18
|
+
install: bundle install --jobs=1 --retry=3
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Cameron Martin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/cameron-martin/pure_promise.svg?branch=master)](https://travis-ci.org/cameron-martin/pure_promise)
|
2
|
+
|
3
|
+
# PurePromise
|
4
|
+
|
5
|
+
My promises library. It tries to be as close to the Promises/A+ spec as possible, with one exception:
|
6
|
+
|
7
|
+
__A promise callback _must_ return a promise__
|
8
|
+
|
9
|
+
This makes it slightly more verbose, but it has some nice properties. I'll explain them here later.
|
10
|
+
|
11
|
+
Influenced by [promise.rb][2], the [Promises/A+ spec][3], and browsers' implementations of promises (for the stuff that's not `#then`).
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'pure_promise'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install pure_promise
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
### Making them asynchronous
|
32
|
+
|
33
|
+
Note: The defer method does not have to yield in the order in which defer was called,
|
34
|
+
it just has to yield some time in the future.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class EMPromise < PurePromise
|
38
|
+
def defer
|
39
|
+
EM.next_tick { yield }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
### Creating promises
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# Create a fulfilled promise
|
48
|
+
PurePromise.fulfill(:value)
|
49
|
+
PurePromise.fulfill
|
50
|
+
|
51
|
+
# Create a rejected promise
|
52
|
+
PurePromise.reject(:value)
|
53
|
+
PurePromise.reject
|
54
|
+
|
55
|
+
# Create a pending promise
|
56
|
+
PurePromise.new
|
57
|
+
|
58
|
+
# Create a promise which fulfills or rejects when fulfill or reject are called.
|
59
|
+
PurePromise.new do |fulfill, reject|
|
60
|
+
if something?
|
61
|
+
fulfill.call(:value)
|
62
|
+
else
|
63
|
+
reject.call(:error)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create a promise with fulfills/rejects when thenable fulfills/rejects
|
68
|
+
PurePromise.resolve(thenable)
|
69
|
+
|
70
|
+
# Create a promise which is rejected to an exception object, with backtrace properly set.
|
71
|
+
PurePromise.error # #<RuntimeError: RuntimeError>
|
72
|
+
PurePromise.error('message') # #<RuntimeError: message>
|
73
|
+
PurePromise.error(TypeError, 'message') # #<TypeError: message>
|
74
|
+
PurePromise.error(TypeError.new('message')) # #<TypeError: message>
|
75
|
+
```
|
76
|
+
|
77
|
+
### Mutating promises
|
78
|
+
|
79
|
+
A promise can only be mutated once. Once it has transitioned from pending, the value cannot be changed.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
promise = Promise.new
|
83
|
+
|
84
|
+
# It is recommended to pass a block to new for fulfilling and rejecting promises,
|
85
|
+
# as this normally makes your code more clear
|
86
|
+
promise.fulfill(:value)
|
87
|
+
promise.reject(:value)
|
88
|
+
|
89
|
+
# Make promise take on the form of thenable
|
90
|
+
# This can be any object that implements a semi-compliant then method,
|
91
|
+
# as described in the Promises/A+ spec
|
92
|
+
promise.resolve(thenable)
|
93
|
+
|
94
|
+
```
|
95
|
+
|
96
|
+
### Accessing promises
|
97
|
+
|
98
|
+
The only way to access a promise's value is through the then/catch methods.
|
99
|
+
|
100
|
+
Each callback __must__ evaluate to a promise. If the action in the callback succeeds, return `PurePromise.fulfill`,
|
101
|
+
otherwise return `PurePromise.reject`.
|
102
|
+
|
103
|
+
`then` and `catch` always return a promise, which fulfills or rejects to the value of the promise returned from the callback when it is executed.
|
104
|
+
|
105
|
+
If a callback raises an error, the promise returned by `then` or `catch` will be rejected with the error as the value.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
|
109
|
+
# Attach a fulfillment callback
|
110
|
+
PurePromise.fulfill(:some_value).then do |value|
|
111
|
+
puts value.inspect
|
112
|
+
PurePromise.fulfill
|
113
|
+
end
|
114
|
+
# :some_value
|
115
|
+
|
116
|
+
# Attach a rejection callback
|
117
|
+
PurePromise.error.catch do |error|
|
118
|
+
puts error.inspect
|
119
|
+
PurePromise.fulfill
|
120
|
+
end
|
121
|
+
# #<RuntimeError: RuntimeError>
|
122
|
+
|
123
|
+
# Attach both
|
124
|
+
PurePromise.fulfill(:some_value).then(proc { |value|
|
125
|
+
puts value.inspect
|
126
|
+
PurePromise.fulfill
|
127
|
+
}, proc { |error|
|
128
|
+
puts error.inspect
|
129
|
+
PurePromise.fulfill
|
130
|
+
})
|
131
|
+
|
132
|
+
```
|
133
|
+
|
134
|
+
## Shortcomings addressed
|
135
|
+
|
136
|
+
This isn't having a dig at anyone else's work, these are just the reasons why I wanted to create my own promises library.
|
137
|
+
I could have got a lot of things wrong too, and I'd love to hear about them in the issues section.
|
138
|
+
|
139
|
+
### In Promises/A+ Spec
|
140
|
+
|
141
|
+
* You cannot wrap anything that implements a `then` method in a promise.
|
142
|
+
This bit me when wanting to pass around a [faye client][1] in a promise system - and it took me forever to debug.
|
143
|
+
PurePromise addresses this by forcing you to return a promise from your callbacks.
|
144
|
+
|
145
|
+
### In Promise.rb
|
146
|
+
|
147
|
+
* IMO, being able to retrieve the value of the promise through an accessor is wrong.
|
148
|
+
What do you return when the promise is pending and _has no value_? Nil? But nil is a valid value for a promise,
|
149
|
+
creating ambiguity.
|
150
|
+
|
151
|
+
## Design goals
|
152
|
+
* Address the above shortcomings.
|
153
|
+
* Limit the public api to as small as possible (then, fulfill, reject, resolve).
|
154
|
+
Everything else should just be convenience methods on top of these.
|
155
|
+
|
156
|
+
## TODO
|
157
|
+
|
158
|
+
* DRY up specs; they are pretty verbose atm.
|
159
|
+
* Get 100% mutation coverage
|
160
|
+
* Release gem
|
161
|
+
|
162
|
+
## Contributing
|
163
|
+
|
164
|
+
1. Fork it ( https://github.com/cameron-martin/pure_promise/fork )
|
165
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
166
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
167
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
168
|
+
5. Create a new Pull Request
|
169
|
+
|
170
|
+
[1]: http://faye.jcoglan.com/browser.html
|
171
|
+
[2]: https://github.com/lgierth/promise.rb
|
172
|
+
[3]: http://promisesaplus.com/
|
data/Rakefile
ADDED
data/lib/pure_promise.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'pure_promise/callback'
|
2
|
+
require 'pure_promise/coercer'
|
3
|
+
|
4
|
+
class PurePromise
|
5
|
+
|
6
|
+
MutationError = Class.new(RuntimeError)
|
7
|
+
|
8
|
+
class << self
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :new, :fulfill, :reject, :resolve
|
12
|
+
|
13
|
+
# TODO: Clean this up, it's pretty messy.
|
14
|
+
def error(message_or_exception=nil, message=nil, backtrace=nil)
|
15
|
+
backtrace ||= caller(2) # Fix for jRuby - See https://github.com/jruby/jruby/issues/1908
|
16
|
+
if message_or_exception.respond_to?(:exception)
|
17
|
+
exception = message_or_exception.exception(message || message_or_exception)
|
18
|
+
else
|
19
|
+
exception = RuntimeError.new(message_or_exception)
|
20
|
+
end
|
21
|
+
exception.set_backtrace(backtrace)
|
22
|
+
reject(exception)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@state = :pending # Pending/fulfilled/rejected
|
28
|
+
@callbacks = []
|
29
|
+
|
30
|
+
yield method(:fulfill), method(:reject) if block_given?
|
31
|
+
end
|
32
|
+
|
33
|
+
# REVIEW: Consider having two callback chains, to avoid having potentially expensive null_callbacks littering @callbacks
|
34
|
+
def then(fulfill_callback=null_callback, reject_callback=null_callback, &block)
|
35
|
+
fulfill_callback = block if block
|
36
|
+
self.class.new.tap do |return_promise|
|
37
|
+
register_callbacks(
|
38
|
+
Callback.new(fulfill_callback, return_promise),
|
39
|
+
Callback.new(reject_callback, return_promise)
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def catch(&block)
|
45
|
+
self.then(null_callback, block || null_callback)
|
46
|
+
end
|
47
|
+
|
48
|
+
def fulfill(value=nil)
|
49
|
+
mutate_state(:fulfilled, value, @callbacks.map(&:first))
|
50
|
+
end
|
51
|
+
|
52
|
+
def reject(value=nil)
|
53
|
+
mutate_state(:rejected, value, @callbacks.map(&:last))
|
54
|
+
end
|
55
|
+
|
56
|
+
# TODO: Rename method to:
|
57
|
+
# receive?
|
58
|
+
# acquire?
|
59
|
+
# take?
|
60
|
+
def resolve(promise)
|
61
|
+
if equal?(promise)
|
62
|
+
raise TypeError, 'Promise cannot be resolved to itself'
|
63
|
+
elsif Coercer.is_thenable?(promise)
|
64
|
+
Coercer.coerce(promise, self.class).resolve_into(self)
|
65
|
+
self
|
66
|
+
else
|
67
|
+
raise TypeError, 'Argument is not a promise'
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def resolve_into(pure_promise)
|
72
|
+
raise TypeError, 'Argument must be of same type as self' unless pure_promise.instance_of?(self.class)
|
73
|
+
|
74
|
+
if fulfilled?
|
75
|
+
pure_promise.fulfill(@value)
|
76
|
+
elsif rejected?
|
77
|
+
pure_promise.reject(@value)
|
78
|
+
else
|
79
|
+
self.then(pure_promise.method(:fulfill), pure_promise.method(:reject))
|
80
|
+
end
|
81
|
+
self
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def defer
|
87
|
+
yield
|
88
|
+
end
|
89
|
+
|
90
|
+
def mutate_state(state, value, callbacks)
|
91
|
+
raise MutationError, 'You can only mutate pending promises' unless pending?
|
92
|
+
|
93
|
+
@state = state
|
94
|
+
@value = value
|
95
|
+
|
96
|
+
run_callbacks(callbacks)
|
97
|
+
# TODO: Find a way of testing this - It makes no visible changes, apart from clearing some memory.
|
98
|
+
@callbacks.clear
|
99
|
+
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
def register_callbacks(fulfill_callback, reject_callback)
|
104
|
+
if fulfilled?
|
105
|
+
defer { fulfill_callback.call(@value) }
|
106
|
+
elsif rejected?
|
107
|
+
defer { reject_callback.call(@value) }
|
108
|
+
else
|
109
|
+
@callbacks << [fulfill_callback, reject_callback]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# This ensures that all callbacks run in order, by setting up an execution chain like
|
114
|
+
# proc { defer { a.call; proc { defer { b.call; ... } }.call } }.call
|
115
|
+
# You might think this is really slow by only running one callback per tick,
|
116
|
+
# but here are some benchmarks with eventmachine: https://gist.github.com/cameron-martin/08abeaeae1bf746ef718
|
117
|
+
#
|
118
|
+
# We do this because we do not want to require implementations of defer to execute blocks in the order they were registered.
|
119
|
+
def run_callbacks(callbacks)
|
120
|
+
callbacks.reverse.inject(proc{}) do |memo, callback|
|
121
|
+
proc { defer { callback.call(@value); memo.call } }
|
122
|
+
end.call
|
123
|
+
end
|
124
|
+
|
125
|
+
def null_callback
|
126
|
+
@null_callback ||= proc { self }
|
127
|
+
end
|
128
|
+
|
129
|
+
[:pending, :fulfilled, :rejected].each do |state|
|
130
|
+
define_method("#{state}?") do
|
131
|
+
@state.equal?(state)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class PurePromise
|
2
|
+
class Callback
|
3
|
+
|
4
|
+
def initialize(callback, return_promise)
|
5
|
+
@callback = callback
|
6
|
+
@return_promise = return_promise
|
7
|
+
end
|
8
|
+
|
9
|
+
# TODO: Return a consistent value here. Nil? self?
|
10
|
+
def call(value)
|
11
|
+
return_value = @callback.call(value)
|
12
|
+
rescue Exception => error
|
13
|
+
@return_promise.reject(error)
|
14
|
+
else
|
15
|
+
@return_promise.resolve(return_value)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# This coerces a thenable into a PurePromise
|
2
|
+
# I wanted to keep this separate because there are a lot of edge cases that need handling
|
3
|
+
# if the thenable doesn't conform to the spec properly.
|
4
|
+
|
5
|
+
class PurePromise
|
6
|
+
class Coercer
|
7
|
+
|
8
|
+
def self.is_thenable?(thenable)
|
9
|
+
thenable.respond_to?(:then)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.coerce(*args, &block)
|
13
|
+
new(*args, &block).coerce
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(thenable, promise_class)
|
17
|
+
raise TypeError, 'Can only coerce a thenable' unless self.class.is_thenable?(thenable)
|
18
|
+
@thenable = thenable
|
19
|
+
@promise_class = promise_class
|
20
|
+
end
|
21
|
+
|
22
|
+
def coerce
|
23
|
+
return @thenable if @thenable.instance_of?(@promise_class)
|
24
|
+
|
25
|
+
@mutated = false
|
26
|
+
coerce_thenable
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def coerce_thenable
|
32
|
+
@promise_class.new.tap do |promise|
|
33
|
+
begin
|
34
|
+
@thenable.then(
|
35
|
+
build_callback(promise, :fulfill),
|
36
|
+
build_callback(promise, :reject)
|
37
|
+
)
|
38
|
+
rescue Exception => error
|
39
|
+
mutate_promise { promise.reject(error) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_callback(promise, method)
|
45
|
+
proc do |value|
|
46
|
+
mutate_promise { promise.public_send(method, value) }
|
47
|
+
promise
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def mutate_promise
|
52
|
+
unless @mutated
|
53
|
+
yield
|
54
|
+
@mutated = true
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|