carbon 0.1.5 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.5
1
+ 0.1.6
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{carbon}
8
- s.version = "0.1.5"
8
+ s.version = "0.1.6"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Derek Kastner", "Seamus Abshere"]
@@ -25,6 +25,8 @@ Gem::Specification.new do |s|
25
25
  "lib/carbon.rb",
26
26
  "lib/carbon/base.rb",
27
27
  "lib/carbon/emission_estimate.rb",
28
+ "lib/carbon/emission_estimate/request.rb",
29
+ "lib/carbon/emission_estimate/response.rb",
28
30
  "spec/lib/carbon_spec.rb",
29
31
  "spec/spec_helper.rb",
30
32
  "spec/specwatchr"
@@ -46,8 +46,6 @@ module Carbon
46
46
  REALTIME_URL = 'http://carbon.brighterplanet.com'
47
47
  ASYNC_URL = 'https://queue.amazonaws.com/121562143717/cm1_production_incoming'
48
48
 
49
- class BlankCallback < ArgumentError # :nodoc:
50
- end
51
49
  class RealtimeEstimateFailed < RuntimeError # :nodoc:
52
50
  end
53
51
  class QueueingFailed < RuntimeError # :nodoc:
@@ -58,11 +56,6 @@ module Carbon
58
56
  # The api key obtained from http://keys.brighterplanet.com
59
57
  mattr_accessor :key
60
58
 
61
- def self.prepare_options(options) # :nodoc:
62
- options[:key] ||= key
63
- options[:mode] ||= options.has_key?(:callback) ? :async : :realtime
64
- end
65
-
66
59
  # You will probably never access this module directly. Instead, you'll use it through the DSL.
67
60
  #
68
61
  # It's mixed into any class that includes <tt>Carbon</tt>.
@@ -82,123 +75,6 @@ module Carbon
82
75
  # japanese-style preferred
83
76
  alias :emits_as :emit_as
84
77
  end
