visionary 0.0.1 → 0.2.0

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 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