carbon 0.1.5 → 0.1.6

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