85
-
86
- # Used internally, but you can look if you want.
87
- #
88
- # Returns the URL to which emissions estimate queries will be POSTed.
89
- #
90
- # For example:
91
- # > my_car._carbon_request_url
92
- # => 'http://carbon.brighterplanet.com/automobiles.json'
93
- def _carbon_request_url(options = {})
94
- ::Carbon.prepare_options options
95
- send "_#{options[:mode]}_carbon_request_url"
96
- end
97
-
98
- def _realtime_carbon_request_url # :nodoc:
99
- "#{::Carbon::REALTIME_URL}/#{self.class.carbon_base.emitter_common_name.pluralize}.json"
100
- end
101
-
102
- def _async_carbon_request_url # :nodoc:
103
- ::Carbon::ASYNC_URL
104
- end
105
-
106
- # Used internally, but you can look if you want.
107
- #
108
- # Returns the request body that will be posted.
109
- #
110
- # For example:
111
- # > my_car._carbon_request_body
112
- # => 'fuel_efficiency=41&model=Ford+Taurus'
113
- def _carbon_request_body(options = {})
114
- ::Carbon.prepare_options options
115
- send "_#{options[:mode]}_carbon_request_body", options
116
- end
117
-
118
- def _async_carbon_request_body(options) # :nodoc:
119
- params = _carbon_request_params options
120
- params[:emitter] = self.class.carbon_base.emitter_common_name
121
- raise ::Carbon::BlankCallback unless options[:callback].present?
122
- params[:callback] = options[:callback]
123
- params[:callback_content_type] = options[:callback_content_type] || 'application/json'
124
- {
125
- :Action => 'SendMessage',
126
- :Version => '2009-02-01',
127
- :MessageBody => params.to_query
128
- }.to_query
129
- end
130
-
131
- def _realtime_carbon_request_body(options) # :nodoc:
132
- _carbon_request_params(options).to_query
133
- end
134
-
135
- # Used internally, but you can look if you want.
136
- #
137
- # Returns the params hash that will be send to the emission estimate server.
138
- def _carbon_request_params(options)
139
- ::Carbon.prepare_options options
140
- params = self.class.carbon_base.translation_table.inject(Hash.new) do |memo, translation|
141
- characteristic, as = translation
142
- current_value = send as
143
- if current_value.present?
144
- if characteristic.is_a? Array # [:mixer, :size]
145
- memo[characteristic[0]] ||= Hash.new # { :mixer => Hash.new }
146
- memo[characteristic[0]][characteristic[1]] = current_value # { :mixer => { :size => 'foo' }}
147
- else # :oven_count
148
- memo[characteristic] = current_value # { :oven_count => 'bar' }
149
- end
150
- end
151
- memo
152
- end
153
- params.merge! options.slice(:timeframe, :key)
154
- params
155
- end
156
-
157
- def _realtime_emission(options = {}) # :nodoc:
158
- attempts = 0
159
- begin
160
- response = _carbon_response options
161
- raise ::Carbon::RateLimited if response.status_code == 403 and response.body =~ /Rate Limit/i
162
- rescue ::Carbon::RateLimited
163
- if attempts < 4
164
- attempts += 1
165
- sleep 0.2 * attempts
166
- retry
167
- else
168
- raise $!, "Rate limited #{attempts} time(s) in a row"
169
- end
170
- end
171
- raise ::Carbon::RealtimeEstimateFailed unless response.success?
172
- ::Carbon::EmissionEstimate.new ::ActiveSupport::JSON.decode(response.body)
173
- end
174
-
175
- def _async_emission(options = {}) # :nodoc:
176
- response = _carbon_response options
177
- raise ::Carbon::QueueingFailed unless response.success?
178
- true
179
- end
180
-
181
- # Used internally, but you can look if you want.
182
- #
183
- # Runs the query and returns the raw response body, which will be in JSON.
184
- #
185
- # For example:
186
- # > my_car._carbon_response.body
187
- # => "{ 'emission' => 410.29, 'emission_units' => 'kilograms', [...] }"
188
- def _carbon_response(options = {})
189
- @last_carbon_request = ::REST::Request.new :post, ::URI.parse(_carbon_request_url(options)), _carbon_request_body(options), {'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8'}
190
- @last_carbon_response = @last_carbon_request.perform
191
- end
192
-
193
- # Returns an object representing the last emission estimate request.
194
- def last_carbon_request
195
- @last_carbon_request
196
- end
197
-
198
- # Returns an object representing the last emission estimate response.
199
- def last_carbon_response
200
- @last_carbon_response
201
- end
202
78
 
203
79
  # Returns an emission estimate.
204
80
  #
@@ -219,8 +95,10 @@ module Carbon
219
95
  # * <tt>:callback</tt> (optional) where to POST the result when it's been calculated. You need a server waiting for it!
220
96
  # * <tt>:callback_content_type</tt> (optional if <tt>:callback</tt> is specified, ignored otherwise) pass a MIME type like 'text/yaml' so we know how to format the result when we send it to your waiting server. Defaults to 'application/json'.
221
97
  # * <tt>:key</tt> (optional, overrides general <tt>Carbon</tt>.<tt>key</tt> setting just for this query) If you want to use different API keys for different queries.
222
- def emission(options = {})
223
- ::Carbon.prepare_options options
224
- send "_#{options[:mode]}_emission", options
98
+ def emission_estimate(options = {})
99
+ @emission_estimate ||= ::Carbon::EmissionEstimate.new self
100
+ @emission_estimate.take_options options
101
+ @emission_estimate
225
102
  end
103
+ alias :emission :emission_estimate
226
104
  end
@@ -1,3 +1,6 @@
1
+ require 'carbon/emission_estimate/response'
2
+ require 'carbon/emission_estimate/request'
3
+
1
4
  module Carbon
2
5
  # Let's start off by saying that <tt>EmissionEstimate</tt> objects quack like numbers.
3
6
  #
@@ -7,52 +10,86 @@ module Carbon
7
10
  #
8
11
  # Note: <b>you need to take care of storing emission estimates to local variables!</b> The gem doesn't cache these for you. Every time you call <tt>emission</tt> it will send another query to the server!
9
12
  class EmissionEstimate
