fakturoid 0.5.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -10
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +11 -0
  5. data/README.md +501 -145
  6. data/Rakefile +3 -1
  7. data/fakturoid.gemspec +36 -25
  8. data/lib/fakturoid/api/account.rb +13 -0
  9. data/lib/fakturoid/api/bank_account.rb +13 -0
  10. data/lib/fakturoid/api/base.rb +18 -0
  11. data/lib/fakturoid/api/event.rb +23 -0
  12. data/lib/fakturoid/api/expense.rb +55 -0
  13. data/lib/fakturoid/api/expense_payment.rb +20 -0
  14. data/lib/fakturoid/api/generator.rb +36 -0
  15. data/lib/fakturoid/api/inbox_file.rb +34 -0
  16. data/lib/fakturoid/api/inventory_item.rb +66 -0
  17. data/lib/fakturoid/api/inventory_move.rb +40 -0
  18. data/lib/fakturoid/api/invoice.rb +62 -0
  19. data/lib/fakturoid/api/invoice_message.rb +14 -0
  20. data/lib/fakturoid/api/invoice_payment.rb +26 -0
  21. data/lib/fakturoid/api/number_format.rb +13 -0
  22. data/lib/fakturoid/api/recurring_generator.rb +36 -0
  23. data/lib/fakturoid/api/subject.rb +42 -0
  24. data/lib/fakturoid/api/todo.rb +20 -0
  25. data/lib/fakturoid/api/user.rb +17 -0
  26. data/lib/fakturoid/api.rb +84 -9
  27. data/lib/fakturoid/client.rb +46 -12
  28. data/lib/fakturoid/config.rb +69 -12
  29. data/lib/fakturoid/oauth/access_token_service.rb +46 -0
  30. data/lib/fakturoid/oauth/credentials.rb +63 -0
  31. data/lib/fakturoid/oauth/flow/authorization_code.rb +53 -0
  32. data/lib/fakturoid/oauth/flow/base.rb +42 -0
  33. data/lib/fakturoid/oauth/flow/client_credentials.rb +27 -0
  34. data/lib/fakturoid/oauth/flow.rb +5 -0
  35. data/lib/fakturoid/oauth/request/api.rb +11 -0
  36. data/lib/fakturoid/oauth/request/base.rb +60 -0
  37. data/lib/fakturoid/oauth/request/oauth.rb +24 -0
  38. data/lib/fakturoid/oauth/request.rb +5 -0
  39. data/lib/fakturoid/oauth/token_response.rb +56 -0
  40. data/lib/fakturoid/oauth.rb +39 -0
  41. data/lib/fakturoid/response.rb +8 -20
  42. data/lib/fakturoid/utils.rb +27 -0
  43. data/lib/fakturoid/version.rb +1 -1
  44. data/lib/fakturoid.rb +22 -22
  45. metadata +47 -53
  46. data/.github/workflows/rubocop.yml +0 -20
  47. data/.github/workflows/tests.yml +0 -27
  48. data/.gitignore +0 -7
  49. data/Gemfile +0 -6
  50. data/lib/fakturoid/api/arguments.rb +0 -21
  51. data/lib/fakturoid/api/http_methods.rb +0 -23
  52. data/lib/fakturoid/client/account.rb +0 -11
  53. data/lib/fakturoid/client/bank_account.rb +0 -11
  54. data/lib/fakturoid/client/event.rb +0 -19
  55. data/lib/fakturoid/client/expense.rb +0 -49
  56. data/lib/fakturoid/client/generator.rb +0 -44
  57. data/lib/fakturoid/client/inventory_items.rb +0 -59
  58. data/lib/fakturoid/client/inventory_moves.rb +0 -36
  59. data/lib/fakturoid/client/invoice.rb +0 -73
  60. data/lib/fakturoid/client/number_format.rb +0 -11
  61. data/lib/fakturoid/client/subject.rb +0 -41
  62. data/lib/fakturoid/client/todo.rb +0 -18
  63. data/lib/fakturoid/client/user.rb +0 -20
  64. data/lib/fakturoid/connection.rb +0 -30
  65. data/lib/fakturoid/request.rb +0 -31
  66. data/test/api_test.rb +0 -24
  67. data/test/config_test.rb +0 -40
  68. data/test/fixtures/blocked_account.json +0 -8
  69. data/test/fixtures/invoice.json +0 -81
  70. data/test/fixtures/invoice.pdf +0 -0
  71. data/test/fixtures/invoice_error.json +0 -7
  72. data/test/fixtures/subjects.json +0 -52
  73. data/test/request_test.rb +0 -20
  74. data/test/response_test.rb +0 -189
  75. data/test/test_helper.rb +0 -19
