em-dextras 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -1
- data/doc/samples/worldbank/fetch_indicator.rb +16 -0
- data/doc/samples/worldbank/fetch_list_of_countries.rb +12 -0
- data/doc/samples/worldbank/for_gnuplot.rb +9 -0
- data/doc/samples/worldbank/parse_worldbank_document.rb +8 -0
- data/doc/samples/worldbank/spec/fetch_indicator_spec.rb +52 -0
- data/doc/samples/worldbank/spec/fetch_list_of_countries_spec.rb +13 -0
- data/doc/samples/worldbank/spec/for_gnuplot_spec.rb +26 -0
- data/doc/samples/worldbank/spec/parse_worldbank_document_spec.rb +27 -0
- data/doc/samples/worldbank/spec/support/spec_helper.rb +4 -0
- data/doc/samples/worldbank/worldbank.rb +44 -0
- data/em-dextras.gemspec +4 -0
- data/lib/em-dextras/chains.rb +129 -17
- data/lib/em-dextras/extension/object/deferrable.rb +17 -0
- data/lib/em-dextras/spec/spec_matchers.rb +20 -0
- data/lib/em-dextras/spec/spy.rb +30 -3
- data/lib/em-dextras/version.rb +1 -1
- data/lib/em-dextras.rb +2 -1
- data/spec/em-dextras/chains_spec.rb +321 -41
- data/spec/em-dextras/extension/object/deferrable_spec.rb +36 -0
- data/spec/em-dextras/spec/spec_matchers_spec.rb +81 -0
- data/spec/em-dextras/spec/spy_spec.rb +107 -8
- data/spec/spec_helper.rb +6 -0
- metadata +49 -4
- data/spec/spec_helper +0 -0
data/.gitignore
CHANGED
@@ -0,0 +1,16 @@
|
|
1
|
+
class FetchIndicator
|
2
|
+
def initialize(indicator)
|
3
|
+
@indicator = indicator
|
4
|
+
end
|
5
|
+
|
6
|
+
def todo(country)
|
7
|
+
id = country["id"]
|
8
|
+
http = EventMachine::HttpRequest.new(indicator_url(id), :connect_timeout => 2, :inactivity_timeout => 3)
|
9
|
+
http.get
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def indicator_url(country_id)
|
14
|
+
"http://api.worldbank.org/countries/#{country_id}/indicators/#@indicator?format=json"
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class FetchListOfCountries
|
2
|
+
def todo(no_input)
|
3
|
+
http = EventMachine::HttpRequest.new(list_of_countries_url, :connect_timeout => 2, :inactivity_timeout => 3)
|
4
|
+
http.get
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def list_of_countries_url
|
10
|
+
'http://api.worldbank.org/countries?format=json'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative './support/spec_helper'
|
2
|
+
require_relative '../fetch_indicator'
|
3
|
+
|
4
|
+
describe FetchIndicator do
|
5
|
+
let(:country) {
|
6
|
+
JSON.parse <<-EOS
|
7
|
+
{
|
8
|
+
"latitude": "-15.7801",
|
9
|
+
"longitude": "-47.9292",
|
10
|
+
"id": "BRA",
|
11
|
+
"iso2Code": "BR",
|
12
|
+
"name": "Brazil",
|
13
|
+
"region": {
|
14
|
+
"value": "Latin America & Caribbean (all income levels)",
|
15
|
+
"id": "LCN"
|
16
|
+
},
|
17
|
+
"adminregion": {
|
18
|
+
"value": "Latin America & Caribbean (developing only)",
|
19
|
+
"id": "LAC"
|
20
|
+
},
|
21
|
+
"incomeLevel": {
|
22
|
+
"value": "Upper middle income",
|
23
|
+
"id": "UMC"
|
24
|
+
},
|
25
|
+
"lendingType": {
|
26
|
+
"value": "IBRD",
|
27
|
+
"id": "IBD"
|
28
|
+
},
|
29
|
+
"capitalCity": "Brasilia"
|
30
|
+
}
|
31
|
+
EOS
|
32
|
+
}
|
33
|
+
|
34
|
+
subject { described_class.new 'SI.DST.10TH.10' }
|
35
|
+
|
36
|
+
it "should successfully request a data series for the given country" do
|
37
|
+
EM.run do
|
38
|
+
subject.todo(country).should succeed_according_to(lambda {|http|
|
39
|
+
http.response.should include 'Brazil'
|
40
|
+
})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "the indicator requested should be the one provided" do
|
45
|
+
EM.run do
|
46
|
+
fetch = FetchIndicator.new('NY.GDP.MKTP.CD')
|
47
|
+
fetch.todo(country).should succeed_according_to(lambda {|http|
|
48
|
+
http.response.should include 'GDP'
|
49
|
+
})
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative './support/spec_helper'
|
2
|
+
require_relative '../fetch_list_of_countries'
|
3
|
+
|
4
|
+
describe FetchListOfCountries do
|
5
|
+
it "should successfully request a list of countries" do
|
6
|
+
EM.run do
|
7
|
+
subject.todo('ignored input').should succeed_according_to(lambda {|request|
|
8
|
+
request.response_header.status.should == 200
|
9
|
+
request.last_effective_url.to_s.should include 'worldbank.org'
|
10
|
+
})
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative './support/spec_helper'
|
2
|
+
require_relative '../for_gnuplot'
|
3
|
+
|
4
|
+
describe ForGnuplot do
|
5
|
+
let (:data_item) {
|
6
|
+
JSON.parse <<-EOS
|
7
|
+
{
|
8
|
+
"date": "2009",
|
9
|
+
"decimal": "0",
|
10
|
+
"value": "1621661507655.08",
|
11
|
+
"country": {
|
12
|
+
"value": "Brazil",
|
13
|
+
"id": "BR"
|
14
|
+
},
|
15
|
+
"indicator": {
|
16
|
+
"value": "GDP (current US$)",
|
17
|
+
"id": "NY.GDP.MKTP.CD"
|
18
|
+
}
|
19
|
+
}
|
20
|
+
EOS
|
21
|
+
}
|
22
|
+
|
23
|
+
it "prints a line with the country, date and value" do
|
24
|
+
subject.invoke(data_item).should == "BR\t2009\t1621661507655.08"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative './support/spec_helper'
|
2
|
+
require_relative '../parse_worldbank_document'
|
3
|
+
|
4
|
+
describe ParseWorldbankDocument do
|
5
|
+
it "should return the second json array element" do
|
6
|
+
http_request = stub(:response => %Q|[{"first":"el"},["second","el"]]|)
|
7
|
+
subject.invoke(http_request).should == ["second","el"]
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should only take the first 10 elements" do
|
11
|
+
http_request = stub(
|
12
|
+
:response => %Q|[{"first":"el"},[#{'"a",'*11}"a"]]|)
|
13
|
+
subject.invoke(http_request).should == Array.new(10, "a")
|
14
|
+
end
|
15
|
+
|
16
|
+
context "corner cases" do
|
17
|
+
it "should return an empty array if there is no second element" do
|
18
|
+
http_request = stub(:response => %Q|[{"first":"el"}]|)
|
19
|
+
subject.invoke(http_request).should == []
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should return an empty array if the second element is empty" do
|
23
|
+
http_request = stub(:response => %Q|[{"first":"el"},[]]|)
|
24
|
+
subject.invoke(http_request).should == []
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'eventmachine'
|
5
|
+
require 'em-http-request'
|
6
|
+
require 'em-dextras'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
require_relative './fetch_list_of_countries'
|
10
|
+
require_relative './parse_worldbank_document'
|
11
|
+
require_relative './fetch_indicator'
|
12
|
+
require_relative './for_gnuplot'
|
13
|
+
|
14
|
+
INCOME_SHARE_BY_TOP_10PC = 'SI.DST.10TH.10'
|
15
|
+
|
16
|
+
class Monitoring
|
17
|
+
def end_of_chain!(value)
|
18
|
+
EM.stop
|
19
|
+
end
|
20
|
+
|
21
|
+
def inform_exception!(exception, stage)
|
22
|
+
STDERR.puts "Error: #{exception} #{exception.backtrace.join("\n") if exception.respond_to?(:backtrace)}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Print
|
27
|
+
include EMDextras::Chains::SynchronousStage
|
28
|
+
def invoke(input)
|
29
|
+
puts input
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
EM.run do
|
34
|
+
EMDextras::Chains.pipe('no input', Monitoring.new, [
|
35
|
+
FetchListOfCountries.new,
|
36
|
+
ParseWorldbankDocument.new,
|
37
|
+
:split,
|
38
|
+
FetchIndicator.new(INCOME_SHARE_BY_TOP_10PC),
|
39
|
+
ParseWorldbankDocument.new,
|
40
|
+
:split,
|
41
|
+
ForGnuplot.new,
|
42
|
+
Print.new
|
43
|
+
], debug: true)
|
44
|
+
end
|
data/em-dextras.gemspec
CHANGED
@@ -21,4 +21,8 @@ Gem::Specification.new do |gem|
|
|
21
21
|
gem.add_development_dependency("guard")
|
22
22
|
gem.add_development_dependency("guard-rspec")
|
23
23
|
gem.add_development_dependency("rb-inotify")
|
24
|
+
|
25
|
+
#for samples
|
26
|
+
gem.add_development_dependency("em-http-request")
|
27
|
+
gem.add_development_dependency("webmock")
|
24
28
|
end
|
data/lib/em-dextras/chains.rb
CHANGED
@@ -1,11 +1,74 @@
|
|
1
1
|
module EMDextras
|
2
2
|
module Chains
|
3
|
+
|
4
|
+
class JoinedDeferrable
|
5
|
+
include EventMachine::Deferrable
|
6
|
+
|
7
|
+
def initialize(deferrables)
|
8
|
+
result_pairs = deferrables.map do |deferrable|
|
9
|
+
[deferrable, :unset]
|
10
|
+
end
|
11
|
+
@results = Hash[result_pairs]
|
12
|
+
@callback_values = []
|
13
|
+
@errback_values = []
|
14
|
+
|
15
|
+
initialize_deferrables!
|
16
|
+
end
|
17
|
+
|
18
|
+
def one_callback(*vs)
|
19
|
+
deferrable, *values = vs
|
20
|
+
@results[deferrable] = :ok
|
21
|
+
@callback_values.push *values
|
22
|
+
|
23
|
+
check_if_complete
|
24
|
+
end
|
25
|
+
|
26
|
+
def one_errback(*vs)
|
27
|
+
deferrable, *values = vs
|
28
|
+
@results[deferrable] = :error
|
29
|
+
@errback_values.push *values
|
30
|
+
|
31
|
+
check_if_complete
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def check_if_complete
|
37
|
+
complete! unless any_was?(:unset)
|
38
|
+
end
|
39
|
+
|
40
|
+
def complete!
|
41
|
+
(self.fail(@errback_values); return) if any_was?(:error)
|
42
|
+
self.succeed(@callback_values)
|
43
|
+
end
|
44
|
+
|
45
|
+
def any_was?(state)
|
46
|
+
@results.any? {|k, v| v == state }
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize_deferrables!
|
50
|
+
ds = @results.keys
|
51
|
+
|
52
|
+
ds.each do |deferrable|
|
53
|
+
deferrable.callback do |*values|
|
54
|
+
self.one_callback deferrable, *values
|
55
|
+
end
|
56
|
+
deferrable.errback do |*values|
|
57
|
+
self.one_errback deferrable, *values
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
ds.each do |d|
|
62
|
+
d.timeout(5, "Expired timeout of #{5} for #{d.inspect}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
3
67
|
module Deferrables
|
4
68
|
def self.succeeded(*args)
|
5
69
|
deferrable = EventMachine::DefaultDeferrable.new
|
6
70
|
deferrable.succeed(*args)
|
7
|
-
deferrable
|
8
|
-
end
|
71
|
+
deferrable end
|
9
72
|
def self.failed(*args)
|
10
73
|
deferrable = EventMachine::DefaultDeferrable.new
|
11
74
|
deferrable.fail(*args)
|
@@ -13,66 +76,115 @@ module EMDextras
|
|
13
76
|
end
|
14
77
|
end
|
15
78
|
|
16
|
-
PipeSetup = Struct.new(:monitoring, :options) do
|
79
|
+
PipeSetup = Struct.new(:monitoring, :options, :result) do
|
17
80
|
def inform_exception!(error_value, stage)
|
18
|
-
|
81
|
+
if options[:context]
|
82
|
+
self.monitoring.inform_exception! error_value, stage, options[:context]
|
83
|
+
else
|
84
|
+
self.monitoring.inform_exception! error_value, stage
|
85
|
+
end
|
19
86
|
end
|
20
87
|
end
|
21
88
|
|
22
89
|
def self.pipe(zero, monitoring, stages, options = {})
|
23
|
-
|
90
|
+
result = create_chain_result(monitoring, options)
|
91
|
+
run_chain zero, stages, PipeSetup.new(monitoring, options, result)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.create_chain_result(monitoring, options)
|
95
|
+
EventMachine::DefaultDeferrable.new.
|
96
|
+
tap {|d| d.callback { |value| notify_end_of_chain!(value, monitoring, options) }}.
|
97
|
+
tap {|d| d.errback { |value| notify_end_of_chain!(value, monitoring, options) }}
|
24
98
|
end
|
25
99
|
|
26
100
|
def self.run_chain input, stages, pipe_setup
|
27
|
-
return if stages.empty?
|
101
|
+
return pipe_setup.result.succeed(input) if stages.empty? || input.nil?
|
28
102
|
|
29
103
|
stage, *rest = *stages
|
30
104
|
|
31
|
-
puts "Running #{stage}(#{input})" if pipe_setup.options[:debug]
|
32
|
-
|
33
105
|
if stage == :split
|
34
106
|
split_chain(input, rest, pipe_setup)
|
35
|
-
return
|
107
|
+
return pipe_setup.result
|
36
108
|
end
|
37
109
|
|
38
|
-
|
39
110
|
deferrable = call(stage, input, pipe_setup)
|
111
|
+
check_stage_is_well_behaved!(deferrable, stage, input, deferrable)
|
40
112
|
deferrable.callback do |value|
|
41
113
|
run_chain value, rest, pipe_setup
|
42
114
|
end
|
43
115
|
deferrable.errback do |error_value|
|
44
116
|
pipe_setup.inform_exception! error_value, stage
|
117
|
+
pipe_setup.result.fail(error_value)
|
45
118
|
end
|
119
|
+
|
120
|
+
pipe_setup.result
|
46
121
|
end
|
47
122
|
|
48
123
|
private
|
124
|
+
|
125
|
+
def self.check_stage_is_well_behaved!(deferrable, stage, input, value)
|
126
|
+
unless deferrable.respond_to?(:callback) && deferrable.respond_to?(:errback)
|
127
|
+
raise InvalidStage, "Stage '#{stage.class.name}' did not return a deferrable object when given input '#{input.to_s[0..10]}', instead it returned '#{value}'!"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
49
131
|
def self.split_chain input, rest, pipe_setup
|
50
132
|
new_options = pipe_setup.options.clone
|
51
133
|
|
52
134
|
context = new_options[:context]
|
53
135
|
if context && context.respond_to?(:split)
|
54
|
-
new_options[:context] = context.split
|
136
|
+
new_options[:context] = context.split
|
55
137
|
end
|
56
138
|
|
57
|
-
|
139
|
+
rest_of_chain = rest
|
58
140
|
|
59
141
|
unless input.respond_to? :each
|
60
142
|
pipe_setup.inform_exception! ArgumentError.new(":split stage expects enumerable input. \"#{input}\" is not enumerable."), :split
|
61
143
|
return
|
62
144
|
end
|
63
|
-
|
64
|
-
|
145
|
+
|
146
|
+
splits_deferrables = input.map do |value|
|
147
|
+
split_result = EventMachine::DefaultDeferrable.new
|
148
|
+
new_pipe_setup = PipeSetup.new(pipe_setup.monitoring, new_options, split_result)
|
149
|
+
run_chain value, rest_of_chain, new_pipe_setup
|
150
|
+
|
151
|
+
split_result
|
152
|
+
end
|
153
|
+
|
154
|
+
join = JoinedDeferrable.new(splits_deferrables)
|
155
|
+
join.callback do |*values|
|
156
|
+
pipe_setup.result.succeed(*values)
|
157
|
+
end
|
158
|
+
join.errback do |*values|
|
159
|
+
pipe_setup.result.fail(*values)
|
65
160
|
end
|
66
161
|
end
|
67
162
|
|
68
163
|
def self.call(stage, input, pipe_setup)
|
69
164
|
todo_method = stage.method(:todo)
|
70
|
-
|
71
|
-
|
165
|
+
arity = todo_method.arity
|
166
|
+
if arity < 0 && pipe_setup.options[:context]
|
167
|
+
stage.todo(input, pipe_setup.options[:context])
|
168
|
+
elsif arity < 0 || arity == 1
|
72
169
|
stage.todo(input)
|
73
|
-
|
170
|
+
elsif arity == 2
|
74
171
|
stage.todo(input, pipe_setup.options[:context])
|
75
172
|
end
|
76
173
|
end
|
174
|
+
|
175
|
+
def self.notify_end_of_chain!(value, monitoring, options)
|
176
|
+
context = options[:context]
|
177
|
+
|
178
|
+
if monitoring.respond_to? :end_of_chain!
|
179
|
+
if context
|
180
|
+
monitoring.end_of_chain!(value, context)
|
181
|
+
else
|
182
|
+
monitoring.end_of_chain!(value)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
class InvalidStage < Exception
|
188
|
+
end
|
77
189
|
end
|
78
190
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module Deferrable
|
3
|
+
def map
|
4
|
+
deferrable_result = EventMachine::DefaultDeferrable.new
|
5
|
+
|
6
|
+
self.callback do |original_value|
|
7
|
+
deferrable_result.succeed yield(original_value)
|
8
|
+
end
|
9
|
+
|
10
|
+
self.errback do |original_value|
|
11
|
+
deferrable_result.fail original_value
|
12
|
+
end
|
13
|
+
|
14
|
+
deferrable_result
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -19,6 +19,26 @@ if defined?(RSpec)
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
RSpec::Matchers.define :succeed_according_to do |proc_expecting|
|
23
|
+
match_unless_raises Exception do |actual_deferred|
|
24
|
+
resolved_value = nil
|
25
|
+
actual_deferred.callback do |value|
|
26
|
+
resolved_value = value
|
27
|
+
end
|
28
|
+
actual_deferred.errback do |error|
|
29
|
+
if error.is_a? Exception
|
30
|
+
raise error
|
31
|
+
else
|
32
|
+
raise "Callback error: #{error.inspect}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
probe_event_machine :check => (lambda do |ignored|
|
37
|
+
proc_expecting.call(resolved_value)
|
38
|
+
end), :timeout => 5
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
22
42
|
RSpec::Matchers.define :be_successful do |expected|
|
23
43
|
match_unless_raises Exception do |actual_deferred|
|
24
44
|
done = false
|
data/lib/em-dextras/spec/spy.rb
CHANGED
@@ -1,20 +1,38 @@
|
|
1
|
+
require 'rspec/mocks/argument_list_matcher'
|
2
|
+
|
1
3
|
module EMDextras::Spec
|
2
4
|
class Spy
|
3
5
|
def initialize(options = {})
|
4
6
|
@calls = []
|
5
7
|
@return_value = options[:default_return]
|
8
|
+
@only_respond_to = options[:only_respond_to]
|
6
9
|
end
|
7
10
|
|
8
11
|
def called?(method_name, *args)
|
9
|
-
|
12
|
+
count_calls(method_name, *args) > 0
|
10
13
|
end
|
11
14
|
|
12
|
-
def
|
15
|
+
def received_n_calls!(number, method_name, *args)
|
13
16
|
probe_event_machine check: (Proc.new do
|
14
|
-
|
17
|
+
received_calls_number = count_calls(method_name, *args)
|
18
|
+
unless (received_calls_number == number )
|
19
|
+
raise ExpectationFailed, "Expected #{method_name} to have been called #{number} times with parameters [#{args.join(",")}] but only received #{received_calls_number} such calls (also received the following calls: #{@calls.inspect})"
|
20
|
+
end
|
15
21
|
end)
|
16
22
|
end
|
17
23
|
|
24
|
+
def received_call!(method_name, *args)
|
25
|
+
received_n_calls!(1, method_name, *args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def not_received_call!(method_name, *args)
|
29
|
+
received_n_calls!(0, method_name, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_to?(symbol)
|
33
|
+
@only_respond_to ? @only_respond_to.include?(symbol) : true
|
34
|
+
end
|
35
|
+
|
18
36
|
def method_missing(method_name, *args, &block)
|
19
37
|
@calls << { :name => method_name, :args => args }
|
20
38
|
@return_value
|
@@ -28,6 +46,15 @@ module EMDextras::Spec
|
|
28
46
|
end
|
29
47
|
end
|
30
48
|
|
49
|
+
def count_calls(method_name, *args)
|
50
|
+
arg_list_matcher = RSpec::Mocks::ArgumentListMatcher.new(*args)
|
51
|
+
|
52
|
+
found = @calls.select do |call|
|
53
|
+
call[:name] == method_name && arg_list_matcher.args_match?(*call[:args])
|
54
|
+
end
|
55
|
+
found.size
|
56
|
+
end
|
57
|
+
|
31
58
|
end
|
32
59
|
|
33
60
|
class ExpectationFailed < Exception
|
data/lib/em-dextras/version.rb
CHANGED