fakturoid 0.4.0 → 1.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -11
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +15 -0
  5. data/README.md +597 -107
  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 -10
  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 +48 -51
  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/invoice.rb +0 -73
  58. data/lib/fakturoid/client/number_format.rb +0 -11
  59. data/lib/fakturoid/client/subject.rb +0 -41
  60. data/lib/fakturoid/client/todo.rb +0 -18
  61. data/lib/fakturoid/client/user.rb +0 -20
  62. data/lib/fakturoid/connection.rb +0 -30
  63. data/lib/fakturoid/request.rb +0 -31
  64. data/test/api_test.rb +0 -24
  65. data/test/config_test.rb +0 -40
  66. data/test/fixtures/blocked_account.json +0 -8
  67. data/test/fixtures/invoice.json +0 -81
  68. data/test/fixtures/invoice.pdf +0 -0
  69. data/test/fixtures/invoice_error.json +0 -7
  70. data/test/fixtures/subjects.json +0 -52
  71. data/test/request_test.rb +0 -20
  72. data/test/response_test.rb +0 -189
  73. 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,12 +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/expense"
10
- require "fakturoid/client/generator"
11
- require "fakturoid/client/event"
12
- 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