visionary 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b1f7a3de513614103461d8bc5cad045a30ef2bba
4
- data.tar.gz: dca2eb952d375daa3a7a5b16b4fa134f6693e0a4
3
+ metadata.gz: 106d4aff2ec2c6f186337b10357075980c005983
4
+ data.tar.gz: 6d322855f784a1e6fbb98e71e71c50153e9e45dd
5
5
  SHA512:
6
- metadata.gz: 391f4a2bf74059462dac270bba16d62edc5819e0f2c922532fac4a53209da9f26b6bbc4afcc269c67484a743d8992b3843edced5c0407952cf0539a0a6d730d1
7
- data.tar.gz: 6a0aa4c2fd6316f1d486f6b989f0ddbf8654601de14cd14cd0ee1f25bb40cc786592a5b804210d4e1441ef886466e8b07721862279c240560b0676101f748026
6
+ metadata.gz: bbae37b484f2fa46352527c067f0568054ae720eb64d22cb4805cbaa3fd50a3a44ac145f0b3031b9f64e4f7e78bd5ea7a39a1f0ef51a569ff6e0525939c17561
7
+ data.tar.gz: 8b86917eab4a30c9b28766499ae527c4dba05d92e0dfcf816d7c94409bf489b13fda20619d34107d3728773932b26571ac8b8a63a89b5550b6e19e8e9cb08bd8
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
+ --require spec_helper
1
2
  --format documentation
2
3
  --color
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Visionary
2
2
 
