carbon 2.2.3 → 3.0.0

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/CHANGELOG CHANGED
@@ -1,3 +1,18 @@
1
+ 3.0.0 / 2012-06-08
2
+
3
+ * Breaking changes
4
+
5
+ * Ruby 1.9 only because it uses celluloid / fibers. Use ~2 if you are on Ruby 1.8
6
+
7
+ * Bug fixes
8
+
9
+ * Make sure #as_impact_query includes API key if it's been set globally
10
+
11
+ * Enhancements
12
+
13
+ * When doing multiple queries, use Celluloid [worker] pools instead of creating new threads and using a homegrown futures class
14
+ * Use RSpec as the API testing framework
15
+
1
16
  2.2.3 / 2012-04-12
2
17
 
3
18
  * Enhancements
data/Gemfile CHANGED
@@ -1,13 +1,3 @@
1
1
  source :rubygems
2
2
 
3
3
  gemspec
4
-
5
- # dev deps
6
- gem 'minitest'
7
- gem 'minitest-reporters'
8
- gem 'timeframe'
9
- gem 'webmock'
10
- gem 'aruba'
11
- gem 'cucumber'
12
- gem 'yard'
13
- gem 'avro'
data/Rakefile CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
3
2
 
4
- require 'rake'
5
- require 'rake/testtask'
6
- Rake::TestTask.new(:test) do |test|
7
- test.libs << 'lib'
8
- test.pattern = 'test/**/*_test.rb'
9
- test.verbose = true
10
- end
3
+ require 'bundler/setup'
4
+
5
+ require 'bundler/gem_tasks'
6
+
7
+ require 'rspec/core/rake_task'
8
+ RSpec::Core::RakeTask.new
9
+ task :test => :spec
11
10
 
12
11
  require 'cucumber/rake/task'
13
12
  Cucumber::Rake::Task.new
@@ -17,7 +16,7 @@ YARD::Rake::YardocTask.new do |y|
17
16
  y.options << '--no-private' << '--title' << "Brighter Planet CM1 client for Ruby"
18
17
  end
19
18
 
20
- task :default => [:test, :cucumber]
19
+ task :default => [:spec, :cucumber]
21
20
 
22
21
  namespace :avro do
23
22
  task :setup do
@@ -33,12 +32,12 @@ namespace :avro do
33
32
  $stdout.write ary.sort.join("\n")
34
33
  end
35
34
  task :json => 'avro:setup' do
36
- $stdout.write MultiJson.encode(@cm1.avro_response_schema)
35
+ $stdout.write MultiJson.dump(@cm1.avro_response_schema)
37
36
  end
38
37
  task :example => 'avro:setup' do
39
38
  require 'tempfile'
40
39
  file = Tempfile.new('com.brighterplanet.Cm1.example.avr')
41
- parsed_schema = Avro::Schema.parse(MultiJson.encode(@cm1.avro_response_schema))
40
+ parsed_schema = Avro::Schema.parse(MultiJson.dump(@cm1.avro_response_schema))
42
41
  writer = Avro::IO::DatumWriter.new(parsed_schema)
43
42
  dw = Avro::DataFile::Writer.new(file, writer, parsed_schema)
44
43
  dw << AvroHelper.recursively_stringify_keys(@cm1.example)
@@ -16,12 +16,24 @@ Gem::Specification.new do |s|
16
16
  s.require_paths = ["lib"]
17
17
 
18
18
  s.add_runtime_dependency 'activesupport'
19
- s.add_runtime_dependency 'multi_json'
20
- s.add_runtime_dependency 'hashie'
21
- s.add_runtime_dependency 'cache_method'
22
-
23
- # CLI
24
19
  s.add_runtime_dependency 'bombshell'
25
- s.add_runtime_dependency 'conversions'
26
20
  s.add_runtime_dependency 'brighter_planet_metadata'
