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 +15 -0
- data/Gemfile +0 -10
- data/Rakefile +10 -11
- data/carbon.gemspec +18 -6
- data/lib/carbon.rb +9 -61
- data/lib/carbon/query.rb +140 -0
- data/lib/carbon/query_pool.rb +11 -0
- data/lib/carbon/shell/emitter.rb +1 -1
- data/lib/carbon/version.rb +1 -1
- data/spec/carbon/query_spec.rb +106 -0
- data/spec/carbon_spec.rb +237 -0
- data/spec/cassettes/2006_Altima.yml +69 -0
- data/spec/cassettes/2006_Altima_in_2010.yml +230 -0
- data/spec/cassettes/2006_Altima_in_2011.yml +1239 -0
- data/spec/cassettes/Flight.yml +59 -0
- data/spec/cassettes/Flight_and_Automobile.yml +60 -0
- data/spec/cassettes/LAX-_SFO_flight.yml +74 -0
- data/spec/cassettes/Monkey.yml +42 -0
- data/spec/cassettes/carbon_bp_com_flight.yml +58 -0
- data/spec/cassettes/flight_with_key_1.yml +59 -0
- data/spec/cassettes/flight_with_key_2.yml +59 -0
- data/spec/cassettes/timeframed_flight.yml +59 -0
- data/spec/helper.rb +25 -0
- data/spec/support/my_nissan_altima.rb +36 -0
- metadata +200 -13
- data/lib/carbon/future.rb +0 -109
- data/test/carbon_test.rb +0 -302
- data/test/helper.rb +0 -25
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
data/Rakefile
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
#!/usr/bin/env rake
|
2
|
-
require "bundler/gem_tasks"
|
3
2
|
|
4
|
-
require '
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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 => [:
|
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.
|
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.
|
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)
|
data/carbon.gemspec
CHANGED
@@ -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
|
data/lib/carbon.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
312
|
-
future.result
|
260
|
+
Carbon.query(*plain_query)
|
313
261
|
end
|
314
262
|
end
|
data/lib/carbon/query.rb
ADDED
@@ -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
|
data/lib/carbon/shell/emitter.rb
CHANGED
@@ -10,7 +10,7 @@ module Carbon
|
|
10
10
|
class << self
|
11
11
|
# @private
|
12
12
|
def characteristics(emitter)
|
13
|
-
::MultiJson.
|
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
|
data/lib/carbon/version.rb
CHANGED
@@ -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
|
+
|