7. Procesando formularios

En este post crearemos un formulario que interactuecon nuetros handlers para crear un nuevo snippet.

El flujo para el este preoceso estara basado en un patron estandar Post-Redirect-Get y trabajara de la siguiente manera:

  1. El usuario vera un formulario en blanco cuando haga un GET al path /snippet/create.
  2. Cuando el usuario complete el formulario hara un POST al servidor al path /snippet/create.
  3. Los datos del formulario sran validados en nuestro handler snippetCreatePost. Si hay algun error en alguno de los campos del formulario sera vuelvo a mostrar con los campos que contengan erroes de manera resaltada. Si todo esta bien los datos del nuevo snippet seran guardados en la base de datos y el usuario sera redirigido a /snippet/view/:id.

Lo que aprenderas:

  • Como parsear y acceder a los datos del formulario que llegan via POST
  • Algunas tecnicas para realizar validaciones comunes en los formularios.
  • Un patron user friendly para avisar al usuario de los errores que hay en los campos que haya enviado.
  • Como mantener tus handlers limpios usando helpers para la validacion de los formularios.

7.1 Configurar un Formulario HTML

Comencemos creando un nuevo template ui/html/pages/create.html

touch ui/html/pages/create.html

Pega esto dentro del archivo:

{{define "title"}}Create a New Snippet{{end}}
{{define "main"}}
<form action='/snippet/create' method='POST'>
  <div>
    <label>Title:</label>
    <input type='text' name='title'>
  </div>
  <div>
    <label>Content:</label>
    <textarea name='content'></textarea>
  </div>
  <div>
    <label>Delete in:</label>
    <input type='radio' name='expires' value='365' checked> One Year
    <input type='radio' name='expires' value='7'> One Week
    <input type='radio' name='expires' value='1'> One Day
  </div>
  <div>
    <input type='submit' value='Publish snippet'>
  </div>
</form>
{{end}}

Ahora pongamos en el nav.html el link para acceder a /snippet/create.html.

{{define "nav"}}
<nav>
  <a href="/"> Home</a>
  <a href='/snippet/create'>Create snippet</a>
</nav>
{{end}}

Por ultimo tenemos que actualizar el handler snippetCreateForm para que renderice el template.

 func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
- 	 w.Write([]byte("Display the form for creating a new snippet..."))
+     data := app.newTemplateData(r)
+     app.render(w, r, http.StatusOK, "create.html", data)
 }

Reinicia el servidor y veamos http://localhost:4000/snippet/create

7.2 Parseo de los datos del formulario

Gracias al trabajo realizado en rutas avanzadas cualquier peticion POST /snippets/create sera despachada por el handler snippetCreatePost.

Podemos dividir la siguiente tarea en dos pasos:

  1. Primero necesitamos usar r.ParseForm() para parsear el body de la peticion. Esto revisa que el body de la peticion este bien formado y luego guarda los datos de la peticion en un map r.PostForm . Si hay algun error durante el parseo del body ( como que no haya body ) entonces retornara un error.Podemos llamar r.ParseForm() de manera segura varias veces en la misma Request sin que haya efecto secundarios.
  2. Podemos obtener los datos del formulario que esten contenidos en r.PostForm usando el metodo r.PostForm.Get() . Por ejemplo si quremos obtener el campo title hacemos r.PostForm.Get(“title”). Si no hay match con el campo entonces retornara un string vacio "". Similar a como los query string parameters funcionan.

Vamos a cmd/web/handlers.go snippetCreatePost y actualizamos con el siguiente codigo:

 func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
-	 // Checking if the request method is a POST is now superfluous and can be
-	 // removed, because this is done automatically by httprouter.
-	 title := "O snail"
-	 content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n– Kobayashi Issa"
-	 expires := 7
+   // First we call r.ParseForm() which adds any data in POST request bodies
+	// to the r.PostForm map. This also works in the same way for PUT and PATCH
+	// requests. If there are any errors, we use our app.ClientError() helper to
+	// send a 400 Bad Request response to the user.
+	err := r.ParseForm()
+	if err != nil {
+		app.clientError(w, http.StatusBadRequest)
+		return
+	}
+	// Use the r.PostForm.Get() method to retrieve the title and content
+	// from the r.PostForm map.
+	title := r.PostForm.Get("title")
+	content := r.PostForm.Get("content")
+	// The r.PostForm.Get() method always returns the form data as a *string*.
+	// However, we're expecting our expires value to be a number, and want to
+	// represent it in our Go code as an integer. So we need to manually covert
+	// the form data to an integer using strconv.Atoi(), and we send a 400 Bad
+	// Request response if the conversion fails.
+	expires, err := strconv.Atoi(r.PostForm.Get("expires"))
+	if err != nil {
+		app.clientError(w, http.StatusBadRequest)
+		return
+	}
 id, err := app.snippets.Insert(title, content, expires)
 if err != nil {
  app.serverError(w, r, err)
  return
 }
