Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ spec/rails_app/tmp/
spec/rails_app/db/*.sqlite3
spec/rails_app/db/*.sqlite3-journal
spec/rails_app/log/*.log

spec/tmp/config/initializers/
.byebug_history
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ group :development, :test do
gem 'faker'
gem 'rubocop'
gem 'rubocop-rspec'
gem 'generator_spec'
end
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ GEM
activesupport (>= 4.2.0)
faker (2.6.0)
i18n (>= 1.6, < 1.8)
generator_spec (0.9.4)
activesupport (>= 3.0.0)
railties (>= 3.0.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
i18n (1.7.0)
Expand Down Expand Up @@ -185,6 +188,7 @@ DEPENDENCIES
errors_savior!
factory_bot
faker
generator_spec
puma (~> 4.3)
rake (~> 10.0)
rspec (~> 3.0)
Expand Down
225 changes: 224 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ Or install it yourself as:

$ gem install errors_savior

## Generator

To create the initializer for configuration details, you can run:

$ rails generate errors_savior:install


## Conventions

Expand All @@ -27,6 +33,164 @@ Or install it yourself as:
| 2xxx | Controller errors |
| 3xxx | Server errors |

## Traditional Rails vs ErrorsSavior

<details><summary>Example error handle without ErrorsSavior</summary>
<p>

```
# Modificated files

# app/controllers/application_controller.rb
# app/controllers/concerns/errors_helper.rb

# (Custom errors)
# app/errors/invalid_schema_error.rb
# app/errors/my_custom_error.rb
# ....
```

```rb
# app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
include ErrorsHelper
end
```

```rb
# app/controllers/concerns/errors_helper.rb

module ErrorsHelper
extend ActiveSupport::Concern

included do
rescue_from ActionController::ParameterMissing, with: :unprocessable_entity
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from ArgumentError, with: :unprocessable_entity
rescue_from MyCustomError, with: :render_my_custom_error
rescue_from Pundit::NotAuthorizedError, with: :unauthorized
# ...

rescue_from InvalidSchemaError do |e|
render json: { message: e.message }, status: :bad_request
end
end

private

def render_error(error, status)
render json: { errors: [error] }, status: status
end

def unprocessable_entity(error)
render_error(error, :unprocessable_entity)
end

def record_not_found(error)
render_error(error, :not_found)
end

def unauthorized
head(:unauthorized)
end

def render_my_custom_error
render_error(MyCustomError.new.message, :not_acceptable)
end
end

```

```rb
# app/errors/invalid_schema_error.rb

class InvalidSchemaError < StandardError
def message
"Provided parameters doesn't validate with the expected schema"
end
end
```

```rb
# app/errors/my_custom_error.rb

class MyCustomError < StandardError
def message
"Custom error, error message"
end
end
```

</p>
</details>


<details><summary>Error handle using Errors Savior</summary>
<p>

```
# Modificated files

# Gemfile
# config/initializers/errors_savior.rb (automatically generated by gem generator)
# config/locales/gems/errors_savior/en.yml (dictionary with errors)
```

```rb
# Gemfile

# ...
gem 'errors_savior'
# ...
```

```rb
# config/initializers/errors_savior.rb

ErrorsSavior.configure do |config|
# ErrorsSavior needs your api_origin metadata if you like to show this information on responses
# config.api_origin = nil

# Merge your own dicionary with ErrorsSavior dictionary
config.external_dictionary_errors_path = 'config/locales/gems/errors_savior'
config.locale = :en
end
```

```rb
# config/locales/gems/errors_savior/en.yml

en:
ArgumentError:
error_code: 1010
http_status_sym: unprocessable_entity
http_status_code: 422
message: 'One or more fields are invalid'

MyCustomError:
error_code: 2010
http_status_sym: not_acceptable
http_status_code: 404
message: 'Custom error, error message'

Pundit::NotAuthorizedError:
error_code: 2020
http_status_sym: unauthorized
http_status_code: 401
message: "User not allowed to perform that action"

InvalidSchemaError:
error_code: 2030
http_status_sym: not_acceptable
http_status_code: 404
message: "Provided parameters doesn't validate with the expected schema"

```
</p>
</details>


## Usage

When an error occurs in your application, you will see a json response body with ErrorsSavior [specific protocol](lib/errors_savior/presenter/protocol.rb), similar to the following:
Expand Down Expand Up @@ -71,6 +235,65 @@ If your application is running in development mode, you will see more informatio
}
```

### Use your own dicionary

It's possible to create your custom dictionary, if you need to modify any default Savior.

For example, you can change this default savior:
```yml
ActiveModel::ValidationError:
error_code: 1001
http_status_sym: unprocessable_entity
http_status_code: 422
message: One or more fields are invalid
```

With another values:
```yml
# config/locales/gems/errors_savior/en.yml

en:
ActiveModel::ValidationError:
error_code: 1234
http_status_sym: bad_request
http_status_code: 400
message: Modified message
```

Remember to set in ErrorsSavior initializers the following variables:
* `external_dictionary_errors_path`
* `locale`

In the example above, we have used:
> config.external_dictionary_errors_path = 'config/locales/gems/errors_savior'

> config.locale = :en

**Note**
ErrorsSavior use `:en` locale as default.
If you need to use another locale, you should override all default Saviors with your custom locale


### Rspec helpers

You only need to add this line in your `rails_helper.rb`

```ruby
# spec/rails_helper.rb

require 'errors_savior/rspec'
```

#### `be_rescuted_by_savior` Rspec matcher
You can use the `be_rescuted_by_savior` matcher to test your endpoints.

```rb
# spec/errors/custom_error_spec.rb
describe DummiesController do
it { expect(response).to be_rescuted_by_savior }
end
```

## Contributing

We hope that you will consider contributing to ErrorsSavior.
Expand All @@ -88,7 +311,7 @@ Everyone interacting in the ErrorsSavior project’s codebases, issue trackers,

## License

**Errors Savior** is available under the [MIT license](LICENSE.md).
**Errors Savior** is available under the [MIT license](LICENSE.txt).


## About
Expand Down
1 change: 1 addition & 0 deletions lib/errors_savior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

require 'errors_savior/saviors/base_savior'
require 'errors_savior/saviors/not_unique'
require 'errors_savior/saviors/parameter_missing'

module ErrorsSavior
extend ActiveSupport::Concern
Expand Down
18 changes: 17 additions & 1 deletion lib/errors_savior/config.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
module ErrorsSavior
module Config
DEFAULT_CONFIG = {}.freeze
DEFAULT_CONFIG = {
api_origin: nil,
external_dictionary_errors_path: '',
locale: :en
}.freeze

module_function

DEFAULT_CONFIG.each do |key, value|
define_method key do
instance_variable_get("@#{key}") || instance_variable_set("@#{key}", value)
end

define_method "#{key}=" do |v|
instance_variable_set("@#{key}", v)
end
end
end
end
30 changes: 26 additions & 4 deletions lib/errors_savior/dictionary/local_dictionary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,39 @@
module ErrorsSavior
module Dictionary
module LocalDictionary
DEFAULT_LOCALE = :en

module_function

def errors_dictionary
# TODO: Use configurable locale
dictionary_path = File.join(__dir__, '..', 'config', 'locale', 'en.yml')
HashWithIndifferentAccess.new(YAML.safe_load(File.read(dictionary_path)))[:en]
errors_savior_dictionary.merge!(external_errors_dictionary)
end

def required_attributes
%i[error_code http_status_sym http_status_code message]
end

module_function :errors_dictionary, :required_attributes
def errors_savior_dictionary
dictionary_path = File.join(__dir__, '..', 'config', 'locale', "#{DEFAULT_LOCALE}.yml")
read_dictionary(dictionary_path, DEFAULT_LOCALE)
end

def external_errors_dictionary
locale = Config.locale
external_dir = Config.external_dictionary_errors_path
return {} if external_dir.empty?

dictionary_path = File.join(Rails.root.join(external_dir, "#{locale}.yml"))
read_dictionary(dictionary_path, locale)
end

def read_dictionary(dir, locale)
HashWithIndifferentAccess.new(YAML.safe_load(File.read(dir)))[locale]
end

def find_savior_by_error_code(error_code)
Hash[*errors_dictionary.detect { |_key, value| value[:error_code] == error_code }]
end
end
end
end
4 changes: 2 additions & 2 deletions lib/errors_savior/protocol/render_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def initialize(savior, error)

def as_json
json = required_error_body
json['errors'] = errors.as_json if @savior.respond_to?(:errors)
json['errors'] = @savior.errors(@error).as_json if @savior.respond_to?(:errors)

Rails.configuration.consider_all_requests_local ? append_metadata(json) : json
end
Expand All @@ -24,7 +24,7 @@ def required_error_body
def append_metadata(json)
json['metadata'] = {}
json['metadata']['error_class'] = @savior.error_class.name
# TODO: json['metadata']['api_origin'] = ErrorsSavior.config.api_origin.enabled?
json['metadata']['api_origin'] = Config.api_origin if Config.api_origin.present?
json['metadata']['stack_trace'] = @error.backtrace

json
Expand Down
Loading