carbon 2.0.3 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,7 @@ require 'carbon/future'
5
5
 
6
6
  module Carbon
7
7
  DOMAIN = 'http://impact.brighterplanet.com'
8
+ CONCURRENCY = 16
8
9
 
9
10
  # @private
10
11
  # Make sure there are no warnings about class vars.
@@ -17,6 +18,7 @@ module Carbon
17
18
  # @return [nil]
18
19
  def Carbon.key=(key)
19
20
  @@key = key
21
+ nil
20
22
  end
21
23
 
22
24
  # Get the key you've set.
@@ -26,51 +28,146 @@ module Carbon
26
28
  @@key
27
29
  end
28
30
 
29
- # Do a simple query.
31
+ # Get impact estimates from Brighter Planet CM1; low-level method that does _not_ require you to define {Carbon::ClassMethods#emit_as} blocks; just pass emitter/param or objects that respond to +#as_impact_query+.
30
32
  #
31
- # See the {file:README.html#API_response section about API responses} for an explanation of +Hashie::Mash+.
33
+ # Return values are {http://rdoc.info/github/intridea/hashie/Hashie/Mash Hashie::Mash} objects because they are a simple way to access a deeply nested response.
32
34
  #
33
- # @param [String] emitter The {http://impact.brighterplanet.com/emitters.json camelcased emitter name}.
34
- # @param [Hash] params Characteristics, your API key (if you didn't set it globally), timeframe, compliance, etc.
35
+ # Here's a map of what's included in a response:
35
36
  #
36
- # @option params [Timeframe] :timeframe (Timeframe.this_year) What time period to focus the calculation on. See {https://github.com/rossmeissl/timeframe timeframe} documentation.
37
- # @option params [Array<Symbol>] :comply ([]) What {http://impact.brighterplanet.com/protocols.json calculation protocols} to require.
38
- # @option params [String, Numeric] _characteristic_ Pieces of data about an emitter. The {http://impact.brighterplanet.com/flights/options Flight characteristics API} lists valid keys like +:aircraft+, +:origin_airport+, etc.
37
+ # certification
38
+ # characteristics.{}.description
39
+ # characteristics.{}.object
40
+ # compliance.[]
41
+ # decisions.{}.description
42
+ # decisions.{}.methodology
43
+ # decisions.{}.object
44
+ # emitter
45
+ # equivalents.{}
46
+ # errors.[]
47
+ # methodology
48
+ # scope
49
+ # timeframe.endDate
50
+ # timeframe.startDate
39
51
  #
40
- # @return [Hashie::Mash, Carbon::Future] An {file:README.html#API_response API response as documented in the README}
52
+ # @overload query(emitter, params)
53
+ # Simplest form.
54
+ # @param [String] emitter The {http://impact.brighterplanet.com/emitters.json emitter name}.
55
+ # @param [optional, Hash] params Characteristics like airline/airport/etc., your API key (if you didn't set it globally), timeframe, compliance, etc.
56
+ # @option params [Timeframe] :timeframe (Timeframe.this_year) What time period to focus the calculation on. See {https://github.com/rossmeissl/timeframe timeframe} documentation.
57
+ # @option params [Array<Symbol>] :comply ([]) What {http://impact.brighterplanet.com/protocols.json calculation protocols} to require.
58
+ # @option params [String, Numeric] <i>characteristic</i> Pieces of data about an emitter. The {http://impact.brighterplanet.com/flights/options Flight characteristics API} lists valid keys like +:aircraft+, +:origin_airport+, etc.
59
+ # @return [Hashie::Mash] The API response, contained in an easy-to-use +Hashie::Mash+
41
60
  #
42
- # @example A flight taken in 2009
43
- # Carbon.query('Flight', :origin_airport => 'MSN', :destination_airport => 'ORD', :date => '2009-01-01', :timeframe => Timeframe.new(:year => 2009), :comply => [:tcr])
44
- def Carbon.query(emitter, params = {})
45
- future = Future.new emitter, params
46
- future.result
47
- end
48
-
49
- # Perform many queries in parallel. Can be *more than 90% faster* than doing them serially (one after the other).
61
+ # @overload query(o)
62
+ # Pass in a single query-able object.
63
+ # @param [#as_impact_query] o An object that responds to +#as_impact_query+, generally because you've declared {Carbon::ClassMethods#emit_as} on its parent class.
64
+ # @return [Hashie::Mash] The API response, contained in an easy-to-use +Hashie::Mash+
50
65
  #
51
- # See the {file:README.html#API_response section about API responses} for an explanation of +Hashie::Mash+.
66
+ # @overload query(os)
67
+ # Get multiple impact estimates for arrays and/or query-able objects concurrently.
68
+ # @param [Array<Array, #as_impact_query>] os An array of arrays in +[emitter, params]+ format and/or objects that respond to +#as_impact_query+.
69
+ # @return [Array<Hashie::Mash>] An array of +Hashie::Mash+ objects in the same order.
52
70
  #
