# Cerberror
![tests](https://github.com/pbrus/cerberror/actions/workflows/ci.yml/badge.svg?branch=master)
[![Python](https://img.shields.io/badge/Python-%203.10%20|%203.11%20|%203.12%20|%203.13%20-blue.svg "Python")](https://www.python.org/)
[![Style](https://img.shields.io/badge/code%20style-Black-black.svg "Black")](https://black.readthedocs.io/en/stable/)
[![License](https://img.shields.io/badge/license-MIT-yellow.svg "MIT license")](https://github.com/pbrus/cerberror/blob/master/LICENSE)

**Cerberror** was designed to customize and handle errors generated by the validator of [Cerberus](https://docs.python-cerberus.org/).

## How it works?

Let's start from checking errors generated by Cerberus:
```python
>>> v = Validator({'params': {'type': 'dict', 'schema': {'var1': {'type': 'integer'}, 'var2': {'allowed': [7, 8, 9]}}}})
>>> v.validate({'params': {'var1': 'Hello World!', 'var2': 3.14}})
>>> v.errors
{'params': [{'var1': ['must be of integer type'], 'var2': ['unallowed value 3.14']}]}
```
At least two problems occur when we want to inform a user what went wrong:
 - predetermined error messages,
 - unclear path to wrong variable.

 Let's define new messages in `msgs.txt` file:
 ```
 ('params', 'var1') 36 "{{value}} is not an integer!"
 ('params', 'var2') 68 "{{value}} not found in {{constraint}}..."
 ```
where the columns contain:
1. A path to the element defined by a tuple. Note that one-element tuple must be defined as `('some_param',)`, not `'some_param',`.
2. An error code from the [API documentation](https://docs.python-cerberus.org/en/stable/api.html#error-codes). A particular path can have many rules which implies many records in `msgs.txt` for that path.
3. A user-defined message. Optionally we can refer to [attributes](https://docs.python-cerberus.org/en/stable/api.html#cerberus.errors.ValidationError) of a particular error using `{{attr}}` expressions. Note that a message must be defined between two `"`, not `'`.

To get new messages we use `Translator` object:
```python
>>> from cerberror import Translator

>>> tr = Translator(v, 'msgs.txt')
>>> tr.translate()
{'params -> var2': ['3.14 not found in [7, 8, 9]...'], 'params -> var1': ['Hello World! is not an integer!']}
```

## Installation

To install the package please type from the command line:
```bash
$ pip3 install cerberror
```

## Features
**Cerberror** has a few features which facilitate the usage of it.
### Updates
Just like the validator of Cerberus, `Translator` object can be updated at any time:
```python
>>> tr = Translator(validator, 'messages.txt')
>>> tr.validator = another_validator
>>> tr.path_to_file = 'another_messages.txt'
```
This feature is particularly useful when we want to update files with messages on the fly. It could be used when we need to change language. Just define a proper file:
```
('params', 'var1') 36 "{{value}} ist keine ganze Zahl!"
('params', 'var2') 68 "{{value}} nicht unter {{constraint}} gefunden..."
```

### Comments
Comments can be added to files with messages. These files are parsing with the usage of the regular expressions. Valid records are those defined in [How it works?](#how-it-works) section. This means that everything else is treated as a comment. However, I recommend to use `#` to mark where they start.
 ```
 # Comment.
 ('params', 'var1') 36 "{{value}} is not an integer!"  # Inline comment.
 ('params', 'var2') 68 "{{value}} not found in {{constraint}}..."
 ```

### Paths
The returned paths are joined with usage of the default value `' -> '`. This behaviour can be changed easily:
```python
>>> tr = Translator(v, 'msgs.txt')
>>> tr.translate('_')
{'params_var2': ['3.14 not found in [7, 8, 9]...'], 'params_var1': ['Hello World! is not an integer!']}
```

### Returns
If the translation will finish successfully, the returned value will be a dictionary composed of `path:message(s)` pairs. Otherwise, `Translator` will return untouched errors generated by Cerberus. We can check the status of the translation using `any_error` property:
```python
>>> tr.any_error
False
>>> tr.errors
{'params -> var2': ['3.14 not found in [7, 8, 9]...'], 'params -> var1': ['Hello World! is not an integer!']}
...
>>> tr.any_error
True
>>> tr.errors
{'params': [{'var1': ['must be of integer type'], 'var2': ['unallowed value 3.14']}]}  # v.errors
```

### Internal errors
Internal errors can be inspected to find out what went wrong. The assumption was that **Cerberror** must not interrupt the translation process. If internal errors occur, `any_error` property will return `True`. Internal errors are always store in `error_list` property. For example:
```
# foo and bar attributes don't exist
('params', 'var1') 36 "{{foo}} is not an integer!"
('params', 'var2') 68 "{{value}} not found in {{bar}}..."
```
```python
>>> tr = Translator(v, 'msgs.txt')
>>> tr.translate()
{'params': [{'var1': ['must be of integer type'], 'var2': ['unallowed value 3.14']}]}
>>> tr.any_error
True
>>> tr.error_list
["Invalid expression '{{bar}}' in file 'msgs.txt'", "Invalid expression '{{foo}}' in file 'msgs.txt'"]
```

## Contribution

New feature, bugs? Issues and pull requests are welcome.

## License

**Cerberror** is licensed under the [MIT license](http://opensource.org/licenses/MIT).