//Update the redirect path to use the new clean URL format.
 http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

Reinicia el servidor e intenta crear un nuevo snippet desde el formulario. Si todo salio bien seras redirigido al post en cuestion.

7.2 Informacion adicional

7.2 El map r.Form

En el codigo anterior venimos usando r.PostForm para acceder a los valores del formulario. Pero una alternativa es usar el map r.Form.

r.PostForm es propagado solo para las peticiones POST, PATCH Y PUT y contiene los datos del formulario desde el body de la peticion.

Por otro lado r.Form es propagado por todos los tipos de peticiones y contiene los dtos del formulario desde el boidy de la peticion y los parametros que pudieran venir via URL (query string parameters). Si nuestro formulario fue enviado desde snippet/create?foo=bar, nosotros tambien podemos obtener el valor de foo llamndo a r.Form.Get("foo") , En caso de que haya un conflicto el body de la peticion tendra mayor peso sobre el query string parameter.

7.2 Los metodos FormValue y PostFormValue

El paquete net/http tambien provee los metodos r.FormValue() y r.PostFormValue(). Son basicamente funciones shortcut que llaman a r.ParseForm por vos y luego obtienen el valor del campo desde r.Form o r.PostForm respectivamente.

Recomiendo evitar estos shortcuts ya que ignora errores de manera silenciosa retornados por r.PostForm y no es lo ideal, esto significa que podriamos encontrarnos con errores por parte de los usuarios pero no tendriamos nigun mecanismo de feedback que nos permita saberlo.

7.2 Campos con valores multiples

r.PostForm.Get() solo retorna el valor del primer campo especifico. Lo que significa que no podemos usarlo con campos que envien multiples valores como un checkbox.

<input type="checkbox" name="items" value="foo"> Foo
<input type="checkbox" name="items" value="bar"> Bar
<input type="checkbox" name="items" value="baz"> Baz

En este caso necesitaras trabajar con el map r.PostForm directamente el tipo subyacente de r.PostForm es url.Values el cual a su vez es de tipo _map[string][]string. Asi que para multiples valores podes hacer un loop sobre ese map asi :

for i, item := range r.PostForm["items"] {
 fmt.Fprintf(w, "%d: Item %s\n", i, item)
}
7.2 Limitando el tamanio del formulario

Al menos que estemos mandando multipart data desde el formulario con el atributo enctype=“multipart/form-data” las peticiones PUT,POST Y PATCH tienen un limite en el body de 10MB , si este limite es exedido r.ParseForm regresara un error.

Si queres cambiar este limite podes usar la funcion http.MaxBytesReader() asi :

// Limit the request body size to 4096 bytes
 r.Body = http.MaxBytesReader(w, r.Body, 4096)
 err := r.ParseForm()
 if err != nil {
   http.Error(w, "Bad Request", http.StatusBadRequest)
   return
  }

En caso de superar ese limite MaxBytesReader setea un flag en http.ResponseWriter el cual hace cerrar la conexion TCP en lugar de procesar los datos.

7.3 Validando los datos del formulario.

En estos momentos no estamos validando los datos que ingresa el usuario en el formulario de ninguna manera. Tenemos que estar serguros de que los datos del formulario estan presentes, y que sigan las reglas que necesitamos.

Para este formulario necesitaremos.

  • Revisar que el title y el content no esten vacios.
  • Revisar que el title no tenga mas de 100 caracteres de largo.
  • Revisar que el campo expires haga match con alguno de los datos permitidos (1,7 o 365 dias)

Estas revisiones son bastantes simples de realizar con unos if y varias funciones de Go con los paquetes de Go strings y unicode/utf8 .