13
+ def initialize(emitter)
14
+ @emitter = emitter
15
+ end
16
+
17
+ def take_options(options = {})
18
+ options.each do |k, v|
19
+ instance_variable_set "@#{k}", v
20
+ end
21
+ end
22
+
23
+ # I can be compared directly to a number.
24
+ def ==(other)
25
+ case other
26
+ when Numeric
27
+ other == response.number
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ # You can ask an EmissionEstimate object for any of the response data provided.
34
+ # This is useful for characteristics that are unique to an emitter.
35
+ #
36
+ # For example:
37
+ # > my_car.emission.model
38
+ # => 'Ford Taurus'
39
+ def method_missing(method_id, *args, &blk)
40
+ if !block_given? and args.empty? and response.data.has_key? method_id.to_s
41
+ response.data[method_id.to_s]
42
+ else
43
+ response.number.send method_id, *args, &blk
44
+ end
45
+ end
46
+
47
+ attr_writer :callback_content_type
48
+ attr_writer :key
49
+
50
+ attr_accessor :callback
51
+ attr_accessor :timeframe
52
+
10
53
  attr_reader :data
11
- def initialize(data)
12
- @data = data
13
- @number = data['emission'].to_f.freeze
54
+ attr_reader :emitter
55
+ def request
56
+ @request ||= Request.new self
57
+ end
58
+ # Here's where caching takes place.
59
+ def response
60
+ current_params = request.params
61
+ @response ||= {}
62
+ return @response[current_params] if @response.has_key? current_params
63
+ @response[current_params] = Response.new self
64
+ end
65
+ def mode
66
+ callback ? :async : :realtime
67
+ end
68
+ def callback_content_type
69
+ @callback_content_type || 'application/json'
14
70
  end
15
- def ==(other) # :nodoc:
16
- other == @number
71
+ def key
72
+ @key || ::Carbon.key
17
73
  end
18
74
  # Another way to access the emission value.
19
75
  # Useful if you don't like treating <tt>EmissionEstimate</tt> objects like <tt>Numeric</tt> objects (even though they do quack like numbers...)
20
76
  def emission_value
21
- @number
77
+ response.number
22
78
  end
23
79
  # The units of the emission.
24
80
  def emission_units
25
- data['emission_units']
26
- end
27
- # The Timeframe the emission estimate covers.
28
- # > my_car.emission.timeframe.to_param
29
- # => '2009-01-01/2010-01-01'
30
- def timeframe
31
- Timeframe.interval data['timeframe']
81
+ response.data['emission_units']
32
82
  end
33
83
  # Errors (and warnings) as reported in the response.
34
84
  # Note: may contain HTML tags like KBD or A
35
85
  def errors
36
- data['errors']
86
+ response.data['errors']
37
87
  end
38
88
  # The URL of the methodology report indicating how this estimate was calculated.
39
89
  # > my_car.emission.methodology
40
90
  # => 'http://carbon.brighterplanet.com/automobiles.html?[...]'
41
91
  def methodology
42
- data['methodology']
43
- end
44
- # You can ask an EmissionEstimate object for any of the response data provided.
45
- # This is useful for characteristics that are unique to an emitter.
46
- #
47
- # For example:
48
- # > my_car.emission.model
49
- # => 'Ford Taurus'
50
- def method_missing(method_id, *args, &blk)
51
- if !block_given? and args.empty? and data.has_key? method_id.to_s
52
- data[method_id.to_s]
53
- else
54
- @number.send method_id, *args, &blk
55
- end
92
+ response.data['methodology']
56
93
  end
57
94
  end
58
95
  end