data/lib/fakturoid/api.rb CHANGED
@@ -1,19 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fakturoid/api/arguments"
4
- require "fakturoid/api/http_methods"
3
+ require_relative "api/base"
4
+
5
+ # Sorted alphabetically
6
+ require_relative "api/account"
7
+ require_relative "api/bank_account"
8
+ require_relative "api/event"
9
+ require_relative "api/expense"
10
+ require_relative "api/expense_payment"
11
+ require_relative "api/generator"
12
+ require_relative "api/inbox_file"
13
+ require_relative "api/inventory_item"
14
+ require_relative "api/inventory_move"
15
+ require_relative "api/invoice"
16
+ require_relative "api/invoice_message"
17
+ require_relative "api/invoice_payment"
18
+ require_relative "api/number_format"
19
+ require_relative "api/recurring_generator"
20
+ require_relative "api/subject"
21
+ require_relative "api/todo"
22
+ require_relative "api/user"
5
23
 
6
24
  module Fakturoid
7
- class Api
8
- extend Arguments
9
- extend HttpMethods
25
+ module Api
26
+ def account
27
+ @account ||= Account.new(self)
28
+ end
29
+
30
+ def bank_accounts
31
+ @bank_accounts ||= BankAccount.new(self)
32
+ end
33
+
34
+ def events
35
+ @events ||= Event.new(self)
36
+ end
37
+
38
+ def expenses
39
+ @expenses ||= Expense.new(self)
40
+ end
41
+
42
+ def expense_payments
43
+ @expense_payments ||= ExpensePayment.new(self)
44
+ end
45
+
46
+ def generators
47
+ @generators ||= Generator.new(self)
48
+ end
49
+
50
+ def inbox_files
51
+ @inbox_files ||= InboxFile.new(self)
52
+ end
53
+
54
+ def inventory_items
55
+ @inventory_items ||= InventoryItem.new(self)
56
+ end
57
+
58
+ def inventory_moves
59
+ @inventory_moves ||= InventoryMove.new(self)
60
+ end
61
+
62
+ def invoices
63
+ @invoices ||= Invoice.new(self)
64
+ end
65
+
66
+ def invoice_messages
67
+ @invoice_messages ||= InvoiceMessage.new(self)
68
+ end
69
+
70
+ def invoice_payments
71
+ @invoice_payments ||= InvoicePayment.new(self)
72
+ end
73
+
74
+ def number_formats
75
+ @number_formats ||= NumberFormat.new(self)
76
+ end
77
+
78
+ def recurring_generators
79
+ @recurring_generators ||= RecurringGenerator.new(self)
80
+ end
81
+
82
+ def subjects
83
+ @subjects ||= Subject.new(self)
84
+ end
10
85
 
11
- def self.configure(&block)
12
- @config ||= Fakturoid::Config.new(&block)
86
+ def todos
87
+ @todos ||= Todo.new(self)
13
88
  end
14
89
 
15
- def self.config
16
- @config
90
+ def users
91
+ @users ||= User.new(self)
17
92
  end
18
93
  end
19
94
  end