21
+ s.add_runtime_dependency 'cache_method'
22
+ s.add_runtime_dependency 'celluloid', '>=0.11.0'
23
+ s.add_runtime_dependency 'conversions'
24
+ s.add_runtime_dependency 'hashie'
25
+ s.add_runtime_dependency 'multi_json'
26
+
27
+ s.add_development_dependency 'aruba'
28
+ s.add_development_dependency 'avro'
29
+ s.add_development_dependency 'cucumber'
30
+ s.add_development_dependency 'fakeweb'
31
+ s.add_development_dependency 'rake'
32
+ s.add_development_dependency 'rspec'
33
+ s.add_development_dependency 'timeframe'
34
+ s.add_development_dependency 'vcr'
35
+ s.add_development_dependency 'yard'
36
+ if RUBY_PLATFORM == 'java'
37
+ s.add_development_dependency 'jruby-openssl'
38
+ end
27
39
  end
@@ -1,7 +1,8 @@
1
1
  require 'active_support/core_ext'
2
2
 
3
+ require 'carbon/query'
4
+ require 'carbon/query_pool'
3
5
  require 'carbon/registry'
4
- require 'carbon/future'
5
6
 
6
7
  module Carbon
7
8
  DOMAIN = 'http://impact.brighterplanet.com'.freeze
@@ -135,63 +136,7 @@ module Carbon
135
136
  # end
136
137
  def Carbon.query(*args)
137
138
  raise ::ArgumentError, "Don't pass a block directly - instead use Carbon.query(array).each (for example)." if block_given?
138
- case Carbon.method_signature(*args)
139
- when :plain_query
140
- plain_query = args
141
- future = Future.wrap plain_query
142
- future.result
143
- when :obj
144
- obj = args.first
145
- future = Future.wrap obj
146
- future.result
147
- when :array
148
- array = args.first
149
- futures = array.map do |obj|
150
- future = Future.wrap obj
151
- future.multi!
152
- future
153
- end
154
- Future.multi(futures).inject({}) do |memo, future|
155
- memo[future.object] = future.result
156
- memo
157
- end
158
- else
159
- raise ::ArgumentError, "You must pass one plain query, or one object that responds to #as_impact_query, or an array of such objects. Please check the docs!"
160
- end
161
- end
162
-
163
- # Determine if a variable is a +[emitter, param]+ style "query"
164
- # @private
165
- def Carbon.is_plain_query?(query)
166
- return false unless query.is_a?(::Array)
167
- return false unless query.first.is_a?(::String) or query.first.is_a?(::Symbol)
168
- return true if query.length == 1
169
- return true if query.length == 2 and query.last.is_a?(::Hash)
170
- false
171
- end
172
-
173
- # Determine what method signature/overloading/calling style is being used
174
- # @private
175
- def Carbon.method_signature(*args)
176
- first_arg = args.first
177
- case args.length
178
- when 1
179
- if is_plain_query?(args)
180
- # query('Flight')
181
- :plain_query
182
- elsif first_arg.respond_to?(:as_impact_query)
183
- # query(my_flight)
184
- :obj
185
- elsif first_arg.is_a?(::Array) and first_arg.all? { |obj| obj.respond_to?(:as_impact_query) or is_plain_query?(obj) }
186
- # query([my_flight, my_flight])
187
- :array
188
- end
189
- when 2
190
- if is_plain_query?(args)
191
- # query('Flight', :origin_airport => 'LAX')
192
- :plain_query
193
- end
194
- end
139
+ Query.perform(*args)
195
140
  end
196
141
 
197
142
  # Called when you +include Carbon+ and adds the class method +emit_as+.
@@ -277,7 +222,11 @@ module Carbon
277
222
  end
278
223
  memo
279
224
  end
280
- [ registration.emitter, params.merge(extra_params) ]
225
+ params.merge! extra_params
226
+ if Carbon.key and not params.has_key?(:key)
227
+ params[:key] = Carbon.key
228
+ end
229
+ [ registration.emitter, params ]
281
230
  end
282
231
 
