From b17dbcd39482777341c2a7b6f7b943972504596e Mon Sep 17 00:00:00 2001 From: Slava Pestov Date: Thu, 13 Nov 2008 21:49:37 -0600 Subject: [PATCH] Document furnace.actions and clean up a few things --- basis/furnace/actions/actions-docs.factor | 170 ++++++++++++++++++ basis/furnace/actions/actions.factor | 13 +- .../features/edit-profile/edit-profile.xml | 2 +- .../features/recover-password/recover-3.xml | 2 +- .../auth/features/registration/register.xml | 2 +- basis/furnace/auth/login/login.xml | 2 +- .../conversations/conversations-docs.factor | 6 + basis/html/forms/forms-docs.factor | 8 + basis/html/forms/forms.factor | 13 +- basis/html/templates/chloe/chloe-docs.factor | 3 + basis/html/templates/chloe/chloe.factor | 3 + extra/webapps/blogs/new-post.xml | 2 +- extra/webapps/user-admin/edit-user.xml | 2 +- extra/webapps/user-admin/new-user.xml | 2 +- 14 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 basis/furnace/actions/actions-docs.factor create mode 100644 basis/furnace/conversations/conversations-docs.factor diff --git a/basis/furnace/actions/actions-docs.factor b/basis/furnace/actions/actions-docs.factor new file mode 100644 index 0000000000..509e0bcdee --- /dev/null +++ b/basis/furnace/actions/actions-docs.factor @@ -0,0 +1,170 @@ +USING: assocs classes help.markup help.syntax io.streams.string +http http.server.dispatchers http.server.responses +furnace.redirection strings multiline ; +IN: furnace.actions + +HELP: +{ $values { "action" action } } +{ $description "Creates a new action." } ; + +HELP: +{ $values + { "path" "a pathname string" } + { "response" response } +} +{ $description "Creates an HTTP response which serves a Chloe template. See " { $link "html.templates.chloe" } "." } ; + +HELP: +{ $values { "page" action } } +{ $description "Creates a new action which serves a Chloe template when servicing a GET request." } ; + +HELP: action +{ $description "The class of Furnace actions. New instances are created with " { $link } ". New instances of subclasses can be created with " { $link new-action } ". The " { $link page-action } " class is a useful subclass." +$nl +"Action slots are documented in " { $link "furnace.actions.config" } "." } ; + +HELP: new-action +{ $values + { "class" class } + { "action" action } +} +{ $description "Constructs a subclass of " { $link action } "." } ; + +HELP: page-action +{ $description "The class of Chloe page actions. These are actions whose " { $slot "display" } " slot is pre-set to serve the Chloe template stored in the " { $slot "page" } " slot." } ; + +HELP: param +{ $values + { "name" string } + { "value" string } +} +{ $description "Outputs the value of a query parameter (if the current request is a GET or HEAD request) or a POST parameter (if the current request is a POST request)." } +{ $notes "Instead of using this word, it is better to use " { $link validate-params } " and then access parameters via " { $link "html.forms.values" } " words." } ; + +HELP: params +{ $var-description "A variable holding an assoc of query parameters (if the current request is a GET or HEAD request) or POST parameters (if the current request is a POST request)." } +{ $notes "Instead of using this word, it is better to use " { $link validate-params } " and then access parameters via " { $link "html.forms.values" } " words." } ; + +HELP: validate-integer-id +{ $description "A utility word which validates an integer parameter named " { $snippet "id" } "." } +{ $examples + { $code + "" + " [" + " validate-integer-id" + " \"id\" value select-tuple from-object" + " ] >>init" + } +} ; + +HELP: validate-params +{ $values + { "validators" "an association list mapping parameter names to validator quotations" } +} +{ $description "Validates query or POST parameters, depending on the request type, and stores them in " { $link "html.forms.values" } ". The validator quotations can execute " { $link "validators" } "." } +{ $examples + "A simple validator from " { $vocab-link "webapps.todo" } "; this word is invoked from the " { $slot "validate" } " quotation of action for editing a todo list item:" + { $code + <" : validate-todo ( -- ) + { + { "summary" [ v-one-line ] } + { "priority" [ v-integer 0 v-min-value 10 v-max-value ] } + { "description" [ v-required ] } + } validate-params ;"> + } +} ; + +HELP: validation-failed +{ $description "Stops processing the current request and takes action depending on the type of the current request:" + { $list + { "For GET or HEAD requests, the client receives a " { $link <400> } " response." } + { "For POST requests, the client is sent back to the page containing the form submission, with current form values and validation errors passed in a " { $link "furnace.conversations" } "." } + } +"This word is called by " { $link validate-params } " and can also be called directly. For more details, see " { $link "furnace.actions.lifecycle" } "." } ; + +ARTICLE: "furnace.actions.page.example" "Furnace page action example" +"The " { $vocab-link "webapps.counter" } " vocabulary defines a subclass of " { $link dispatcher } ":" +{ $code "TUPLE: counter-app < dispatcher ;" } +"The " { $snippet "" } " constructor word creates a new instance of the " { $snippet "counter-app" } " class, and adds a " { $link page-action } " instance to the dispatcher. This " { $link page-action } " has its " { $slot "template" } " slot set as follows," +{ $code "{ counter-app \"counter\" } >>template" } +"This means the action will serve the Chloe template located at " { $snippet "resource:extra/webapps/counter/counter.xml" } " upon receiving a GET request." ; + +ARTICLE: "furnace.actions.page" "Furnace page actions" +"Page actions implement the common case of an action that simply serves a Chloe template in response to a GET request." +{ $subsection page-action } +{ $subsection } +"When using a page action, instead of setting the " { $slot "display" } " slot, the " { $slot "template" } " slot is set instead. The " { $slot "init" } ", " { $slot "authorize" } ", " { $slot "validate" } " and " { $slot "submit" } " slots can still be set as usual." +$nl +"The " { $slot "template" } " slot of a " { $link page-action } " contains a pair with shape " { $snippet "{ responder name }" } ", where " { $snippet "responder" } " is a responder class, usually a subclass of " { $link dispatcher } ", and " { $snippet "name" } " is the name of a template file, without the " { $snippet ".xml" } " extension, relative to the directory containing the responder's vocabulary source file." +{ $subsection "furnace.actions.page.example" } ; + +ARTICLE: "furnace.actions.config" "Furnace action configuration" +"Actions have the following slots:" +{ $table + { { $slot "rest" } { "A parameter name to map the rest of the URL, after the action name, to. If this is not set, then navigating to a URL where the action is not the last path component will return to the client with an error." } } + { { $slot "init" } { "A quotation called at the beginning of a GET or HEAD request. Typically this quotation configures " { $link "html.forms" } " and parses query parameters." } } + { { $slot "authorize" } { "A quotation called at the beginning of a GET, HEAD or POST request. In GET requests, it is called after the " { $slot "init" } " quotation; in POST requests, it is called after the " { $slot "validate" } " quotation. By convention, this quotation performs custom authorization checks which depend on query parameters or POST parameters." } } + { { $slot "display" } { "A quotation called after the " { $slot "init" } " quotation in a GET request. This quotation must return an HTTP " { $link response } "." } } + { { $slot "validate" } { "A quotation called at the beginning of a POST request to validate POST parameters." } } + { { $slot "submit" } { "A quotation called after the " { $slot "validate" } " quotation in a POST request. This quotation must return an HTTP " { $link response } "." } } +} +"At least one of the " { $slot "display" } " and " { $slot "submit" } " slots must be set, otherwise the action will be useless." ; + +ARTICLE: "furnace.actions.validation" "Form validation with actions" +"The action code is set up so that the " { $slot "init" } " quotation can validate query parameters, and the " { $slot "validate" } " quotation can validate POST parameters." +$nl +"A word to validate parameters and make them available as HTML form values (see " { $link "html.forms.values" } "); typically this word is invoked from the " { $slot "init" } " and " { $slot "validate" } " quotations:" +{ $subsection validate-params } +"The above word expects an association list mapping parameter names to validator quotations; validator quotations can use the words in the " +"Custom validation logic can invoke a word when validation fails; " { $link validate-params } " invokes this word for you:" +{ $subsection validation-failed } +"If validation fails, no more action code is executed, and the client is redirected back to the originating page, where validation errors can be displayed. Note that validation errors are rendered automatically by the " { $link "html.components" } " words, and in particular, " { $link "html.templates.chloe" } " use these words." ; + +ARTICLE: "furnace.actions.lifecycle" "Furnace action lifecycle" +{ $heading "GET request lifecycle" } +"A GET request results in the following sequence of events:" +{ $list + { "The " { $snippet "init" } " quotation is called." } + { "The " { $snippet "authorize" } " quotation is called." } + { "If the GET request was generated as a result of form validation failing during a POST, then the form values entered by the user, along with validation errors, are stored in " { $link "html.forms.values" } "." } + { "The " { $snippet "display" } " quotation is called; it is expected to output an HTTP " { $link response } " on the stack." } +} +"Any one of the above steps can perform validation; if " { $link validation-failed } " is called during a GET request, the client receives a " { $link <400> } " error." +{ $heading "HEAD request lifecycle" } +"A HEAD request proceeds exactly like a GET request. The only difference is that the " { $slot "body" } " slot of the " { $link response } " object is never rendered." +{ $heading "POST request lifecycle" } +"A POST request results in the following sequence of events:" +{ $list + { "The " { $snippet "validate" } " quotation is called." } + { "The " { $snippet "authorize" } " quotation is called." } + { "The " { $snippet "submit" } " quotation is called; it is expected to output an HTTP " { $link response } " on the stack. By convention, this response should be a " { $link } "." } +} +"Any one of the above steps can perform validation; if " { $link validation-failed } " is called during a POST request, the client is sent back to the page containing the form submission, with current form values and validation errors passed in a " { $link "furnace.conversations" } "." ; + +ARTICLE: "furnace.actions.impl" "Furnace actions implementation" +"The following words are used by the action implementation and there is rarely any reason to call them directly:" +{ $subsection new-action } +{ $subsection param } +{ $subsection params } ; + +ARTICLE: "furnace.actions" "Furnace actions" +"The " { $vocab-link "furnace.actions" } " vocabulary implements a type of responder, called an " { $emphasis "action" } ", which handles the form validation lifecycle." +$nl +"Other than form validation capability, actions are also often simpler to use than implementing new responders directly, since creating a new class is not required, and the action dispatches on the request type (GET, HEAD, or POST)." +$nl +"The class of actions:" +{ $subsection action } +"Creating a new action:" +{ $subsection } +"Once created, an action needs to be configured; typically the creation and configuration of an action is encapsulated into a single word:" +{ $subsection "furnace.actions.config" } +"Validating forms with actions:" +{ $subsection "furnace.actions.validation" } +"More about the form validation lifecycle:" +{ $subsection "furnace.actions.lifecycle" } +"A convenience class:" +{ $subsection "furnace.actions.page" } +"Low-level features:" +{ $subsection "furnace.actions.impl" } ; + +ABOUT: "furnace.actions" diff --git a/basis/furnace/actions/actions.factor b/basis/furnace/actions/actions.factor index 7505b3c612..6c56a8ad7b 100644 --- a/basis/furnace/actions/actions.factor +++ b/basis/furnace/actions/actions.factor @@ -22,18 +22,7 @@ SYMBOL: params SYMBOL: rest -: render-validation-messages ( -- ) - form get errors>> - [ -
    - [
  • escape-string write
  • ] each -
- ] unless-empty ; - -CHLOE: validation-messages - drop [ render-validation-messages ] [code] ; - -TUPLE: action rest authorize init display validate submit ; +TUPLE: action rest init authorize display validate submit ; : new-action ( class -- action ) new [ ] >>init [ ] >>validate [ ] >>authorize ; inline diff --git a/basis/furnace/auth/features/edit-profile/edit-profile.xml b/basis/furnace/auth/features/edit-profile/edit-profile.xml index f486f4e246..878bdd64fb 100644 --- a/basis/furnace/auth/features/edit-profile/edit-profile.xml +++ b/basis/furnace/auth/features/edit-profile/edit-profile.xml @@ -62,7 +62,7 @@

