carbon 2.0.3 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +2 -0
- data/CHANGELOG +11 -0
- data/Gemfile +1 -0
- data/README.markdown +2 -2
- data/Rakefile +29 -0
- data/{developer_notes → developer}/MULTI.markdown +0 -0
- data/{developer_notes → developer}/REDUCE_HTTP_CONNECTIONS.markdown +0 -0
- data/developer/avro_helper.rb +81 -0
- data/developer/cm1_avro.rb +955 -0
- data/lib/carbon.rb +146 -72
- data/lib/carbon/future.rb +34 -40
- data/lib/carbon/registry.rb +18 -0
- data/lib/carbon/version.rb +1 -1
- data/test/carbon_test.rb +151 -102
- metadata +23 -20
data/lib/carbon.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
37
|
-
#
|
38
|
-
#
|
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
|
-
# @
|
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
|
-
# @
|
43
|
-
#
|
44
|
-
|
45
|
-
|
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
|
-
#
|
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
|
-
# @
|
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
|
-
# @
|
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
|
-
# @
|
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
|
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
|
-
# ['
|
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.
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
#
|
264
|
+
# See {Carbon.query} for an explanation of the return value, a +Hashie::Mash+.
|
163
265
|
#
|
164
|
-
#
|
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
|
-
|
287
|
+
query_array = as_impact_query extra_params
|
288
|
+
future = Future.wrap query_array
|
215
289
|
future.result
|
216
290
|
end
|
217
291
|
end
|
data/lib/carbon/future.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
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
|
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
|
-
|
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,
|
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
|
41
|
-
|
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
|
90
|
-
|
91
|
-
|
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
|
-
|
91
|
+
Future.single self
|
92
|
+
@result
|
99
93
|
end
|
100
94
|
end
|
101
95
|
cache_method :result, 3_600 # one hour
|
data/lib/carbon/registry.rb
CHANGED
@@ -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
|
#
|
data/lib/carbon/version.rb
CHANGED
data/test/carbon_test.rb
CHANGED
@@ -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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
64
|
+
it "is gentle about errors" do
|
65
|
+
result = Carbon.query('Monkey')
|
66
|
+
result.success.must_equal false
|
136
67
|
end
|
137
|
-
|
138
|
-
|
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
|
-
|
141
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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.
|
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
|