@@ -1,14 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fakturoid/client/account"
4
- require "fakturoid/client/bank_account"
5
- require "fakturoid/client/number_format"
6
- require "fakturoid/client/user"
7
- require "fakturoid/client/subject"
8
- require "fakturoid/client/invoice"
9
- require "fakturoid/client/inventory_items"
10
- require "fakturoid/client/inventory_moves"
11
- require "fakturoid/client/expense"
12
- require "fakturoid/client/generator"
13
- require "fakturoid/client/event"
14
- require "fakturoid/client/todo"
3
+ module Fakturoid
4
+ class Client
5
+ extend Forwardable
6
+ include Api
7
+
8
+ attr_reader :config
9
+
10
+ # Authorization methods
11
+ def_delegators :@oauth, :authorization_uri, :authorize, :revoke_access, :perform_request
12
+
13
+ def self.configure(&block)
14
+ @config ||= Fakturoid::Config.new(&block) # rubocop:disable Naming/MemoizedInstanceVariableName
15
+ end
16
+
17
+ def self.config
18
+ @config
19
+ end
20
+
21
+ def initialize(config = {})
22
+ raise ConfigurationError, "Configuration is missing" if self.class.config.nil?
23
+
24
+ @config = self.class.config.duplicate(config)
25
+ @oauth = Oauth.new(self)
26
+ end
27
+
28
+ def account=(account)
29
+ config.account = account
30
+ end
31
+
32
+ def credentials
33
+ config.credentials
34
+ end
35
+
36
+ def credentials=(values)
37
+ config.credentials = values
38
+ end
39
+
40
+ def credentials_updated_callback(&block)
41
+ config.credentials_updated_callback = block
42
+ end
43
+
44
+ def call_credentials_updated_callback
45
+ config.credentials_updated_callback&.call(config.credentials)
46
+ end
47
+ end
48
+ end
@@ -2,39 +2,96 @@
2
2
 
3
3
  module Fakturoid
4
4
  class Config
5
- attr_accessor :email, :api_key, :account
5
+ attr_accessor :email, :account, :client_id, :client_secret, :oauth_flow, :redirect_uri, :credentials_updated_callback
6
6
  attr_writer :user_agent
7
7
 
8
- ENDPOINT = "https://app.fakturoid.cz/api/v2"
8
+ SUPPORTED_FLOWS = %w[authorization_code client_credentials].freeze
9
+ API_ENDPOINT = "https://app.fakturoid.cz/api/v3"
10
+ # API_ENDPOINT = "http://app.fakturoid.localhost/api/v3" # For development purposes
11
+ OAUTH_ENDPOINT = "#{API_ENDPOINT}/oauth".freeze
9
12
 
10
- def initialize(&_block)
13
+ def initialize
11
14
  yield self
15
+
16
+ validate_configuration
17
+ end
18
+
19
+ def credentials
20
+ @credentials ||= Oauth::Credentials.new
21
+ end
22
+
23
+ def credentials=(values)
24
+ @credentials = values.is_a?(Hash) ? Oauth::Credentials.new(values) : values
12
25
  end
13
26
 
14
27
  def user_agent
15
- if !defined?(@user_agent) || @user_agent.nil? || @user_agent.empty?
28
+ if Utils.empty?(@user_agent)
16
29
  "Fakturoid ruby gem (#{email})"
17
30
  else
18
31
  @user_agent
19
32
  end
20
33
  end
21
34
 
22
- def endpoint
23
- "#{ENDPOINT}/accounts/#{account}"
35
+ def api_endpoint
36
+ raise ConfigurationError, "Account slug is required" if Utils.empty?(account)
37
+
38
+ "#{API_ENDPOINT}/accounts/#{account}"
39
+ end
40
+
41
+ def api_endpoint_without_account
42
+ API_ENDPOINT
43
+ end
44
+
45
+ def oauth_endpoint
46
+ OAUTH_ENDPOINT
24
47
  end
