yoga_pants 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +53 -0
- data/Rakefile +11 -0
- data/fixtures/vcr_cassettes/before_block.yml +165 -0
- data/fixtures/vcr_cassettes/bulk.yml +69 -0
- data/fixtures/vcr_cassettes/bulk_document_index_error.yml +69 -0
- data/fixtures/vcr_cassettes/bulk_error.yml +355 -0
- data/fixtures/vcr_cassettes/error.yml +290 -0
- data/fixtures/vcr_cassettes/failover_refused.yml +25 -0
- data/fixtures/vcr_cassettes/failover_timeout.yml +25 -0
- data/fixtures/vcr_cassettes/indexing.yml +48 -0
- data/fixtures/vcr_cassettes/invalid_request.yml +267 -0
- data/fixtures/vcr_cassettes/missing.yml +267 -0
- data/fixtures/vcr_cassettes/multi_search.yml +37 -0
- data/lib/yoga_pants/client.rb +144 -0
- data/lib/yoga_pants/connection.rb +150 -0
- data/lib/yoga_pants/json.rb +25 -0
- data/lib/yoga_pants/version.rb +3 -0
- data/lib/yoga_pants.rb +10 -0
- data/spec/integration/basic_spec.rb +205 -0
- data/spec/spec_helper.rb +9 -0
- data/yoga_pants.gemspec +26 -0
- metadata +138 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
module YogaPants
|
2
|
+
class Client
|
3
|
+
# This class will handle:
|
4
|
+
# * connecting to ES nodes
|
5
|
+
# * failing over to nodes in a list
|
6
|
+
# * ES-specific error handling
|
7
|
+
|
8
|
+
class RequestError < RuntimeError
|
9
|
+
attr_reader :http_error
|
10
|
+
|
11
|
+
def initialize(message, http_error = nil)
|
12
|
+
@http_error = nil
|
13
|
+
super(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :hosts, :options, :active_host
|
18
|
+
|
19
|
+
def initialize(hosts, options = {})
|
20
|
+
@hosts = [hosts].flatten.freeze # Accept 1 or more hosts
|
21
|
+
@options = options
|
22
|
+
@max_retries = options[:max_retries] || 10
|
23
|
+
@retries = 0
|
24
|
+
reset_hosts
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset_hosts
|
28
|
+
@active_hosts = hosts.dup
|
29
|
+
@active_host = @active_hosts.shift
|
30
|
+
end
|
31
|
+
|
32
|
+
def get(path, args = {})
|
33
|
+
with_error_handling do
|
34
|
+
connection.get(path, args)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def post(path, args = {})
|
39
|
+
with_error_handling do
|
40
|
+
connection.post(path, args)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def put(path, args = {})
|
45
|
+
with_error_handling do
|
46
|
+
connection.put(path, args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(path, args = {})
|
51
|
+
with_error_handling do
|
52
|
+
connection.delete(path, args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
BULK_OPERATIONS_WITH_DATA = [:index, :create].freeze
|
57
|
+
def bulk(path, operations, args = {})
|
58
|
+
path = path.sub(%r{/(?:_bulk)?$}, '/_bulk')
|
59
|
+
|
60
|
+
with_error_handling do
|
61
|
+
payload = StringIO.new
|
62
|
+
|
63
|
+
operations.each do |action, metadata, data|
|
64
|
+
payload << JSON.dump({action => metadata})
|
65
|
+
payload << "\n"
|
66
|
+
if BULK_OPERATIONS_WITH_DATA.include?(action.to_sym)
|
67
|
+
payload << JSON.dump(data)
|
68
|
+
payload << "\n"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
payload.rewind
|
73
|
+
connection.post(path, :query_string => args, :body => payload.read)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def multi_search(path, operations, args={})
|
78
|
+
path = path.sub(%r{/?(?:_msearch)?$}, '/_msearch')
|
79
|
+
|
80
|
+
with_error_handling do
|
81
|
+
payload = StringIO.new
|
82
|
+
|
83
|
+
operations.each do |header, body|
|
84
|
+
payload << JSON.dump(header) << "\n"
|
85
|
+
payload << JSON.dump(body) << "\n"
|
86
|
+
end
|
87
|
+
|
88
|
+
payload.rewind
|
89
|
+
connection.get(path, :query_string => args, :body => payload.read)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def exists?(path, args = {})
|
94
|
+
with_error_handling do
|
95
|
+
begin
|
96
|
+
if path.count("/") >= 3 # More than
|
97
|
+
connection.get(path, args)
|
98
|
+
else
|
99
|
+
connection.head(path).status_code == 200
|
100
|
+
end
|
101
|
+
rescue Connection::HTTPError => e
|
102
|
+
if e.status_code == 404
|
103
|
+
false
|
104
|
+
else
|
105
|
+
raise e
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def reset
|
112
|
+
connection.reset
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def connection
|
118
|
+
@connection ||= Connection.new(active_host, options[:connection])
|
119
|
+
end
|
120
|
+
|
121
|
+
def pick_next_host
|
122
|
+
@active_host = @active_hosts.shift
|
123
|
+
@connection = nil
|
124
|
+
reset_hosts if active_host.nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
def with_error_handling(&block)
|
128
|
+
block.call.tap do
|
129
|
+
@retries = 0
|
130
|
+
end
|
131
|
+
rescue Connection::HTTPError => e
|
132
|
+
|
133
|
+
if @retries <= @max_retries
|
134
|
+
@retries += 1
|
135
|
+
pick_next_host
|
136
|
+
retry
|
137
|
+
elsif e.body.is_a?(Hash) && error = e.body['error']
|
138
|
+
raise RequestError.new("ElasticSearch Error: #{error}", e).tap { |ex| ex.set_backtrace(e.backtrace) }
|
139
|
+
else
|
140
|
+
raise RequestError.new(e.message, e).tap { |ex| ex.set_backtrace(e.backtrace) }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module YogaPants
|
2
|
+
class Connection
|
3
|
+
# This class is/will be an abstraction layer for the underlying
|
4
|
+
# HTTP client.
|
5
|
+
# TODO: Use https://github.com/rubiii/httpi so we don't have to deal
|
6
|
+
# with interfacing with multiple HTTP libraries
|
7
|
+
|
8
|
+
attr_accessor :host, :options
|
9
|
+
|
10
|
+
class HTTPError < RuntimeError
|
11
|
+
attr_reader :response, :status_code
|
12
|
+
def initialize(message, response = nil)
|
13
|
+
@response = response
|
14
|
+
if response
|
15
|
+
@status_code = response.status_code
|
16
|
+
super(message + "\nBody: #{response.body}")
|
17
|
+
else
|
18
|
+
super(message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def body
|
23
|
+
return nil if response.nil?
|
24
|
+
@body ||= begin
|
25
|
+
JSON.load(response.body)
|
26
|
+
rescue MultiJson::DecodeError
|
27
|
+
response.body
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(host, options = {})
|
33
|
+
@host = host.chomp('/')
|
34
|
+
@options = options || {}
|
35
|
+
@http = HTTPClient.new
|
36
|
+
|
37
|
+
default_timeout = @options[:timeout] || 5
|
38
|
+
http.connect_timeout = @options[:connect_timeout] || default_timeout
|
39
|
+
http.send_timeout = @options[:send_timeout] || default_timeout
|
40
|
+
http.receive_timeout = @options[:receive_timeout] || default_timeout
|
41
|
+
end
|
42
|
+
|
43
|
+
# Body can be a string or hash
|
44
|
+
|
45
|
+
def get(path, args = {})
|
46
|
+
with_error_handling do
|
47
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
48
|
+
http.get(url_for(path), :query => query_string, :body => body)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def post(path, args = {})
|
54
|
+
with_error_handling do
|
55
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
56
|
+
response = http.post(url_for(path), :query => query_string, :body => body)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def put(path, args = {})
|
62
|
+
with_error_handling do
|
63
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
64
|
+
response = http.put(url_for(path), :query => query_string, :body => body)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete(path, args = {})
|
70
|
+
with_error_handling do
|
71
|
+
parse_arguments_and_handle_response(args) do |query_string, body|
|
72
|
+
response = http.delete(url_for(path), :query => query_string, :body => body)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def head(path, args = {})
|
78
|
+
with_error_handling do
|
79
|
+
query_string, _ = parse_arguments(args)
|
80
|
+
http.head(url_for(path), :query => query_string)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def reset
|
85
|
+
http.reset_all
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def parse_arguments_and_handle_response(args, &block)
|
91
|
+
query_string, body = parse_arguments(args)
|
92
|
+
parse_and_handle_response(
|
93
|
+
block.call(query_string, body)
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse_and_handle_response(response)
|
98
|
+
case response.status_code
|
99
|
+
when 200..299
|
100
|
+
JSON.load(response.body)
|
101
|
+
else
|
102
|
+
raise HTTPError.new("Error performing HTTP request: #{response.status_code} #{response.reason}", response)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_arguments(args)
|
107
|
+
[args[:query_string], jsonify_body(args[:body])]
|
108
|
+
end
|
109
|
+
|
110
|
+
def with_error_handling(&block)
|
111
|
+
block.call
|
112
|
+
rescue HTTPError => e
|
113
|
+
raise e
|
114
|
+
rescue Errno::ECONNREFUSED
|
115
|
+
raise HTTPError.new("Connection refused to #{host}")
|
116
|
+
rescue HTTPClient::ConnectTimeoutError
|
117
|
+
raise HTTPError.new("Connection timed out to #{host}")
|
118
|
+
rescue HTTPClient::SendTimeoutError
|
119
|
+
raise HTTPError.new("Request send timed out to #{host}")
|
120
|
+
rescue HTTPClient::ReceiveTimeoutError
|
121
|
+
raise HTTPError.new("Receive timed out from #{host}")
|
122
|
+
rescue => e
|
123
|
+
raise HTTPError.new("Unhandled exception within YogaPants::Connection: #{e} - #{e.message}").tap { |ex| ex.set_backtrace(e.backtrace) }
|
124
|
+
end
|
125
|
+
|
126
|
+
def jsonify_body(string_or_hash)
|
127
|
+
return nil if string_or_hash.nil?
|
128
|
+
case string_or_hash
|
129
|
+
when Hash
|
130
|
+
JSON.dump(string_or_hash)
|
131
|
+
when String
|
132
|
+
string_or_hash
|
133
|
+
else
|
134
|
+
raise ArgumentError.new("Unrecognised body class #{string_or_hash.class}")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def http
|
139
|
+
@http
|
140
|
+
end
|
141
|
+
|
142
|
+
def url_for(path)
|
143
|
+
if path[0..0] == "/"
|
144
|
+
"#{host}#{path}"
|
145
|
+
else
|
146
|
+
"#{host}/#{path}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
# FIXME: This whole thing is cancer. tire uses multi_json 1.0.3 which
|
4
|
+
# uses decode/encode.
|
5
|
+
|
6
|
+
module YogaPants
|
7
|
+
class JSON
|
8
|
+
def self.load(*args)
|
9
|
+
if MultiJson.respond_to?(:decode)
|
10
|
+
MultiJson.decode(*args)
|
11
|
+
else
|
12
|
+
MultiJson.load(*args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.dump(*args)
|
17
|
+
if MultiJson.respond_to?(:encode)
|
18
|
+
MultiJson.encode(*args)
|
19
|
+
else
|
20
|
+
MultiJson.dump(*args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/yoga_pants.rb
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module YogaPants
|
4
|
+
describe "basic integration tests" do
|
5
|
+
subject do
|
6
|
+
Client.new("http://localhost:9200/")
|
7
|
+
end
|
8
|
+
|
9
|
+
before do
|
10
|
+
VCR.use_cassette('before_block') do
|
11
|
+
if subject.exists?("/yoga_pants_test")
|
12
|
+
subject.delete("/yoga_pants_test")
|
13
|
+
end
|
14
|
+
|
15
|
+
if subject.exists?("/yoga_pants_test_1")
|
16
|
+
subject.delete("/yoga_pants_test_1")
|
17
|
+
end
|
18
|
+
|
19
|
+
subject.post("/yoga_pants_test")
|
20
|
+
subject.put("/yoga_pants_test/doc/_mapping", :body => {
|
21
|
+
:doc => {
|
22
|
+
:properties => {
|
23
|
+
:foo => {:type => 'string'},
|
24
|
+
:bar => {:type => 'integer'}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
})
|
28
|
+
subject.post("/yoga_pants_test/_refresh")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "indexes a valid document" do
|
33
|
+
VCR.use_cassette('indexing') do
|
34
|
+
subject.post("/yoga_pants_test/doc/1", :body => {
|
35
|
+
:foo => 'bar'
|
36
|
+
})
|
37
|
+
subject.get("/yoga_pants_test/doc/1").should include({
|
38
|
+
'_index' => 'yoga_pants_test',
|
39
|
+
'_type' => 'doc',
|
40
|
+
'_id' => '1',
|
41
|
+
'exists' => true,
|
42
|
+
'_source' => {
|
43
|
+
'foo' => 'bar'
|
44
|
+
}
|
45
|
+
})
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "bulk operations" do
|
50
|
+
it 'does bulk operations just fine' do
|
51
|
+
VCR.use_cassette('bulk') do
|
52
|
+
subject.bulk("/", [
|
53
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 2}, {:foo => 'hello bulk'}],
|
54
|
+
[:index, {:_index => 'yoga_pants_test_1', :_type => 'doc2', :_id => 2}, {:foo => 'hello bulk 2'}]
|
55
|
+
], :refresh => true)
|
56
|
+
|
57
|
+
subject.get("/yoga_pants_test/doc/2").should include({
|
58
|
+
'_index' => 'yoga_pants_test',
|
59
|
+
'_type' => 'doc',
|
60
|
+
'_id' => '2',
|
61
|
+
'exists' => true,
|
62
|
+
'_source' => {
|
63
|
+
'foo' => 'hello bulk'
|
64
|
+
}
|
65
|
+
})
|
66
|
+
|
67
|
+
subject.get("/yoga_pants_test_1/doc2/2").should include({
|
68
|
+
'_index' => 'yoga_pants_test_1',
|
69
|
+
'_type' => 'doc2',
|
70
|
+
'_id' => '2',
|
71
|
+
'exists' => true,
|
72
|
+
'_source' => {
|
73
|
+
'foo' => 'hello bulk 2'
|
74
|
+
}
|
75
|
+
})
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'does not break on per-document index errors' do
|
80
|
+
VCR.use_cassette('bulk_document_index_error') do
|
81
|
+
ret = subject.bulk("/", [
|
82
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 1}, {:bar => 1}],
|
83
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 2}, {:bar => 'invalid'}],
|
84
|
+
])
|
85
|
+
errors = ret['items'].select { |operation| operation['index']['error'] }
|
86
|
+
errors.length.should == 1
|
87
|
+
errors.first["index"]["_id"].should == '2'
|
88
|
+
subject.exists?("/yoga_pants_test/doc/1").should be_true
|
89
|
+
subject.exists?("/yoga_pants_test/doc/2").should be_false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'throws an exception when ES barfs' do
|
94
|
+
VCR.use_cassette('bulk_error') do
|
95
|
+
expect do
|
96
|
+
subject.bulk("/", [
|
97
|
+
[:index, {:_index => 'yoga_pants_test', :_type => 'doc', :_id => 1}, {:bar => 1}],
|
98
|
+
[:index, {:_index => '', :_type => 'doc', :_id => 2}, {:bar => 'invalid'}],
|
99
|
+
])
|
100
|
+
end.to raise_error(Client::RequestError, "ElasticSearch Error: ElasticSearchException[String index out of range: 0]; nested: StringIndexOutOfBoundsException[String index out of range: 0]; ")
|
101
|
+
|
102
|
+
subject.exists?("/yoga_pants_test/doc/1").should be_false
|
103
|
+
subject.exists?("/yoga_pants_test/doc/2").should be_false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "multi search" do
|
109
|
+
it 'does bulk operations just fine' do
|
110
|
+
VCR.use_cassette('multi_search') do
|
111
|
+
result = subject.multi_search("/", [
|
112
|
+
[{'index' => 'yoga_pants_test', 'type' => 'doc'}, {'query' => {'match_all' => {}}}],
|
113
|
+
[{'index' => 'yoga_pants_test_1', 'type' => 'doc2'}, {'query' => {'match_all' => {}}}],
|
114
|
+
])
|
115
|
+
|
116
|
+
result.should have_key('responses')
|
117
|
+
result['responses'].size.should == 2
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it "raises an exception on missing documents" do
|
124
|
+
VCR.use_cassette('missing') do
|
125
|
+
expect { subject.get("/yoga_pants_test/doc/not_exist") }.to raise_error(Client::RequestError, "Error performing HTTP request: 404 Not Found\nBody: #{'{"_index":"yoga_pants_test","_type":"doc","_id":"not_exist","exists":false}'}")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "raises an exception on an invalid request" do
|
130
|
+
VCR.use_cassette('invalid_request') do
|
131
|
+
expect { subject.get("/this_does_not_exist") }.to raise_error(Client::RequestError, "Error performing HTTP request: 400 Bad Request\nBody: No handler found for uri [/this_does_not_exist] and method [GET]")
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it "raises an exception when ES returns a hash with an error object" do
|
136
|
+
VCR.use_cassette('error') do
|
137
|
+
subject.post("/yoga_pants_test/doc/1", :body => {
|
138
|
+
:foo => 'bar'
|
139
|
+
})
|
140
|
+
expect { subject.get("/yoga_pants_test/doc/1/_mlt?min_term_freq=invalid") }.to raise_error(Client::RequestError, "ElasticSearch Error: Failed to parse int parameter [min_term_freq] with value [invalid]")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "network-related failures" do
|
147
|
+
let(:host) { "http://localhost:9200" }
|
148
|
+
subject do
|
149
|
+
Client.new(host, :connection => {:connect_timeout => 0.01})
|
150
|
+
end
|
151
|
+
|
152
|
+
context "connection refused" do
|
153
|
+
let(:host) { "http://localhost:1" } # Unlikely to be anything running on this port
|
154
|
+
|
155
|
+
it "raises an RequestError" do
|
156
|
+
VCR.use_cassette('connection_refused') do
|
157
|
+
expect { subject.exists?("/foo") }.to raise_error(Client::RequestError, "Connection refused to http://localhost:1")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context "connection timed out" do
|
163
|
+
let(:host) { "http://192.168.34.58" } # Ewww but yeah.
|
164
|
+
|
165
|
+
it "raises an RequestError" do
|
166
|
+
VCR.use_cassette('timed_out') do
|
167
|
+
expect { subject.exists?("/foo") }.to raise_error(Client::RequestError, "Connection timed out to #{host}")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe "failing over to other nodes" do
|
174
|
+
subject do
|
175
|
+
Client.new(hosts)
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "connection refused on first node" do
|
179
|
+
let(:hosts) { ["http://localhost:1/", "http://localhost:9200/"] }
|
180
|
+
it "automatically fails over" do
|
181
|
+
VCR.use_cassette('failover_refused') do
|
182
|
+
subject.exists?("/foo").should == false
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "connection timed out on first node" do
|
188
|
+
let(:hosts) { ["http://10.13.37.3:1/", "http://localhost:9200/"] }
|
189
|
+
it "automatically fails over" do
|
190
|
+
VCR.use_cassette('failover_timeout') do
|
191
|
+
subject.exists?("/foo").should == false
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe "no working hosts" do
|
197
|
+
let(:hosts) { ["http://localhost:1/", "http://localhost:2/"] }
|
198
|
+
it "throws an exception" do
|
199
|
+
VCR.use_cassette('failover_impossible') do
|
200
|
+
expect { subject.exists?("/foo") }.to raise_error(Client::RequestError, /Connection refused to/)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'vcr'
|
2
|
+
require 'yoga_pants'
|
3
|
+
|
4
|
+
VCR.configure do |c|
|
5
|
+
c.cassette_library_dir = 'fixtures/vcr_cassettes'
|
6
|
+
c.hook_into :webmock
|
7
|
+
c.allow_http_connections_when_no_cassette = true if ENV['ALLOW_NO_CASSETTE']
|
8
|
+
c.ignore_localhost = true if ENV['INTEGRATION']
|
9
|
+
end
|
data/yoga_pants.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/yoga_pants/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jack Chen (chendo)"]
|
6
|
+
gem.description = %q{A super lightweight interface to ElasticSearch's HTTP REST API}
|
7
|
+
gem.summary = <<-TEXT.strip
|
8
|
+
A super lightweight interface to ElasticSearch's HTTP REST API.
|
9
|
+
TEXT
|
10
|
+
gem.homepage = "https://github.com/chendo/yoga_pants"
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.name = "yoga_pants"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = YogaPants::VERSION
|
18
|
+
|
19
|
+
gem.add_runtime_dependency 'httpclient', '2.2.5'
|
20
|
+
gem.add_runtime_dependency 'multi_json'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
|
+
gem.add_development_dependency 'vcr'
|
24
|
+
gem.add_development_dependency 'webmock'
|
25
|
+
gem.add_development_dependency 'rake'
|
26
|
+
end
|