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