bettery 0.0.1

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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/.yardopts +5 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +38 -0
  9. data/Rakefile +6 -0
  10. data/bettery.gemspec +26 -0
  11. data/lib/bettery.rb +28 -0
  12. data/lib/bettery/client.rb +170 -0
  13. data/lib/bettery/client/projects.rb +45 -0
  14. data/lib/bettery/configurable.rb +41 -0
  15. data/lib/bettery/default.rb +92 -0
  16. data/lib/bettery/error.rb +115 -0
  17. data/lib/bettery/project.rb +45 -0
  18. data/lib/bettery/response/raise_error.rb +21 -0
  19. data/lib/bettery/version.rb +3 -0
  20. data/spec/bettery/client/projects_spec.rb +38 -0
  21. data/spec/bettery/client_spec.rb +307 -0
  22. data/spec/bettery/project_spec.rb +51 -0
  23. data/spec/bettery_spec.rb +51 -0
  24. data/spec/cassettes/Bettery_Client/_get/handles_query_params.json +1 -0
  25. data/spec/cassettes/Bettery_Client/_head/handles_query_params.json +1 -0
  26. data/spec/cassettes/Bettery_Client/_last_response/caches_the_last_agent_response.json +1 -0
  27. data/spec/cassettes/Bettery_Client_Projects/_project/returns_the_matching_project.json +1 -0
  28. data/spec/cassettes/Bettery_Client_Projects/_project_/returns_false_if_the_project_doesn_t_exist.json +1 -0
  29. data/spec/cassettes/Bettery_Client_Projects/_project_/returns_true_if_the_project_exists.json +1 -0
  30. data/spec/cassettes/Bettery_Client_Projects/_projects/returns_projects_on_betterplace.json +1 -0
  31. data/spec/spec_helper.rb +84 -0
  32. data/yard/default/fulldoc/html/css/common.css +3 -0
  33. data/yard/default/layout/html/setup.rb +17 -0
  34. metadata +117 -0
