carbon 2.0.3 → 2.1.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.
@@ -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