53
- # @param [Array<Array>] queries Multiple queries like you would pass to {Carbon.query}
71
+ # @note We make up to 16 requests concurrently (hardcoded, per the Brighter Planet Terms of Service) and it can be more than 90% faster than running queries serially!
54
72
  #
55
- # @return [Array<Hashie::Mash>] An array of {file:README.html#API_response API responses}, each a +Hashie::Mash+, in the same order as the queries.
73
+ # @note Using concurrency on JRuby, you may get errors like SOCKET: SET COMM INACTIVITY UNIMPLEMENTED 10 because under the hood we're using {https://github.com/igrigorik/em-http-request em-http-request}, which suffers from {https://github.com/eventmachine/eventmachine/issues/155 an issue with +pending_connect_timeout+}.
56
74
  #
57
- # @note You may get errors like +SOCKET: SET COMM INACTIVITY UNIMPLEMENTED 10+ on JRuby because under the hood we're using {https://github.com/igrigorik/em-http-request em-http-request}, which suffers from {https://github.com/eventmachine/eventmachine/issues/155 an issue with +pending_connect_timeout+}.
75
+ # @example A flight taken in 2009
76
+ # Carbon.query('Flight', :origin_airport => 'MSN', :destination_airport => 'ORD', :date => '2009-01-01', :timeframe => Timeframe.new(:year => 2009), :comply => [:tcr])
77
+ #
78
+ # @example How do I use a +Hashie::Mash+?
79
+ # 1.8.7 :001 > require 'rubygems'
80
+ # => true
81
+ # 1.8.7 :002 > require 'hashie/mash'
82
+ # => true
83
+ # 1.8.7 :003 > mash = Hashie::Mash.new(:hello => 'world')
84
+ # => #<Hashie::Mash hello="world">
85
+ # 1.8.7 :004 > mash.hello
86
+ # => "world"
87
+ # 1.8.7 :005 > mash['hello']
88
+ # => "world"
89
+ # 1.8.7 :006 > mash[:hello]
90
+ # => "world"
91
+ # 1.8.7 :007 > mash.keys
92
+ # => ["hello"]
58
93
  #
59
- # @example Two flights and an automobile trip
94
+ # @example Other examples of what's in the response
95
+ # my_impact.carbon.object.value
96
+ # my_impact.characteristics.airline.description
97
+ # my_impact.equivalents.lightbulbs_for_a_week
98
+ #
99
+ # @example Flights and cars (concurrently, as arrays)
60
100
  # queries = [
61
- # ['Flight', :origin_airport => 'MSN', :destination_airport => 'ORD', :date => '2009-01-01', :timeframe => Timeframe.new(:year => 2009), :comply => [:tcr]],
62
- # ['Flight', :origin_airport => 'SFO', :destination_airport => 'LAX', :date => '2011-09-29', :timeframe => Timeframe.new(:year => 2011), :comply => [:iso]],
63
- # ['AutomobileTrip', :make => 'Nissan', :model => 'Altima', :timeframe => Timeframe.new(:year => 2008), :comply => [:tcr]]
101
+ # ['Flight', {:origin_airport => 'MSN', :destination_airport => 'ORD', :date => '2009-01-01', :timeframe => Timeframe.new(:year => 2009), :comply => [:tcr]}],
102
+ # ['Flight', {:origin_airport => 'SFO', :destination_airport => 'LAX', :date => '2011-09-29', :timeframe => Timeframe.new(:year => 2011), :comply => [:iso]}],
103
+ # ['Automobile', {:make => 'Nissan', :model => 'Altima', :timeframe => Timeframe.new(:year => 2008), :comply => [:tcr]}]
64
104
  # ]
65
- # Carbon.multi(queries)
66
- def Carbon.multi(queries)
67
- futures = queries.map do |emitter, params|
68
- future = Future.new emitter, params
69
- future.multi!
70
- future
71
- end
72
- Future.multi(futures).map do |future|
105
+ # Carbon.query(queries)
106
+ #
107
+ # @example Flights and cars (concurrently, as query-able objects)
108
+ # Carbon.query(MyFlight.all+MyCar.all)
109
+ #
110
+ # @example Cars month-by-month
111
+ # cars_by_month = MyCar.all.inject([]) do |memo, my_car|
112
+ # months.each do |first_day_of_the_month|
113
+ # my_car.as_impact_query(:date => first_day_of_the_month)
114
+ # end
115
+ # end
116
+ # Carbon.query(cars_by_month)
117
+ def Carbon.query(*args)
118
+ case Carbon.method_signature(*args)
119
+ when :query_array
120
+ query_array = args
121
+ future = Future.wrap query_array
122
+ future.result
123
+ when :o
124
+ o = args.first
125
+ future = Future.wrap o
73
126
  future.result