Vamos a handlers.go y actualicemos snippetCreatePost para incluir las validaciones apropiadas asi:

 func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
 	err := r.ParseForm()
 	if err != nil {
 		app.clientError(w, http.StatusBadRequest)
 		return
 	}
 
 	title := r.PostForm.Get("title")
 	content := r.PostForm.Get("content")
 
 	expires, err := strconv.Atoi(r.PostForm.Get("expires"))
 	if err != nil {
 		app.clientError(w, http.StatusBadRequest)
 		return
 	}

+	// Initialize a map to hold any validation errors for the form fields.
+	fieldErrors := make(map[string]string)
+
+	// Check that the title value is not blank and is not more than 100
+	// characters long. If it fails either of those checks, add a message to the
+	// errors map using the field name as the key.
+	if strings.TrimSpace(title) == "" {
+		fieldErrors["title"] = "This field cannot be blank"
+	} else if utf8.RuneCountInString(title) > 100 {
+		fieldErrors["title"] = "This field cannot be more than 100 characters long"
+	}
+
+	// Check that the Content value isn't blank.
+	if strings.TrimSpace(content) == "" {
+		fieldErrors["content"] = "This field cannot be blank"
+	}
+
+	// Check the expires value matches one of the permitted values (1, 7 or
+	// 365).
+	if expires != 1 && expires != 7 && expires != 365 {
+		fieldErrors["expires"] = "This field must equal 1, 7 or 365"
+	}
+	// If there are any errors, dump them in a plain text HTTP response and
+	// return from the handler.
+	if len(fieldErrors) > 0 {
+		fmt.Fprint(w, fieldErrors)
+		return
+	}

  	id, err := app.snippets.Insert(title, content, expires)
  	if err != nil {
  		app.serverError(w, r, err)
  		return
  	}
  	// Update the redirect path to use the new clean URL format.
  	http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

Cuando revisamos el largo del campo title usamos utf8.RuneCountInString() en vez de len() . El motivo es que nosotros necesitamos que se cuenten el numero de caracteres unicode en vez del numero de bytes, por ejemplo “Zoë” contiene 3 caracteres unicode, pero 4 bytes , ya que ë usa dieresis.

Vamos a probarlo, Reinicia el servidor e intenta enviar el formulario con un title que tenga mas de 100 caracteres y el content en blanco.

Deberias ver algo como esto sobre una pantalla en blanco : map[content:This field cannot be blank title:This field cannot be more than 100 characters long]

Podes encontrar muchos patrones de validaciones para formularios aca

7.4 Mostrando errores y rellenando los campos.

Ahora que tenemos nuestro handler snippetCreatePost validando los datos, lo siguiente es manejar esos errores de manera agraciada.

Si tenemos algun error lo que queremos hacer es volver a mostrar el formulario, resaltando los campos en los cuales hubo un error y rellenandolos con los datos previos a que sean enviados , de manera que el usurio no tenga que volver a escribir todo otravez.

Para hacer esto vamos a agregar un nuevo campo Form a nuestra estructura templateData : cmd/web/templates.go .

 package main
 
 import (
 	"html/template"
 	"path/filepath"
 	"time"
 
 	"github.com/nahueldev23/snippetbox/internal/models"
 )
 
 type templateData struct {
 	CurrentYear int
 	Snippet     models.Snippet
 	Snippets    []models.Snippet
+	Form        any
 }
...

Vamos a usar ese campo Form para pasar las validaciones de los errores y los datos enviados para regreasarlos al tempate cuadno re rendericemos el formulario HTML.

Abramos el archivo cmd/web/handlers.go y definamos la estructura snippetCreateForm para contener los datos recibidos y las validaciones de los errores.

 package main

...

+ type snippetCreateForm struct {
+	Title       string
+	Content     string
+	Expires     int
+	FieldErrors map[string]string
+ }

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

-	title := r.PostForm.Get("title")
-	content := r.PostForm.Get("content")

	expires, err := strconv.Atoi(r.PostForm.Get("expires"))
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

-	fieldErrors := make(map[string]string)

-	if strings.TrimSpace(title) == "" {
-		fieldErrors["title"] = "This field cannot be blank"
-	} else if utf8.RuneCountInString(title) > 100 {
-		fieldErrors["title"] = "This field cannot be more than 100 characters long"
-	}

	
-	if strings.TrimSpace(content) == "" {
-		fieldErrors["content"] = "This field cannot be blank"
-	} if expires != 1 && expires != 7 && expires != 365 {
-		fieldErrors["expires"] = "This field must equal 1, 7 or 365"
-	}