3
- TODO: Write a gem description
3
+ Simple futures implementation in ruby.
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,11 +18,189 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- TODO: Write usage instructions here
21
+ ### Enabling `future` and `promise` helpers
22
+
23
+ ```ruby
24
+ Visionary.setup!
25
+ ```
26
+
27
+ ### Futures
28
+
29
+ #### Deferring computation
30
+
31
+ Use `Kernel#future(&blk)` to define future and run it immediately:
32
+
33
+ ```ruby
34
+ future { do_some_hard_work(some: :data) }
35
+ ```
36
+
37
+ it is an alternative to:
38
+
39
+ ```ruby
40
+ Visionary::Future.new { do_some_hard_work(some: :data) }.run
41
+ ```
42
+
43
+ #### Checking the status of computation
44
+
45
+ You can check the status of computation inside of future by using `#state`:
46
+
47
+ ```ruby
48
+ hard_work = future { do_some_hard_work(some: :data) }
49
+ hard_work.state # => :pending
50
+ sleep(5.0)
51
+ hard_work.state # => :completed
52
+ ```
53
+
54
+ It can have 3 values: `:pending`, `:completed` and `:failed`.
55
+
56
+ #### Getting the result of computation
57
+
58
+ Once future has a state of `:completed`, it will hold the result of computation in `#value`:
59
+
60
+ ```ruby
61
+ hard_work = future { do_some_hard_work(some: :data) }
62
+ do_something_else
63
+ hard_work.value # => 42
64
+ ```
65
+
66
+ When the future is completed it becomes `frozen`:
67
+
68
+ ```ruby
69
+ hard_work.state # => :completed
70
+ hard_work.frozen? # => true
71
+ hard_work.run # raises RuntimeError: can't modify frozen Visionary::Future
72
+ ```
73
+
74
+ #### Explicitly awaiting for result with blocking
75
+
76
+ To block execution of until future is completed, you can use `#await` method on future:
77
+
78
+ ```ruby
79
+ hard_work = future { do_some_hard_work(some: :data) }
80
+ hard_work.await
81
+ hard_work.state # => :completed
82
+ ```
83
+
84
+ #### Failed computations
85
+
86
+ ```ruby
87
+ hard_work = future { raise RuntimeError }
88
+ hard_work.await
89
+ hard_work.state # => :failed
90
+ hard_work.error # => #<RuntimeError: RuntimeError>
91
+ ```
92
+
93
+ #### Awaiting for result without blocking
94
+
95
+ To await for computation to complete and do something else after that without blocking use `#then` method on future:
96
+
97
+ ```ruby
98
+ hard_work = future { do_some_hard_work(some: :data) }
99
+ easy_task = hard_work.then { |result| make_it_awesome(result) }
100
+ easy_task # => #<Visionary::Future:0x0000012345678 @block=...>
101
+ do_something_else
102
+ easy_task.value # => "awesome 42"
103
+ ```
104
+
105
+ Under the hood it creates another future that will be run when current future is completed.
106
+
107
+ When previous future have failed, the failure will propagate to all waiting futures:
108
+
109
+ ```ruby
110
+ hard_work = future { do_some_hard_work_and_loudly_fail! }
111
+ easy_task = hard_work.then { |result| make_it_awesome(result) }
112
+ easy_task.await
113
+ hard_work.state # => :failed
114
+ easy_task.state # => :failed
115
+ easy_task.error == hard_work.error # => true
116
+ ```
117
+
118
+ ### Promises
119
+
120
+ #### Creating a promise
121
+
122
+ You could use `Kernel#promise` or `Visionary::Promise.new`. They are the same:
123
+
124
+ ```ruby
125
+ p = promise # => #<Visionary::Promise:0x0000012345678>
126
+ p2 = Visionary::Promise.new # => #<Visionary::Promise:0x0000012345679>
127
+ ```
128
+
129
+ #### Obtaining a promised future
130
+
131
+ ```ruby
132
+ p = promise
133
+ p.future # => #<Visionary::Future:0x0000012345678 @block=nil, @state=:pending>
134
+ ```
135
+
136
+ #### Completing a promised future
137
+
138
+ ```ruby
139
+ def hard_working_task
140
+ p = promise
141
+
142
+ call_some_external_api(
143
+ success_callback: -> { |value| p.complete(value) }
144
+ )
145
+
146
+ p
147
+ end
148
+ ```
149
+
150
+ Promised future can be used as normal future:
151
+
152
+ ```ruby
153
+ answer = hard_working_task
154
+
155
+ answer.state # => :pending
156
+
157
+ do_something_else
158
+
159
+ answer.state # => :completed
160
+ answer.value # => 42
161
+ ```
162
+
163
+ #### Failing a promised future
164
+
165
+ ```ruby
166
+ def hard_working_task_often_fails
167
+ p = promise
168
+
169
+ call_some_faulty_external_api(
170
+ success_callback: -> { ... }
171
+ failure_callback: -> { |error| p.fail(error) }
172
+ )
173
+
174
+ p
175
+ end
176
+ ```
177
+
178
+ #### Complex example what it can be used for
179
+
180
+ ```ruby
181
+ def gather_data_from_multiple_sources(user_name)
182
+ promises = {
183
+ user: promise,
184
+ friends: promise,
185
+ likes: promise
186
+ }
187
+
188
+ user_call = future { api_call(:fetch_user, user_name) }
189
+ user_call.then { |user| promises[:user].complete(user) }
190
+
191
+ friends_call = user_call.then { |user| api_call(:fetch_friends, user) }
192
+ friends_call.then { |friends| promises[:friends].complete(friends) }
193
+
194
+ likes_call = user_call.then { |user| api_call(:fetch_likes, user) }
195
+ likes_call.then { |likes| promises[:likes].complete(likes) }
196
+
197
+ promises.inject({}) { |acc, kv| acc.merge kv[0] => kv[1].future }
198
+ end
199
+ ```
22
200
 
23
201
  ## Contributing
24
202
 
