a-ruby-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/lib/a-ruby-promise.rb +1 -0
- data/lib/promise.rb +95 -0
- data/lib/promise/version.rb +3 -0
- data/spec/aplus_spec.rb +46 -0
- data/spec/deferred.rb +16 -0
- data/spec/promise_spec.rb +50 -0
- data/spec/promises_aplus.rb +58 -0
- data/spec/promises_aplus/2_1_2_spec.rb +57 -0
- data/spec/promises_aplus/2_1_3_spec.rb +76 -0
- data/spec/promises_aplus/2_2_1_spec.rb +49 -0
- data/spec/promises_aplus/2_2_2_spec.rb +156 -0
- data/spec/promises_aplus/2_2_3_spec.rb +142 -0
- data/spec/promises_aplus/2_2_4_spec.rb +188 -0
- data/spec/promises_aplus/2_2_5_spec.rb +7 -0
- data/spec/promises_aplus/2_2_6_spec.rb +238 -0
- data/spec/promises_aplus/2_2_7_spec.rb +99 -0
- data/spec/spec_helper.rb +44 -0
- metadata +146 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d5e232579789fc7da3951b73e4eaf841cade3a27
|
4
|
+
data.tar.gz: 5abeb4887d52eb58764b13f6ec793cc9299a6382
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 72bd28e4bc5cc0621bf7a7203a089d6e5006444c8c11ed2a994f7094536217d2f081fd26d6ab2562c727431c82aea400a75b4658133ef7712c54c09c4553021d
|
7
|
+
data.tar.gz: 98d4668c57d5b47f257cdf01461e106aa527f58c707d72ba775e91eee30942ae871ff4cf66ea4d4f5f784119ee9a38913d93b1c8f1e50060b40eb1c2bc736bfc
|
@@ -0,0 +1 @@
|
|
1
|
+
require "promise"
|
data/lib/promise.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require "promise/version"
|
2
|
+
|
3
|
+
class Promise
|
4
|
+
|
5
|
+
attr_reader :state, :value, :reason
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
@state = :pending
|
9
|
+
@value = nil
|
10
|
+
@reason = nil
|
11
|
+
@callbacks = []
|
12
|
+
@errbacks = []
|
13
|
+
instance_eval(&block) if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
%w[pending fulfilled rejected].each do |state|
|
17
|
+
define_method("#{state}?") { @state == state.to_sym }
|
18
|
+
end
|
19
|
+
|
20
|
+
def then(on_fulfilled = nil, on_rejected = nil)
|
21
|
+
result = Promise.new
|
22
|
+
|
23
|
+
call_and_fulfill = ->(value) {
|
24
|
+
begin
|
25
|
+
if function?(on_fulfilled)
|
26
|
+
new_value = on_fulfilled.call(value)
|
27
|
+
if Promise === new_value
|
28
|
+
new_value.then(->(v) { result.fulfill(v) }, ->(r) { result.reject(r) })
|
29
|
+
else
|
30
|
+
result.fulfill(new_value)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
result.fulfill(value)
|
34
|
+
end
|
35
|
+
rescue Exception => e
|
36
|
+
result.reject(e)
|
37
|
+
end
|
38
|
+
}
|
39
|
+
|
40
|
+
call_and_reject = ->(reason) {
|
41
|
+
begin
|
42
|
+
if function?(on_rejected)
|
43
|
+
new_value = on_rejected.call(reason)
|
44
|
+
if Promise === new_value
|
45
|
+
new_value.then(->(v) { result.fulfill(v) }, ->(r) { result.reject(r) })
|
46
|
+
else
|
47
|
+
result.fulfill(new_value)
|
48
|
+
end
|
49
|
+
else
|
50
|
+
result.reject(reason)
|
51
|
+
end
|
52
|
+
rescue Exception => e
|
53
|
+
result.reject(e)
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
case @state
|
58
|
+
when :pending
|
59
|
+
@callbacks << call_and_fulfill
|
60
|
+
@errbacks << call_and_reject
|
61
|
+
when :fulfilled
|
62
|
+
call_and_fulfill.call(@value)
|
63
|
+
when :rejected
|
64
|
+
call_and_reject.call(@reason)
|
65
|
+
end
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def fulfill(value)
|
73
|
+
return unless @state == :pending
|
74
|
+
@state = :fulfilled
|
75
|
+
@value = value
|
76
|
+
while callback = @callbacks.shift
|
77
|
+
callback.call(value) rescue nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def reject(reason)
|
82
|
+
return unless @state == :pending
|
83
|
+
@state = :rejected
|
84
|
+
@reason = reason
|
85
|
+
while errback = @errbacks.shift
|
86
|
+
errback.call(reason) rescue nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def function?(obj)
|
93
|
+
obj.respond_to? :call
|
94
|
+
end
|
95
|
+
end
|
data/spec/aplus_spec.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
# Test suite from the `aplus` Python Promise implementation.
|
4
|
+
# https://github.com/xogeny/aplus#testing
|
5
|
+
|
6
|
+
describe Promise do
|
7
|
+
describe "Handles the case where the arguments to then are not functions or promises." do
|
8
|
+
it "3.2.6.4 pending" do
|
9
|
+
d = deferred
|
10
|
+
p1 = d.promise
|
11
|
+
p2 = p1.then(5)
|
12
|
+
d.fulfill(10)
|
13
|
+
p1.value.must_equal 10
|
14
|
+
p2.state.must_equal :fulfilled
|
15
|
+
p2.value.must_equal 10
|
16
|
+
end
|
17
|
+
|
18
|
+
it "3.2.6.4 fulfilled" do
|
19
|
+
p1 = resolved(10)
|
20
|
+
p2 = p1.then(5)
|
21
|
+
p1.value.must_equal 10
|
22
|
+
p2.state.must_equal :fulfilled
|
23
|
+
p2.value.must_equal 10
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "Handles the case where the arguments to then are values, not functions or promises." do
|
28
|
+
it "3.2.6.5 pending" do
|
29
|
+
d = deferred
|
30
|
+
p1 = d.promise
|
31
|
+
p2 = p1.then(nil, 5)
|
32
|
+
d.reject("Error")
|
33
|
+
p1.reason.must_equal "Error"
|
34
|
+
p2.state.must_equal :rejected
|
35
|
+
p2.reason.must_equal "Error"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "3.2.6.5 rejected" do
|
39
|
+
p1 = rejected("Error")
|
40
|
+
p2 = p1.then(nil, 5)
|
41
|
+
p1.reason.must_equal "Error"
|
42
|
+
p2.state.must_equal :rejected
|
43
|
+
p2.reason.must_equal "Error"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/spec/deferred.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Deferred
|
2
|
+
attr_reader :promise
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@promise = Promise.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def fulfill(value)
|
9
|
+
@promise.__send__(:fulfill, value)
|
10
|
+
end
|
11
|
+
alias_method :resolve, :fulfill
|
12
|
+
|
13
|
+
def reject(reason)
|
14
|
+
@promise.__send__(:reject, reason)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
def timeout_promise(promise, timeout_in_seconds)
|
4
|
+
Promise.new do
|
5
|
+
promise.then method(:fulfill)
|
6
|
+
Thread.new do
|
7
|
+
sleep timeout_in_seconds
|
8
|
+
reject "Timeout reached"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Promise do
|
14
|
+
describe "simple fulfillment" do
|
15
|
+
it "must call success handler after fulfill" do
|
16
|
+
v = nil
|
17
|
+
d = deferred
|
18
|
+
p = d.promise
|
19
|
+
p.then ->(value) { v = value }
|
20
|
+
v.must_equal nil
|
21
|
+
d.fulfill(42)
|
22
|
+
v.must_equal 42
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "timeout" do
|
27
|
+
it "fulfills quickly" do
|
28
|
+
d = deferred
|
29
|
+
p1 = d.promise
|
30
|
+
p2 = timeout_promise(p1, 0.05)
|
31
|
+
d.fulfill "ok"
|
32
|
+
p2.state.must_equal :fulfilled
|
33
|
+
p2_value = nil
|
34
|
+
p2.then ->(value) { p2_value = value }
|
35
|
+
p2_value.must_equal "ok"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "rejects after some time" do
|
39
|
+
d = deferred
|
40
|
+
p1 = d.promise
|
41
|
+
p2 = timeout_promise(p1, 0.05)
|
42
|
+
sleep 0.1
|
43
|
+
d.fulfill "ok"
|
44
|
+
p2.state.must_equal :rejected
|
45
|
+
p2_reason = nil
|
46
|
+
p2.then nil, ->(reason) { p2_reason = reason }
|
47
|
+
p2_reason.must_equal "Timeout reached"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# http://promises-aplus.github.io/promises-spec/
|
2
|
+
# https://github.com/promises-aplus/promises-tests
|
3
|
+
module PromisesAplus
|
4
|
+
def setup
|
5
|
+
@done = false
|
6
|
+
end
|
7
|
+
|
8
|
+
def teardown
|
9
|
+
time_limit = Time.now + 2
|
10
|
+
loop do
|
11
|
+
return if @done == true
|
12
|
+
raise Minitest::Assertion.new("`done` was never called") if Time.now >= time_limit
|
13
|
+
sleep 0.05
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def done!
|
18
|
+
@done = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.test_fulfilled(base, value, &test)
|
22
|
+
base.it "already-fulfilled" do
|
23
|
+
test.call(resolved(value), method(:done!))
|
24
|
+
end
|
25
|
+
|
26
|
+
base.it "immediately-fulfilled" do
|
27
|
+
d = deferred()
|
28
|
+
test.call(d.promise, method(:done!))
|
29
|
+
d.resolve(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
base.it "eventually-fulfilled" do
|
33
|
+
d = deferred()
|
34
|
+
test.call(d.promise, method(:done!))
|
35
|
+
short_sleep
|
36
|
+
d.resolve(value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.test_rejected(base, reason, &test)
|
41
|
+
base.it "already-rejected" do
|
42
|
+
test.call(rejected(reason), method(:done!))
|
43
|
+
end
|
44
|
+
|
45
|
+
base.it "immediately-rejected" do
|
46
|
+
d = deferred
|
47
|
+
test.call(d.promise, method(:done!))
|
48
|
+
d.reject(reason)
|
49
|
+
end
|
50
|
+
|
51
|
+
base.it "eventually-rejected" do
|
52
|
+
d = deferred
|
53
|
+
test.call(d.promise, method(:done!))
|
54
|
+
short_sleep
|
55
|
+
d.reject(reason)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative "../spec_helper"
|
3
|
+
require_relative "../promises_aplus"
|
4
|
+
|
5
|
+
dummy = { dummy: "dummy" }
|
6
|
+
|
7
|
+
describe "2.1.2.1: When fulfilled, a promise: must not transition to any other state." do
|
8
|
+
include PromisesAplus
|
9
|
+
|
10
|
+
PromisesAplus.test_fulfilled(self, dummy) do |promise, done|
|
11
|
+
on_fulfilled_called = false
|
12
|
+
|
13
|
+
promise.then ->(v) {
|
14
|
+
onFulfilledCalled = true
|
15
|
+
}, ->(r) {
|
16
|
+
on_fulfilled_called.must_equal false
|
17
|
+
done.call
|
18
|
+
}
|
19
|
+
|
20
|
+
short_sleep and done.call
|
21
|
+
end
|
22
|
+
|
23
|
+
it "trying to fulfill then immediately reject" do
|
24
|
+
d = deferred
|
25
|
+
on_fulfilled_called = false
|
26
|
+
d.promise.then ->(v) {
|
27
|
+
on_fulfilled_called = true
|
28
|
+
}, ->(r) {
|
29
|
+
on_fulfilled_called.must_equal false
|
30
|
+
done!
|
31
|
+
}
|
32
|
+
d.resolve(dummy)
|
33
|
+
d.reject(dummy)
|
34
|
+
|
35
|
+
short_sleep and done!
|
36
|
+
end
|
37
|
+
|
38
|
+
it "trying to fulfill then reject, delayed" do
|
39
|
+
d = deferred
|
40
|
+
on_fulfilled_called = false
|
41
|
+
|
42
|
+
d.promise.then ->(v) {
|
43
|
+
on_fulfilled_called = true
|
44
|
+
}, ->(r) {
|
45
|
+
on_fulfilled_called.must_equal false
|
46
|
+
done!
|
47
|
+
}
|
48
|
+
|
49
|
+
d.resolve(dummy)
|
50
|
+
Thread.new do
|
51
|
+
short_sleep
|
52
|
+
d.reject(dummy)
|
53
|
+
end
|
54
|
+
|
55
|
+
2.times { short_sleep } and done!
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative "../spec_helper"
|
3
|
+
require_relative "../promises_aplus"
|
4
|
+
|
5
|
+
dummy = { dummy: "dummy" } # we fulfill or reject with this when we don't intend to test against it
|
6
|
+
|
7
|
+
describe "2.1.3.1: When rejected, a promise: must not transition to any other state." do
|
8
|
+
include PromisesAplus
|
9
|
+
|
10
|
+
PromisesAplus.test_rejected(self, dummy) do |promise, done|
|
11
|
+
on_rejected_called = false
|
12
|
+
|
13
|
+
promise.then ->(v) {
|
14
|
+
on_rejected_called.must_equal false
|
15
|
+
done.call
|
16
|
+
}, ->(r) {
|
17
|
+
on_rejected_called = true
|
18
|
+
}
|
19
|
+
|
20
|
+
short_sleep and done.call
|
21
|
+
end
|
22
|
+
|
23
|
+
it "trying to reject then immediately fulfill" do
|
24
|
+
d = deferred
|
25
|
+
on_rejected_called = false
|
26
|
+
|
27
|
+
d.promise.then ->(v) {
|
28
|
+
on_rejected_called.must_equal false
|
29
|
+
done!
|
30
|
+
}, ->(r) {
|
31
|
+
on_rejected_called = true
|
32
|
+
}
|
33
|
+
|
34
|
+
d.reject(dummy)
|
35
|
+
d.resolve(dummy)
|
36
|
+
short_sleep and done!
|
37
|
+
end
|
38
|
+
|
39
|
+
it "trying to reject then fulfill, delayed" do
|
40
|
+
d = deferred
|
41
|
+
on_rejected_called = false
|
42
|
+
|
43
|
+
d.promise.then ->(v) {
|
44
|
+
on_rejected_called.must_equal false
|
45
|
+
done!
|
46
|
+
}, ->(r) {
|
47
|
+
on_rejected_called = true
|
48
|
+
}
|
49
|
+
|
50
|
+
Thread.new do
|
51
|
+
short_sleep
|
52
|
+
d.reject(dummy)
|
53
|
+
d.resolve(dummy)
|
54
|
+
end
|
55
|
+
2.times { short_sleep } and done!
|
56
|
+
end
|
57
|
+
|
58
|
+
it "trying to reject immediately then fulfill delayed" do
|
59
|
+
d = deferred
|
60
|
+
on_rejected_called = false
|
61
|
+
|
62
|
+
d.promise.then ->(v) {
|
63
|
+
on_rejected_called.must_equal false
|
64
|
+
done!
|
65
|
+
}, ->(r) {
|
66
|
+
on_rejected_called = true
|
67
|
+
}
|
68
|
+
|
69
|
+
d.reject(dummy)
|
70
|
+
Thread.new do
|
71
|
+
short_sleep
|
72
|
+
d.resolve(dummy)
|
73
|
+
end
|
74
|
+
2.times { short_sleep } and done!
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative "../spec_helper"
|
3
|
+
require_relative "../promises_aplus"
|
4
|
+
|
5
|
+
dummy = { dummy: "dummy" }
|
6
|
+
|
7
|
+
describe "2.2.1: Both `onFulfilled` and `onRejected` are optional arguments." do
|
8
|
+
include PromisesAplus
|
9
|
+
|
10
|
+
describe "2.2.1.1: If `onFulfilled` is not a function, it must be ignored." do
|
11
|
+
[nil, false, 5, Object.new].each do |non_function|
|
12
|
+
describe "applied to a directly-rejected promise" do
|
13
|
+
it "`onFulfilled` is `#{non_function.inspect}`" do
|
14
|
+
rejected(dummy).then(non_function, ->(r) {
|
15
|
+
done!
|
16
|
+
})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "applied to a promise rejected and then chained off of" do
|
21
|
+
it "`onFulfilled` is #{non_function.inspect}" do
|
22
|
+
rejected(dummy).then(->(v) { }, nil).then(non_function, ->(r) {
|
23
|
+
done!
|
24
|
+
})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "2.2.1.2: If `onRejected` is not a function, it must be ignored." do
|
31
|
+
[nil, false, 5, Object.new].each do |non_function|
|
32
|
+
describe "applied to a directly-fulfilled promise" do
|
33
|
+
it "`onRejected` is `#{non_function.inspect}`" do
|
34
|
+
resolved(dummy).then(->(v) {
|
35
|
+
done!
|
36
|
+
}, non_function)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "applied to a promise fulfilled and then chained off of" do
|
41
|
+
it "`onRejected` is `#{non_function.inspect}`" do
|
42
|
+
resolved(dummy).then(nil, ->(r) { }).then(->(v) {
|
43
|
+
done!
|
44
|
+
}, non_function)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|