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 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
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --warnings
3
+ --require spec_helper
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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pure_promise.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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