25
- 1. Fork it ( https://github.com/[my-github-username]/visionary/fork )
203
+ 1. Fork it ( https://github.com/waterlink/visionary/fork )
26
204
  2. Create your feature branch (`git checkout -b my-new-feature`)
27
205
  3. Commit your changes (`git commit -am 'Add some feature'`)
28
206
  4. Push to the branch (`git push origin my-new-feature`)
@@ -1,5 +1,10 @@
1
1
  require "visionary/version"
2
+ require "visionary/future"
3
+ require "visionary/promise"
2
4
 
3
5
  module Visionary
4
- # Your code goes here...
6
+ def self.setup!
7
+ Future.setup!
8
+ Promise.setup!
9
+ end
5
10
  end
@@ -0,0 +1,94 @@
1
+ module Visionary
2
+ class Future
3
+
4
+ class << self
5
+ def setup!
6
+ unless setted_up
7
+ Kernel.send :define_method, :future do |&blk|
8
+ Future.new(&blk).run
9
+ end
10
+ self.setted_up = true
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ attr_accessor :setted_up
17
+ end
18
+
19
+ attr_reader :state, :error, :value
20
+
21
+ def initialize(&blk)
22
+ @block = blk
23
+ self.state = :pending
24
+ end
25
+
26
+ def run
27
+ raise RuntimeError, "This future have been already started" if thread
28
+ @thread = Thread.new { run! }
29
+ self
30
+ end
31
+
32
+ def then(&blk)
33
+ fut = Future.new { blk.call(value) }
34
+ fut.waiting_for = self
35
+
36
+ case state
37
+ when :pending
38
+ callbacks << fut
39
+ when :completed
40
+ fut.run
41
+ when :failed
42
+ fut.fail_with(error)
43
+ end
44
+
45
+ fut
46
+ end
47
+
48
+ def await
49
+ waiting_for && waiting_for.await
50
+
51
+ unless thread
52
+ run
53
+ end
54
+
55
+ thread.join
56
+ end
57
+
58
+ protected
59
+
60
+ attr_accessor :waiting_for
61
+
62
+ def complete_with(value)
63
+ @value = value
64
+ @state = :completed
65
+ callbacks.each { |callback| callback.run }
66
+ ensure
67
+ freeze
68
+ end
69
+
70
+ def fail_with(error)
71
+ @error = error
72
+ @state = :failed
73
+ callbacks.each { |callback| callback.fail_with(error) }
74
+ ensure
75
+ freeze
76
+ end
77
+
78
+ private
79
+
80
+ attr_writer :state
81
+ attr_reader :block, :thread
82
+
83
+ def run!
84
+ complete_with(block.call)
85
+ rescue => e
86
+ fail_with(e)
87
+ end
88
+
89
+ def callbacks
90
+ @callbacks ||= []
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,34 @@
1
+ module Visionary
2
+ class Promise
3
+
4
+ class << self
5
+ def setup!
6
+ unless setted_up
7
+ Kernel.send :define_method, :promise do
8
+ Promise.new
9
+ end
10
+ self.setted_up = true
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ attr_accessor :setted_up
17
+ end
18
+
19
+ def future
20
+ @future ||= Future.new
21
+ end
22
+
23
+ def complete(computed_value)
24
+ future.instance_eval { complete_with(computed_value) }
25
+ freeze
26
+ end
27
+
28
+ def fail(provided_error)
29
+ future.instance_eval { fail_with(provided_error) }
30
+ freeze
31
+ end
32
+
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Visionary
2
- VERSION = "0.0.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,2 +1,4 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'visionary'
3
+
4
+ Visionary.setup!
@@ -0,0 +1,256 @@
1
+ module Visionary
2
+ RSpec.describe Future do
3
+
4
+ describe "Kernel#future" do
5
+ it "delegates to Future.new and chains with run" do
6
+ fut = double("Future")
7
+ block = -> { :i_am_a_block }
8
+
9
+ allow(Future).to receive(:new).with(no_args) do |&blk|
10
+ if blk == block
11
+ fut
12
+ else
13
+ nil
14
+ end
15
+ end
16
+
17
+ expect(fut).to receive(:run)
18
+
19
+ future(&block)
20
+ end
21
+ end
22
+
23
+ describe "#state" do
24
+ it "is :pending when future is still computing the result" do
25
+ fut = future { do_something; 42 }
26
+
27
+ expect(fut.state).to eq(:pending)
28
+ end
29
+
30
+ it "is :failed when future failed to compute the result" do
31
+ fut = future { raise RuntimeError }
32
+
33
+ do_something
34
+
35
+ expect(fut.state).to eq(:failed)
36
+ end
37
+
38
+ it "is :completed when future managed to compute the result" do
39
+ fut = future { 42 }
40
+
41
+ do_something
42
+
43
+ expect(fut.state).to eq(:completed)
44
+ end
45
+ end
46
+
47
+ describe "#frozen?" do
48
+ it "is true when future is completed" do
49
+ fut = future { 42 }
50
+
51
+ do_something
52
+
53
+ expect(fut.frozen?).to eq(true)
54
+ end
55
+
56
+ it "is true when future is failed" do
57
+ fut = future { raise RuntimeError }
58
+
59
+ do_something
60
+
61
+ expect(fut.frozen?).to eq(true)
62
+ end
63
+
64
+ it "is false when future is pending" do
65
+ fut = future { do_something; 42 }
66
+
67
+ expect(fut.frozen?).to eq(false)
68
+ end
69
+ end
70
+
71
+ describe "#value" do
72
+ it "contains computation result when future is completed" do
73
+ fut = future { 42 }
74
+
75
+ do_something
76
+
77
+ expect(fut.value).to eq(42)
78
+ end
79
+
80
+ it "contains nil when future is pending" do
81
+ fut = future { do_something; 42 }
82
+
83
+ expect(fut.value).to eq(nil)
84
+ end
85
+
86
+ it "contains nil when future is failed" do
87
+ fut = future { raise RuntimeError }
88
+
89
+ do_something
90
+
91
+ expect(fut.value).to eq(nil)
92
+ end
93
+ end
94
+
95
+ describe "#error" do
96
+ it "contains error when future is failed" do
97
+ error = RuntimeError.new(description: "Great runtime error")
98
+ fut = future { raise error }
99
+
100
+ do_something
101
+
102
+ expect(fut.error).to be(error)
103
+ end
104
+ end
105
+
106
+ describe "#then" do
107
+ it "returns a future" do
108
+ fut = future { do_something; 42 }
109
+
110
+ fut2 = fut.then { do_something; :else }
111
+
112
+ expect(fut2).to be_a(Future)
113
+ end
114
+
115
+ it "returns a new future" do
116
+ fut = future { do_something; 42 }
117
+
118
+ fut2 = fut.then { do_something; :else }
119
+
120
+ expect(fut2).not_to be(fut)
121
+ end
122
+
123
+ it "eventually calculates something else" do
124
+ fut = future { do_something; 42 }
125
+ fut2 = fut.then { do_something; :else }
126
+
127
+ 4.times { do_something }
128
+
129
+ expect(fut2.value).to eq(:else)
130
+ end
131
+
132
+ it "passes result to the provided block when completed" do
133
+ fut = future { do_something; 42 }
134
+ fut2 = fut.then { |answer| do_something; "answer is: #{answer}" }
135
+
136
+ 4.times { do_something }
137
+
138
+ expect(fut2.value).to eq("answer is: 42")
139
+ end
140
+
141
+ it "raises the same error in a new future when failed" do
142
+ error = RuntimeError.new(description: "Great runtime error")
143
+ fut = future { do_something; raise error }
144
+ fut2 = fut.then { |answer| do_something; "answer is: #{answer}" }
145
+
146
+ 2.times { do_something }
147
+
148
+ expect(fut2.error).to eq(error)
149
+ end
150
+
151
+ it "does not start calculation of a new future until initial is completed" do
152
+ fut = future { 2.times { do_something }; 42 }
153
+ fut2 = fut.then { |answer| "answer is: #{answer}" }
154
+
155
+ do_something
156
+
157
+ expect(fut2.state).to eq(:pending)
158
+ end
159
+
160
+ context "when initial future is already completed" do
161
+ it "immediately starts computation for a new future" do
162
+ something = double("Something", notify: nil)
163
+ fut = future { 42 }
164
+
165
+ do_something
166
+
167
+ fut2 = fut.then do |answer|
168
+ something.notify(answer)
169
+ 2.times { do_something }
170
+ answer + 3
171
+ end
172
+
173
+ do_something
174
+
175
+ expect(something).to have_received(:notify).with(42)
176
+ end
177
+ end
178
+
179
+ context "when initial future is already failed" do
180
+ it "immediately fails a new future" do
181
+ fut = future { raise RuntimeError }
182
+ do_something
183
+
184
+ fut2 = fut.then { |answer| do_something; answer + 3 }
185
+
186
+ expect(fut2.state).to eq(:failed)
187
+ end
188
+
189
+ end
190
+ end
191
+
192
+ describe "#await" do
193
+ it "awaits for computation to complete" do
194
+ fut = future { do_something; 42 }
195
+
196
+ fut.await
197
+
198
+ expect(fut.state).to eq(:completed)
199
+ end
200
+
201
+ it "awaits for computation to fail" do
202
+ fut = future { do_something; raise RuntimeError }
203
+
204
+ fut.await
205
+
206
+ expect(fut.state).to eq(:failed)
207
+ end
208
+
209
+ it "awaits for computation chain to complete" do
210
+ fut = future { do_something; 42 }
211
+ fut2 = fut.then { |answer| do_something; "Answer is: #{answer}" }
212
+ fut3 = fut2.then { |report| do_something; "WTF? Why #{report.downcase}?" }
213
+
214
+ fut3.await
215
+
216
+ expect(fut3.value).to eq("WTF? Why answer is: 42?")
217
+ end
218
+
219
+ it "does not fails when awaits more than once" do
220
+ fut = future { do_something; 42 }
221
+
222
+ expect {
223
+ fut.await
224
+ fut.await
225
+ fut.await
226
+ }.not_to raise_error
227
+ end
228
+ end
229
+
230
+ describe "#run" do
231
+ it "runs future" do
232
+ fut = Future.new { do_something; 42 }
233
+
234
+ 2.times { do_something }
235
+
236
+ fut.run
237
+ expect(fut.state).to eq(:pending)
238
+ end
239
+
240
+ it "cannot be run when it is already running" do
241
+ fut = future { do_something; 42 }
242
+
243
+ expect {
244
+ fut.run
245
+ }.to raise_error(RuntimeError)
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ def do_something
252
+ sleep(0.01)
253
+ end
254
+
255
+ end
256
+ end
@@ -0,0 +1,75 @@
1
+ module Visionary
2
+ RSpec.describe Promise do
3
+
4
+ describe "Kernel#promise" do
5
+ it "creates new promise" do
6
+ p = double("Promise")
7
+
8
+ allow(Promise).to receive(:new) { p }
9
+
10
+ expect(promise).to be(p)
11
+ end
12
+ end
13
+
14
+ describe "#future" do
15
+ it "returns associated future" do
16
+ expect(promise.future).to be_a(Future)
17
+ end
18
+
19
+ it "returns the exact future all the times" do
20
+ p = promise
21
+ expect(p.future).to be(p.future)
22
+ end
23
+
24
+ it "returns pending future" do
25
+ expect(promise.future.state).to eq(:pending)
26
+ end
27
+ end
28
+
29
+ describe "#complete" do
30
+ it "completes future" do
31
+ p = promise
32
+ expect { p.complete(42) }
33
+ .to change { p.future.state }
34
+ .from(:pending)
35
+ .to(:completed)
36
+ end
37
+
38
+ it "stores passed value as a value in future" do
39
+ p = promise
40
+ p.complete(42)
41
+ expect(p.future.value).to eq(42)
42
+ end
43
+
44
+ it "becomes frozen" do
45
+ p = promise
46
+ p.complete(42)
47
+ expect(p.frozen?).to eq(true)
48
+ end
49
+ end
50
+
51
+ describe "#fail" do
52
+ it "failes future" do
53
+ p = promise
54
+ expect { p.fail(RuntimeError.new) }
55
+ .to change { p.future.state }
56
+ .from(:pending)
57
+ .to(:failed)
58
+ end
59
+
60
+ it "stores passed error as an error in future" do
61
+ p = promise
62
+ error = RuntimeError.new("Crazy description")
63
+ p.fail(error)
64
+ expect(p.future.error).to be(error)
65
+ end
66
+
67
+ it "becomes frozen" do
68
+ p = promise
69
+ p.fail(RuntimeError.new)
70
+ expect(p.frozen?).to eq(true)
71
+ end
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1 @@
1
+ RSpec.describe "something"
@@ -1,11 +1,5 @@
1
- require 'spec_helper'
2
-
3
1
  describe Visionary do
4
- it 'has a version number' do
2
+ it "has a version number" do
5
3
  expect(Visionary::VERSION).not_to be nil
6
4
  end
7
-
8
- it 'does something useful' do
9
- expect(false).to eq(true)
10
- end
11
5
  end
@@ -21,4 +21,6 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency "bundler", "~> 1.6"
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "cucumber"
25
+ spec.add_development_dependency "aruba"
24
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: visionary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Fedorov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-26 00:00:00.000000000 Z
11
+ date: 2014-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: cucumber
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: aruba
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  description: ''
56
84
  email:
57
85
  - waterlink000@gmail.com
@@ -67,8 +95,13 @@ files:
67
95
  - README.md
68
96
  - Rakefile
69
97
  - lib/visionary.rb
98
+ - lib/visionary/future.rb
99
+ - lib/visionary/promise.rb
70
100
  - lib/visionary/version.rb
71
101
  - spec/spec_helper.rb
102
+ - spec/visionary/future_spec.rb
103
+ - spec/visionary/promise_spec.rb
104
+ - spec/visionary/promised_future_spec.rb
72
105
  - spec/visionary_spec.rb
73
106
  - visionary.gemspec
74
107
  homepage: ''
@@ -97,4 +130,7 @@ specification_version: 4
97
130
  summary: Simple futures implementation in ruby.
98
131
  test_files:
99
132
  - spec/spec_helper.rb
133
+ - spec/visionary/future_spec.rb
134
+ - spec/visionary/promise_spec.rb
135
+ - spec/visionary/promised_future_spec.rb
100
136
  - spec/visionary_spec.rb