25
48
 
26
- def endpoint_without_account
27
- ENDPOINT
49
+ def authorization_uri(state: nil)
50
+ params = {
51
+ client_id: client_id,
52
+ redirect_uri: redirect_uri,
53
+ response_type: "code"
54
+ }
55
+ params[:state] = state unless Utils.empty?(state)
56
+
57
+ connection = Faraday::Connection.new(oauth_endpoint)
58
+ connection.build_url(nil, params)
28
59
  end
29
60
 
30
- def faraday_v1?
31
- major_faraday_version == "1"
61
+ def access_token_auth_header
62
+ "#{credentials.token_type} #{credentials.access_token}"
63
+ end
64
+
65
+ def authorization_code_flow?
66
+ oauth_flow == "authorization_code"
67
+ end
68
+
69
+ def client_credentials_flow?
70
+ oauth_flow == "client_credentials"
71
+ end
72
+
73
+ # We can create multiple instances of the client, make sure we isolate the config for each
74
+ # as it contains credentials which must not be shared.
75
+ def duplicate(new_config)
76
+ self.class.new do |config|
77
+ config.email = email
78
+ config.account = new_config[:account] || account
79
+ config.user_agent = user_agent
80
+ config.client_id = client_id
81
+ config.client_secret = client_secret
82
+ config.oauth_flow = oauth_flow # 'client_credentials', 'authorization_code'
83
+ # only authorization_code
84
+ config.redirect_uri = redirect_uri
85
+ end
32
86
  end
33
87
 
34
88
  private
35
89
 
36
- def major_faraday_version
37
- @major_faraday_version ||= Faraday::VERSION.split(".").first
90
+ def validate_configuration
91
+ raise ConfigurationError, "Missing or unsupported OAuth flow, supported flows are - `authorization_code`, `client_credentials`" unless SUPPORTED_FLOWS.include?(oauth_flow)
92
+ raise ConfigurationError, "`email` or `user` agent is required" if Utils.empty?(email) && Utils.empty?(user_agent)
93
+ raise ConfigurationError, "Client credentials are required" if Utils.empty?(client_id) || Utils.empty?(client_secret)
94
+ raise ConfigurationError, "`redirect_uri` is required for Authorization Code Flow" if authorization_code_flow? && Utils.empty?(redirect_uri)
38
95
  end
39
96
  end