- +

diff --git a/basis/furnace/auth/features/recover-password/recover-3.xml b/basis/furnace/auth/features/recover-password/recover-3.xml index a8ea635a1f..2df400ffe2 100644 --- a/basis/furnace/auth/features/recover-password/recover-3.xml +++ b/basis/furnace/auth/features/recover-password/recover-3.xml @@ -32,7 +32,7 @@

- +

diff --git a/basis/furnace/auth/features/registration/register.xml b/basis/furnace/auth/features/registration/register.xml index b0d6971d1b..45c090905e 100644 --- a/basis/furnace/auth/features/registration/register.xml +++ b/basis/furnace/auth/features/registration/register.xml @@ -63,7 +63,7 @@

- +

diff --git a/basis/furnace/auth/login/login.xml b/basis/furnace/auth/login/login.xml index 766c097ca5..917c182fb3 100644 --- a/basis/furnace/auth/login/login.xml +++ b/basis/furnace/auth/login/login.xml @@ -36,7 +36,7 @@

- +

diff --git a/basis/furnace/conversations/conversations-docs.factor b/basis/furnace/conversations/conversations-docs.factor new file mode 100644 index 0000000000..5e161f2457 --- /dev/null +++ b/basis/furnace/conversations/conversations-docs.factor @@ -0,0 +1,6 @@ +USING: help.markup help.syntax ; +IN: furnace.conversations + +ARTICLE: "furnace.conversations" "Furnace conversation scope" + +; diff --git a/basis/html/forms/forms-docs.factor b/basis/html/forms/forms-docs.factor index 6556d2eac2..089a516072 100644 --- a/basis/html/forms/forms-docs.factor +++ b/basis/html/forms/forms-docs.factor @@ -85,6 +85,14 @@ HELP: validate-values { $values { "assoc" assoc } { "validators" "an assoc mapping value names to quotations" } } { $description "Validates values in the assoc by looking up the corresponding validation quotation, and storing the results in named values of the current form." } ; +HELP: validation-error +{ $values { "message" string } } +{ $description "Reports a validation error not associated with a specific form field." } +{ $notes "Such errors can be rendered by calling the " { $link render-validation-errors } " word." } ; + +HELP: render-validation-errors +{ $description "Renders any validation errors reported by calls to the " { $link validation-error } " word." } ; + ARTICLE: "html.forms.forms" "HTML form infrastructure" "The below words are used to implement the " { $vocab-link "furnace.actions" } " vocabulary. Calling them directly is rarely necessary." $nl diff --git a/basis/html/forms/forms.factor b/basis/html/forms/forms.factor index c1c1aa3def..f92f8d0764 100644 --- a/basis/html/forms/forms.factor +++ b/basis/html/forms/forms.factor @@ -1,7 +1,8 @@ ! Copyright (C) 2008 Slava Pestov ! See http://factorcode.org/license.txt for BSD license. -USING: kernel accessors strings namespaces assocs hashtables -mirrors math fry sequences words continuations ; +USING: kernel accessors strings namespaces assocs hashtables io +mirrors math fry sequences words continuations html.elements +xml.entities ; IN: html.forms TUPLE: form errors values validation-failed ; @@ -104,3 +105,11 @@ C: validation-error : validate-values ( assoc validators -- ) swap '[ [ dup _ at ] dip validate-value ] assoc-each ; + +: render-validation-errors ( -- ) + form get errors>> + [ +
    + [
  • escape-string write
  • ] each +