127
+ when :os
128
+ os = args.first
129
+ futures = os.map do |o|
130
+ future = Future.wrap o
131
+ future.multi!
132
+ future
133
+ end
134
+ Future.multi(futures).map do |future|
135
+ future.result
136
+ end
137
+ end
138
+ end
139
+
140
+ # Determine if a variable is a +[emitter, param]+ style "query"
141
+ # @private
142
+ def Carbon.is_query_array?(query)
143
+ return false unless query.is_a?(::Array)
144
+ return false unless query.first.is_a?(::String) or query.first.is_a?(::Symbol)
145
+ return true if query.length == 1
146
+ return true if query.length == 2 and query.last.is_a?(::Hash)
147
+ false
148
+ end
149
+
150
+ # Determine what method signature/overloading/calling style is being used
151
+ # @private
152
+ def Carbon.method_signature(*args)
153
+ first_arg = args.first
154
+ case args.length
155
+ when 1
156
+ if is_query_array?(args)
157
+ # query('Flight')
158
+ :query_array
159
+ elsif first_arg.respond_to?(:as_impact_query)
160
+ # query(my_flight)
161
+ :o
162
+ elsif first_arg.is_a?(::Array) and first_arg.all? { |o| o.respond_to?(:as_impact_query) or is_query_array?(o) }
163
+ # query([my_flight, my_flight])
164
+ :os
165
+ end
166
+ when 2
167
+ if is_query_array?(args)
168
+ # query('Flight', :origin_airport => 'LAX')
169
+ :query_array
170
+ end
74
171
  end
75
172
  end
76
173
 
@@ -84,18 +181,14 @@ module Carbon
84
181
  module ClassMethods
85
182
  # DSL for declaring how to represent this class an an emitter.
86
183
  #
184
+ # See also {Carbon::Registry::Registrar#provide}.
185
+ #
87
186
  # You get this when you +include Carbon+ in a class.
88
187
  #
89
188
  # @param [String] emitter The {http://impact.brighterplanet.com/emitters.json camelcased emitter name}.
90
189
  #
91
190
  # @return [nil]
92
191
  #
93
- # Things to note in the MyFlight example:
94
- #
95
- # * Sending +:origin+ to Brighter Planet *as* +:origin_airport+. Otherwise Brighter Planet won't recognize +:origin+.
96
- # * Saying we're *keying* on one code or another. Otherwise Brighter Planet will first try against full names and possibly other columns.
97
- # * Giving *blocks* to pull codes from +MyAircraft+ and +MyAirline+ objects. Otherwise you might get a querystring like +airline[iata_code]=#<MyAirline [...]>+
98
- #
99
192
  # @example MyFlight
100
193
  # # A a flight in your data warehouse
101
194
  # class MyFlight
@@ -135,6 +228,15 @@ module Carbon
135
228
  end
136
229
 
137
230
  # A query like what you could pass into +Carbon.query+.
231
+ #
232
+ # @param [Hash] extra_params Anything you want to override.
233
+ #
234
+ # @option extra_params [Timeframe] :timeframe
235
+ # @option extra_params [Array<Symbol>] :comply
236
+ # @option extra_params [String] :key In case you didn't define it globally, or want to use a different one here.
237
+ # @option extra_params [String, Numeric] <i>characteristic</i> Override pieces of data about an emitter.
238
+ #
239
+ # @return [Array] Something you could pass into +Carbon.query+.
138
240
  def as_impact_query(extra_params = {})
139
241
  registration = Registry.instance[self.class.name]
140
242
  params = registration.characteristics.inject({}) do |memo, (method_id, translation_options)|
@@ -155,34 +257,18 @@ module Carbon
155
257
  [ registration.emitter, params.merge(extra_params) ]
156
258
  end
157
259
 
158
- # Get an impact estimate from Brighter Planet CM1.
260
+ # Get an impact estimate from Brighter Planet CM1; high-level convenience method that requires a {Carbon::ClassMethods#emit_as} block.
159
261
  #
160
262
  # You get this when you +include Carbon+ in a class.
161
263
  #
162
- # The return value is a {http://rdoc.info/github/intridea/hashie/Hashie/Mash Hashie::Mash} because it's a simple way to access a deep response object.
264
+ # See {Carbon.query} for an explanation of the return value, a +Hashie::Mash+.
163
265
  #
164
- # Here's a map of what's included in a response:
165
- #
166
- # certification
167
- # characteristics.{}.description
168
- # characteristics.{}.object
169
- # compliance.[]
170
- # decisions.{}.description
171
- # decisions.{}.methodology
172
- # decisions.{}.object
173
- # emitter
174
- # equivalents.{}
175
- # errors.[]
176
- # methodology
177
- # scope
178
- # timeframe.endDate
179
- # timeframe.startDate
180
- #
181
- # @param [Hash] extra_params Anything that your +emit_as+ won't include.
266
+ # @param [Hash] extra_params Anything you want to override.
182
267
  #
183
268
  # @option extra_params [Timeframe] :timeframe