283
232
  # Get an impact estimate from Brighter Planet CM1; high-level convenience method that requires a {Carbon::ClassMethods#emit_as} block.
@@ -308,7 +257,6 @@ module Carbon
308
257
  # => "http://impact.brighterplanet.com/flights?[...]"
309
258
  def impact(extra_params = {})
310
259
  plain_query = as_impact_query extra_params
311
- future = Future.wrap plain_query
312
- future.result
260
+ Carbon.query(*plain_query)
313
261
  end
314
262
  end
@@ -0,0 +1,140 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'multi_json'
4
+ require 'hashie/mash'
5
+ require 'cache_method'
6
+
7
+ module Carbon
8
+ # @private
9
+ class Query
10
+ def Query.pool
11
+ @pool || Thread.exclusive do
12
+ @pool ||= QueryPool.pool(:size => CONCURRENCY)
13
+ end
14
+ end
15
+
16
+ def Query.perform(*args)
17
+ case method_signature(*args)
18
+ when :plain_query, :obj
19
+ new(*args).result
20
+ when :array
21
+ queries = args.first.map do |plain_query_or_obj|
22
+ query = new(*plain_query_or_obj)
23
+ pool.perform! query
24
+ query
25
+ end
26
+ ticks = 0
27
+ begin
28
+ sleep(0.1*(2**ticks)) # exponential wait
29
+ ticks += 1
30
+ end until queries.all? { |query| query.done? }
31
+ queries.inject({}) do |memo, query|
32
+ memo[query.object] = query.result
33
+ memo
34
+ end
35
+ else
36
+ raise ::ArgumentError, "You must pass one plain query, or one object that responds to #as_impact_query, or an array of such objects. Please check the docs!"
37
+ end
38
+ end
39
+
40
+ # Determine if a variable is a +[emitter, param]+ style "query"
41
+ # @private
42
+ def Query.is_plain_query?(query)
43
+ return false unless query.is_a?(Array)
44
+ return false unless query.first.is_a?(String) or query.first.is_a?(Symbol)
45
+ return true if query.length == 1
46
+ return true if query.length == 2 and query.last.is_a?(Hash)
47
+ false
48
+ end
49
+
50
+ # Determine what method signature/overloading/calling style is being used
51
+ # @private
52
+ def Query.method_signature(*args)
53
+ first_arg = args.first
54
+ case args.length
55
+ when 1
56
+ if is_plain_query?(args)
57
+ # query('Flight')
58
+ :plain_query
59
+ elsif first_arg.respond_to?(:as_impact_query)
60
+ # query(my_flight)
61
+ :obj
62
+ elsif first_arg.is_a?(::Array) and first_arg.all? { |obj| obj.respond_to?(:as_impact_query) or is_plain_query?(obj) }
63
+ # query([my_flight, my_flight])
64
+ :array
65
+ end
66
+ when 2
67
+ if is_plain_query?(args)
68
+ # query('Flight', :origin_airport => 'LAX')
69
+ :plain_query
70
+ end
71
+ end
72
+ end
73
+
74
+ attr_reader :emitter
75
+ attr_reader :params
76
+ attr_reader :domain
77
+ attr_reader :uri
78
+ attr_reader :object
79
+
80
+ def initialize(*args)
81
+ case Query.method_signature(*args)
82
+ when :plain_query
83
+ @object = args
84
+ @emitter, @params = *args
85
+ when :obj
86
+ @object = args.first
87
+ @emitter, @params = *object.as_impact_query
88
+ else
89
+ raise ArgumentError, "Carbon::Query.new must be called with a plain query or an object that responds to #as_impact_query"
90
+ end
91
+ @params ||= {}
92
+ @domain = params.delete(:domain) || Carbon.domain
93
+ if Carbon.key and not params.has_key?(:key)
94
+ params[:key] = Carbon.key
95
+ end
96
+ @uri = URI.parse("#{domain}/#{emitter.underscore.pluralize}.json")
97
+ end
98
+
99
+ def done?
100
+ not @result.nil? or cache_method_cached?(:result)
101
+ end
102
+
103
+ def result(extra_params = {})
104
+ @result ||= get_result(extra_params)
105
+ end
106
+ cache_method :result, 3_600 # one hour
107
+
108
+ def as_cache_key
109
+ [ @domain, @emitter, @params ]
110
+ end
111
+
112
+ def hash
113
+ as_cache_key.hash
114
+ end
115
+
116
+ def eql?(other)
117
+ as_cache_key == other.as_cache_key
118
+ end
119
+ alias :== :eql?
120
+
121
+ private
122
+
123
+ def get_result(extra_params = {})
124
+ raw = Net::HTTP.post_form uri, params.merge(extra_params)
125
+ code = raw.code.to_i
126
+ body = raw.body
127
+ memo = Hashie::Mash.new
128
+ memo.code = code
129
+ case code
130
+ when (200..299)
131
+ memo.success = true
132
+ memo.merge! MultiJson.load(body)
133
+ else
134
+ memo.success = false
135
+ memo.errors = [body]
136
+ end
137
+ memo
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,11 @@
1
+ require 'celluloid'
2
+
3
+ module Carbon
4
+ class QueryPool
5
+ include Celluloid
6
+
7
+ def perform(query)
8
+ query.result
9
+ end
10
+ end
11
+ end
@@ -10,7 +10,7 @@ module Carbon
10
10
  class << self
