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