carbon 2.2.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+