em-dextras 0.2.0 → 0.3.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.
- 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