-	if len(fieldErrors) > 0 {
-		fmt.Fprint(w, fieldErrors)
-		return
-	}

-	id, err := app.snippets.Insert(title, content, expires)

  // Create an instance of the snippetCreateForm struct containing the values
	// from the form and an empty map for any validation errors.
	form := snippetCreateForm{
		Title:       r.PostForm.Get("title"),
		Content:     r.PostForm.Get("content"),
		Expires:     expires,
		FieldErrors: map[string]string{},
	}
	// Update the validation checks so that they operate on the snippetCreateForm
	// instance.
	if strings.TrimSpace(form.Title) == "" {
		form.FieldErrors["title"] = "This field cannot be blank"
	} else if utf8.RuneCountInString(form.Title) > 100 {
		form.FieldErrors["title"] = "This field cannot be more than 100 characters long"
	}
	if strings.TrimSpace(form.Content) == "" {
		form.FieldErrors["content"] = "This field cannot be blank"
	}
	if form.Expires != 1 && form.Expires != 7 && form.Expires != 365 {
		form.FieldErrors["expires"] = "This field must equal 1, 7 or 365"
	}
	// If there are any validation errors, then re-display the create.html template,
	// passing in the snippetCreateForm instance as dynamic data in the Form
	// field. Note that we use the HTTP status code 422 Unprocessable Entity
	// when sending the response to indicate that there was a validation error.
	if len(form.FieldErrors) > 0 {
		data := app.newTemplateData(r)
		data.Form = form
		app.render(w, r, http.StatusUnprocessableEntity, "create.html", data)
		return
	}
	// We also need to update this line to pass the data from the
	// snippetCreateForm instance to our Insert() method.
	id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)

	if err != nil {
		app.serverError(w, r, err)
		return
	}

	http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

Con esto si tenemos algun error en las validaciones re renderizaremos el template create.html
teniendo acceso a los datos de error y los previamente enviados con la ayuda del campo Form.

Podemos ahora Reinicia el servidor y ver que compile correctamente.

7.4 Actualizando el template HTML

Lo siguiente es actualizar create.html para que muestre los errores y rellene los campos con los datos anteriores .

Rellenar los campos es facil, tenemos que poder usar {{.Form.Title}} y {{.Form.Content}} como hicimos en post anteriores mostrando los snippets .

Para los errores, el tipo que manejan nuestros FieldsErrors es map[string]string , en el cual podemos acceder al valor a traves de la key name . Por ejemplo si queremos ver la validacion del error title usaremos el tag {{.Form.FieldErrors.title}} en nuesro template.

Distintno a como lo hacemos en las estructuras, las key de los maps no tienen que ser capitalizados para poder ser accedidos desde el template.

Con esto en mente vamos a actualizar create.html .

{{define "title"}}Create a New Snippet{{end}} {{define "main"}}
<form action="/snippet/create" method="POST">
  <div>
    <label>Title:</label>
    <!-- Use the `with` action to render the value of .Form.FieldErrors.title
if it is not empty. -->
    {{with .Form.FieldErrors.title}}
    <label class="error">{{.}}</label>
    {{end}}
    <!-- Re-populate the title data by setting the `value` attribute. -->
    <input type="text" name="title" value="{{.Form.Title}}" />
  </div>
  <div>
    <label>Content:</label>
    <!-- Likewise render the value of .Form.FieldErrors.content if it is not
empty. -->
    {{with .Form.FieldErrors.content}}
    <label class="error">{{.}}</label>
    {{end}}
    <!-- Re-populate the content data as the inner HTML of the textarea. -->
    <textarea name="content">{{.Form.Content}}</textarea>
  </div>
  <div>
    <label>Delete in:</label>
    <!-- And render the value of .Form.FieldErrors.expires if it is not empty. -->
    {{with .Form.FieldErrors.expires}}
    <label class="error">{{.}}</label>
    {{end}}
    <!-- Here we use the `if` action to check if the value of the re-populated
expires field equals 365. If it does, then we render the `checked`
attribute so that the radio input is re-selected. -->
    <input type="radio" name="expires" value="365" {{if (eq .Form.Expires 365)}}checked{{end}} />
    One Year
    <!-- And we do the same for the other possible values too... -->
    <input type="radio" name="expires" value="7" {{if (eq .Form.Expires 7)}}checked{{end}} />
    One Week
    <input type="radio" name="expires" value="1" {{if (eq .Form.Expires 1)}}checked{{end}} />
    One Day
  </div>
  <div>
    <input type="submit" value="Publish snippet" />
  </div>