+ ] unless-empty ; diff --git a/basis/html/templates/chloe/chloe-docs.factor b/basis/html/templates/chloe/chloe-docs.factor index f390aad238..402b6e68a9 100644 --- a/basis/html/templates/chloe/chloe-docs.factor +++ b/basis/html/templates/chloe/chloe-docs.factor @@ -154,6 +154,9 @@ ARTICLE: "html.templates.chloe.tags.form" "Chloe link and form tags" "" } } } + { { $snippet "t:validation-errors" } { + "Renders validation errors in the current form which are not associated with any field. Such errors are reported by invoking " { $link validation-error } "." + } } } ; ARTICLE: "html.templates.chloe.tags" "Standard Chloe tags" diff --git a/basis/html/templates/chloe/chloe.factor b/basis/html/templates/chloe/chloe.factor index 1bc4684d5c..da3f80e9a5 100644 --- a/basis/html/templates/chloe/chloe.factor +++ b/basis/html/templates/chloe/chloe.factor @@ -65,6 +65,9 @@ CHLOE: comment drop ; CHLOE: call-next-template drop reset-buffer \ call-next-template , ; +CHLOE: validation-errors + drop [ render-validation-errors ] [code] ; + : attr>word ( value -- word/f ) ":" split1 swap lookup ; diff --git a/extra/webapps/blogs/new-post.xml b/extra/webapps/blogs/new-post.xml index 9cb0250518..a2741ccd4e 100644 --- a/extra/webapps/blogs/new-post.xml +++ b/extra/webapps/blogs/new-post.xml @@ -13,5 +13,5 @@ - + diff --git a/extra/webapps/user-admin/edit-user.xml b/extra/webapps/user-admin/edit-user.xml index f41e8a97b4..27b6beaec6 100644 --- a/extra/webapps/user-admin/edit-user.xml +++ b/extra/webapps/user-admin/edit-user.xml @@ -51,7 +51,7 @@

- +

diff --git a/extra/webapps/user-admin/new-user.xml b/extra/webapps/user-admin/new-user.xml index 7acdd384ba..d3cf681165 100644 --- a/extra/webapps/user-admin/new-user.xml +++ b/extra/webapps/user-admin/new-user.xml @@ -46,7 +46,7 @@

- +