Controllers

Generator Arguments

Resolve typed controller action and BeforeAction arguments from route params and request input.

By Guillermo Alvarez - Published - Updated

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.