40
97
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ class AccessTokenService
6
+ attr_reader :oauth, :client
7
+
8
+ def initialize(oauth)
9
+ @oauth = oauth
10
+ @client = oauth.client
11
+ end
12
+
13
+ def perform_request(method, path, params)
14
+ check_access_token
15
+ fetch_access_token if client.config.credentials.access_token_expired?
16
+
17
+ retried = false
18
+
19
+ begin
20
+ Request::Api.new(method, path, client).call(params)
21
+ rescue AuthenticationError
22
+ raise if retried
23
+ retried = true
24
+ fetch_access_token
25
+
26
+ retry
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def check_access_token
33
+ raise ArgumentError, "OAuth access was not authorized by user" unless oauth.authorized?
34
+ return unless Utils.empty?(client.config.credentials.access_token)
35
+
36
+ fetch_access_token
37
+ end
38
+
39
+ def fetch_access_token
40
+ oauth.fetch_access_token.tap do
41
+ client.call_credentials_updated_callback
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ class Credentials
6
+ EXPIRY_BUFFER_IN_SECONDS = 10
7
+ MAX_EXPIRY_IN_SECONDS = 2 * 3600 # 2 hours
8
+
9
+ attr_accessor :access_token, :refresh_token, :token_type
10
+ attr_reader :expires_at
11
+
12
+ def initialize(values = {})
13
+ update(values)
14
+ end
15
+
16
+ def update(values)
17
+ values = values.transform_keys(&:to_sym)
18
+
19
+ self.access_token = values[:access_token]
20
+ self.refresh_token = values[:refresh_token] unless Utils.empty?(values[:refresh_token])
21
+ self.expires_at = values[:expires_at] || values[:expires_in]
22
+ self.token_type ||= values[:token_type]
23
+ end
24
+
25
+ def expires_at=(value)
26
+ @expires_at = parse_expires_at(value)
27
+ end
28
+
29
+ def expires_in=(value)
30
+ self.expires_at = value
31
+ end
32
+
33
+ def access_token_expired?
34
+ Time.now > (expires_at - EXPIRY_BUFFER_IN_SECONDS)
35
+ end
36
+
37
+ def as_json
38
+ {
39
+ access_token: access_token,
40
+ refresh_token: refresh_token,
41
+ expires_at: expires_at.to_datetime, # `DateTime` serializes into is8601, `Time` doesn't, so it can be saved as JSON safely.
42
+ token_type: token_type
43
+ }
44
+ end
45
+
46
+ private
47
+
48
+ def parse_expires_at(value)
49
+ case value
50
+ when DateTime
51
+ value.to_time
52
+ when String
53
+ Time.parse(value)
54
+ when Integer # `value` in seconds
55
+ raise ArgumentError, "`expires_at` cannot be unix timestamp (was #{value.inspect})" if value > MAX_EXPIRY_IN_SECONDS
56
+ Time.now + value
57
+ else
58
+ value
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ module Flow
6
+ class AuthorizationCode
7
+ include Base
8
+
9
+ GRANT_TYPE = "authorization_code"
10
+
11
+ def authorization_uri(state: nil)
12
+ client.config.authorization_uri(state: state)
13
+ end
14
+
15
+ def authorize(code:)
16
+ payload = {
17
+ grant_type: GRANT_TYPE,
18
+ redirect_uri: client.config.redirect_uri,
19
+ code: code
20
+ }
21
+
22
+ response = perform_request(HTTP_POST, "token.json", payload: payload)
23
+ client.config.credentials.update(response.body)
24
+ client.call_credentials_updated_callback
25
+ response
26
+ end
27
+
28
+ def fetch_access_token
29
+ payload = {
30
+ grant_type: "refresh_token",
31
+ refresh_token: client.config.credentials.refresh_token
32
+ }
33
+
34
+ response = perform_request(HTTP_POST, "token.json", payload: payload)
35
+ client.config.credentials.update(response.body)
36
+ response
37
+ end
38
+
39
+ def revoke_access
40
+ payload = {
41
+ token: client.config.credentials.refresh_token
42
+ }
43
+
44
+ perform_request(HTTP_POST, "revoke.json", payload: payload)
45
+ end
46
+
47
+ def authorized?
48
+ !Utils.empty?(client.config.credentials.refresh_token)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ module Flow
6
+ module Base
7
+ attr_reader :client
8
+
9
+ def initialize(client)
10
+ @client = client
11
+ end
12
+
13
+ def authorization_uri(state: nil)
14
+ raise NotImplementedError, "Authorization path is not supported"
15
+ end
16
+
17
+ def authorize(code:)
18
+ raise NotImplementedError, "Authorize is not supported"
19
+ end
20
+
21
+ def fetch_access_token
22
+ raise NotImplementedError, "Fetch access token is not supported"
23
+ end
24
+
25
+ def revoke_access
26
+ raise NotImplementedError, "Revoke access is not supported"
27
+ end
28
+
29
+ def authorized?
30
+ raise NotImplementedError, "Authorized is not supported"
31
+ end
32
+
33
+ protected
34
+
35
+ def perform_request(method, path, params)
36
+ raw_response = Request::Oauth.new(method, path, client).call(params)
37
+ TokenResponse.new(raw_response)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ module Flow
6
+ class ClientCredentials
7
+ include Base
8
+
9
+ GRANT_TYPE = "client_credentials"
10
+
11
+ def fetch_access_token
12
+ payload = {
13
+ grant_type: GRANT_TYPE
14
+ }
15
+
16
+ response = perform_request(HTTP_POST, "token.json", payload: payload)
17
+ client.config.credentials.update(response.body)
18
+ response
19
+ end
20
+
21
+ def authorized?
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "flow/base"
4
+ require_relative "flow/authorization_code"
5
+ require_relative "flow/client_credentials"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ module Request
6
+ class Api
7
+ include Base
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ module Request
6
+ module Base
7
+ attr_reader :method, :path, :client
8
+
9
+ HTTP_METHODS = [:get, :post, :patch, :delete].freeze
10
+ REQUEST_TIMEOUT = 10
11
+
12
+ def initialize(method, path, client)
13
+ @method = method
14
+ @path = path
15
+ @client = client
16
+ end
17
+
18
+ def call(params = {})
19
+ raise ArgumentError, "Unknown http method: #{method}" unless HTTP_METHODS.include?(method.to_sym)
20
+
21
+ request_params = params[:request_params] || {}
22
+
23
+ http_connection = connection(params)
24
+
25
+ http_connection.send(method) do |req|
26
+ req.url path, request_params
27
+ req.body = MultiJson.dump(params[:payload]) if params.key?(:payload)
28
+ end
29
+ end
30
+
31
+ protected
32
+
33
+ def default_options(options = {})
34
+ {
35
+ headers: {
36
+ content_type: "application/json",
37
+ accept: "application/json",
38
+ user_agent: client.config.user_agent
39
+ },
40
+ url: options[:url] || endpoint,
41
+ request: {
42
+ timeout: REQUEST_TIMEOUT
43
+ }
44
+ }
45
+ end
46
+
47
+ def endpoint
48
+ client.config.api_endpoint
49
+ end
50
+
51
+ def connection(options = {})
52
+ Faraday.new default_options(options) do |conn|
53
+ conn.headers[:authorization] = client.config.access_token_auth_header
54
+ conn.adapter Faraday.default_adapter
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ module Request
6
+ class Oauth
7
+ include Base
8
+
9
+ protected
10
+
11
+ def connection(options = {})
12
+ Faraday.new default_options(options) do |conn|
13
+ conn.set_basic_auth(client.config.client_id, client.config.client_secret)
14
+ conn.adapter Faraday.default_adapter
15
+ end
16
+ end
17
+
18
+ def endpoint
19
+ client.config.oauth_endpoint
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "request/base"
4
+ require_relative "request/api"
5
+ require_relative "request/oauth"
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fakturoid
4
+ class Oauth
5
+ class TokenResponse
6
+ attr_reader :client, :response, :body
7
+
8
+ def initialize(response)
9
+ @response = response
10
+ @body = MultiJson.load(response.env.body) unless Utils.empty?(response.env.body)
11
+
12
+ handle_response
13
+ end
14
+
15
+ def status_code
16
+ response.env["status"]
17
+ end
18
+
19
+ def refresh_token
20
+ body["refresh_token"]
21
+ end
22
+
23
+ def access_token
24
+ body["access_token"]
25
+ end
26
+
27
+ def expires_in
28
+ body["expires_in"]
29
+ end
30
+
31
+ def token_type
32
+ body["token_type"]
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class.name}:#{object_id} @body=\"#{body}\" @status_code=\"#{status_code}\">"
37
+ end
38
+
39
+ private
40
+
41
+ def handle_response
42
+ case status_code
43
+ when 400 then raise error(OauthError, "OAuth request failed")
44
+ when 401 then raise error(AuthenticationError, "OAuth authentication failed")
45
+ else
46
+ raise error(ServerError, "Server error") if status_code >= 500
47
+ raise error(ClientError, "Client error") if status_code >= 400
48
+ end
49
+ end
50
+
51
+ def error(klass, message = nil)
52
+ klass.new message, status_code, body
53
+ end
54
+ end
55
+ end
56
+ end