pure_promise 0.0.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 +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
|
+
[](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
|