@@ -0,0 +1,57 @@
1
+ module Carbon
2
+ class EmissionEstimate
3
+ class Request
4
+ attr_reader :parent
5
+ def initialize(parent)
6
+ @parent = parent
7
+ end
8
+ def body
9
+ send "#{parent.mode}_body"
10
+ end
11
+ def async_body # :nodoc:
12
+ params = params
13
+ params[:emitter] = parent.emitter.class.carbon_base.emitter_common_name
14
+ params[:callback] = parent.callback
15
+ params[:callback_content_type] = parent.callback_content_type
16
+ {
17
+ :Action => 'SendMessage',
18
+ :Version => '2009-02-01',
19
+ :MessageBody => params.to_query
20
+ }.to_query
21
+ end
22
+ def realtime_body # :nodoc:
23
+ params.to_query
24
+ end
25
+ # Used internally, but you can look if you want.
26
+ #
27
+ # Returns the params hash that will be send to the emission estimate server.
28
+ def params
29
+ params = parent.emitter.class.carbon_base.translation_table.inject({}) do |memo, translation|
30
+ characteristic, as = translation
31
+ current_value = parent.emitter.send as
32
+ if current_value.present?
33
+ if characteristic.is_a? Array # [:mixer, :size]
34
+ memo[characteristic[0]] ||= {} # { :mixer => Hash.new }
35
+ memo[characteristic[0]][characteristic[1]] = current_value # { :mixer => { :size => 'foo' }}
36
+ else # :oven_count
37
+ memo[characteristic] = current_value # { :oven_count => 'bar' }
38
+ end
39
+ end
40
+ memo
41
+ end
42
+ params[:timeframe] = parent.timeframe
43
+ params[:key] = parent.key
44
+ params
45
+ end
46
+ def realtime_url # :nodoc:
47
+ "#{::Carbon::REALTIME_URL}/#{parent.emitter.class.carbon_base.emitter_common_name.pluralize}.json"
48
+ end
49
+ def async_url # :nodoc:
50
+ ::Carbon::ASYNC_URL
51
+ end
52
+ def url
53
+ send "#{parent.mode}_url"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ module Carbon
2
+ class EmissionEstimate
3
+ class Response
4
+ attr_reader :parent
5
+ attr_reader :data
6
+ attr_reader :number
7
+ attr_reader :raw_request
8
+ attr_reader :raw_response
9
+ def initialize(parent)
10
+ @parent = parent
11
+ send "load_#{parent.mode}_data"
12
+ end
13
+ def load_realtime_data # :nodoc:
14
+ attempts = 0
15
+ begin
16
+ response = perform
17
+ raise ::Carbon::RateLimited if response.status_code == 403 and response.body =~ /Rate Limit/i
18
+ rescue ::Carbon::RateLimited
19
+ if attempts < 4
20
+ attempts += 1
21
+ sleep 0.2 * attempts
22
+ retry
23
+ else
24
+ raise $!, "Rate limited #{attempts} time(s) in a row"
25
+ end
26
+ end
27
+ raise ::Carbon::RealtimeEstimateFailed unless response.success?
28
+ @data = ::ActiveSupport::JSON.decode response.body
29
+ @number = data['emission'].to_f.freeze
30
+ end
31
+ def load_async_data # :nodoc:
32
+ response = perform
33
+ raise ::Carbon::QueueingFailed unless response.success?
34
+ @data = {}
35
+ @number = nil
36
+ end
37
+ def perform # :nodoc:
38
+ @raw_request = ::REST::Request.new :post, ::URI.parse(parent.request.url), parent.request.body, {'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8'}
39
+ @raw_response = @raw_request.perform
40
+ end
41
+ end
42
+ end
43
+ end
@@ -33,53 +33,90 @@ describe Carbon do
33
33
  c.model = 'Acura'
34
34
  c.model_year = 2003
35
35
  c.fuel_economy = 32
36
- e = c.emission
37
- e.should == 134.599
38
- e.emission_units.should == 'kilograms'
36
+ c.emission.should == 134.599
37
+ c.emission.emission_units.should == 'kilograms'
38
+ end
39
+
40
+ describe 'caching' do
41
+ it "should keep around estimates if the parameters don't change" do
42
+ c = RentalCar.new
43
+ c.model = 'Acura'
44
+ c.model_year = 2003
45
+ c.fuel_economy = 32
46
+ c.emission.should == 134.599
47
+ first_raw_request = c.emission.response.raw_request
48
+ c.emission.should == 134.599
49
+ c.emission.response.raw_request.object_id.should == first_raw_request.object_id
50
+ end
51
+
52
+ it "should recalculate if parameters change" do
53
+ c = RentalCar.new
54
+ c.model = 'Acura'
55
+ c.model_year = 2003
56
+ c.fuel_economy = 32
57
+ c.emission.should == 134.599
58
+ first_raw_request = c.emission.response.raw_request
59
+ c.model = 'Honda'
60
+ c.emission.should == 134.599
61
+ c.emission.response.raw_request.object_id.should_not == first_raw_request.object_id
62
+ end
39
63
  end
