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 tipoload
: define como será feito a transformação do dado no formato de dicionárioto_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