Skip to content

Instantly share code, notes, and snippets.

@dlisboa
Last active May 24, 2024 19:47
Show Gist options
  • Save dlisboa/1dc648912282fb4f911e0546963cb9db to your computer and use it in GitHub Desktop.
Save dlisboa/1dc648912282fb4f911e0546963cb9db to your computer and use it in GitHub Desktop.
package main
import "net/url"
import "regexp"
import "fmt"
import "os"
import "encoding/json"
type Username struct {
String string
Valid bool
}
type User struct {
Username Username `json:"username"`
}
func (u *User) Valid() bool {
return u.Username.Valid
}
func (u *User) UnmarshalFormData(values url.Values) error {
name, err := NewUsername(values.Get("username"))
if err != nil {
return err
}
u.Username = name
return nil
}
var re = regexp.MustCompile(`^\w{3,}$`)
func NewUsername(s string) (Username, error) {
if !re.MatchString(s) {
return Username{}, fmt.Errorf("Username invalid: %#v", s)
}
return Username{s, true}, nil
}
func (u Username) MarshalJSON() ([]byte, error) {
if u.Valid {
return json.Marshal(u.String)
}
return json.Marshal("!INVALID!")
}
type FormDataUnmarshaler interface {
UnmarshalFormData(url.Values) error
}
func decode[T FormDataUnmarshaler](into T, values url.Values) error {
err := into.UnmarshalFormData(values)
if err != nil {
return fmt.Errorf("decode: %w", err)
}
return nil
}
func main() {
var u User
values := url.Values{}
fmt.Println("with invalid form data username")
values.Set("username", "<inv@lid>**")
doDecode(u, values)
fmt.Println("with valid form data username")
values.Set("username", "dlisboa")
doDecode(u, values)
fmt.Println("with empty form data username")
values = url.Values{}
doDecode(u, values)
}
func doDecode(u User, vals url.Values) {
err := decode(&u, vals)
if err != nil {
fmt.Printf("%s, user: %+v\n", err, u)
}
json.NewEncoder(os.Stdout).Encode(u)
}
@dlisboa
Copy link
Author

dlisboa commented May 24, 2024

This kind of validation has a few properties:

  • it's idiomatic, works the same as UnmarshalText/UnmarshalJSON. If you look at the UnmarshalJSON implementation, it validates the input inside of it
  • allows one to call a general decode(T, url.Values) function, or call T.UnmarshalFormData(url.Values) explicitly
  • does not use reflection for validation: all of the validation happens in the UnmarshalFormData method using unreflected code
  • because of the above, the code is easy to follow and more efficient
  • uses the type system to enforce some boundaries: User doesn't have a string as username, but a Username type. Not all strings are valid usernames. If Usernames are only created using NewUsername we have assurance that when you pass a username to a function func f(u Username) it is already valid. The invalid strings stop at the barrier (the handler that called UnmarshalFormData)

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