You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
8.1 KiB
185 lines
8.1 KiB
5 months ago
|
+++++++++++++++++
|
||
|
FormEncode Design
|
||
|
+++++++++++++++++
|
||
|
|
||
|
:author: Ian Bicking <ianb@colorstudy.com>
|
||
|
:version: |release|
|
||
|
:date: |today|
|
||
|
|
||
|
.. contents::
|
||
|
|
||
|
This is a document to describe why FormEncode looks the way it looks,
|
||
|
and how it fits into other applications. It also talks some about the
|
||
|
false starts I've made.
|
||
|
|
||
|
Basic Metaphor
|
||
|
==============
|
||
|
|
||
|
FormEncode performs look-before-you-leap validation. The idea being
|
||
|
that you check all the data related to an operation, then apply it.
|
||
|
This is in contrast to a transactional system, where you just start
|
||
|
applying the data and if there's a problem you raise an exception.
|
||
|
Someplace else you catch the exception and roll back the transaction.
|
||
|
Of course FormEncode works fine with such a system, but because
|
||
|
nothing is done until everything validates, you can use this without
|
||
|
transactions.
|
||
|
|
||
|
FormEncode generally works on primitive types. These are things like
|
||
|
strings, lists, dictionaries, integers, etc. This fits in with
|
||
|
look-before-you-leap; often your domain objects won't exist until
|
||
|
after you apply the user's request, so it's necessary to work on an
|
||
|
early form of the data. Also, FormEncode doesn't know anything about
|
||
|
your domain objects or classes; it's just easier to keep it this way.
|
||
|
|
||
|
Validation only operates on a single "value" at a time. This is
|
||
|
Python, collections are easy, and collections are themselves a single
|
||
|
"value" made up of many pieces. A "Schema validator" is a validator
|
||
|
made up of many subvalidators. By using this single metaphor, without
|
||
|
separating the concept of "field" and "form", it is possible to create
|
||
|
reusable validators that work on compound structures, to validate
|
||
|
"whole forms" instead of just single fields, and to support better
|
||
|
validation composition.
|
||
|
|
||
|
Also, "validation" and "conversion" are generally applied at the same
|
||
|
time. In the documentation this is frequently just referred to as
|
||
|
"validation", but anywhere validation can happen, conversion can also
|
||
|
happen.
|
||
|
|
||
|
Domain Objects
|
||
|
==============
|
||
|
|
||
|
These are your objects, specific to your application. I know nothing
|
||
|
about them, and cannot know. FormEncode doesn't do anything with
|
||
|
these objects, and doesn't try to know anything about them.
|
||
|
|
||
|
Validation as directional, not intrinsic
|
||
|
========================================
|
||
|
|
||
|
One false start from earlier projects was an attempt to tie validators
|
||
|
into the objects they validate against. E.g., you might have a
|
||
|
SQLObject_ class like::
|
||
|
|
||
|
class Address(SQLObject):
|
||
|
fname = StringCol(notNull=True)
|
||
|
lname = StringCol(notNull=True)
|
||
|
mi = StringCol()
|
||
|
|
||
|
.. _SQLObject: http://sqlobject.org
|
||
|
|
||
|
It is tempting to take the restrictions of the ``Address`` class and
|
||
|
automatically come up with a validation schema. This may yet be a
|
||
|
viable goal (and to a degree is attainable), but in practical terms
|
||
|
validation tends to be both more and less restrictive. Also,
|
||
|
validation is contextual; what validation you apply is dependent on
|
||
|
the source of the data.
|
||
|
|
||
|
Often in an API we are more restrictive than we may be in a user
|
||
|
interface, demanding that everything be specified explicitly. In a UI
|
||
|
we may assist the user by filling in values on their behalf. The
|
||
|
specifics of this depend on the UI and the objects in question.
|
||
|
|
||
|
At the same time, we are often more restrictive in a UI. For
|
||
|
instance, we may demand that the user enter something that appears to
|
||
|
be a valid phone number. But for historical reasons, we may not make
|
||
|
that demand for objects that already exist, or we may put in a tight
|
||
|
restriction on the UI keeping in mind that it can more easily be
|
||
|
relaxed and refined than a restriction in the domain objects or
|
||
|
underlying database. Also, we may trust the programmer to use the API
|
||
|
in a reasonable way, but we seldom trust user data in the same way.
|
||
|
|
||
|
In essence, there is an "inside" and an "outside" to the program.
|
||
|
FormEncode is a toolkit for bridging those two areas in a sensible and
|
||
|
secure way. The specific way we bridge this depends on the nature of
|
||
|
the user interface. An XML-RPC interface can make some assumptions
|
||
|
that a GUI cannot make. An HTML interface can typically make even
|
||
|
fewer assumptions, including the basic integrity of the input data. It
|
||
|
isn't reasonable that the object should know about all means of
|
||
|
inputs, and the varying UI requirements of those inputs; user
|
||
|
interfaces are volatile, and more art than science, but domain objects
|
||
|
work better when they remain stable. For this reason the validation
|
||
|
schemas are kept in separate objects.
|
||
|
|
||
|
It also didn't work well to annotate domain objects with validation
|
||
|
schemas, though the option remains open. This is experimentation that
|
||
|
belongs outside of the core of FormEncode, simply because it's more
|
||
|
specific to your domain than it is to FormEncode.
|
||
|
|
||
|
Two sides, two aspects
|
||
|
======================
|
||
|
|
||
|
FormEncode does both validation and conversion at the same time.
|
||
|
Validation necessarily happens with every conversion; for instance,
|
||
|
you may want to convert string representation of dates to internal
|
||
|
date objects; that conversion can fail if the string representation is
|
||
|
malformed.
|
||
|
|
||
|
To keep things simple, there's only one operation: conversion. An
|
||
|
exception raised means there was an error. If you just want to
|
||
|
validate, that's a conversion that doesn't change anything.
|
||
|
|
||
|
Similarly, there's two sides to the system, the foreign data and the
|
||
|
local data. In Validator the local data is called "python" (meaning,
|
||
|
a natural Python data structure), so we convert ``to_python`` and
|
||
|
``from_python``. Unlike some systems, validators explicitly convert
|
||
|
in *both* directions.
|
||
|
|
||
|
For instance, consider the date conversion. In one form, you may want
|
||
|
a date like ``mm/dd/yyyy``. It's easy enough to make the necessary
|
||
|
converter; but the date object that the converter produces doesn't
|
||
|
know how it's supposed to be formatted for that form. Using
|
||
|
``repr()`` or *any* method that binds an object to its form
|
||
|
representation is a bad idea. The converter best knows how to undo
|
||
|
its work. So a date converter that expects ``mm/dd/yyyy`` will also
|
||
|
know how to turn a datetime into that format.
|
||
|
|
||
|
(This becomes even more interesting with compound validators.)
|
||
|
|
||
|
Presentation
|
||
|
============
|
||
|
|
||
|
At one time FormEncode included form generation in addition to
|
||
|
validation. The form generation worked okay; it was reasonably
|
||
|
attractive, and in many ways quite powerful. I might revisit it. But
|
||
|
generation is limited. It works *great* at first, then you hit a wall
|
||
|
-- you want to make a change, and you just *can't*, it doesn't fit
|
||
|
into the automatic generation.
|
||
|
|
||
|
There are also many ways to approach the generation; again it's
|
||
|
something that is tied to the framework, the presentation layer, and
|
||
|
the domain objects, and FormEncode doesn't know anything about those.
|
||
|
|
||
|
FormEncode does provide `htmlfill <htmlfill.html>`_. *You* produce
|
||
|
the form however you want. Write it out by hand. Use a templating
|
||
|
language. Use a form generator. Whatever. Then htmlfill (which
|
||
|
specifically understands HTML) fills in the form and any error
|
||
|
messages. There are several advantages to this:
|
||
|
|
||
|
* Using ``htmlfill``, form generation is easy. You can just think
|
||
|
about how to map a form description or model class to simple HTML.
|
||
|
You don't have to think about any of the low-level stuff about
|
||
|
filling attributes with defaults or past request values.
|
||
|
|
||
|
* ``htmlfill`` works with anything that produces HTML. There's zero
|
||
|
preference for any particular templating language, or even general
|
||
|
style of templating language.
|
||
|
|
||
|
* If you do form generation, but it later turns out to be
|
||
|
insufficiently flexible, you can put the generated form into your
|
||
|
template and extend it there; you'll lose automatic synchronization
|
||
|
with your models, but you won't lose any functionality.
|
||
|
|
||
|
* Hand-written forms are just as functional as generated forms.
|
||
|
|
||
|
Declarative and Imperative
|
||
|
==========================
|
||
|
|
||
|
All of the objects -- schemas, repeating elements, individual
|
||
|
validators -- can be created imperatively, though more declarative
|
||
|
styles often look better (specifically using subclassing instead of
|
||
|
construction). You are free to build the objects either way.
|
||
|
|
||
|
An example of programmatically building form generation:
|
||
|
``htmlfill_schemabuilder`` looks for special attributes in an HTML
|
||
|
form and builds a validation schema from that.
|
||
|
|