</form>
{{end}}

Hay algo mas que tenemos que hacer, si intentamos correr la app ahora, nos arrojara un 500 internal server error , esto es porque el handler snippetCreate actualmente no tiene seteado el valor de templateData.Form, lo que significa que cualdno el template quiere leer {{with .Form.FieldErrors.title}} Form es nil.

Arreglemoslo , vamos al handler snippetCreate e inicialicemos snippetCreateForm y pasemoslo al template asi cmd/web/handlers.go:

func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
	data := app.newTemplateData(r)

	// Initialize a new createSnippetForm instance and pass it to the template.
	// Notice how this is also a great opportunity to set any default or
	// 'initial' values for the form --- here we set the initial value for the
	// snippet expiry to 365 days.
	data.Form = snippetCreateForm{
		Expires: 365,
	}

	app.render(w, r, http.StatusOK, "create.html", data)
}

Ahora Reinicia el servidor y deberia funcionar correctamente junto con los errores.

7.5 Creando helpers para la validacion.

Vamos a pasar nuestras validaciones a helpers de manera que podamos reutilizar el codigo en el resto de nuesta app de ser necesario, esto hara que nuestra app siga funcionando como antes pero quitaremos codigo de nuestros handlers.

7.5 Agregando el paquete validator.

Creemos el archivo.

$ mkdir internal/validator
$ touch internal/validator/validator.go

Agregemos en validator.go el siguiente codigo:

package validator

import (
	"slices"
	"strings"
	"unicode/utf8"
)

// Define a new Validator struct which contains a map of validation error messages
// for our form fields.
type Validator struct {
	FieldErrors map[string]string
}

// Valid() returns true if the FieldErrors map doesn't contain any entries.
func (v *Validator) Valid() bool {
	return len(v.FieldErrors) == 0
}

// AddFieldError() adds an error message to the FieldErrors map (so long as no
// entry already exists for the given key).
func (v *Validator) AddFieldError(key, message string) {
	// Note: We need to initialize the map first, if it isn't already
	// initialized.
	if v.FieldErrors == nil {
		v.FieldErrors = make(map[string]string)
	}
	if _, exists := v.FieldErrors[key]; !exists {
		v.FieldErrors[key] = message
	}
}

// CheckField() adds an error message to the FieldErrors map only if a
// validation check is not 'ok'.
func (v *Validator) CheckField(ok bool, key, message string) {
	if !ok {
		v.AddFieldError(key, message)
	}
}

// NotBlank() returns true if a value is not an empty string.
func NotBlank(value string) bool {
	return strings.TrimSpace(value) != ""
}

// MaxChars() returns true if a value contains no more than n characters.
func MaxChars(value string, n int) bool {
	return utf8.RuneCountInString(value) <= n
}

// PermittedValue() returns true if a value is in a list of specific permitted
// values.
func PermittedValue[T comparable](value T, permittedValues ...T) bool {
	return slices.Contains(permittedValues, value)
}

El codigo anterior define una estructura Validator el cual contien un map de mensajes de errores. El Validator provee un metodo CheckField para agregar errores de manera condicional y un metodo Valid() que nos dice si el map de errores esta vacio o no. Tambien agregamos NotBlank() , MaxChars() y PermittedValue() para ayudarnos a la realizacion de algunas validaciones especificas.

PermittedValue() es una funcion generica que trabajara con valores de diferentes tipos. veremos mas de esto mas adelante en este post.

Usando los helpers

Comencemos poniendo a Validator en accion.

Volvamos a cmd/web/handlers.go y actualicemoslo para embeber la estructura Validator en la estructura snippetCreateForm y luego lo usaremos para realizar las validaciones de los datos.

Si no estas familiarizado con el concepto embedding Go Eli Bendersky escribio una buena introduccion podes leerla antes de seguir.

 package main

 import (
 	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
 	"strings"
 	"unicode/utf8"
 
 	"github.com/julienschmidt/httprouter" // New import
 	"github.com/nahueldev23/snippetbox/internal/models"
 	"github.com/nahueldev23/snippetbox/internal/validator"
 )