11
11
  # @private
12
12
  def characteristics(emitter)
13
- ::MultiJson.decode ::Net::HTTP.get(::URI.parse("http://impact.brighterplanet.com/#{emitter.underscore.pluralize}/options.json"))
13
+ ::MultiJson.load ::Net::HTTP.get(::URI.parse("http://impact.brighterplanet.com/#{emitter.underscore.pluralize}/options.json"))
14
14
  rescue
15
15
  # oops
16
16
  end
@@ -1,3 +1,3 @@
1
1
  module Carbon
2
- VERSION = "2.2.3"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -0,0 +1,106 @@
1
+ require 'helper'
2
+ require 'carbon/query'
3
+ require 'my_nissan_altima'
4
+
5
+ describe Carbon::Query do
6
+ let(:query) { Carbon::Query.new 'Flight' }
7
+
8
+ describe '#as_impact_query' do
9
+ it 'sets up an query to be run by Carbon.query' do
10
+ a = MyNissanAltima.new(2006)
11
+ a.as_impact_query.should == ["Automobile", {:make=>"Nissan", :model=>"Altima", :year=>2006, "automobile_fuel[code]"=>"R", :key=>Carbon.key}]
12
+ end
13
+ it 'only includes non-nil params' do
14
+ a = MyNissanAltima.new(2006)
15
+ a.as_impact_query[1].keys.should include(:year)
16
+ a.as_impact_query[1].keys.should_not include(:nil_model)
17
+ a.as_impact_query[1].keys.should_not include(:nil_make)
18
+ end
19
+ it 'includes Carbon.key' do
20
+ begin
21
+ random_key = rand(1e11)
22
+ old_carbon_key = Carbon.key
23
+ Carbon.key = random_key
24
+ a = MyNissanAltima.new(2006)
25
+ a.as_impact_query[1][:key].should == random_key
26
+ ensure
27
+ Carbon.key = old_carbon_key
28
+ end
29
+ end
30
+ it "allows key to be set" do
31
+ begin
32
+ random_key = rand(1e11)
33
+ old_carbon_key = Carbon.key
34
+ Carbon.key = random_key
35
+ a = MyNissanAltima.new(2006)
36
+ a.as_impact_query(:key => 'i want to use this key!')[1][:key].should == 'i want to use this key!'
37
+ ensure
38
+ Carbon.key = old_carbon_key
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ describe '.method_signature' do
45
+ it 'recognizes emitter_param' do
46
+ Carbon::Query.method_signature('Flight').should == :plain_query
47
+ Carbon::Query.method_signature('Flight', :origin_airport => 'LAX').should == :plain_query
48
+ Carbon::Query.method_signature(:flight).should == :plain_query
49
+ Carbon::Query.method_signature(:flight, :origin_airport => 'LAX').should == :plain_query
50
+ end
51
+ it 'recognizes an object' do
52
+ Carbon::Query.method_signature(MyNissanAltima.new(2006)).should == :obj
53
+ end
54
+ it 'recognizes an array of signatures' do
55
+ Carbon::Query.method_signature([MyNissanAltima.new(2001)]).should == :array
56
+ Carbon::Query.method_signature([['Flight']]).should == :array
57
+ Carbon::Query.method_signature([['Flight', {:origin_airport => 'LAX'}]]).should == :array
58
+ Carbon::Query.method_signature([['Flight'], ['Flight']]).should == :array
59
+ Carbon::Query.method_signature([['Flight', {:origin_airport => 'LAX'}], ['Flight', {:origin_airport => 'LAX'}]]).should == :array
60
+ [MyNissanAltima.new(2006), ['Flight'], ['Flight', {:origin_airport => 'LAX'}]].permutation.each do |p|
61
+ Carbon::Query.method_signature(p).should == :array
62
+ end
63
+ end
64
+ it "does not accept splats for concurrent queries" do
65
+ Carbon::Query.method_signature(['Flight'], ['Flight']).should be_nil
66
+ Carbon::Query.method_signature(MyNissanAltima.new(2001), MyNissanAltima.new(2001)).should be_nil
67
+ [MyNissanAltima.new(2006), ['Flight'], ['Flight', {:origin_airport => 'LAX'}]].permutation.each do |p|
68
+ Carbon::Query.method_signature(*p).should be_nil
69
+ end
70
+ end
71
+ it "does not like weirdness" do
72
+ Carbon::Query.method_signature('Flight', 'Flight').should be_nil
73
+ Carbon::Query.method_signature('Flight', ['Flight']).should be_nil
74
+ Carbon::Query.method_signature(['Flight'], 'Flight').should be_nil
75
+ Carbon::Query.method_signature(['Flight', 'Flight']).should be_nil
76
+ Carbon::Query.method_signature(['Flight', ['Flight']]).should be_nil
77
+ Carbon::Query.method_signature([['Flight'], 'Flight']).should be_nil
78
+ Carbon::Query.method_signature(MyNissanAltima.new(2001), [MyNissanAltima.new(2001)]).should be_nil
79
+ Carbon::Query.method_signature([MyNissanAltima.new(2001)], MyNissanAltima.new(2001)).should be_nil
80
+ Carbon::Query.method_signature([MyNissanAltima.new(2001)], [MyNissanAltima.new(2001)]).should be_nil
81
+ Carbon::Query.method_signature([MyNissanAltima.new(2001), [MyNissanAltima.new(2001)]]).should be_nil
82
+ Carbon::Query.method_signature([[MyNissanAltima.new(2001)], MyNissanAltima.new(2001)]).should be_nil
83
+ Carbon::Query.method_signature([[MyNissanAltima.new(2001)], [MyNissanAltima.new(2001)]]).should be_nil
84
+ end
85
+ end
86
+
87
+ describe '.perform' do
88
+ it 'returns a single result for a single query' do
89
+ VCR.use_cassette 'Flight', :record => :once do
90
+ Carbon::Query.perform('Flight').should be_a(Hashie::Mash)
91
+ end
92
+ end
93
+ it 'returns a hash of queries and results for multiple queries' do
94
+ results = nil
95
+ VCR.use_cassette 'Flight and Automobile', :record => :once do
96
+ results = Carbon::Query.perform([['Flight'], ['Automobile']])
97
+ end
98
+ results.length.should == 2
99
+ results.keys.should == [['Flight'], ['Automobile']]
100
+ results.values.each do |val|
101
+ val.should be_a(Hashie::Mash)
102
+ end
103
+ end
104
+ end
105
+ end
106
+