pure_promise 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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