...

 type snippetCreateForm struct {
	Title       string
	Content     string
	Expires     int
-	FieldErrors map[string]string
+  validator.Validator
 }

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}



	expires, err := strconv.Atoi(r.PostForm.Get("expires"))
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

  // Create an instance of the snippetCreateForm struct containing the values
	// from the form and an empty map for any validation errors.
	form := snippetCreateForm{
		Title:       r.PostForm.Get("title"),
		Content:     r.PostForm.Get("content"),
		Expires:     expires,
+    // Remove the FieldErrors assignment from here.
-		FieldErrors: map[string]string{},
	}


-	if strings.TrimSpace(form.Title) == "" {
-		form.FieldErrors["title"] = "This field cannot be blank"
-	} else if utf8.RuneCountInString(form.Title) > 100 {
-		form.FieldErrors["title"] = "This field cannot be more than 100 characters long"
-	}
-	if strings.TrimSpace(form.Content) == "" {
-		form.FieldErrors["content"] = "This field cannot be blank"
-	}
-	if form.Expires != 1 && form.Expires != 7 && form.Expires != 365 {
-		form.FieldErrors["expires"] = "This field must equal 1, 7 or 365"
-	}
-
-	if len(form.FieldErrors) > 0 {
-		data := app.newTemplateData(r)
-		data.Form = form
-		app.render(w, r, http.StatusUnprocessableEntity, "create.html", data)
-		return
-	}

	// Because the Validator struct is embedded by the snippetCreateForm struct,
	// we can call CheckField() directly on it to execute our validation checks.
	// CheckField() will add the provided key and error message to the
	// FieldErrors map if the check does not evaluate to true. For example, in
	// the first line here we "check that the form.Title field is not blank". In
	// the second, we "check that the form.Title field has a maximum character
	// length of 100" and so on.
	form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
	form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
	form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
	form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")
	// Use the Valid() method to see if any of the checks failed. If they did,
	// then re-render the template passing in the form in the same way as
	// before.
	if !form.Valid() {
		data := app.newTemplateData(r)
		data.Form = form
		app.render(w, r, http.StatusUnprocessableEntity, "create.html", data)
		return
	}
	id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)

	if err != nil {
		app.serverError(w, r, err)
		return
	}

	http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

Ahora tenemos un paquete internal/validator con reglas de validacion y logica que puede ser reusada a traves de nuestra app , y que puede ser facilmente extendida con nuevas reglas en el futuro. Los datos y los errores estan encapsultados en la misma estrucura snippetCreateForm el cual es facil de pasar a los templates.

Podemos reinicar el servidor y corroborar que esta funcionando todo bien.

Informacion adicional

Generics

Los tipos genericos tambien conocido como parametric polymorphism nos permite escribir codigo que trabaja con diferentes tipos.

En versiones anteriores de Go, si querias contar cuantas veces aparecia un caracter en un []string slice tenias que escribir dos funciones diferentes para que una cuente los datos de topo string y otra los int

// Count how many times the value v appears in the slice s.
func countString(v string, s []string) int {
	count := 0
	for _, vs := range s {
		if v == vs {
			count++
		}
	}
	return count
}
func countInt(v int, s []int) int {
	count := 0
	for _, vs := range s {
		if v == vs {
			count++
		}
	}
	return count
}

Ahora con los genericos podemos tener un solo count() que trabaje con []string , []int, o cualquier otro slice que este en comparable type. El codigo se ve algo asi:

func count[T comparable](v T, s []T) int {
	count := 0
	for _, vs := range s {
		if v == vs {
			count++
		}
	}
	return count
}

No es necesario usar genericos y esta bien no hacerlo.

Pero incluso con esas advertencias puede ser util escribir codigo genereico en ciertos escenarios, podes considerar hacerlo si:

  • Te encontras escribiendo codigo repetitivo para diferentes tipos de datos. Ejemplos de esto puede ser operaciones comunes con slices,maps o channels (o helpers para crear validaciones o aserciones de test con diferentes tipos de datos).
  • Cuando te encuentras usando interfaces vacias interface un ejemplo podria ser si estas creando una estructura de datos como colas, cache o linked list, lo cuales necesitan operar con diferentes tipos.

En contraste, probablemente no debas usar genereicos:

  • Si hace que tu codigo sea menos legible y menos limpio.
  • Si todos los tipos con los que necesitas trabajar tienen en comun un conjunto de metodos , en ese caso es mejor usar una interface.
type Calculable interface {
    Calcular() int
}

type TipoA struct {
    // ...
}

func (a TipoA) Calcular() int {
    // Implementación de Calcular para TipoA
}

type TipoB struct {
    // ...
}

