Controllers
Generator Arguments
Resolve typed controller action and BeforeAction arguments from route params and request input.
Ask for route params
Controller actions and BeforeAction hooks can request generated arguments:
func (c *PostsController) Show(postID int) error {
post, err := c.posts.Get(postID)
if err != nil {
return err
}
c.Set("post", post)
return nil
}
String and integer arguments resolve from named route parameters in path order.
Use all params when needed
Slice arguments receive all named route params:
func (c *CommentsController) Show(params []string) error {
teamID := params[0]
postID := params[1]
commentID := params[2]
_ = teamID
_ = postID
_ = commentID
return nil
}
For /teams/{team_id}/posts/{post_id}/comments/{comment_id}, the slice order
matches the path order.
Generate request input
Add a GenX method when an action or hook needs a custom type:
type PostInput struct {
Title string
}
func (c *PostsController) GenPostInput(
r *http.Request,
) (PostInput, error) {
if err := r.ParseForm(); err != nil {
return PostInput{}, err
}
return PostInput{Title: r.Form.Get("title")}, nil
}
func (c *PostsController) Create(input PostInput) error {
post, err := c.posts.Create(input.Title)
if err != nil {
return err
}
c.Set("post", post)
return nil
}
Generators return T or (T, error).
Decode forms in generators
For submitted forms, keep the form struct in its own file near the controller and let a generator own decoding:
// password_form.go
type PasswordForm struct {
Username string
Password string
}
func (c *SessionsController) GenPasswordForm() (*PasswordForm, error) {
form := &PasswordForm{}
if err := c.Decode(form); err != nil {
return nil, err
}
return form, nil
}
func (c *SessionsController) Create(form *PasswordForm) error {
username := strings.TrimSpace(form.Username)
if err := c.sessions.SignIn(username, form.Password); err != nil {
return err
}
return c.RedirectToRoute(
"admin",
lazycontroller.RedirectStatus(http.StatusSeeOther),
)
}
The action receives *PasswordForm and stays focused on application work
instead of parsing the request directly. Decode parses the current request
form through lazyschema; if decoding fails, the generator returns that error,
GoLazy skips the action, and the error goes through the normal
HandleError(http.ResponseWriter, *http.Request, error) path.
Application validation failures belong in the action. Set
http.StatusUnprocessableEntity, restore the form data needed by the view, and
render the form again. Successful creates should redirect, usually through a
named route:
type PostForm struct {
Title string `validate:"presence;min:3;max:120"`
Content string `validate:"presence"`
}
func (c *PostsController) Create(form *PostForm) error {
if err := lazyerrors.Validator(form); err != nil {
c.Status(http.StatusUnprocessableEntity)
c.Set("form", form)
c.Set("post_errors", err)
return c.Render("new")
}
post, err := c.posts.Create(form.Title, form.Content)
if err != nil {
return err
}
if err := c.FlashSet("notice", "Post created"); err != nil {
return err
}
return c.RedirectToRoute(
"post",
post.ID,
lazycontroller.RedirectStatus(http.StatusSeeOther),
)
}
lazyerrors.Validator returns an ordinary joined error. The joined leaves are
lazyerrors.ValidationError values, so templates can use
field_errors_for .post_errors "title" or errors_for .post_errors to render
field-specific messages.
Compose generated values
Generators may depend on standard arguments and other generated types:
func (c *PostsController) GenPost(
postID int,
) (postservice.Post, error) {
return c.posts.Get(postID)
}
func (c *PostsController) Show(post postservice.Post) error {
c.Set("post", post)
return nil
}
Generated values are cached inside the current controller request, so one
generated type is created only once even if BeforeAction and the routed
action both need it.
Let errors flow normally
Generator errors use the same path as action errors. When a generator returns a
non-nil error, GoLazy does not call the action or hook that requested it. It
passes that error to the controller error path instead. If the concrete
controller, or an embedded base controller, implements
HandleError(http.ResponseWriter, *http.Request, error), that method receives
the generator error exactly as it would receive an action error. If a
BeforeAction generator fails, the routed action does not run.
func (c *PostsController) GenPostInput(
r *http.Request,
) (PostInput, error) {
if r.ContentLength > 1<<20 {
return PostInput{}, lazycontroller.Error(
http.StatusRequestEntityTooLarge,
fmt.Errorf("form too large"),
)
}
// ...
}
Use this for request-derived values that should be typed action or
BeforeAction arguments. For shared authentication, keep the framework base
controller generic and define application-owned auth types in your app:
type User struct {
ID string
}
type AuthenticatedUser struct {
*User
}
var errLoginRequired = errors.New("login required")
func (c *BaseController) GenUser() (*User, error) {
value, ok, err := c.SessionGet("user_id")
if err != nil {
return nil, err
}
userID, _ := value.(string)
if !ok || userID == "" {
return nil, nil
}
return c.users.FindByID(userID)
}
func (c *BaseController) GenAuthenticatedUser(
user *User,
) (*AuthenticatedUser, error) {
if user == nil {
return nil, errLoginRequired
}
return &AuthenticatedUser{User: user}, nil
}
func (c *BaseController) BeforeAction(user *User) error {
c.Set("current_user", user)
return nil
}
func (c *BaseController) HandleError(
w http.ResponseWriter,
r *http.Request,
err error,
) error {
if errors.Is(err, errLoginRequired) {
return c.RedirectToRoute(
"login",
lazycontroller.RedirectStatus(http.StatusSeeOther),
)
}
return c.Base.HandleError(w, r, err)
}
func (c *AdminController) Index(
user *AuthenticatedUser,
) error {
c.Set("title", "Dashboard")
return nil
}
GenUser loads the optional current user once. BeforeAction exposes that user
to views for both public and protected pages. Protected actions request
*AuthenticatedUser; the wrapper type is owned by the application and the
generator turns a missing user into the normal controller error path.