184
269
  # @option extra_params [Array<Symbol>] :comply
185
270
  # @option extra_params [String] :key In case you didn't define it globally, or want to use a different one here.
271
+ # @option extra_params [String, Numeric] <i>characteristic</i> Override pieces of data about an emitter.
186
272
  #
187
273
  # @return [Hashie::Mash]
188
274
  #
@@ -197,21 +283,9 @@ module Carbon
197
283
  # => "kilograms"
198
284
  # ?> my_impact.methodology
199
285
  # => "http://impact.brighterplanet.com/flights?[...]"
200
- #
201
- # @example How do I use a Hashie::Mash?
202
- # ?> mash['hello']
203
- # => "world"
204
- # ?> mash.hello
205
- # => "world"
206
- # ?> mash.keys
207
- # => ["hello"]
208
- #
209
- # @example Other examples of what's in the response
210
- # my_impact.carbon.object.value
211
- # my_impact.characteristics.airline.description
212
- # my_impact.equivalents.lightbulbs_for_a_week
213
286
  def impact(extra_params = {})
214
- future = Future.new(*as_impact_query(extra_params))
287
+ query_array = as_impact_query extra_params
288
+ future = Future.wrap query_array
215
289
  future.result
216
290
  end
217
291
  end
@@ -3,59 +3,42 @@ require 'net/http'
3
3
  require 'cache_method'
4
4
  require 'hashie/mash'
5
5
  require 'multi_json'
6
+ require 'em-http-request'
6
7
 
7
8
  module Carbon
8
9
  # @private
9
10
  class Future
10
11
  class << self
12
+ def wrap(query_array_or_o)
13
+ if query_array_or_o.is_a?(::Array)
14
+ new(*query_array_or_o)
15
+ else
16
+ new(*query_array_or_o.as_impact_query)
17
+ end
18
+ end
19
+
11
20
  def single(future)
12
21
  uri = ::URI.parse("#{Carbon::DOMAIN}/#{future.emitter.underscore.pluralize}.json")
13
22
  raw_result = ::Net::HTTP.post_form(uri, future.params)
14
- result = ::Hashie::Mash.new
15
- case raw_result
16
- when ::Net::HTTPSuccess
17
- result.status = raw_result.code.to_i
18
- result.success = true
19
- result.merge! ::MultiJson.decode(raw_result.body)
20
- else
21
- result.status = raw_result.code.to_i
22
- result.success = false
23
- result.errors = [raw_result.body]
24
- end
25
- result
23
+ future.finalize raw_result.code.to_i, raw_result.body
24
+ future
26
25
  end
27
26
 
28
27
  def multi(futures)
29
- uniq_pending_futures = futures.uniq.select do |future|
30
- future.pending?
31
- end
28
+ uniq_pending_futures = futures.uniq.select { |future| future.pending? }
32
29
  return futures if uniq_pending_futures.empty?
33
- require 'em-http-request'
30
+ pool_size = [Carbon::CONCURRENCY, uniq_pending_futures.length].min
34
31
  multi = ::EventMachine::MultiRequest.new
32
+ pool = (0..(pool_size-1)).map { ::EventMachine::HttpRequest.new(Carbon::DOMAIN) }
33
+ pool_idx = 0
35
34
  ::EventMachine.run do
36
35
  uniq_pending_futures.each do |future|
37
- multi.add future, ::EventMachine::HttpRequest.new(Carbon::DOMAIN).post(:path => "/#{future.emitter.underscore.pluralize}.json", :body => future.params)
36
+ multi.add future, pool[pool_idx].post(:path => "/#{future.emitter.underscore.pluralize}.json", :body => future.params)
37
+ pool_idx = (pool_idx + 1) % pool_size
38
38
  end
39
39
  multi.callback do
40
- multi.responses[:callback].each do |future, http|
41
- result = ::Hashie::Mash.new
42
- result.status = http.response_header.status
43
- if (200..299).include?(result.status)
44
- result.success = true
45
- result.merge! ::MultiJson.decode(http.response)
46
- else
47
- result.success = false
48
- result.errors = [http.response]
49
- end
50
- future.result = result
51
- end
52
- multi.responses[:errback].each do |future, http|
53
- result = ::Hashie::Mash.new
54
- result.status = http.response_header.status
55
- result.success = false
56
- result.errors = ['Timeout or other network error.']
57
- future.result = result
58
- end
40
+ multi.responses[:callback].each { |future, http| future.finalize http.response_header.status, http.response }
41
+ multi.responses[:errback].each { |future, http| future.finalize http.response_header.status }
59
42
  ::EventMachine.stop
60
43
  end
61
44
  end
@@ -86,16 +69,27 @@ module Carbon
86
69
  @result.nil? and !cache_method_cached?(:result)
87
70
  end
88
71
 
