Support multiple validators, but stop at first exception #30

Closed
attilaolah opened this Issue Dec 14, 2011 · 8 comments

Comments

Projects
None yet
4 participants
Contributor

attilaolah commented Dec 14, 2011

It would be handy if there would be something like All that would stop at the first validation error.

Some validations are quick & easy, like Email, but some can be pretty expensive, like checking if the email address is already in use. There's no need for that check, if the email is not valid in the first place.

I'm happy to attach a pull request, if somebody would be smart enough to suggest a name.

An alternative would be to support a list or tuple of validators as the validator argument to SchemaNode and stop at the first failure by default.

Or maybe I'm just missing the obvious way to do this, in which case the docs should be updated (which I'm happy to do once somebody explains me how to do what I want).

Member

kiorky commented Dec 14, 2011

Make a pull request that add a 'blocking' kwarg to the init method and extract it in call.

Then in except just raise e instead of adding the message to the list if blocking is True.

Please add a test for this feature !

Thx !

Contributor

attilaolah commented Dec 14, 2011

Thanks, I'll attach a pull request by the end of the week.

Contributor

attilaolah commented Dec 10, 2012

This was getting overly complicated in cases where mixing deferred and non-deferred validators was necessary. I find that using the def validator(...) syntax (subclassing SchemaNode) and accessing node.bindings instead of using the colander.deferred wrapper, all this can be achieved.

I don't think it would make sense to add the blocking keyword arg. The new API is easier to use.

attilaolah closed this Dec 10, 2012

lei12 commented Feb 3, 2014

@attilaolah Could you explain more specifically how you did solve the mixing of deferred and non-deferred validators by using a def validator(...) syntax ? I seem to face the same issue regarding e-mail validation (need both colander.Email() validator + checking if the email address is already in use), but couldn't find how to combine the two of them. Since I get the schema directly from my SQLAlchemy model through ColanderAlchemy, may it not work the same way ?

Owner

mmerickel commented Feb 3, 2014

@lei12 You simply have to defer the entire thing.

def email_already_in_use_validator(node, kw):
    return ...

@colander.deferred
def my_validator(node, kw):
    return colander.All(
        colander.Email(),
        email_already_in_use_validator(node, kw),
    )
Contributor

attilaolah commented Feb 4, 2014

Or, something like this:

class Email(colander.SchemaNode):
    """Email validator. Runs multiple validators sequentially."""

    schema_type = colander.String

    def validator(self, node, cstruct):
        self._validate_email(cstruct)
        self._validate_unique(cstruct)

    _validate_email = colander.Email()

    def _validate_unique(self, email):
        if some.db.lookup(email=email).exists():
            self.raise_invalid(u'email already in use')

Then in your User schema:

class User(colander.MappingSchema):
    email = Email()

lei12 commented Feb 5, 2014

Since I do not declare the colander schema myself (colanderalchemy deduces from the sqlalchemy mapping), but can pass a colander validator to the mapped column like this:

email = Column(Unicode(255), nullable=False, unique=True, 
                info={'colanderalchemy': {'validator': my_validator}},
                )

I guess mmerickel's draft is more appropriate, but then,
@mmerickel what should I put in the email_already_in_use_validator function so that it raises the exception if the email exists already ?

Besides, I get an error at pserve time in with the query that I can't sort out, it looks like it tries to execute the query already :

def email_already_in_use_validator(node, kw):
    mail = node.name
    res = DBSession().query(X_abo_essai).filter(X_abo_essai.xa_mail==mail).one()

=> sqlalchemy.orm.exc.NoResultFound: No row was found for one()

lei12 commented Feb 6, 2014

Got it, using colander.Function to pass my custom validator

def email_already_in_use_validator(email):
    if bool(DBSession().query(User).filter_by(email=email).count()):
        return False
    return True

@colander.deferred
def deferred_email_custom_validator(node, kw):
    return colander.All(
        colander.Email(),
        colander.Function(email_already_in_use_validator, msg="Email already in use"),
    )

then in mapping:

email = Column(Unicode(255), nullable=False, unique=True, 
                info={'colanderalchemy': {'validator': deferred_email_custom_validator}},
                )

Thanks for your help and support !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment