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 +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
|
+
|