89
- def result=(result)
90
- @result = result
91
- self.result # force this to be cached
72
+ def finalize(code, body = nil)
73
+ memo = ::Hashie::Mash.new
74
+ memo.code = code
75
+ case code
76
+ when (200..299)
77
+ memo.success = true
78
+ memo.merge! ::MultiJson.decode(body)
79
+ else
80
+ memo.success = false
81
+ memo.errors = [body]
82
+ end
83
+ @result = memo
84
+ self.result # make sure it gets cached
92
85
  end
93
86
 
94
87
  def result
95
88
  if @result
96
89
  @result
97
90
  elsif not multi?
98
- @result = Future.single(self)
91
+ Future.single self
92
+ @result
99
93
  end
100
94
  end
101
95
  cache_method :result, 3_600 # one hour
@@ -23,6 +23,8 @@ module Carbon
23
23
 
24
24
  # Indicate that you will send in a piece of data about the emitter.
25
25
  #
26
+ # Called inside of {Carbon::ClassMethods#emit_as} blocks.
27
+ #
26
28
  # @param [Symbol] method_id What method to call to get the value in question.
27
29
  #
28
30
  # @option translation_options [Symbol] :as (name of the method) If your method name does not match the Brighter Planet characteristic name.
@@ -34,6 +36,22 @@ module Carbon
34
36
  #
35
37
  # @yield [] Pass a block for the common use case of calling a method on a object.
36
38
  #
39
+ # Things to note in the MyFlight example:
40
+ #
41
+ # * Sending +:origin+ to Brighter Planet *as* +:origin_airport+. Otherwise Brighter Planet won't recognize +:origin+.
42
+ # * Saying we're *keying* on one code or another. Otherwise Brighter Planet will first try against full names and possibly other columns.
43
+ # * Giving *blocks* to pull codes from +MyAircraft+ and +MyAirline+ objects. Otherwise you might get a querystring like +airline[iata_code]=#<MyAirline [...]>+
44
+ #
45
+ # @example The canonical MyFlight example
46
+ # emit_as 'Flight' do
47
+ # provide :segments_per_trip
48
+ # provide :trips
49
+ # provide :origin, :as => :origin_airport, :key => :iata_code
50
+ # provide :destination, :as => :destination_airport, :key => :iata_code
51
+ # provide(:airline, :key => :iata_code) { |f| f.airline.try(:iata_code) }
52
+ # provide(:aircraft, :key => :icao_code) { { |f| f.aircraft.try(:icao_code) }
53
+ # end
54
+ #
37
55
  # @example Your method is named one thing but should be sent +:as+ something else.
38
56
  # provide :my_distance, :as => :distance
39
57
  #
@@ -1,3 +1,3 @@
1
1
  module Carbon
2
- VERSION = "2.0.3"
2
+ VERSION = "2.1.0"
3
3
  end
@@ -34,7 +34,6 @@ class MyNissanAltima
34
34
  provide :model
35
35
  provide :model_year, :as => :year
36
36
  provide :fuel_type, :as => :automobile_fuel, :key => :code
37
-
38
37
  provide(:nil_make) { |my_nissan_altima| my_nissan_altima.nil_make.try(:blam!) }
39
38
  provide :nil_model
40
39
  end
@@ -46,114 +45,81 @@ describe Carbon do
46
45
  end
47
46
 
48
47
  describe :query do
49
- it "calculates flight impact" do
50
- result = Carbon.query('Flight', :origin_airport => 'LAX', :destination_airport => 'SFO', :segments_per_trip => 1, :trips => 1)
51
- result.decisions.carbon.object.value.must_be_close_to 200, 50
52
- end
53
- it "gets back characteristics" do
54
- result = Carbon.query('Flight', :origin_airport => 'LAX', :destination_airport => 'SFO', :segments_per_trip => 1, :trips => 1)
55
- result.characteristics.origin_airport.description.must_match %r{lax}i
56
- end
57
- it "tells you if the query is successful" do
58
- result = Carbon.query('Flight')
59
- result.success.must_equal true
60
- end
61
- it "is gentle about errors" do
62
- result = Carbon.query('Monkey')
63
- result.success.must_equal false
64
- end
65
- it "sends timeframe properly" do
66
- result = Carbon.query('Flight', :timeframe => Timeframe.new(:year => 2009))
67
- result.timeframe.startDate.must_equal '2009-01-01'
68
- result.timeframe.endDate.must_equal '2010-01-01'
69
- end
70
- it "sends key properly" do
71
- with_web_mock do
72
- WebMock.stub_request(:post, 'http://impact.brighterplanet.com/flights.json').with(:key => 'carbon_test').to_return(:status => 500, :body => 'Good job')
73
- result = Carbon.query('Flight')
74
- result.errors.first.must_equal 'Good job'
48
+ describe '(one at a time)' do
49
+ it "calculates flight impact" do
50
+ result = Carbon.query('Flight', :origin_airport => 'LAX', :destination_airport => 'SFO', :segments_per_trip => 1, :trips => 1)
51
+ result.decisions.carbon.object.value.must_be_close_to 200, 50
75
52
  end
