carbon 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,10 @@ Carbon is a Ruby API wrapper for the {Brighter Planet emission estimate web serv
4
4
 
5
5
  By querying the web service, it can estimate the carbon emissions of many real-life objects, such as cars and houses, based on particular attributes that they may have.
6
6
 
7
+ == Full documentation
8
+
9
+ Give the quick start a try! Once you've exhausted that, read the {carbon gem RDoc}[http://rdoc.info/projects/brighterplanet/carbon] for more.
10
+
7
11
  == Quick start
8
12
 
9
13
  <b>You'll need a Brighter Planet API key. See the "API keys" section below for details.</b>
@@ -52,20 +56,32 @@ When you want to calculate emissions, simply call <tt>RentalCar#emission</tt>.
52
56
  => "kilograms"
53
57
  > my_emission.methodology
54
58
  => "http://carbon.brighterplanet.com/automobiles.html?[...]"
55
-
56
- === Asynchronous queries
57
59
 
58
- To request an emission estimate asynchronously, simply pass an URL as the <tt>:callback</tt> option to <tt>#emission</tt>:
60
+ == API keys
59
61
 
60
- > RentalCar.new.emission :callback => http://example.com/my/callback/handler
62
+ You should get an API key from http://keys.brighterplanet.com and set it globally:
61
63
 
62
- A good way to test this is to set up a {PostBin}[http://postbin.org]
64
+ Carbon.key = '12903019230128310293'
63
65
 
64
- == Documentation
66
+ Now all of your queries will use that key.
67
+
68
+ == A note on <tt>#to_param</tt>
69
+
70
+ Once you understand how to map attributes using the DSL, you might still be confused about what the gem actually calls on your objects.
65
71
 
66
- Read the {carbon gem RDoc}[http://rdoc.info/projects/brighterplanet/carbon] for more.
72
+ The answer: <tt>#to_characteristic</tt> and, failing that, <tt>#to_param</tt>.
67
73
 
68
- == Exceptions
74
+ Let's say <tt>RentalCar#make</tt> returns a <tt>Make</tt> object. You should define either <tt>Make#to_characteristic</tt> or <tt>Make#to_param</tt>. The gem <b>doesn't know</b> that you want it to call <tt>Make#name</tt> or <tt>Make#epa_code</tt> or anything else.
75
+
76
+ == A note on asynchronous queries
77
+
78
+ To request an emission estimate asynchronously, simply pass an URL as the <tt>:callback</tt> option to <tt>#emission</tt>:
79
+
80
+ > RentalCar.new.emission :callback => http://example.com/my/callback/handler
81
+
82
+ A good way to test this is to set up a {PostBin}[http://postbin.org].
83
+
84
+ == A note on exceptions
69
85
 
70
86
  Since this gem connects to a web service, you need to be ready for network problems and latency. For example:
71
87
 
@@ -89,14 +105,6 @@ Since this gem connects to a web service, you need to be ready for network probl
89
105
  # Please contact us at staff@brighterplanet.com if you see too many of these.
90
106
  end
91
107
 
92
- == API keys
93
-
94
- You should get an API key from http://keys.brighterplanet.com and set it globally:
95
-
96
- Carbon.key = '12903019230128310293'
97
-
98
- Now all of your queries will use that key.
99
-
100
108
  == Copyright
101
109
 
102
110
  Copyright (c) 2010 Brighter Planet.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.1
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{carbon}
8
- s.version = "0.2.0"
8
+ s.version = "0.2.1"
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", "Andy Rossmeissl"]
@@ -51,6 +51,8 @@ module Carbon
51
51
  end
52
52
  class RateLimited < RuntimeError # :nodoc:
53
53
  end
54
+ class TriedToUseAsyncResponseAsNumber < RuntimeError # :nodoc:
55
+ end
54
56
 
55
57
  # The api key obtained from http://keys.brighterplanet.com
56
58
  mattr_accessor :key
@@ -23,7 +23,7 @@ module Carbon
23
23
  #
24
24
  # Two general rules:
25
25
  # * Take note of what Brighter Planet expects to receive. If you send <tt>fuel_economy</tt> and we're expecting <tt>fuel_efficiency</tt>, we won't understand! (Hint: use the <tt>:as</tt> option.)
26
- # * Make sure <tt>#to_param</tt> is set up. The gem always calls <tt>#to_param</tt> before sending, so that's your change to key things by "EPA code" (fictional) or whatever else you want. (Hint: use the <tt>:key</tt> option.)
26
+ # * Make sure <tt>#to_characteristic</tt> or <tt>#to_param</tt> is set up. The gem always calls one of these (it will try <tt>#to_characteristic</tt> first) before sending, so that's your change to key things by "EPA code" (fictional) or whatever else you want. (Hint: use the <tt>:key</tt> option.)
27
27
  #
28
28
  # There are two optional parameters:
29
29
  # * <tt>:as</tt> - if Brighter Planet expects <tt>fuel_efficiency</tt>, and you say <tt>mpg</tt>, you can write <tt>provide :mpg, :as => :fuel_efficiency</tt>. This will result in a query like <tt>?fuel_efficiency=XYZ</tt>.
@@ -37,7 +37,7 @@ module Carbon
37
37
  # provide :manufacturer, :as => :make, :key => :epa_code # make[epa_code]=my_car.manufacturer.to_param
38
38
  # end
39
39
  #
40
- # Note that no matter what you send to us, the gem always calls <b>to_param</b> on the emitter. In this example, it's up to you to make sure my_car.manufacturer.to_param returns an epa_code.
40
+ # Note that no matter what you send to us, the gem always calls <b><tt>#to_characteristic</tt></b> (or, failing that, <tt>#to_param</tt>) on the emitter. In this example, it's up to you to make sure my_car.manufacturer.to_param returns an epa_code.
41
41
  def provide(attr_name, options = {})
42
42
  options = options.symbolize_keys
43
43
  characteristic = if options.has_key? :as
@@ -2,7 +2,9 @@ require 'carbon/emission_estimate/response'
2
2
  require 'carbon/emission_estimate/request'
3
3
 
4
4
  module Carbon
5
- # Let's start off by saying that <tt>EmissionEstimate</tt> objects quack like numbers.
5
+ # Let's start off by saying that realtime <tt>EmissionEstimate</tt> objects quack like numbers.
6
+ #
7
+ # If you ask for a callback, on the other hand, you can't use them as numbers.
6
8
  #
7
9
  # So, you can just say <tt>my_car.emission</tt> and you'll get something like <tt>4308.29</tt>.
8
10
  #
@@ -14,16 +16,17 @@ module Carbon
14
16
  @emitter = emitter
15
17
  end
16
18
 
17
- def take_options(options = {})
18
- options.each do |k, v|
19
+ VALID_OPTIONS = [:callback_content_type, :key, :callback, :timeframe]
20
+ def take_options(options)
21
+ return if options.blank?
22
+ options.slice(*VALID_OPTIONS).each do |k, v|
19
23
  instance_variable_set "@#{k}", v
20
24
  end
21
25
  end
22
26
 
23
- # I can be compared directly to a number.
27
+ # I can be compared directly to a number, unless I'm an async request.
24
28
  def ==(other)
25
- case other
26
- when Numeric
29
+ if other.is_a? ::Numeric and mode == :realtime
27
30
  other == response.number
28
31
  else
29
32
  super
@@ -39,8 +42,11 @@ module Carbon
39
42
  def method_missing(method_id, *args, &blk)
40
43
  if !block_given? and args.empty? and response.data.has_key? method_id.to_s
41
44
  response.data[method_id.to_s]
42
- else
45
+ elsif ::Float.method_defined? method_id
46
+ raise TriedToUseAsyncResponseAsNumber if mode == :async
43
47
  response.number.send method_id, *args, &blk
48
+ else
49
+ super
44
50
  end
45
51
  end
46
52
 
@@ -50,7 +56,6 @@ module Carbon
50
56
  attr_accessor :callback
51
57
  attr_accessor :timeframe
52
58
 
53
- attr_reader :data
54
59
  attr_reader :emitter
55
60
  def request
56
61
  @request ||= Request.new self
@@ -6,27 +6,30 @@ module Carbon
6
6
  @parent = parent
7
7
  end
8
8
  def body
9
- send "#{parent.mode}_body"
9
+ params.to_query
10
+ end
11
+ def params
12
+ send "#{parent.mode}_params"
10
13
  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
14
+ def async_params # :nodoc:
15
+ hash = _params
16
+ hash[:emitter] = parent.emitter.class.carbon_base.emitter_common_name
17
+ hash[:callback] = parent.callback
18
+ hash[:callback_content_type] = parent.callback_content_type
16
19
  {
17
20
  :Action => 'SendMessage',
18
21
  :Version => '2009-02-01',
19
- :MessageBody => params.to_query
20
- }.to_query
22
+ :MessageBody => hash.to_query
23
+ }
21
24
  end
22
- def realtime_body # :nodoc:
23
- params.to_query
25
+ def realtime_params # :nodoc:
26
+ _params
24
27
  end
25
28
  # Used internally, but you can look if you want.
26
29
  #
27
30
  # 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|
31
+ def _params
32
+ hash = parent.emitter.class.carbon_base.translation_table.inject({}) do |memo, translation|
30
33
  characteristic, key = translation
31
34
  if characteristic.is_a? ::Array
32
35
  current_value = parent.emitter.send characteristic[0]
@@ -35,8 +38,12 @@ module Carbon
35
38
  current_value = parent.emitter.send characteristic
36
39
  as = characteristic
37
40
  end
38
- current_value = current_value.to_param
39
- if current_value.present?
41
+ current_value = begin
42
+ current_value.to_characteristic
43
+ rescue NoMethodError
44
+ current_value.to_param
45
+ end
46
+ if current_value.is_a?(FalseClass) or current_value.present?
40
47
  if key
41
48
  memo[as] ||= {}
42
49
  memo[as][key] = current_value
@@ -46,9 +53,9 @@ module Carbon
46
53
  end
47
54
  memo
48
55
  end
49
- params[:timeframe] = parent.timeframe if parent.timeframe
50
- params[:key] = parent.key if parent.key
51
- params
56
+ hash[:timeframe] = parent.timeframe if parent.timeframe
57
+ hash[:key] = parent.key if parent.key
58
+ hash
52
59
  end
53
60
  def realtime_url # :nodoc:
54
61
  "#{::Carbon::REALTIME_URL}/#{parent.emitter.class.carbon_base.emitter_common_name.pluralize}.json"
@@ -1,5 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
+ CALLBACK_URL = 'http://www.postbin.org/1dj0146'
4
+
3
5
  class RentalCar
4
6
  include Carbon
5
7
  attr_accessor :model, :model_year, :fuel_economy
@@ -26,6 +28,9 @@ class DonutFactory
26
28
  class Mixer
27
29
  attr_accessor :upc
28
30
  def to_param
31
+ raise "Use #to_characteristic instead please"
32
+ end
33
+ def to_characteristic
29
34
  upc
30
35
  end
31
36
  end
@@ -88,6 +93,15 @@ describe Carbon do
88
93
  c.emission(:timeframe => Timeframe.new(:year => 2009)).should == 134.599
89
94
  c.emission.response.raw_request.object_id.should_not == first_raw_request.object_id
90
95
  end
96
+
97
+ it "should recalculate if the callback changes" do
98
+ c = RentalCar.new
99
+ c.model = 'Acura'
100
+ c.emission.should == 134.599
101
+ first_raw_request = c.emission.response.raw_request
102
+ c.emission.callback = CALLBACK_URL
103
+ c.emission.response.raw_request.object_id.should_not == first_raw_request.object_id
104
+ end
91
105
  end
92
106
 
93
107
  describe 'synchronous (realtime) requests' do
@@ -100,7 +114,7 @@ describe Carbon do
100
114
  it 'send complex params' do
101
115
  d = DonutFactory.new
102
116
  d.mixer.upc = 123
103
- d.emission.request.body.should =~ /mixer\[upc\]=123/
117
+ d.emission.request.body.should include({:mixer => { :upc => 123 }}.to_query)
104
118
  end
105
119
 
106
120
  it 'should not send attributes that are blank' do
@@ -109,6 +123,12 @@ describe Carbon do
109
123
  d.emission.request.body.should_not =~ /oven_count/
110
124
  d.emission.request.body.should_not =~ /timeframe/
111
125
  end
126
+
127
+ it 'should send attributes that are false' do
128
+ d = DonutFactory.new
129
+ d.mixer.upc = false
130
+ d.emission.request.body.should include({:mixer => { :upc => 'false' }}.to_query)
131
+ end
112
132
 
113
133
  it 'should send the key' do
114
134
  d = DonutFactory.new
@@ -127,30 +147,81 @@ describe Carbon do
127
147
  c = RentalCar.new
128
148
  t = Timeframe.new(:year => 2009)
129
149
  c.emission.timeframe = t
130
- c.emission.timeframe.should == t
150
+ c.emission.request.body.should include(t.to_query(:timeframe))
131
151
  end
132
152
 
133
153
  it 'should accept timeframes inline' do
134
154
  c = RentalCar.new
135
155
  t = Timeframe.new(:year => 2009)
136
156
  c.emission(:timeframe => t)
137
- c.emission.timeframe.should == t
157
+ c.emission.request.body.should include(t.to_query(:timeframe))
138
158
  end
139
159
 
140
160
  it 'should not generate post bodies with lots of empty params' do
141
161
  c = RentalCar.new
142
162
  c.emission :timeframe => Timeframe.new(:year => 2009)
143
163
  c.emission.request.body.should_not include('&&')
164
+ c.emission.request.body.should_not =~ /=[^a-z0-9]/i
144
165
  end
145
166
  end
146
167
 
147
168
  describe 'asynchronous (queued) requests' do
148
169
  it 'should post a message to SQS' do
149
170
  c = RentalCar.new
150
- c.emission.callback = 'http://www.postbin.org/1dj0146'
171
+ c.emission.callback = CALLBACK_URL
151
172
  c.emission.request.url.should =~ /queue.amazonaws.com/
173
+ end
174
+
175
+ it 'should have nil data in its response' do
176
+ c = RentalCar.new
177
+ c.emission.callback = CALLBACK_URL
178
+ c.emission.emission_value.should be_nil
179
+ c.emission.emission_units.should be_nil
180
+ c.emission.methodology.should be_nil
181
+ end
182
+
183
+ it "should not compare itself to numbers" do
184
+ c = RentalCar.new
185
+ c.emission.callback = CALLBACK_URL
186
+ c.emission.should_not == 0.0
187
+ end
188
+
189
+ it 'should not allow itself to be treated as a number' do
190
+ c = RentalCar.new
191
+ c.emission.callback = CALLBACK_URL
192
+ lambda {
193
+ c.emission + 5
194
+ }.should raise_error(::Carbon::TriedToUseAsyncResponseAsNumber)
195
+ lambda {
196
+ c.emission.to_f
197
+ }.should raise_error(::Carbon::TriedToUseAsyncResponseAsNumber)
198
+ end
199
+ end
200
+
201
+ describe 'internally' do
202
+ it "should ignore invalid options passed to #emission" do
203
+ c = RentalCar.new
204
+ t = Timeframe.new(:year => 2009)
205
+ c.emission :timeframe => t, :method_missing => 'helo there', :response => 'foo'
206
+ c.emission.instance_variable_get(:@timeframe).object_id.should == t.object_id
207
+ c.emission.instance_variable_get(:@method_missing).should be_nil
208
+ c.emission.instance_variable_get(:@response).should be_nil
209
+ end
210
+
211
+ it "should raise an error on EmissionEstimate if method isn't found" do
212
+ c = RentalCar.new
213
+ lambda {
214
+ c.emission.foobar
215
+ }.should raise_error(NoMethodError, /EmissionEstimate/)
216
+ end
217
+
218
+ it "should use #to_characteristic instead of #to_param if it's available" do
219
+ d = DonutFactory.new
220
+ lambda {
221
+ d.mixer.to_param
222
+ }.should raise_error(RuntimeError, /instead please/)
152
223
  lambda {
153
- c.emission :timeframe => Timeframe.new(:year => 2009), :callback => 'http://www.postbin.org/1dj0146'
224
+ d.emission.to_f
154
225
  }.should_not raise_error
155
226
  end
156
227
  end
@@ -10,17 +10,20 @@ require 'active_support/json/encoding'
10
10
  require 'carbon'
11
11
 
12
12
  require 'fakeweb'
13
- {
14
- 'automobiles' => {
13
+ [
14
+ [ /http:\/\/carbon.brighterplanet.com\/automobiles/, {
15
15
  'emission' => '134.599',
16
16
  'emission_units' => 'kilograms',
17
17
  'methodology' => 'http://carbon.brighterplanet.com/something'
18
- },
19
- 'factories' => {
18
+ }.to_json],
19
+ [ /http:\/\/carbon.brighterplanet.com\/factories/, {
20
20
  'emission' => 1000.0,
21
21
  'emission_units' => 'kilograms',
22
22
  'methodology' => 'http://carbon.brighterplanet.com/something'
23
- }
24
- }.each do |k, v|
25
- FakeWeb.register_uri :post, /http:\/\/carbon.brighterplanet.com\/#{k}/, :body => v.to_json
26
- end
23
+ }.to_json],
24
+ [ /https:\/\/queue.amazonaws.com/,
25
+ 'Nothing to see here.']
26
+ ].each do |url_matcher, response|
27
+ FakeWeb.register_uri :post, url_matcher, :body => response
28
+ end
29
+
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: 23
4
+ hash: 21
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 2
9
- - 0
10
- version: 0.2.0
9
+ - 1
10
+ version: 0.2.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Derek Kastner