func (b TipoB) Calcular() int {
    // Implementación de Calcular para TipoB
}
  • Es preferible no escribir con genericos por default, y luego ir agregando genericos a medida que vayamos necesitando.

7.6 Form parsing automatico.

Podemos simplificar nuestro handler snippetCreatePost usando librerias de terceros, como go-playground/form o gorilla/schema para que automaticamente decodifique los datos del formulario en la estructura createSnippetForm .Usar un decoder automatico es totalmente opcional , pero puede ayudar a ahorrar tiempo y tipeo, especialmente si tu aplicacion tiene muchos formularios o si encesitas preocesar formularios muy largos.

Vamos a ver como usar el paquete go-playground/form . Instalemoslo.

$ go get github.com/go-playground/form/v4@v4
go get: added github.com/go-playground/form/v4 v4.2.1

7.6 Usando del decodificador del formulario.

Para hacer este trabajo, lo primero que tenemos que hacer es inicializar un nuevo *form.Decoder en nuestro main.go y dejarlo disponible para nuestros handlers como dependencia asi:

  package main
  
  import (
  	"database/sql"
  	"flag"
  	"html/template"
  	"log"
  	"net/http"
  	"os"
  
  	"log/slog"
  
  	"github.com/go-playground/form/v4"
  	_ "github.com/go-sql-driver/mysql"
  	"github.com/nahueldev23/snippetbox/internal/models"
  )
  
  type application struct {
  	logger        *slog.Logger
  	errorLog      *log.Logger
  	infoLog       *log.Logger
  	snippets      *models.SnippetModel
  	templateCache map[string]*template.Template
+	  formDecoder   *form.Decoder
  }
  
  func main() {
  	addr := flag.String("addr", ":4000", "HTTP network address")
    
+    //tu password en xxxx
  	dsn := flag.String("dsn", "web:xxxx/snippetbox?parseTime=true", "HTTP network address")
  
  	flag.Parse()
  
  	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
  
  	infoLog := log.New(os.Stdout, "INFO \t", log.Ldate|log.Ltime)
  	errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
  
  	db, err := openDB(*dsn)
  	if err != nil {
  		errorLog.Fatal(err)
  	}
  
  	defer db.Close()
  	templateCache, err := newTemplateCache()
  	if err != nil {
  		errorLog.Fatal(err)
  	}
  
    // Initialize a decoder instance...
+	  formDecoder := form.NewDecoder()
  
  	app := &application{
  		logger:        logger,
  		errorLog:      errorLog,
  		infoLog:       infoLog,
  		snippets:      &models.SnippetModel{DB: db},
  		templateCache: templateCache,
+   formDecoder:   formDecoder,  
  	}
  
  	srv := &http.Server{
  		Addr:     *addr,
  		ErrorLog: errorLog,
  		Handler:  app.routes(),
  	}
  
  	logger.Info("Starting server", "addr", *addr)
  
  	err = srv.ListenAndServe()
  	logger.Error(err.Error())
  	os.Exit(1)
}

  func openDB(dsn string) (*sql.DB, error) {
  	db, err := sql.Open("mysql", dsn)
  	if err != nil {
  		return nil, err
  	}
  	if err = db.Ping(); err != nil {
  		return nil, err
  	}
  	return db, nil
  }

LO siguente es ir a cmd/web/handlers.go y actualicemoslo para hacer uso de decoder asi:

  // Update our snippetCreateForm struct to include struct tags which tell the
  // decoder how to map HTML form values into the different struct fields. So, for
  // example, here we're telling the decoder to store the value from the HTML form
  // input with the name "title" in the Title field. The struct tag `form:"-"`
  // tells the decoder to completely ignore a field during decoding.
  type snippetCreateForm struct {
  	Title               string `form:"title"`
  	Content             string `form:"content"`
  	Expires             int    `form:"expires"`
  	validator.Validator `form:"-"`
  }

  func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
  	err := r.ParseForm()
  	if err != nil {
  		app.clientError(w, http.StatusBadRequest)
  		return
  	}
  
-	expires, err := strconv.Atoi(r.PostForm.Get("expires"))
-	if err != nil {
-		app.clientError(w, http.StatusBadRequest)
-		return
-	}
-
-		form := snippetCreateForm{
-		Title:   r.PostForm.Get("title"),
-		Content: r.PostForm.Get("content"),
-		Expires: expires,
-	}