76
- end
77
- end
78
-
79
- describe :multi do
80
- before do
81
- @queries = []
82
- @queries << ['Flight', {:origin_airport => 'LAX', :destination_airport => 'SFO', :segments_per_trip => 1, :trips => 1}]
83
- @queries << ['Flight', {:origin_airport => 'MSN', :destination_airport => 'ORD', :segments_per_trip => 1, :trips => 1}]
84
- @queries << ['Flight', {:origin_airport => 'IAH', :destination_airport => 'DEN', :segments_per_trip => 1, :trips => 1}]
85
- @queries << ['RailTrip', {:distance => 25}]
86
- @queries << ['RailTrip', {:rail_class => 'commuter'}]
87
- @queries << ['RailTrip', {:rail_traction => 'electric'}]
88
- @queries << ['AutomobileTrip', {:make => 'Nissan', :model => 'Altima'}]
89
- @queries << ['AutomobileTrip', {:make => 'Toyota', :model => 'Prius'}]
90
- @queries << ['AutomobileTrip', {:make => 'Ford', :model => 'Taurus'}]
91
- @queries << ['Residence', {:urbanity => 'City'}]
92
- @queries << ['Residence', {:zip_code => '53703'}]
93
- @queries << ['Residence', {:bathrooms => 4}]
94
- @queries << ['Monkey', {:bananas => '1'}]
95
- @queries << ['Monkey', {:bananas => '2'}]
96
- @queries << ['Monkey', {:bananas => '3'}]
97
- @queries = @queries.sort_by { rand }
98
- end
99
- it "doesn't hang up on 0 queries" do
100
- Timeout.timeout(0.5) { Carbon.multi([]) }.must_equal []
101
- end
102
- it "runs multiple queries at once" do
103
- reference_results = @queries.map do |query|
104
- Carbon.query(*query)
105
- end
106
- flush_cache! # important!
107
- multi_results = Carbon.multi(@queries)
108
- error_count = 0
109
- multi_results.each do |result|
110
- if result.success
111
- result.decisions.carbon.object.value.must_be :>, 0
112
- result.decisions.carbon.object.value.must_be :<, 10_000
113
- else
114
- error_count += 1
115
- end
53
+ it "can be used on an object that response to #as_impact_query" do
54
+ Carbon.query(MyNissanAltima.new(2006)).decisions.must_equal MyNissanAltima.new(2006).impact.decisions
116
55
  end
117
- error_count.must_equal 3
118
- reference_results.each_with_index do |reference_result, idx|
119
- if reference_result.success
120
- multi_results[idx].decisions.must_equal reference_result.decisions
121
- else
122
- multi_results[idx].must_equal reference_result
123
- end
56
+ it "gets back characteristics" do
57
+ result = Carbon.query('Flight', :origin_airport => 'LAX', :destination_airport => 'SFO', :segments_per_trip => 1, :trips => 1)
58
+ result.characteristics.origin_airport.description.must_match %r{lax}i
124
59
  end
125
- end
126
- it "is faster than just calling .query over and over" do
127
- # warm up the cache on the other end
128
- @queries.each { |query| Carbon.query(*query) }
129
- flush_cache! # important!
130
- single_threaded_time = ::Benchmark.realtime do
131
- @queries.each { |query| Carbon.query(*query) }
60
+ it "tells you if the query is successful" do
61
+ result = Carbon.query('Flight')
62
+ result.success.must_equal true
132
63
  end
133
- flush_cache! # important!
134
- multi_threaded_time = ::Benchmark.realtime do
135
- Carbon.multi(@queries)
64
+ it "is gentle about errors" do
65
+ result = Carbon.query('Monkey')
66
+ result.success.must_equal false
136
67
  end
137
- cached_single_threaded_time = ::Benchmark.realtime do
138
- @queries.each { |query| Carbon.query(*query) }
68
+ it "sends timeframe properly" do
69
+ result = Carbon.query('Flight', :timeframe => Timeframe.new(:year => 2009))
70
+ result.timeframe.startDate.must_equal '2009-01-01'
71
+ result.timeframe.endDate.must_equal '2010-01-01'
139
72
  end
140
- cached_multi_threaded_time = ::Benchmark.realtime do
141
- Carbon.multi(@queries)
73
+ it "sends key properly" do
74
+ with_web_mock do
75
+ WebMock.stub_request(:post, 'http://impact.brighterplanet.com/flights.json').with(:key => 'carbon_test').to_return(:status => 500, :body => 'Good job')
76
+ result = Carbon.query('Flight')
77
+ result.errors.first.must_equal 'Good job'
78
+ end
142
79
  end
