sábado, 11 de junio de 2016

Colander: O de como validar un JSON en Pyramid

Confieso que he dejado alguna aplicación sin que, una vez validados los datos en el cliente, los valide en el servidor.
Pero esta vez quiero hacer las cosas bien, así que encontré de suerte a Colander (Con una documentación bastante útil hasta ahora) que es capaz de realizar algo como validar los datos JSON una vez llegan a nuestra aplicación. Y se integra bien con Pyramid. Y no hay ningún problema con los test, excepto escribirlos.
Añadimos colander como dependencia de nuestro proyecto en requires de ./ambiente/aplicacion/setup.py
(...)

requires = [
    'pyramid',
    'pyramid_debugtoolbar',
    'waitress',
    'nose',
    'webtest',
    'coverage',
    'colander'
    ]

(...)
Lo instalamos
# Este sí, desde el directorio ./ambiente/aplicacion es la mejor idea
python setup.py develop
Creamos un fichero ./ambiente/aplicacion/aplicacion/schemas/usuario.py el que creamos una clase que, descendiendo de algún tipo específico de Colander, sea capaz de definir la estructura de nuestro objeto JSON, aunque en realidad podemos hacer validaciones de cadenas simples y todo bien.
mkdir ./ambiente/aplicacion/aplicacion/schemas
touch ./ambiente/aplicacion/aplicacion/schemas/__init__.py
Y creamos el fichero ./ambiente/aplicacion/aplicacion/schemas/usuario.py con el siguiente contenido:
# coding: utf-8
import colander

class Palabra(colander.SequenceSchema):
    # Palabra es un string utf-8 de al menos dos caracteres 
    palabra = colander.SchemaNode(colander.String('utf-8'), validator=colander.Length(min=2))

class UsuarioEsquema(colander.MappingSchema):
    # nombre es un string utf-8
    nombre = colander.SchemaNode(colander.String('utf-8'))
    # apellido es un string utf-8
    apellido = colander.SchemaNode(colander.String('utf-8'))
    # palabras es una lista de palabras
    palabras = Palabra(validator=colander.Length(min=1))
if __name__ == '__main__':
    esquema = Data()
    data = {'nombre': 'Alexander', 'apellido': 'Ortíz', 'palabras': ['usuario', 'formidable']}
    data = {'nombre': 389, 'palabras': ['usuario', 'formidable']}
Y en lo que ya parece una costumbre, actualizamos nuestra vista ./ambiente/aplicacion/aplicacion/views/actividades.py
# coding: utf-8
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPBadRequest
from colander import Invalid

from ..fichero_app.ficheros import Usuarios
from ..schemas.usuario import UsuarioEsquema

# Empieza el trabajo con la autenticación
from pyramid.security import Allow, Deny, Everyone, NO_PERMISSION_REQUIRED, Authenticated

# Dejamos por acá un esquema listo para usarse
esquema = UsuarioEsquema()

@view_config(route_name='ficheros_listado', renderer='json', permission='listar')
def ficheros_listado(request):
    """Cuando request_method se configura acà, el mensaje es diferente porque 
    la operación alcanzada es diferente
        The resource could not be found.


     predicate mismatch for view get_listar_ficheros (request_method = GET,HEAD)
    Por tanto lo mejor es configurarlo allá en __init__
    """
    ficheros = Usuarios()
    listado = ficheros.listado()
    return {'respuesta': listado}

@view_config(route_name='ficheros_detalle', renderer='json', permission='detallar')
def ficheros_detalle(request):
    usuario = request.matchdict['usuario']
    ficheros = Usuarios()
    detalle = ficheros.detalle(usuario)
    return {'respuesta': detalle}

@view_config(route_name='ficheros_creacion', renderer='json', permission='creacion')
def ficheros_creacion(request):
    try:
        usuario = request.json_body['usuario']
        data = esquema.deserialize(request.json_body['data'])
    except Invalid as e:
        return HTTPBadRequest(json_body=e.asdict())
    except Exception as e:
        return HTTPBadRequest()
    ficheros = Usuarios()
    creacion = ficheros.creacion(usuario, data)
    return {'respuesta': creacion}

@view_config(route_name='ficheros_modificacion', renderer='json', permission='modificacion')
def ficheros_modificacion(request):
    usuario = request.matchdict['usuario']
    try:
        data = esquema.deserialize(request.json_body['data'])
    except Invalid as e:
        return HTTPBadRequest(json_body=e.asdict())
    except Exception as e:
        return HTTPBadRequest()
    ficheros = Usuarios()
    modificacion = ficheros.modificacion(usuario, data)
    return {'respuesta': modificacion}

@view_config(route_name='ficheros_borrado', renderer='json', permission='borrado')
def ficheros_borrado(request):
    usuario = request.matchdict['usuario']
    ficheros = Usuarios()
    borrado = ficheros.borrado(usuario)

    return {'respuesta': borrado}
Y para mantener las buenas costumbres, agregamos a Creacion un método test_ficheros_creacion_malformed en ./ambiente/aplicacion/aplicacion/tests/testFuncionales.py de la siguiente forma:
    def test_ficheros_creacion_malformed(self):
        datos = {'usuario': 'fcornejo',  'data': {'nombre': 'Flor', 'apellido':'Cornejo', 'palabras': ['ente', 'obvio']}}
        # Dañamos el nombre, así nos aseguramos que sea ese contenido el equivocado 
        datos['data']['nombre'] = 389
        respuesta = self.testapp.post_json('/ficheros', status=400, params=datos)
        self.assertRegexpMatches(respuesta.json_body['nombre'], '389 is not a string')

No hay comentarios:

Publicar un comentario

Otros apuntes interesantes

Otros apuntes interesantes