+  // Declare a new empty instance of the snippetCreateForm struct.
+	var form snippetCreateForm
+	// Call the Decode() method of the form decoder, passing in the current
+	// request and *a pointer* to our snippetCreateForm struct. This will
+	// essentially fill our struct with the relevant values from the HTML form.
+	// If there is a problem, we return a 400 Bad Request response to the client.
+	err = app.formDecoder.Decode(&form, r.PostForm)
+	if err != nil {
+		app.clientError(w, http.StatusBadRequest)
+		return
+	}

  form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
	form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
	form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
	form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")

	if !form.Valid() {
		data := app.newTemplateData(r)
		data.Form = form
		app.render(w, r, http.StatusUnprocessableEntity, "create.html", data)
		return
	}
	id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
	if err != nil {
		app.serverError(w, r, err)
		return
	}
	// Update the redirect path to use the new clean URL format.
	http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

Nosotros podemos usar una estructura simple con tags para definir un mapa entre nuestro formulario HTML y nuestros campos de destino y desenvolvemos los datos de formulario en el destino solo con unas pocas lineas de codigo , indistintamente de cuan largo sea el formulario.

Otra cosa importante es que hace la conversion de tipos automaticamente.

Pero tenemos un problema. Cuando llamamos app.formDecoder.Decode() necesita que el destino de la decodificacion sea un puntero que no sea nil. Si intentamos pasar algo que no sea un non-nil pointer , entonces Decode() retornara un error form.InvalidDecodeError.

Si llega a pasar, es un error critico de nuestra aplicacion.Por lo que necesitamos revisar este error especialmente y manejarlo como un caso especial, en vez de solo retornar 400 bad request

7.6 Creando un helper decodePostForm

Para hacerlo vamos a crear un nuevo helper decodePostForm() que hara tres cosas:

  • Llamar a r.ParseForm para la peticion actual.
  • Llamar app.formDecoder.Decode() para desempaquetar los datos del formulario HTML hacia el destino.
  • Revisar si hay un error form.InvalidDecodeError y disparar un panic si lo vemos alguna vez.

Vamos a cmd/web/helpers.go y agregamos lo siguiente:

package main

import (
	"bytes"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/go-playground/form/v4"
)

// Create a new decodePostForm() helper method. The second parameter here, dst,
// is the target destination that we want to decode the form data into.
func (app *application) decodePostForm(r *http.Request, dst any) error {
	// Call ParseForm() on the request, in the same way that we did in our
	// createSnippetPost handler.
	err := r.ParseForm()
	if err != nil {
		return err
	}
	// Call Decode() on our decoder instance, passing the target destination as
	// the first parameter.
	err = app.formDecoder.Decode(dst, r.PostForm)
	if err != nil {
		// If we try to use an invalid target destination, the Decode() method
		// will return an error with the type *form.InvalidDecoderError.We use
		// errors.As() to check for this and raise a panic rather than returning
		// the error.
		var invalidDecoderError *form.InvalidDecoderError
		if errors.As(err, &invalidDecoderError) {
			panic(err)
		}
		// For all other errors, we return them as normal.
		return err
	}
	return nil
}

Con esto realizado, vamos a haceer la ultima simplificacion a nuestro handler createSnippetForm hagamos uso de decodePostForm y quitemos r.ParseForm asi:

package main
...

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
-	 err := r.ParseForm()
+  var form snippetCreateForm
+  err := app.decodePostForm(r, &form)
	if err != nil {
		app.clientError(w, http.StatusBadRequest)
		return
	}

-	var form snippetCreateForm

-	err = app.formDecoder.Decode(&form, r.PostForm)
-	if err != nil {
-		app.clientError(w, http.StatusBadRequest)
-		return
-	}

	form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
	form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
	form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
	form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")

	if !form.Valid() {
		data := app.newTemplateData(r)
		data.Form = form
		app.render(w, r, http.StatusUnprocessableEntity, "create.html", data)
		return
	}
	id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
	if err != nil {
		app.serverError(w, r, err)
		return
	}
	// Update the redirect path to use the new clean URL format.
	http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

Nuestro código de controlador ahora es conciso y claro en términos de su comportamiento y lo que está haciendo. Y tenemos un patrón general en su lugar para el procesamiento y la validación de formularios que podemos reutilizar fácilmente en otros formularios en nuestro proyecto, como los formularios de registro y inicio de sesión de usuarios que construiremos pronto.