143
- multi_threaded_time.must_be :<, single_threaded_time
144
- cached_single_threaded_time.must_be :<, multi_threaded_time
145
- cached_multi_threaded_time.must_be :<, multi_threaded_time
146
- $stderr.puts " Multi-threaded was #{((single_threaded_time - multi_threaded_time) / single_threaded_time * 100).round}% faster than single-threaded"
147
- $stderr.puts " Cached single-threaded was #{((multi_threaded_time - cached_single_threaded_time) / multi_threaded_time * 100).round}% faster than uncached multi-threaded"
148
- $stderr.puts " Cached multi-threaded was #{((multi_threaded_time - cached_multi_threaded_time) / multi_threaded_time * 100).round}% faster than uncached multi-threaded"
149
80
  end
150
- it "safely uniq's and caches queries" do
151
- reference_results = @queries.map do |query|
152
- Carbon.query(*query)
81
+ describe '(in parallel)' do
82
+ before do
83
+ @queries = []
84
+ @queries << ['Flight', {:origin_airport => 'LAX', :destination_airport => 'SFO', :segments_per_trip => 1, :trips => 1}]
85
+ @queries << ['Flight', {:origin_airport => 'MSN', :destination_airport => 'ORD', :segments_per_trip => 1, :trips => 1}]
86
+ @queries << ['Flight', {:origin_airport => 'IAH', :destination_airport => 'DEN', :segments_per_trip => 1, :trips => 1}]
87
+ @queries << ['RailTrip', {:distance => 25}]
88
+ @queries << ['RailTrip', {:rail_class => 'commuter'}]
89
+ @queries << ['RailTrip', {:rail_traction => 'electric'}]
90
+ @queries << ['AutomobileTrip', {:make => 'Nissan', :model => 'Altima'}]
91
+ @queries << ['AutomobileTrip', {:make => 'Toyota', :model => 'Prius'}]
92
+ @queries << ['AutomobileTrip', {:make => 'Ford', :model => 'Taurus'}]
93
+ @queries << ['Residence', {:urbanity => 'City'}]
94
+ @queries << ['Residence', {:zip_code => '53703'}]
95
+ @queries << ['Residence', {:bathrooms => 4}]
96
+ @queries << ['Monkey', {:bananas => '1'}]
97
+ @queries << ['Monkey', {:bananas => '2'}]
98
+ @queries << ['Monkey', {:bananas => '3'}]
99
+ @queries = @queries.sort_by { rand }
100
+ end
101
+ it "doesn't hang up on 0 queries" do
102
+ Timeout.timeout(0.5) { Carbon.query([]) }.must_equal []
103
+ end
104
+ it "can be used on objects that respond to #as_impact_query" do
105
+ Carbon.query([MyNissanAltima.new(2001), MyNissanAltima.new(2006)]).map(&:decisions).must_equal Carbon.query([MyNissanAltima.new(2001).as_impact_query, MyNissanAltima.new(2006).as_impact_query]).map(&:decisions)
153
106
  end
154
- flush_cache! # important!
155
- 3.times do
156
- multi_results = Carbon.multi(@queries)
107
+ it "runs multiple queries at once" do
108
+ reference_results = @queries.map do |query|
109
+ Carbon.query(*query)
110
+ end
111
+ flush_cache! # important!
112
+ multi_results = Carbon.query(@queries)
113
+ error_count = 0
114
+ multi_results.each do |result|
115
+ if result.success
116
+ result.decisions.carbon.object.value.must_be :>, 0
117
+ result.decisions.carbon.object.value.must_be :<, 10_000
118
+ else
119
+ error_count += 1
120
+ end
121
+ end
122
+ error_count.must_equal 3
157
123
  reference_results.each_with_index do |reference_result, idx|
158
124
  if reference_result.success
159
125
  multi_results[idx].decisions.must_equal reference_result.decisions
@@ -162,9 +128,92 @@ describe Carbon do
162
128
  end
163
129
  end
164
130
  end
131
+ it "is faster than single threaded" do
132
+ # warm up the cache on the other end
133
+ @queries.each { |query| Carbon.query(*query) }
134
+ flush_cache! # important!
135
+ single_threaded_time = ::Benchmark.realtime do
136
+ @queries.each { |query| Carbon.query(*query) }
137
+ end
138
+ flush_cache! # important!
139
+ multi_threaded_time = ::Benchmark.realtime do
140
+ Carbon.query(@queries)
141
+ end
142
+ cached_single_threaded_time = ::Benchmark.realtime do
143
+ @queries.each { |query| Carbon.query(*query) }
144
+ end
145
+ cached_multi_threaded_time = ::Benchmark.realtime do
146
+ Carbon.query(@queries)
147
+ end
148
+ multi_threaded_time.must_be :<, single_threaded_time
149
+ cached_single_threaded_time.must_be :<, multi_threaded_time
150
+ cached_multi_threaded_time.must_be :<, multi_threaded_time
151
+ $stderr.puts " Multi-threaded was #{((single_threaded_time - multi_threaded_time) / single_threaded_time * 100).round}% faster than single-threaded"
152
+ $stderr.puts " Cached single-threaded was #{((multi_threaded_time - cached_single_threaded_time) / multi_threaded_time * 100).round}% faster than uncached multi-threaded"
153
+ $stderr.puts " Cached multi-threaded was #{((multi_threaded_time - cached_multi_threaded_time) / multi_threaded_time * 100).round}% faster than uncached multi-threaded"
154
+ end
155
+ it "safely uniq's and caches queries" do
156
+ reference_results = @queries.map do |query|
157
+ Carbon.query(*query)
158
+ end
159
+ flush_cache! # important!
160
+ 3.times do
161
+ multi_results = Carbon.query(@queries)
162
+ reference_results.each_with_index do |reference_result, idx|
163
+ if reference_result.success
164
+ multi_results[idx].decisions.must_equal reference_result.decisions
165
+ else
166
+ multi_results[idx].must_equal reference_result
167
+ end
168
+ end
169
+ end
170
+ end
165
171
  end
