Hadron (lib de validação)

O Hadron é uma lib desenvolvida na Itflex, que usamos para cria as entidades e os requests do UseCase. Devemos utilizar para elimina trabalho desnecessário, da forma tradicional de definir uma entidade não é produtivo. Exemplo:

class Role:
    def __init__(self, id=None, name=None, scopes=None, created_at=None, updated_at=None): 
        self.id = id
        self.name = name
        self.scopes = scopes
        self.created_at = created_at
        self.updated_at = updated_at

# Exemplo de inicialização 
role = Role(
    id=10,
    name="Test",
    scopes=["teste.test"]
)

Neste formato temos que valida campo a campo, exemplo campo name:

  • Verifica se o tipo de dado é string.
  • Verifica se a string esta no formato correto, ex: o name não pode ter caracter especial do tipo []()@#$.

Neste formato as validações vão da muito trabalho:

  • Ficam repetidas entre os casos de uso.
  • O código fica muito extenso. E de difícil manutenção.

Com o Hadron conseguimos definir, qual o tipo de dado que cada campo vai receber e adiciona validações encima do valor do campo.

from hadron import Model, types, validators

class Role(Model):
    id = types.Int()
    name = types.String(validators=[validators.length(min=3, max=30)])
    scopes = types.List(types.String, required=False, default=[])
    created_at = types.DateTime()
    updated_at = types.DateTime()

# Exemplo de inicialização 
role = Role(
    id="error",
    name="T",
    scopes=["test.test"]
)

# Exemplo resposta de erro
>>> role.validate()
False
>>> role.errors
{
  'id': 'Invalid type for field [id], expected int, given [str]', 
  'name': "The field 'name' length must be between '3' and '30'", 
  'created_at': 'Field is required.', 
  'updated_at': 'Field is required.'
}
>>>

Ao executa o método validade, será executado todas as validações definidas nos campos. A própriedade errors vai conter todos erros encontrados, e no formato que usamos como resposta da api.

Outras formas de inicializa o objeto

No primeiro exemplo de inicialização, foi passado os valores no construtor da classe.

role = Role(
    id="error",
    name="T",
    scopes=["test.test"]
)

Porém existe outras formas de fazer a mesma coisa.

  • Passando um dicionário
data = {"id": 10, "name": "Test", "scopes": ["teste.test"]}

role = Role(data)
  • Passando um dicionário e argumento nomeado
data = {"name": "Test", "scopes": ["teste.test"]}

role = Role(id=10, **data)

O Hadron não permite troca o valor do campo

No python padrão quando instanciamos uma classe, as propriedades do objeto são mutáveis.

# Instanciando objeto que não usa o Hadron
role = Role(
    id=10,
    name="Test",
    scopes=["teste.test"]
)

# Após instanciado, permite alterar o valor para propriedade
role.name = 'Error'

Desta forma, tivemos vários problemas relacionado ao passa objeto por referência. Que é quando um objeto é passado para uma função e dentro da função é modificado os valores do objeto, isso faz perde o controle do objeto. Se tiver varias funções que modificam os valores, e tiver bug não conseguimos saber qual a função que esta provocando o bug.

Por este motivo o Hadron, não permite modifica os valores do objeto, e vai retorna erro se for feito.

>>> role.name='teste'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/vilmarschm/git/itflex/server/backend/hadron/model.py", line 321, in __setattr__
    raise NotImplementedError
NotImplementedError

Quando for necessário alterar algum valor o objeto a única forma e utiliza o método replace. O método replace altera o valor da propriedade retornando outra instância de objeto. Desta forma não temos os problemas do passa objeto por referência.

role = role.replace(name='teste')

Definindo request para o UseCase

Utilizamos herança para definir a classe request, neste exemplo foi criado a classe CreateRole herdando da classe Role. Utilizamos esta forma para não precisa escrever o mesmo campo em diversos lugares. E evita que o mesmo campo seja escrito com validações diferentes.

class CreateRole(Role):
    consumer = types.Any()

    class Meta:
        exclude = ["id", "created_at", "updated_at"]

class UpdateRole(Role):
    consumer = types.Any()

    class Meta:
        include = ["id","name", "scopes"]

Quando usar a herança, na maioria dos casos tem campos que não deve existir no request. Para isso usamos o include ou exclude.

Definindo um novo tipo de campo

Para definir um novo tipo é necessário:

  • Criar o arquivo deste novo tipo, em hadron/types/{nome}.py
    • Definir a classe deste novo tipo
  • Adiciona import deste novo tipo no hadron/types/__init__.py

Todos os tipos deve herda de from hadron.type_def import TypeDef, desta forma facilata o trabalho e segue o mesmo padrão dos outros tipos. Ao herda é necessário entender como esse tipo vai funciona, para identificar os métodos que vão precisar ser sobreescrito.

Alguns métodos importantes:

  • validate_type: defene como será feito a validação de tipo
  • load: define como será feito a transformação do dado no formato de dicionário
  • to_dict: define como será feito a transformação do valor armazenado neste campo para dicionário.

Segue abaixo o exemplo:

from hadron.exceptions import INVALID_TYPE_MSG, get_type
from hadron.type_def import TypeDef

class Boolean(TypeDef):
    def validate_type(self, value, field_name="", root=None, parent=None):
        if not isinstance(value, bool):
            return INVALID_TYPE_MSG.format(
                field_name, "boolean", get_type(value)
            )

        return

Definindo um novo validador

Para definir um novo validador é necessário:

  • Criar o arquivo deste novo validador, em hadron/validators/{nome}.py
    • Definir a classe deste novo validador
  • Adiciona import deste novo validador no hadron/validators/__init__.py

Todos os tipos deve herda de from hadron.type_def import TypeDef, desta forma facilata o trabalho e segue o mesmo padrão dos outros tipos. Ao herda é necessário entender como esse tipo vai funciona, para identificar os métodos que vão precisar ser sobreescrito.

Segue abaixo o exemplo:

MSG = "The field '{name}' must be lower than '{max}'"


def validator_max(max=None):
    def validator(val, field_name="", root=None, parent=None):
        if max is None:
            return

        if val > max:
            return MSG.format(name=field_name, max=max)

        return

    return validator