carbon 0.0.7 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
@@ -0,0 +1,81 @@
1
+ = Carbon
2
+
3
+ Carbon is a Ruby API wrapper for the {Brighter Planet emission estimate web service}[http://carbon.brighterplanet.com], which is located at http://carbon.brighterplanet.com.
4
+
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
+
7
+ == Quick start
8
+
9
+ First get the gem!
10
+
11
+ $ gem install carbon
12
+
13
+ Carbon works by extending any Ruby class you're using to represent an emission source. For instance, let's say you have a plain Ruby class <tt>RentalCar</tt> that represents a rental car on your lot: (<tt>ActiveRecord</tt> models work fine too!)
14
+
15
+ class RentalCar
16
+ attr_accessor :year, :make, :model, :fuel_efficiency, :daily_distance_average, :purchase_date, :retirement_date
17
+ end
18
+
19
+ In order to calculate carbon emissions, we need to map the car's relevant attributes to correctly-named characteristics that the {web service}[http://carbon.brighterplanet.com] will recognize:
20
+
21
+ class RentalCar
22
+ include Carbon
23
+ [...]
24
+ emit_as :automobile do
25
+ provide :model_year, :as => :year # you say tomayto, I say tomahto
26
+ provide :make
27
+ provide :model
28
+ provide :fuel_efficiency
29
+ provide :daily_distance_estimate, :as => daily_distance_average
30
+ provide :acquisition, :as => :purchase_date
31
+ provide :retirement, :as => :retirement_date
32
+ end
33
+ end
34
+
35
+ When you want to calculate emissions, simply call <tt>RentalCar</tt>#<tt>emission</tt>... (and <b>store the result to a local variable!</b> Otherwise it will make a web service call every time.)
36
+
37
+ > my_car = RentalCar.new([...])
38
+ => #<RentalCar [...]>
39
+ > my_emission = my_car.emission
40
+ => #<Carbon::EmissionEstimate [...]>
41
+ > my_emission.to_f
42
+ => 4919.2
43
+ > my_emission.emission_units
44
+ => "kilograms"
45
+ > my_emission.methodology
46
+ => "http://carbon.brighterplanet.com/automobiles.html?[...]"
47
+
48
+ Read the {carbon gem RDoc}[http://rdoc.info/projects/brighterplanet/carbon] for more!
49
+
50
+ == A note on API keys
51
+
52
+ You should get an API key from http://keys.brighterplanet.com and set it globally:
53
+
54
+ Carbon.key = '12903019230128310293'
55
+
56
+ Now all of your queries will use that key.
57
+
58
+ == A note on modes
59
+
60
+ When you send in a query, you have two options for how you want the result back:
61
+
62
+ * realtime - you get the answer back immediately. More expensive.
63
+ * async - the answer is POSTed back to a URL that you specify. You must have a server waiting for it! Cheaper.
64
+
65
+ The default is realtime:
66
+
67
+ > my_emission = RentalCar.new.emission
68
+ => #<Carbon::EmissionEstimate [...]>
69
+
70
+ A good way to test the "async" mode is to set up a {PostBin}[http://postbin.org]
71
+
72
+ > RentalCar.new.emission :mode => :async, :callback => 'http://postbin.org/1dj0145'
73
+ => true # useless, but go check out http://postbin.org/1dj0145
74
+
75
+ You can set the mode globally with
76
+
77
+ Carbon.mode = :async
78
+
79
+ == Copyright
80
+
81
+ Copyright (c) 2010 Brighter Planet.
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gemspec|
7
+ gemspec.name = 'carbon'
8
+ gemspec.summary = %q{A gem for calculating carbon footprints using Brighter Planet's Carbon Middleware service}
9
+ gemspec.description = %q{Carbon allows you to easily calculate the carbon footprint of various activities. This is an API for the Brighter Planet Carbon Middleware service.}
10
+ gemspec.email = 'derek.kastner@brighterplanet.com'
11
+ gemspec.homepage = 'http://carbon.brighterplanet.com/libraries'
12
+ gemspec.authors = ['Derek Kastner', 'Seamus Abshere']
13
+ gemspec.add_dependency 'activesupport', '>=3.0.0.beta2'
14
+ gemspec.add_dependency 'nap', '>=0.4'
15
+ gemspec.add_dependency 'timeframe', '>=0.0.6'
16
+
17
+ gemspec.add_development_dependency 'fakeweb', '>=1.2.8'
18
+ # sabshere 7/16/10 if you're having trouble running specs, try "rspec spec" and/or "sudo gem install rspec --pre"
19
+ gemspec.add_development_dependency 'rspec', '>=2.0.0.beta.17'
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'rake/testtask'
27
+ Rake::TestTask.new(:test) do |test|
28
+ test.libs << 'lib' << 'test'
29
+ test.pattern = 'test/**/test_*.rb'
30
+ test.verbose = true
31
+ end
32
+
33
+ begin
34
+ require 'rcov/rcovtask'
35
+ Rcov::RcovTask.new do |test|
36
+ test.libs << 'test'
37
+ test.pattern = 'test/**/test_*.rb'
38
+ test.verbose = true
39
+ end
40
+ rescue LoadError
41
+ task :rcov do
42
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
43
+ end
44
+ end
45
+
46
+ task :test => :check_dependencies
47
+
48
+ task :default => :test
49
+
50
+ require 'rake/rdoctask'
51
+ Rake::RDocTask.new do |rdoc|
52
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "decider #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,67 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{carbon}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Derek Kastner", "Seamus Abshere"]
12
+ s.date = %q{2010-07-19}
13
+ s.description = %q{Carbon allows you to easily calculate the carbon footprint of various activities. This is an API for the Brighter Planet Carbon Middleware service.}
14
+ s.email = %q{derek.kastner@brighterplanet.com}
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "MIT-LICENSE.txt",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "carbon.gemspec",
25
+ "lib/carbon.rb",
26
+ "lib/carbon/base.rb",
27
+ "lib/carbon/emission_estimate.rb",
28
+ "spec/lib/carbon_spec.rb",
29
+ "spec/spec_helper.rb",
30
+ "spec/specwatchr"
31
+ ]
32
+ s.homepage = %q{http://carbon.brighterplanet.com/libraries}
33
+ s.rdoc_options = ["--charset=UTF-8"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.7}
36
+ s.summary = %q{A gem for calculating carbon footprints using Brighter Planet's Carbon Middleware service}
37
+ s.test_files = [
38
+ "spec/lib/carbon_spec.rb",
39
+ "spec/spec_helper.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
48
+ s.add_runtime_dependency(%q<nap>, [">= 0.4"])
49
+ s.add_runtime_dependency(%q<timeframe>, [">= 0.0.6"])
50
+ s.add_development_dependency(%q<fakeweb>, [">= 1.2.8"])
51
+ s.add_development_dependency(%q<rspec>, [">= 2.0.0.beta.17"])
52
+ else
53
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
54
+ s.add_dependency(%q<nap>, [">= 0.4"])
55
+ s.add_dependency(%q<timeframe>, [">= 0.0.6"])
56
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
57
+ s.add_dependency(%q<rspec>, [">= 2.0.0.beta.17"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<activesupport>, [">= 3.0.0.beta2"])
61
+ s.add_dependency(%q<nap>, [">= 0.4"])
62
+ s.add_dependency(%q<timeframe>, [">= 0.0.6"])
63
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
64
+ s.add_dependency(%q<rspec>, [">= 2.0.0.beta.17"])
65
+ end
66
+ end
67
+
@@ -1,29 +1,228 @@
1
- $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__))
2
-
3
- require 'carbon/emitter'
4
- require 'carbon/emissions_calculation'
1
+ require 'uri'
2
+ require 'blockenspiel'
3
+ require 'rest' # provided by nap gem
4
+ require 'timeframe'
5
+ %w{
6
+ active_support/core_ext/module/attribute_accessors
7
+ active_support/core_ext/class/attribute_accessors
8
+ active_support/core_ext/hash/keys
9
+ active_support/core_ext/hash/reverse_merge
10
+ active_support/core_ext/object/to_query
11
+ active_support/core_ext/array/wrap
12
+ active_support/inflector/inflections
13
+ active_support/json/decoding
14
+ }.each do |active_support_3_requirement|
15
+ require active_support_3_requirement
16
+ end
17
+ require 'carbon/base'
18
+ require 'carbon/emission_estimate'
5
19
 
20
+ # A module (aka mixin) that lets you estimate carbon emissions by querying the {Brighter Planet carbon middleware emission estimate web service}[http://carbon.brighterplanet.com].
21
+ #
22
+ # class RentalCar
23
+ # include Carbon
24
+ # [...]
25
+ # emit_as :automobile do
26
+ # provide :make
27
+ # provide :model
28
+ # provide :model_year
29
+ # end
30
+ # end
31
+ #
32
+ # The DSL consists of the methods <tt>emit_as</tt> and <tt>provide</tt>.
33
+ #
34
+ # In this example, the DSL says:
35
+ # * A rental car emits carbon like an "automobile", which is one of Brighter Planet's recognized emitter classes.
36
+ # * Your implementation can provide up to three data points about a rental car: its make, its model, and its model year (but not necessarily all of them, all the time.)
37
+ #
38
+ # Once you've mixed in <tt>Carbon</tt>, you get the method <tt>emission</tt>, which you can call at any time to request an emission estimate.
6
39
  module Carbon
7
- class << self
8
- def api_key
9
- @api_key if defined?(@api_key)
10
- end
11
- def api_key=(val)
12
- @api_key = val
13
- end
40
+ def self.included(klass) # :nodoc:
41
+ klass.cattr_accessor :carbon_base
42
+ klass.extend ClassMethods
43
+ end
44
+
45
+ MODES = [ :realtime, :async ]
46
+ REALTIME_URL = 'http://carbon.brighterplanet.com'
47
+ ASYNC_URL = 'https://queue.amazonaws.com/121562143717/cm1_production_incoming'
48
+
49
+ class UnrecognizedMode < ArgumentError # :nodoc:
50
+ end
51
+ class BlankCallback < ArgumentError # :nodoc:
52
+ end
53
+ class RealtimeEstimateFailed < RuntimeError # :nodoc:
54
+ end
55
+ class QueueingFailed < RuntimeError # :nodoc:
56
+ end
14
57
 
15
- def base_url
16
- @base_url ||= 'http://carbon.brighterplanet.com'
17
- end
18
- def base_url=(val)
19
- @base_url = val
20
- end
58
+ # The api key obtained from http://keys.brighterplanet.com
59
+ mattr_accessor :key
21
60
 
22
- def debug=(val)
23
- @debug = val
61
+ mattr_accessor :_mode
62
+ # Return the current mode. Defaults to <tt>:realtime</tt>.
63
+ def self.mode
64
+ _mode || :realtime
65
+ end
66
+ # Set the current mode.
67
+ # * Realtime mode (<tt>:realtime</tt>) means you get the answer back immediately.
68
+ # * Async mode (<tt>:async</tt>) means the answer will be POSTed back to you at a URL you specify. You must have a server waiting to receive it!
69
+ def self.mode=(str)
70
+ self._mode = str.to_sym
71
+ raise UnrecognizedMode unless MODES.include? mode
72
+ end
73
+
74
+ def self.default_options # :nodoc:
75
+ {
76
+ :key => key,
77
+ :mode => mode
78
+ }
79
+ end
80
+
81
+ # You will probably never access this module directly. Instead, you'll use it through the DSL.
82
+ #
83
+ # It's mixed into any class that includes <tt>Carbon</tt>.
84
+ module ClassMethods
85
+ # Indicate that this class "emits as" an <tt>:automobile</tt>, <tt>:flight</tt>, or another of the Brighter Planet emitter classes.
86
+ #
87
+ # See the {emission estimate web service use documentation}[http://carbon.brighterplanet.com/use]
88
+ #
89
+ # For example,
90
+ # emit_as :automobile do
91
+ # provide :make
92
+ # end
93
+ def emit_as(emitter_common_name, &block)
94
+ self.carbon_base ||= ::Carbon::Base.new self, emitter_common_name
95
+ ::Blockenspiel.invoke block, carbon_base
24
96
  end
25
- def debug
26
- @debug ||= false
97
+ # japanese-style preferred
98
+ alias :emits_as :emit_as
99
+ end
100
+
101
+ # Used internally, but you can look if you want.
102
+ #
103
+ # Returns the URL to which emissions estimate queries will be POSTed.
104
+ #
105
+ # For example:
106
+ # > my_car._carbon_request_url
107
+ # => 'http://carbon.brighterplanet.com/automobiles.json'
108
+ def _carbon_request_url(options = {})
109
+ options.reverse_merge! ::Carbon.default_options
110
+ send "_#{options[:mode]}_carbon_request_url"
111
+ end
112
+
113
+ def _realtime_carbon_request_url # :nodoc:
114
+ "#{::Carbon::REALTIME_URL}/#{self.class.carbon_base.emitter_common_name.pluralize}.json"
115
+ end
116
+
117
+ def _async_carbon_request_url # :nodoc:
118
+ ::Carbon::ASYNC_URL
119
+ end
120
+
121
+ # Used internally, but you can look if you want.
122
+ #
123
+ # Returns the request body that will be posted.
124
+ #
125
+ # For example:
126
+ # > my_car._carbon_request_body
127
+ # => 'fuel_efficiency=41&model=Ford+Taurus'
128
+ def _carbon_request_body(options = {})
129
+ options.reverse_merge! ::Carbon.default_options
130
+ send "_#{options[:mode]}_carbon_request_body", options
131
+ end
132
+
133
+ def _async_carbon_request_body(options) # :nodoc:
134
+ params = _carbon_request_params options
135
+ params[:emitter] = self.class.carbon_base.emitter_common_name.pluralize
136
+ raise ::Carbon::BlankCallback unless options[:callback].present?
137
+ params[:callback] = options[:callback]
138
+ params[:callback_content_type] = options[:callback_content_type] || 'application/json'
139
+ {
140
+ :Action => 'SendMessage',
141
+ :Version => '2009-02-01',
142
+ :MessageBody => params.to_query
143
+ }.to_query
144
+ end
145
+
146
+ def _realtime_carbon_request_body(options) # :nodoc:
147
+ _carbon_request_params(options).to_query
148
+ end
149
+
150
+ # Used internally, but you can look if you want.
151
+ #
152
+ # Returns the params hash that will be send to the emission estimate server.
153
+ def _carbon_request_params(options)
154
+ options.reverse_merge! ::Carbon.default_options
155
+ params = self.class.carbon_base.translation_table.inject(Hash.new) do |memo, translation|
156
+ characteristic, as = translation
157
+ current_value = send as
158
+ if current_value.present?
159
+ if characteristic.is_a? Array # [:mixer, :size]
160
+ memo[characteristic[0]] ||= Hash.new # { :mixer => Hash.new }
161
+ memo[characteristic[0]][characteristic[1]] = current_value # { :mixer => { :size => 'foo' }}
162
+ else # :oven_count
163
+ memo[characteristic] = current_value # { :oven_count => 'bar' }
164
+ end
165
+ end
166
+ memo
27
167
  end
168
+ params.merge! options.slice(:timeframe, :key)
169
+ params
170
+ end
171
+
172
+ def _realtime_emission(options = {}) # :nodoc:
173
+ response = _carbon_response options
174
+ raise ::Carbon::RealtimeEstimateFailed unless response.success?
175
+ ::Carbon::EmissionEstimate.new ::ActiveSupport::JSON.decode(response.body)
176
+ end
177
+
178
+ def _async_emission(options = {}) # :nodoc:
179
+ response = _carbon_response options
180
+ raise ::Carbon::QueueingFailed unless response.success?
181
+ true
182
+ end
183
+
184
+ # Used internally, but you can look if you want.
185
+ #
186
+ # Runs the query and returns the raw response body, which will be in JSON.
187
+ #
188
+ # For example:
189
+ # > my_car._carbon_response.body
190
+ # => "{ 'emission' => 410.29, 'emission_units' => 'kilograms', [...] }"
191
+ def _carbon_response(options = {})
192
+ @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'}
193
+ @last_carbon_response = @last_carbon_request.perform
194
+ end
195
+
196
+ # Returns an object representing the last emission estimate request.
197
+ def last_carbon_request
198
+ @last_carbon_request
199
+ end
200
+
201
+ # Returns an object representing the last emission estimate response.
202
+ def last_carbon_response
203
+ @last_carbon_response
204
+ end
205
+
206
+ # Returns an emission estimate.
207
+ #
208
+ # Note: <b>You need to take care of storing the return value to a local variable!</b> Every call to <tt>emission</tt> runs a query.
209
+ #
210
+ # You can use it like a number...
211
+ # > my_car.emission + 5.1
212
+ # => 415.39
213
+ # Or you can get information about the response
214
+ # > my_car.emission.methodology
215
+ # => 'http://carbon.brighterplanet.com/automobiles.html?[...]'
216
+ #
217
+ # === Options:
218
+ #
219
+ # * <tt>:timeframe</tt> (optional) pass an instance of Timeframe[http://github.com/rossmeissl/timeframe] to request an emission for a specific time period.
220
+ # * <tt>:callback</tt> (required in <tt>:async</tt> mode, ignored otherwise) where to POST the result when it's been calculated. You need a server waiting for it!
221
+ # * <tt>:callback_content_type</tt> (optional in <tt>:async</tt> mode, 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'.
222
+ # * <tt>:mode</tt> (optional, overrides general <tt>Carbon</tt>.<tt>mode</tt> setting just for this query) If some of your queries are realtime and some are asynchronous, you can override the setting here.
223
+ # * <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.
224
+ def emission(options = {})
225
+ options.reverse_merge! ::Carbon.default_options
226
+ send "_#{options[:mode]}_emission", options
28
227
  end
29
228
  end
@@ -0,0 +1,52 @@
1
+ module Carbon
2
+ # You will probably never access this class directly. Instead, you'll touch it through the DSL.
3
+ #
4
+ # An instance of this appears on any class that includes <tt>Carbon</tt>.
5
+ class Base
6
+ include Blockenspiel::DSL
7
+ attr_reader :klass
8
+ attr_reader :emitter_common_name
9
+ def initialize(klass, emitter_common_name)
10
+ @klass = klass
11
+ @emitter_common_name = emitter_common_name.to_s
12
+ end
13
+ # A completed translation table will look like:
14
+ # {[:mixer, :size]=>"mixer_size",
15
+ # :personnel=>:employees,
16
+ # :smokestack_size=>"smokestack_size",
17
+ # :oven_count=>"oven_count",
18
+ # [:mixer, :wattage]=>"mixer_wattage"}
19
+ def translation_table # :nodoc:
20
+ @translation_table ||= Hash.new
21
+ end
22
+ # Indicate that you will send in a piece of data about the emitter.
23
+ #
24
+ # If you don't use the <tt>:as</tt> option, the name of the getter method will be guessed.
25
+ #
26
+ # There are two optional parameters:
27
+ # * <tt>:of</tt> - the owner of a nested characteristic. So to send <tt>model[name]</tt> you write <tt>provide :name, :of => :model</tt>
28
+ # * <tt>:as</tt> - the local getter name. So if Brighter Planet expects <tt>fuel_efficiency</tt>, and you only have <tt>fuel_economy</tt>, you can write <tt>provide :fuel_efficiency, :as => :fuel_economy</tt>.
29
+ #
30
+ # For example:
31
+ #
32
+ # emit_as :automobile do
33
+ # # Official Brighter Planet characteristic name Your name for it
34
+ # provide :model, :as => :carline_class # model carline_class
35
+ # provide :name, :of => :make, :as => :mfr_name # make[name] mfr_name
36
+ # end
37
+ def provide(attr_name, options = {})
38
+ options = options.symbolize_keys
39
+ characteristic = if options.has_key? :of
40
+ # [ :mixer, :size ]
41
+ [options[:of], attr_name]
42
+ else
43
+ # :oven_count
44
+ attr_name
45
+ end
46
+ # translation_table[[:mixer,:size]] = 'mixer_size'
47
+ translation_table[characteristic] = options[:as] || Array.wrap(characteristic).join('_')
48
+ end
49
+ # japanese-style preferred
50
+ alias :provides :provide
51
+ end
52
+ end
@@ -0,0 +1,58 @@
1
+ module Carbon
2
+ # Let's start off by saying that <tt>EmissionEstimate</tt> objects quack like numbers.
3
+ #
4
+ # So, you can just say <tt>my_car.emission</tt> and you'll get something like <tt>4308.29</tt>.
5
+ #
6
+ # At the same time, they contain all the data you get back from the emission estimate web service. For example, you could say <tt>puts my_donut_factor.emission.oven_count</tt> (see the tests) and you'd get back the oven count used in the calculation, if any.
7
+ #
8
+ # 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
+ class EmissionEstimate
10
+ attr_reader :data
11
+ def initialize(data)
12
+ @data = data
13
+ @number = data['emission'].to_f.freeze
14
+ end
15
+ def ==(other) # :nodoc:
16
+ other == @number
17
+ end
18
+ # Another way to access the emission value.
19
+ # Useful if you don't like treating <tt>EmissionEstimate</tt> objects like <tt>Numeric</tt> objects (even though they do quack like numbers...)
20
+ def emission_value
21
+ @number
22
+ end
23
+ # The units of the emission.
24
+ 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']
32
+ end
33
+ # Errors (and warnings) as reported in the response.
34
+ # Note: may contain HTML tags like KBD or A
35
+ def errors
36
+ data['errors']
37
+ end
38
+ # The URL of the methodology report indicating how this estimate was calculated.
39
+ # > my_car.emission.methodology
40
+ # => 'http://carbon.brighterplanet.com/automobiles.html?[...]'
41
+ 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
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ class RentalCar
4
+ include Carbon
5
+ attr_accessor :make, :model, :model_year, :fuel_economy
6
+ emit_as :automobile do
7
+ provide :make
8
+ provide :model
9
+ provide :model_year
10
+ provide :fuel_efficiency, :as => :fuel_economy
11
+ end
12
+ end
13
+
14
+ class DonutFactory
15
+ include Carbon
16
+ attr_accessor :smokestack_size, :oven_count, :mixer_size, :mixer_wattage, :employees
17
+ emit_as :factory do
18
+ provide :smokestack_size
19
+ provide :oven_count
20
+ provide :size, :of => :mixer
21
+ provide :wattage, :of => :mixer
22
+ provide :personnel, :as => :employees
23
+ end
24
+ end
25
+
26
+ describe Carbon do
27
+ before(:each) do
28
+ Carbon.key = 'valid'
29
+ end
30
+
31
+ it 'should be simple to use' do
32
+ c = RentalCar.new
33
+ c.model = 'Acura'
34
+ c.model_year = 2003
35
+ c.fuel_economy = 32
36
+ e = c.emission
37
+ e.should == 134.599
38
+ e.emission_units.should == 'kilograms'
39
+ end
40
+
41
+ describe 'synchronous (realtime) requests' do
42
+ before(:each) do
43
+ Carbon.mode = :realtime
44
+ end
45
+
46
+ it 'should handle complex attributes like mixer[size]' do
47
+ d = DonutFactory.new
48
+ d.mixer_size = 20
49
+ d._carbon_request_body.should =~ /mixer\[size\]=20/
50
+ end
51
+
52
+ it 'should not send attributes that are blank' do
53
+ d = DonutFactory.new
54
+ d.mixer_size = 20
55
+ d._carbon_request_body.should_not =~ /oven_count/
56
+ end
57
+
58
+ it 'should send the key' do
59
+ d = DonutFactory.new
60
+ d._carbon_request_body.should =~ /key=valid/
61
+ end
62
+
63
+ it 'should override defaults' do
64
+ d = DonutFactory.new
65
+ d.emission(:key => 'ADifferentOne')
66
+ d.last_carbon_request.body.should =~ /key=ADifferentOne/
67
+ end
68
+
69
+ it 'should accept timeframes' do
70
+ c = RentalCar.new
71
+ c.emission :timeframe => Timeframe.new(:year => 2009)
72
+ c.last_carbon_request.body.should =~ /timeframe=2009-01-01%2F2010-01-01/
73
+ end
74
+
75
+ it 'should not generate post bodies with lots of empty params' do
76
+ c = RentalCar.new
77
+ c.emission :timeframe => Timeframe.new(:year => 2009)
78
+ c.last_carbon_request.body.should_not include('&&')
79
+ end
80
+ end
81
+
82
+ describe 'asynchronous (queued) requests' do
83
+ before(:each) do
84
+ Carbon.mode = :async
85
+ end
86
+
87
+ it 'should raise an exception if no callback is provided' do
88
+ c = RentalCar.new
89
+ lambda {
90
+ c.emission :timeframe => Timeframe.new(:year => 2009)
91
+ }.should raise_error(Carbon::BlankCallback)
92
+ end
93
+
94
+ it 'should post a message to SQS' do
95
+ c = RentalCar.new
96
+ c._carbon_request_url.should =~ /queue.amazonaws.com/
97
+ c.emission :timeframe => Timeframe.new(:year => 2009), :callback => 'http://example.com/callback?id=999'
98
+ end
99
+ end
100
+ end
101
+
102
+ # an average car emits 6 tons of carbon in a year
103
+ # it 'should actually do a request!' do
104
+ # FakeWeb.clean_registry
105
+ # c = RentalCar.new
106
+ # c.emission.to_i.should be_close(5500, 500)
107
+ # c.emission.emission_units.should == 'kilograms'
108
+ # end
109
+
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'rspec'
3
+ begin
4
+ require 'ruby-debug'
5
+ rescue
6
+ end
7
+
8
+ require 'carbon'
9
+
10
+ require 'fakeweb'
11
+ {
12
+ 'automobiles' => {
13
+ 'emission' => '134.599',
14
+ 'emission_units' => 'kilograms',
15
+ 'methodology' => 'http://carbon.brighterplanet.com/something'
16
+ },
17
+ 'factories' => {
18
+ 'emission' => 1000.0,
19
+ 'emission_units' => 'kilograms',
20
+ 'methodology' => 'http://carbon.brighterplanet.com/something'
21
+ }
22
+ }.each do |k, v|
23
+ FakeWeb.register_uri :post, /http:\/\/carbon.brighterplanet.com\/#{k}/, :body => v.to_json
24
+ end
@@ -0,0 +1,60 @@
1
+ # Run me with:
2
+ #
3
+ # $ watchr specs.watchr.rb
4
+
5
+ # --------------------------------------------------
6
+ # Convenience Methods
7
+ # --------------------------------------------------
8
+ def all_test_files
9
+ Dir['spec/**/*_spec.rb'] - ['spec/spec_helper.rb']
10
+ end
11
+
12
+ def run(cmd)
13
+ puts "\e[H\e[2J" #clear console
14
+ puts(cmd)
15
+ system(cmd)
16
+ end
17
+
18
+ def run_all_tests
19
+ cmd = "spec spec"
20
+ run(cmd)
21
+ end
22
+
23
+ def run_spec(file_name)
24
+ return unless File.exist?(file_name)
25
+ file_text = File.read(file_name)
26
+ if file_text =~ /#\s*wip/
27
+ current_line = 2
28
+ exec_line = nil
29
+ file_text.each_line do |line|
30
+ if line =~ /#\s*wip/
31
+ exec_line ||= current_line
32
+ end
33
+ current_line += 1
34
+ end
35
+ run "spec #{file_name}:#{exec_line}"
36
+ else
37
+ run "spec #{file_name}"
38
+ end
39
+ end
40
+
41
+ # --------------------------------------------------
42
+ # Watchr Rules
43
+ # --------------------------------------------------
44
+ watch('^spec/.+_spec.rb' ) { |m| run_spec(m[0]) }
45
+ watch('^app/(.+)/(.+)\.rb') { |m| run_spec("spec/#{m[1]}/#{m[2]}_spec.rb") }
46
+ watch('^lib/(.*)\.rb' ) { |m| run_spec("spec/lib/#{m[1]}_spec.rb") }
47
+ watch('^spec/spec_helper\.rb') { run_all_tests }
48
+ watch('actors_controller') { |m| run "spec spec/controllers" }
49
+
50
+ # --------------------------------------------------
51
+ # Signal Handling
52
+ # --------------------------------------------------
53
+ # Ctrl-\
54
+ Signal.trap('QUIT') do
55
+ puts " --- Running all tests ---\n\n"
56
+ run_all_tests
57
+ end
58
+
59
+ # Ctrl-C
60
+ Signal.trap('INT') { abort("\n") }
metadata CHANGED
@@ -1,117 +1,161 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carbon
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 25
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
- - 0
8
- - 7
9
- version: 0.0.7
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
10
11
  platform: ruby
11
12
  authors:
12
13
  - Derek Kastner
14
+ - Seamus Abshere
13
15
  autorequire:
14
16
  bindir: bin
15
17
  cert_chain: []
16
18
 
17
- date: 2010-04-28 00:00:00 -04:00
19
+ date: 2010-07-19 00:00:00 -05:00
18
20
  default_executable:
19
21
  dependencies:
20
22
  - !ruby/object:Gem::Dependency
21
23
  name: activesupport
22
24
  prerelease: false
23
25
  requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
24
27
  requirements:
25
28
  - - ">="
26
29
  - !ruby/object:Gem::Version
30
+ hash: 299253626
27
31
  segments:
32
+ - 3
33
+ - 0
28
34
  - 0
29
- version: "0"
35
+ - beta2
36
+ version: 3.0.0.beta2
30
37
  type: :runtime
31
38
  version_requirements: *id001
32
39
  - !ruby/object:Gem::Dependency
33
- name: httparty
40
+ name: nap
34
41
  prerelease: false
35
42
  requirement: &id002 !ruby/object:Gem::Requirement
43
+ none: false
36
44
  requirements:
37
45
  - - ">="
38
46
  - !ruby/object:Gem::Version
47
+ hash: 3
39
48
  segments:
40
49
  - 0
41
- version: "0"
50
+ - 4
51
+ version: "0.4"
42
52
  type: :runtime
43
53
  version_requirements: *id002
44
54
  - !ruby/object:Gem::Dependency
45
- name: fakeweb
55
+ name: timeframe
46
56
  prerelease: false
47
57
  requirement: &id003 !ruby/object:Gem::Requirement
58
+ none: false
48
59
  requirements:
49
60
  - - ">="
50
61
  - !ruby/object:Gem::Version
62
+ hash: 19
51
63
  segments:
52
64
  - 0
53
- version: "0"
54
- type: :development
65
+ - 0
66
+ - 6
67
+ version: 0.0.6
68
+ type: :runtime
55
69
  version_requirements: *id003
56
70
  - !ruby/object:Gem::Dependency
57
- name: rspec
71
+ name: fakeweb
58
72
  prerelease: false
59
73
  requirement: &id004 !ruby/object:Gem::Requirement
74
+ none: false
60
75
  requirements:
61
76
  - - ">="
62
77
  - !ruby/object:Gem::Version
78
+ hash: 15
63
79
  segments:
64
- - 0
65
- version: "0"
80
+ - 1
81
+ - 2
82
+ - 8
83
+ version: 1.2.8
66
84
  type: :development
67
85
  version_requirements: *id004
86
+ - !ruby/object:Gem::Dependency
87
+ name: rspec
88
+ prerelease: false
89
+ requirement: &id005 !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ hash: 62196417
95
+ segments:
96
+ - 2
97
+ - 0
98
+ - 0
99
+ - beta
100
+ - 17
101
+ version: 2.0.0.beta.17
102
+ type: :development
103
+ version_requirements: *id005
68
104
  description: Carbon allows you to easily calculate the carbon footprint of various activities. This is an API for the Brighter Planet Carbon Middleware service.
69
- email: derek@brighterplanet.com
105
+ email: derek.kastner@brighterplanet.com
70
106
  executables: []
71
107
 
72
108
  extensions: []
73
109
 
74
110
  extra_rdoc_files:
75
- - MIT-LICENSE.txt
111
+ - README.rdoc
76
112
  files:
77
- - lib/carbon/emissions_calculation.rb
78
- - lib/carbon/emitter/characteristic.rb
79
- - lib/carbon/emitter/class_methods.rb
80
- - lib/carbon/emitter/options.rb
81
- - lib/carbon/emitter.rb
82
- - lib/carbon.rb
113
+ - .gitignore
83
114
  - MIT-LICENSE.txt
115
+ - README.rdoc
116
+ - Rakefile
117
+ - VERSION
118
+ - carbon.gemspec
119
+ - lib/carbon.rb
120
+ - lib/carbon/base.rb
121
+ - lib/carbon/emission_estimate.rb
122
+ - spec/lib/carbon_spec.rb
123
+ - spec/spec_helper.rb
124
+ - spec/specwatchr
84
125
  has_rdoc: true
85
- homepage:
126
+ homepage: http://carbon.brighterplanet.com/libraries
86
127
  licenses: []
87
128
 
88
129
  post_install_message:
89
- rdoc_options: []
90
-
130
+ rdoc_options:
131
+ - --charset=UTF-8
91
132
  require_paths:
92
133
  - lib
93
134
  required_ruby_version: !ruby/object:Gem::Requirement
135
+ none: false
94
136
  requirements:
95
137
  - - ">="
96
138
  - !ruby/object:Gem::Version
139
+ hash: 3
97
140
  segments:
98
141
  - 0
99
142
  version: "0"
100
143
  required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
101
145
  requirements:
102
- - - ">"
146
+ - - ">="
103
147
  - !ruby/object:Gem::Version
148
+ hash: 3
104
149
  segments:
105
- - 1
106
- - 3
107
- - 1
108
- version: 1.3.1
150
+ - 0
151
+ version: "0"
109
152
  requirements: []
110
153
 
111
154
  rubyforge_project:
112
- rubygems_version: 1.3.6
155
+ rubygems_version: 1.3.7
113
156
  signing_key:
114
157
  specification_version: 3
115
158
  summary: A gem for calculating carbon footprints using Brighter Planet's Carbon Middleware service
116
- test_files: []
117
-
159
+ test_files:
160
+ - spec/lib/carbon_spec.rb
161
+ - spec/spec_helper.rb
@@ -1,64 +0,0 @@
1
- require 'uri'
2
- require 'active_support/inflector'
3
- require 'httparty'
4
-
5
- module Carbon
6
- class EmissionsCalculation
7
- class NotYetCalculated < StandardError; end
8
- class CalculationRequestFailed < StandardError; end
9
-
10
- include HTTParty
11
-
12
- attr_accessor :options, :source
13
- attr_reader :value, :methodology_url, :committees
14
-
15
- def initialize(options, source)
16
- self.options = options
17
- self.source = source
18
- end
19
-
20
- def methodology_url
21
- raise NotYetCalculated if result.nil?
22
- @methodology_url
23
- end
24
- def value
25
- raise NotYetCalculated if result.nil?
26
- @value
27
- end
28
-
29
- def calculate!
30
- fetch_calculation
31
- @value = result['emission']
32
- @methodology_url = result['methodology']
33
- @committees = result['committees']
34
- end
35
-
36
- private
37
- def result
38
- @result
39
- end
40
-
41
- def fields
42
- fields_hash = options.characteristics.inject({}) do |hsh, characteristic|
43
- value = source.send(characteristic.field)
44
- hsh[characteristic.name.to_sym] = value if value
45
- hsh
46
- end
47
- { :body => { options.emitter_type => fields_hash } }
48
- end
49
-
50
- def fetch_calculation
51
- url = URI.join(Carbon.base_url, options.emitter_type.to_s.pluralize)
52
- options = fields.merge(:headers => { 'Accept' => 'application/json' })
53
- response = self.class.post(url.to_s, options)
54
-
55
- unless (200..399).include?(response.code)
56
- raise CalculationRequestFailed, response.body
57
- end
58
-
59
- puts response.body if Carbon.debug
60
-
61
- @result = JSON.parse(response.body)
62
- end
63
- end
64
- end
@@ -1,18 +0,0 @@
1
- require 'carbon/emitter/class_methods'
2
-
3
- module Carbon
4
- module Emitter
5
- class NotYetCalculated < StandardError; end
6
-
7
- def self.included(target)
8
- target.extend Carbon::Emitter::ClassMethods
9
- end
10
-
11
- def emission
12
- return @emission unless @emission.nil?
13
- @emission = Carbon::EmissionsCalculation.new(self.class.emitter_options, self)
14
- @emission.calculate!
15
- @emission
16
- end
17
- end
18
- end
@@ -1,19 +0,0 @@
1
- module Carbon
2
- module Emitter
3
- class Characteristic
4
- attr_accessor :name, :field
5
-
6
- def self.from_options_hash(name, options)
7
- new(:name => name, :field => options[:with])
8
- end
9
-
10
- def initialize(options = {})
11
- options.each { |name, value| self.send("#{name}=", value) unless value.nil? }
12
- end
13
-
14
- def field
15
- @field ||= self.name
16
- end
17
- end
18
- end
19
- end
@@ -1,20 +0,0 @@
1
- require 'carbon/emitter/options'
2
-
3
- module Carbon
4
- module Emitter
5
- module ClassMethods
6
- def emits_as(emitter_type, &blk)
7
- self.emitter_type = emitter_type
8
-
9
- self.emitter_options = Carbon::Emitter::Options.new(emitter_type)
10
- self.emitter_options.instance_eval &blk if block_given?
11
- end
12
-
13
- def emitter_type; @emitter_type; end
14
- def emitter_type=(val); @emitter_type = val; end
15
-
16
- def emitter_options; @emitter_options; end
17
- def emitter_options=(val); @emitter_options = val; end
18
- end
19
- end
20
- end
@@ -1,30 +0,0 @@
1
- require 'carbon/emitter/characteristic'
2
-
3
- module Carbon
4
- module Emitter
5
- class Options
6
- attr_accessor :emitter_type, :characteristics
7
-
8
- def initialize(emitter_type)
9
- self.emitter_type = emitter_type.to_sym
10
- end
11
-
12
- def keys
13
- characteristics.map { |c| c.name }
14
- end
15
-
16
- def [](name)
17
- characteristics.find { |c| c.name == name }
18
- end
19
-
20
- def characteristics
21
- @characteristics ||= []
22
- end
23
-
24
- def provides(characteristic_name, options = {})
25
- characteristics <<
26
- Carbon::Emitter::Characteristic.from_options_hash(characteristic_name, options)
27
- end
28
- end
29
- end
30
- end