@@ -0,0 +1,92 @@
1
+ require 'bettery/response/raise_error'
2
+ require 'bettery/version'
3
+
4
+ module Bettery
5
+
6
+ # Default configuration options for {Client}
7
+ module Default
8
+
9
+ # Default API endpoint
10
+ API_BASE_ENDPOINT = "https://api.betterplace.org"
11
+
12
+ # Default User Agent header string
13
+ USER_AGENT = "Bettery Ruby Gem #{Bettery::VERSION}"
14
+
15
+ # Default media type
16
+ MEDIA_TYPE = "application/json"
17
+
18
+ # Default localization
19
+ LOCALE = "de"
20
+
21
+ # Default Faraday middleware stack
22
+ MIDDLEWARE = Faraday::RackBuilder.new do |builder|
23
+ builder.use Bettery::Response::RaiseError
24
+ builder.adapter Faraday.default_adapter
25
+ end
26
+
27
+ class << self
28
+
29
+ # Configuration options
30
+ # @return [Hash]
31
+ def options
32
+ Hash[Bettery::Configurable.keys.map{|key| [key, send(key)]}]
33
+ end
34
+
35
+ # Default locale from ENV or {LOCALE}
36
+ # @return [String]
37
+ def locale
38
+ ENV['BETTERY_LOCALE'] || LOCALE
39
+ end
40
+
41
+ # Default API base endpoint from ENV or {API_BASE_ENDPOINT}
42
+ # @return [String]
43
+ def api_base_endpoint
44
+ ENV['BETTERY_API_BASE_ENDPOINT'] || API_BASE_ENDPOINT
45
+ end
46
+
47
+ # Default options for Faraday::Connection
48
+ # @return [Hash]
49
+ def connection_options
50
+ {
51
+ headers: {
52
+ accept: default_media_type,
53
+ user_agent: user_agent
54
+ }
55
+ }
56
+ end
57
+
58
+ # Default media type from ENV or {MEDIA_TYPE}
59
+ # @return [String]
60
+ def default_media_type
61
+ ENV['BETTERY_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE
62
+ end
63
+
64
+ # Default middleware stack for Faraday::Connection
65
+ # from {MIDDLEWARE}
66
+ # @return [String]
67
+ def middleware
68
+ MIDDLEWARE
69
+ end
70
+
71
+ # Default pagination page size from ENV
72
+ # @return [Fixnum] Page size
73
+ def per_page
74
+ page_size = ENV['BETTERY_PER_PAGE']
75
+
76
+ page_size.to_i if page_size
77
+ end
78
+
79
+ # Default proxy server URI for Faraday connection from ENV
80
+ # @return [String]
81
+ def proxy
82
+ ENV['BETTERY_PROXY']
83
+ end
84
+
85
+ # Default User-Agent header string from ENV or {USER_AGENT}
86
+ # @return [String]
87
+ def user_agent
88
+ ENV['BETTERY_USER_AGENT'] || USER_AGENT
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,115 @@
1
+ module Bettery
2
+ # Custom error class for rescuing from all Betterplace errors
3
+ class Error < StandardError
4
+
5
+ # Returns the appropriate Bettery::Error sublcass based
6
+ # on status and response message
7
+ #
8
+ # @param [Hash] response HTTP response
9
+ # @return [Bettery::Error]
10
+ def self.from_response(response)
11
+ status = response[:status].to_i
12
+ body = response[:body].to_s
13
+ headers = response[:response_headers]
14
+
15
+ if klass = case status
16
+ when 400 then Bettery::BadRequest
17
+ when 403 then Bettery::Forbidden
18
+ when 404 then Bettery::NotFound
19
+ when 400..499 then Bettery::ClientError
20
+ when 500 then Bettery::InternalServerError
21
+ when 502 then Bettery::BadGateway
22
+ when 503 then Bettery::ServiceUnavailable
23
+ when 500..599 then Bettery::ServerError
24
+ end
25
+ klass.new(response)
26
+ end
27
+ end
28
+
29
+ def initialize(response=nil)
30
+ @response = response
31
+ super(build_error_message)
32
+ end
33
+
34
+ private
35
+
36
+ def data
37
+ @data ||=
38
+ if (body = @response[:body]) && !body.empty?
39
+ if body.is_a?(String) &&
40
+ @response[:response_headers] &&
41
+ @response[:response_headers][:content_type] =~ /json/
42
+
43
+ Sawyer::Agent.serializer.decode(body)
44
+ else
45
+ body
46
+ end
47
+ else
48
+ nil
49
+ end
50
+ end
51
+
52
+ def response_message
53
+ case data
54
+ when Hash
55
+ data[:message]
56
+ when String
57
+ data
58
+ end
59
+ end
60
+
61
+ def response_error_name
62
+ "Error: #{data[:name]}" if data.is_a?(Hash) && data[:name]
63
+ end
64
+
65
+ def response_error_reason
66
+ data[:reason] if data.is_a?(Hash) && data[:reason]
67
+ end
68
+
69
+ def response_error_backtrace
70
+ return nil unless data.is_a?(Hash) && !Array(data[:backtrace]).empty?
71
+
72
+ summary = "\nError backtrace:\n"
73
+ summary << data[:backtrace].join("\n")
74
+
75
+ summary
76
+ end
77
+
78
+ def build_error_message
79
+ return nil if @response.nil?
80
+
81
+ message = "#{@response[:method].to_s.upcase} "
82
+ message << "#{@response[:url]}: "
83
+ message << "#{@response[:status]} - "
84
+ message << "#{response_error_name}\n" unless response_error_name.nil?
85
+ message << "#{response_error_reason}\n" unless response_error_reason.nil?
86
+ message << "#{response_message}\n" unless response_message.nil?
87
+ message << "#{response_error_backtrace}" unless response_error_backtrace.nil?
88
+ message
89
+ end
90
+ end
91
+
92
+ # Raised on errors in the 400-499 range
93
+ class ClientError < Error; end
94
+
95
+ # Raised when Betterplace returns a 400 HTTP status code
96
+ class BadRequest < ClientError; end
97
+
98
+ # Raised when Betterplace returns a 403 HTTP status code
99
+ class Forbidden < ClientError; end
100
+
101
+ # Raised when Betterplace returns a 404 HTTP status code
102
+ class NotFound < ClientError; end
103
+
104
+ # Raised on errors in the 500-599 range
105
+ class ServerError < Error; end
106
+
107
+ # Raised when Betterplace returns a 500 HTTP status code
108
+ class InternalServerError < ServerError; end
109
+
110
+ # Raised when Betterplace returns a 502 HTTP status code
111
+ class BadGateway < ServerError; end
112
+
113
+ # Raised when Betterplace returns a 503 HTTP status code
114
+ class ServiceUnavailable < ServerError; end
115
+ end
@@ -0,0 +1,45 @@
1
+ module Bettery
2
+ class Project
3
+ attr_accessor :id
4
+ REGEXP = /\Ahttps?.+\/projects\/(?<id>\d+).*\z/
5
+
6
+ def initialize(project)
7
+ case project
8
+ when Integer
9
+ @id = project
10
+ when String
11
+ @id = extract_id_from_url(project)
12
+ when Project
13
+ @id = project.id
14
+ when Hash
15
+ @id = project[:id]
16
+ end
17
+ end
18
+
19
+ # @return [String] Project API path
20
+ def path
21
+ "projects/#{id}.json"
22
+ end
23
+
24
+ # Get the api path for a project
25
+ # @param project [Integer, String, Hash, Project] A Betterplace project.
26
+ # @return [String] Api path.
27
+ def self.path project
28
+ new(project).path
29
+ end
30
+
31
+ # Project URL based on {Bettery::Client#api_endpoint}
32
+ # @return [String]
33
+ def url
34
+ File.join(Bettery.api_endpoint, path)
35
+ end
36
+
37
+ private
38
+
39
+ def extract_id_from_url(url)
40
+ if match = REGEXP.match(url)
41
+ match[:id].to_i
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ require 'faraday'
2
+ require 'bettery/error'
3
+
4
+ module Bettery
5
+ # Faraday response middleware
6
+ module Response
7
+
8
+ # This class raises an Bettery-flavored exception based
9
+ # HTTP status codes returned by the API
10
+ class RaiseError < Faraday::Response::Middleware
11
+
12
+ private
13
+
14
+ def on_complete(response)
15
+ if error = Bettery::Error.from_response(response)
16
+ raise error
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Bettery
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Bettery::Client::Projects do
4
+ before do
5
+ Bettery.reset!
6
+ @client = bettery_client
7
+ end
8
+
9
+ describe ".project", :vcr do
10
+ it "returns the matching project" do
11
+ project = @client.project(12345)
12
+ expect(project.id).to eq(12345)
13
+ expect(project.title).to eq("Finanzierung der Nachsorge")
14
+ assert_requested :get, betterplace_url("/projects/12345.json")
15
+ end
16
+ end # .project
17
+
18
+ describe ".projects", :vcr do
19
+ it "returns projects on betterplace" do
20
+ projects = Bettery.projects
21
+ expect(projects.data).to be_kind_of Array
22
+ assert_requested :get, betterplace_url("/projects.json")
23
+ end
24
+ end # .projects
25
+
26
+ describe ".project?", :vcr do
27
+ it "returns true if the project exists" do
28
+ result = @client.project?(12345)
29
+ expect(result).to be true
30
+ assert_requested :get, betterplace_url("/projects/12345.json")
31
+ end
32
+ it "returns false if the project doesn't exist" do
33
+ result = @client.project?(112233445566778899)
34
+ expect(result).to be false
35
+ assert_requested :get, betterplace_url("/projects/112233445566778899.json")
36
+ end
37
+ end # .project?
38
+ end
@@ -0,0 +1,307 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ describe Bettery::Client do
5
+ before do
6
+ Bettery.reset!
7
+ end
8
+
9
+ after do
10
+ Bettery.reset!
11
+ end
12
+
13
+ describe "module configuration" do
14
+ before do
15
+ Bettery.reset!
16
+ Bettery.configure do |config|
17
+ Bettery::Configurable.keys.each do |key|
18
+ config.send("#{key}=", "Some #{key}")
19
+ end
20
+ end
21
+ end
22
+
23
+ after do
24
+ Bettery.reset!
25
+ end
26
+
27
+ it "inherits the module configuration" do
28
+ client = Bettery::Client.new
29
+ Bettery::Configurable.keys.each do |key|
30
+ expect(client.instance_variable_get(:"@#{key}")).to eq("Some #{key}")
31
+ end
32
+ end
33
+
34
+ describe "with class level configuration" do
35
+ before do
36
+ @opts = {
37
+ connection_options: {ssl: {verify: false}},
38
+ per_page: 40
39
+ }
40
+ end
41
+
42
+ it "overrides module configuration" do
43
+ client = Bettery::Client.new(@opts)
44
+ expect(client.per_page).to eq(40)
45
+ end
46
+
47
+ it "can set configuration after initialization" do
48
+ client = Bettery::Client.new
49
+ client.configure do |config|
50
+ @opts.each do |key, value|
51
+ config.send("#{key}=", value)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ describe ".agent" do
59
+ before do
60
+ Bettery.reset!
61
+ end
62
+
63
+ it "acts like a Sawyer agent" do
64
+ expect(Bettery.client.agent).to respond_to :start
65
+ end
66
+
67
+ it "caches the agent" do
68
+ agent = Bettery.client.agent
69
+ expect(agent.object_id).to eq(Bettery.client.agent.object_id)
70
+ end
71
+ end # .agent
72
+
73
+ describe ".last_response", :vcr do
74
+ it "caches the last agent response" do
75
+ Bettery.reset!
76
+ client = Bettery.client
77
+ expect(client.last_response).to be_nil
78
+ client.get "projects.json"
79
+ expect(client.last_response.status).to eq(200)
80
+ end
81
+ end # .last_response
82
+
83
+ describe ".get", :vcr do
84
+ before(:each) do
85
+ Bettery.reset!
86
+ end
87
+
88
+ it "handles query params" do
89
+ Bettery.get "projects.json", foo: "bar"
90
+ assert_requested :get, "https://api.betterplace.org/de/api_v4/projects.json?foo=bar"
91
+ end
92
+
93
+ it "handles headers" do
94
+ request = stub_get("zen").
95
+ with(query: {foo: "bar"}, headers: {accept: "text/plain"})
96
+ Bettery.get "zen", foo: "bar", accept: "text/plain"
97
+ assert_requested request
98
+ end
99
+ end # .get
100
+
101
+ describe ".head", :vcr do
102
+ it "handles query params" do
103
+ Bettery.reset!
104
+ Bettery.head "projects.json", foo: "bar"
105
+ assert_requested :head, "https://api.betterplace.org/de/api_v4/projects.json?foo=bar"
106
+ end
107
+
108
+ it "handles headers" do
109
+ Bettery.reset!
110
+ request = stub_head("projects.json").
111
+ with(query: {foo: "bar"}, headers: {accept: "text/plain"})
112
+ Bettery.head "projects.json", foo: "bar", accept: "text/plain"
113
+ assert_requested request
114
+ end
115
+ end # .head
116
+
117
+ describe "when making requests" do
118
+ before do
119
+ Bettery.reset!
120
+ @client = Bettery.client
121
+ end
122
+
123
+ it "Accepts application/json by default" do
124
+ VCR.use_cassette 'projects' do
125
+ request = stub_get("projects.json").
126
+ with(headers: {accept: "application/json"})
127
+ @client.get "projects.json"
128
+ assert_requested request
129
+ expect(@client.last_response.status).to eq(200)
130
+ end
131
+ end
132
+
133
+ it "allows Accept'ing another media type" do
134
+ request = stub_get("projects.json").
135
+ with(headers: {accept: "application/vnd.betterplace.beta.diff+json"})
136
+ @client.get "projects.json", accept: "application/vnd.betterplace.beta.diff+json"
137
+ assert_requested request
138
+ expect(@client.last_response.status).to eq(200)
139
+ end
140
+
141
+ it "sets a default user agent" do
142
+ request = stub_get("projects.json").
143
+ with(headers: {user_agent: Bettery::Default.user_agent})
144
+ @client.get "projects.json"
145
+ assert_requested request
146
+ expect(@client.last_response.status).to eq(200)
147
+ end
148
+
149
+ it "sets a custom user agent" do
150
+ user_agent = "Mozilla/5.0 I am Spartacus!"
151
+ request = stub_get("projects.json").
152
+ with(headers: {user_agent: user_agent})
153
+ client = Bettery::Client.new(user_agent: user_agent)
154
+ client.get "projects.json"
155
+ assert_requested request
156
+ expect(client.last_response.status).to eq(200)
157
+ end
158
+
159
+ it "sets a proxy server" do
160
+ Bettery.configure do |config|
161
+ config.proxy = 'http://proxy.example.com:80'
162
+ end
163
+ conn = Bettery.client.send(:agent).instance_variable_get(:"@conn")
164
+ expect(conn.proxy[:uri].to_s).to eq('http://proxy.example.com')
165
+ end
166
+
167
+ it "passes along request headers for POST" do
168
+ headers = {"X-Betterplace-Foo" => "bar"}
169
+ request = stub_post("projects.json").
170
+ with(headers: headers).
171
+ to_return(status: 201)
172
+ client = Bettery::Client.new
173
+ client.post "projects.json", headers: headers
174
+ assert_requested request
175
+ expect(client.last_response.status).to eq(201)
176
+ end
177
+ end
178
+
179
+ context "error handling" do
180
+ before do
181
+ Bettery.reset!
182
+ VCR.turn_off!
183
+ end
184
+
185
+ after do
186
+ VCR.turn_on!
187
+ end
188
+
189
+ it "raises on 404" do
190
+ stub_get('booya').to_return(status: 404)
191
+ expect { Bettery.get('booya') }.to raise_error Bettery::NotFound
192
+ end
193
+
194
+ it "raises on 500" do
195
+ stub_get('boom').to_return(status: 500)
196
+ expect { Bettery.get('boom') }.to raise_error Bettery::InternalServerError
197
+ end
198
+
199
+ it "includes a message" do
200
+ stub_get('boom').
201
+ to_return \
202
+ status: 404,
203
+ headers: {
204
+ content_type: "application/json",
205
+ },
206
+ body: {message: "Couldn't find Project with id=boom"}.to_json
207
+ begin
208
+ Bettery.get('boom')
209
+ rescue Bettery::NotFound => e
210
+ expect(e.message).to include("GET https://api.betterplace.org/de/api_v4/boom: 404 - Couldn't find Project with id=boom")
211
+ end
212
+ end
213
+
214
+ it "includes all error info" do
215
+ stub_get('boom').
216
+ to_return \
217
+ status: 404,
218
+ headers: {
219
+ content_type: "application/json",
220
+ },
221
+ body: {
222
+ name: "GeneralError",
223
+ reason: "Record Not Found",
224
+ backtrace: [
225
+ "/path/to/file:23:in `method'",
226
+ "/path/to/file:42:in `method2'"
227
+ ],
228
+ message: "Couldn't find Project with id=666"
229
+ }.to_json
230
+ begin
231
+ Bettery.get('boom')
232
+ rescue Bettery::NotFound => e
233
+ expect(e.message).to include("GET https://api.betterplace.org/de/api_v4/boom: 404 - Error: GeneralError")
234
+ expect(e.message).to include("Record Not Found")
235
+ expect(e.message).to include("Couldn't find Project with id=666")
236
+ expect(e.message).to include("/path/to/file:23:in `method'")
237
+ expect(e.message).to include("/path/to/file:42:in `method2'")
238
+ end
239
+ end
240
+
241
+ it "raises Forbidden on 403" do
242
+ stub_get('some/admin/stuffs').to_return(status: 403)
243
+ expect { Bettery.get('some/admin/stuffs') }.to raise_error Bettery::Forbidden
244
+ end
245
+
246
+ it "handle an error with a text body" do
247
+ stub_get('boom').to_return \
248
+ status: 400,
249
+ body: "Client error"
250
+ begin
251
+ Bettery.get('boom')
252
+ rescue Bettery::BadRequest => e
253
+ expect(e.message).to include("Client error")
254
+ end
255
+ end
256
+
257
+ it "raises on unknown client errors" do
258
+ stub_get('user').to_return \
259
+ status: 418,
260
+ headers: {
261
+ content_type: "application/json",
262
+ },
263
+ body: {message: "I'm a teapot"}.to_json
264
+ expect { Bettery.get('user') }.to raise_error Bettery::ClientError
265
+ end
266
+
267
+ it "raises on unknown server errors" do
268
+ stub_get('user').to_return \
269
+ status: 509,
270
+ headers: {
271
+ content_type: "application/json",
272
+ },
273
+ body: {message: "Bandwidth exceeded"}.to_json
274
+ expect { Bettery.get('user') }.to raise_error Bettery::ServerError
275
+ end
276
+
277
+ it "raises Bettery::BadGateway on 502 server errors" do
278
+ stub_get('user').to_return \
279
+ status: 502,
280
+ headers: {
281
+ content_type: "application/json",
282
+ },
283
+ body: {message: "BadGateway"}.to_json
284
+ expect { Bettery.get('user') }.to raise_error Bettery::BadGateway
285
+ end
286
+
287
+ it "raises Bettery::ServiceUnavailable on 503 server errors" do
288
+ stub_get('user').to_return \
289
+ status: 503,
290
+ headers: {
291
+ content_type: "application/json",
292
+ },
293
+ body: {message: "Service Unavailable"}.to_json
294
+ expect { Bettery.get('user') }.to raise_error Bettery::ServiceUnavailable
295
+ end
296
+
297
+ it "handles an error response with an array body" do
298
+ stub_get('user').to_return \
299
+ status: 500,
300
+ headers: {
301
+ content_type: "application/json"
302
+ },
303
+ body: [].to_json
304
+ expect { Bettery.get('user') }.to raise_error Bettery::ServerError
305
+ end
306
+ end
307
+ end