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 CHANGED
@@ -7,7 +7,6 @@ Gemfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
10
- doc/
11
10
  lib/bundler/man
12
11
  pkg
13
12
  rdoc
@@ -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,9 @@
1
+ class ForGnuplot
2
+ include EMDextras::Chains::SynchronousStage
3
+ def invoke(data_item)
4
+ country_code = data_item["country"]["id"]
5
+ date = data_item["date"]
6
+ value = data_item["value"]
7
+ "#{country_code}\t#{date}\t#{value}"
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ class ParseWorldbankDocument
2
+ include EMDextras::Chains::SynchronousStage
3
+ def invoke(http)
4
+ document_body = http.response
5
+ json = JSON.parse document_body
6
+ (json.size > 1 && json[1]) ? json[1].take(10) : []
7
+ end
8
+ 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,4 @@
1
+ require 'em-http-request'
2
+ require 'em-dextras'
3
+ require 'em-dextras/spec'
4
+ require 'json'
@@ -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
@@ -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
- self.monitoring.inform_exception! error_value, stage
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
- run_chain zero, stages, PipeSetup.new(monitoring, options)
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
- new_pipe_setup = PipeSetup.new(pipe_setup.monitoring, new_options)
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
- input.each do |value|
64
- run_chain value, rest, new_pipe_setup
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
- case todo_method.arity
71
- when 1
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
- when 2
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
@@ -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
- @calls.include? :name => method_name, :args => args
12
+ count_calls(method_name, *args) > 0
10
13
  end
11
14
 
12
- def received_call!(method_name, *args)
15
+ def received_n_calls!(number, method_name, *args)
13
16
  probe_event_machine check: (Proc.new do
14
- check_if_received_call(method_name, *args)
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
@@ -1,5 +1,5 @@
1
1
  module Em
2
2
  module Dextras
3
- VERSION = "0.2.0"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/em-dextras.rb CHANGED
@@ -1,9 +1,10 @@
1
+ require "eventmachine"
2
+
1
3
  require "em-dextras/version"
2
4
 
3
5
  require "em-dextras/chains"
4
6
  require "em-dextras/chains/synchronous_stage"
5
7
 
6
- require "eventmachine"
7
8
 
8
9
  module EMDextras
9
10
  # Your code goes here...