carbon 0.0.7 → 0.1.1

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