40
64
 
41
65
  describe 'synchronous (realtime) requests' do
42
66
  it 'should handle complex attributes like mixer[size]' do
43
67
  d = DonutFactory.new
44
68
  d.mixer_size = 20
45
- d._carbon_request_body.should =~ /mixer\[size\]=20/
69
+ d.emission.request.body.should =~ /mixer\[size\]=20/
46
70
  end
47
71
 
48
72
  it 'should not send attributes that are blank' do
49
73
  d = DonutFactory.new
50
74
  d.mixer_size = 20
51
- d._carbon_request_body.should_not =~ /oven_count/
75
+ d.emission.request.body.should_not =~ /oven_count/
52
76
  end
53
77
 
54
78
  it 'should send the key' do
55
79
  d = DonutFactory.new
56
- d._carbon_request_body.should =~ /key=valid/
80
+ d.emission.request.body.should =~ /key=valid/
57
81
  end
58
82
 
59
83
  it 'should override defaults' do
60
84
  d = DonutFactory.new
61
- d.emission(:key => 'ADifferentOne')
62
- d.last_carbon_request.body.should =~ /key=ADifferentOne/
85
+ key = 'ADifferentOne'
86
+ d.emission.key.should == 'valid'
87
+ d.emission.key = key
88
+ d.emission.key.should == key
63
89
  end
64
90
 
65
91
  it 'should accept timeframes' do
66
92
  c = RentalCar.new
67
- c.emission :timeframe => Timeframe.new(:year => 2009)
68
- c.last_carbon_request.body.should =~ /timeframe=2009-01-01%2F2010-01-01/
93
+ t = Timeframe.new(:year => 2009)
94
+ c.emission.timeframe = t
95
+ c.emission.timeframe.should == t
96
+ end
97
+
98
+ it 'should accept timeframes inline' do
99
+ c = RentalCar.new
100
+ t = Timeframe.new(:year => 2009)
101
+ c.emission(:timeframe => t)
102
+ c.emission.timeframe.should == t
69
103
  end
70
104
 
71
105
  it 'should not generate post bodies with lots of empty params' do
72
106
  c = RentalCar.new
73
107
  c.emission :timeframe => Timeframe.new(:year => 2009)
74
- c.last_carbon_request.body.should_not include('&&')
108
+ c.emission.request.body.should_not include('&&')
75
109
  end
76
110
  end
77
111
 
78
112
  describe 'asynchronous (queued) requests' do
79
113
  it 'should post a message to SQS' do
80
114
  c = RentalCar.new
81
- c._carbon_request_url(:callback => 'http://www.postbin.org/1dj0146').should =~ /queue.amazonaws.com/
82
- c.emission :timeframe => Timeframe.new(:year => 2009), :callback => 'http://www.postbin.org/1dj0146'
115
+ c.emission.callback = 'http://www.postbin.org/1dj0146'
116
+ c.emission.request.url.should =~ /queue.amazonaws.com/
117
+ lambda {
118
+ c.emission :timeframe => Timeframe.new(:year => 2009), :callback => 'http://www.postbin.org/1dj0146'
119
+ }.should_not raise_error
83
120
  end
84
121
  end
85
122
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carbon
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 5
10
- version: 0.1.5
9
+ - 6
10
+ version: 0.1.6
11
11
  platform: ruby
12
12
  authors:
13
13
  - Derek Kastner
@@ -118,6 +118,8 @@ files:
118
118
  - lib/carbon.rb
119
119
  - lib/carbon/base.rb
120
120
  - lib/carbon/emission_estimate.rb
121
+ - lib/carbon/emission_estimate/request.rb
122
+ - lib/carbon/emission_estimate/response.rb
121
123
  - spec/lib/carbon_spec.rb
122
124
  - spec/spec_helper.rb
123
125
  - spec/specwatchr