166
172
  end
167
-
173
+
174
+ describe :method_signature do
175
+ it "recognizes emitter_param" do
176
+ Carbon.method_signature('Flight').must_equal :query_array
177
+ Carbon.method_signature('Flight', :origin_airport => 'LAX').must_equal :query_array
178
+ Carbon.method_signature(:flight).must_equal :query_array
179
+ Carbon.method_signature(:flight, :origin_airport => 'LAX').must_equal :query_array
180
+ end
181
+ it "recognizes o" do
182
+ Carbon.method_signature(MyNissanAltima.new(2006)).must_equal :o
183
+ end
184
+ it "recognizes os" do
185
+ Carbon.method_signature([MyNissanAltima.new(2001)]).must_equal :os
186
+ Carbon.method_signature([['Flight']]).must_equal :os
187
+ Carbon.method_signature([['Flight', {:origin_airport => 'LAX'}]]).must_equal :os
188
+ Carbon.method_signature([['Flight'], ['Flight']]).must_equal :os
189
+ Carbon.method_signature([['Flight', {:origin_airport => 'LAX'}], ['Flight', {:origin_airport => 'LAX'}]]).must_equal :os
190
+ [MyNissanAltima.new(2006), ['Flight'], ['Flight', {:origin_airport => 'LAX'}]].permutation.each do |p|
191
+ Carbon.method_signature(p).must_equal :os
192
+ end
193
+ end
194
+ it "does not want splats for concurrent queries" do
195
+ Carbon.method_signature(['Flight'], ['Flight']).must_be_nil
196
+ Carbon.method_signature(MyNissanAltima.new(2001), MyNissanAltima.new(2001)).must_be_nil
197
+ [MyNissanAltima.new(2006), ['Flight'], ['Flight', {:origin_airport => 'LAX'}]].permutation.each do |p|
198
+ Carbon.method_signature(*p).must_be_nil
199
+ end
200
+ end
201
+ it "does not like weirdness" do
202
+ Carbon.method_signature('Flight', 'Flight').must_be_nil
203
+ Carbon.method_signature('Flight', ['Flight']).must_be_nil
204
+ Carbon.method_signature(['Flight'], 'Flight').must_be_nil
205
+ Carbon.method_signature(['Flight', 'Flight']).must_be_nil
206
+ Carbon.method_signature(['Flight', ['Flight']]).must_be_nil
207
+ Carbon.method_signature([['Flight'], 'Flight']).must_be_nil
208
+ Carbon.method_signature(MyNissanAltima.new(2001), [MyNissanAltima.new(2001)]).must_be_nil
209
+ Carbon.method_signature([MyNissanAltima.new(2001)], MyNissanAltima.new(2001)).must_be_nil
210
+ Carbon.method_signature([MyNissanAltima.new(2001)], [MyNissanAltima.new(2001)]).must_be_nil
211
+ Carbon.method_signature([MyNissanAltima.new(2001), [MyNissanAltima.new(2001)]]).must_be_nil
212
+ Carbon.method_signature([[MyNissanAltima.new(2001)], MyNissanAltima.new(2001)]).must_be_nil
213
+ Carbon.method_signature([[MyNissanAltima.new(2001)], [MyNissanAltima.new(2001)]]).must_be_nil
214
+ end
215
+ end
216
+
168
217
  describe "mixin" do
169
218
  describe :emit_as do
170
219
  it "overwrites old emit_as blocks" do
@@ -176,7 +225,7 @@ describe Carbon do
176
225
  end
177
226
  end
178
227
  describe '#as_impact_query' do
179
- it "sets up an query to be run by Carbon.multi" do
228
+ it "sets up an query to be run by Carbon.query" do
180
229
  a = MyNissanAltima.new(2006)
181
230
  a.as_impact_query.must_equal ["Automobile", {:make=>"Nissan", :model=>"Altima", :year=>2006, "automobile_fuel[code]"=>"R"}]
182
231
  end