goaltools / goal

Goal is a toolkit for high productivity web development in Go language in the spirit of Revel Framework that is built around the concept of code generation.
BSD 2-Clause "Simplified" License
87 stars 3 forks source link

Definition of controller #8

Open ghost opened 9 years ago

ghost commented 9 years ago

This issue is created to discuss the controllers: current state and what changes are needed (e.g. for #1).

Controllers are described here and actions here.

Controller

Any struct type that has actions is a controller.

type SampleController struct {
}

func (c SampleController) Index() http.Handler {
}

Action

Any exported method of a controller that returns http.Handler as a first argument.

Magic actions: Before and After

Just like regular actions but are executed automatically before / after every action of their controller.

type Profiles struct {
}

func (c Profiles) Before() http.Handler {
}

func (c Profiles) Index() http.Handler {
}

Arguments and results

Actions may expect any number of arguments of builtin types and return as many results as necessary.

func (c *Controller) MyValidAction(name string, age int) (http.Handler, MyType, error) {
}

The arguments will be taken from (in that order):

  1. The url /:path parameters (if default's router is used).
  2. The url ?query parameters.
  3. Submitted form values.

    Magic methods: Initially and Finally

https://github.com/colegion/goal/blob/gh-pages/manual/handlers/controllers.md#magic-methods

func (c *App) Initially(w http.ResponseWriter, r *http.Request) (finish bool) {
}

Initially is started before any action and Finally after. Moreover, Finally uses defer, so it will be started no matter what (i.e. even in case of action's panic).

Controller hierarchy

https://github.com/colegion/goal/blob/gh-pages/manual/handlers/controllers.md#inheritance

type ParentController struct {
}

type ParentController2 struct {
}

type ChildController struct {
    *ParentController
    *ParentController2
}

Child controller will inherit magic methods and magic actions of its parent controllers.


Code generation

To illustrate the idea of handlers package generation here is an example. If we have the following controllers package:

package controllers

type App struct {
    *SomeParent
}

func (c App) Before() http.Handler {
    if c.NotAuthorized() {
        return c.Redirect("/profiles/login")
    }
    return nil
}

func (c App) Index(page int) http.Handler {
    return c.RenderTemplate("/App/Index.html")
}

func (c App) Finally(w http.ResponseWriter, r *http.Request) bool {
    return false
}

we'll get the handlers package generated (actually it will be a bit more complex /here's an actual one/, but for clarity it was simplified):

// This package is automatically generated, don't edit.
package handlers

import (
    "http"

    c0 "github.com/user/project/controllers"
    c1 "path/to/some/parent"
)

func AppIndex(w http.ResponseWriter, r *http.Request) {
    c := &c0.App{}
    c.SomeParent := &c1.SomeParent{}
    defer c.Finally(w, r)

    if res := c.Before(); res != nil {
        res.ServeHTTP(w, r)
        return
    }

    res := c.Index(
        strconv.Int("page", r.Form),
    )
    res.ServeHTTP(w, r)
}
ghost commented 9 years ago

For implementation of #1 it will be necessary to modify "magic methods" (Initially and Finally). I don't want to introduce a new one, so they will have the signature:

func (c *Controller) Initially(http.ResponseWriter, *http.Request, map[string]string) bool

instead of the one they have now. The third argument will be used for passing current controller and action names from generated handler to the action (we are not using reflection, right?). The only concern is a use of map for passing just 2 arguments. Probably, []string is a better option or even []interface{}. So, after the change code above (in the previous comment) will look like:

func AppIndex(w http.ResponseWriter, r *http.Request) {
    ...
    defer c.Finally(w, r, []interface{}{"App", "Index"})
    ...
}
ghost commented 8 years ago

"Fields binding" has been implemented. Related info: https://github.com/colegion/goal/issues/37#issuecomment-179391953

Now it's possible to get request, response, current action name or controller name by creating a field with special tag in your controller:

type Controller struct {
    MyRequest        http.Request        `bind:"request"`
    MyResponseWriter http.ResponseWriter `bind:"response"`
    MyAction         string              `bind:"action"`
    MyController     string              `bind:"controller"`
}

Name of the field doesn't matter but it must be exported (i.e. Request, not request).

ghost commented 8 years ago

"Magic" Initially and Finally methods have been removed. Now there is only Before and After actions. But After works as Finally, i.e. it is called even if Before or SomeAction return non-nil result.

func (c *App) Before() http.Handler {
    println("Started first...")
    return nil
}

func (c *App) Index() http.Handler {
    println("Started if 'Before' returned nil...")
    return c.Render()
}

func (c *App) After() http.Handler {
    println("Started at the end...")
    return nil
}

In future versions I want to remove "fields binding" that are described in the comment above and use a standard binding mechanism for them. For illustration, now it's possible to bind parameters as follows:

func (c *App) Greet(name string) http.Handler

A limited number of types are supported (e.g. int, string, float32, etc.). The plan is to add support of http.Request, http.ResponseWriter for Request and Response.

The question is what to do with "current action name" and "current controller name", they are both of type string. I don't want to make user import custom packages. So, probably the following approach will be taken:

type action string
type controller string
func (c *App) Index(currentAction action, currentController controller) http.Handler {
}

Variables of type action and controller will be binded to current action/controller values if the types are custom types based on strings.

xpbliss commented 8 years ago

What's the difference between Initially and Before, like others Filter?

Use case such as Access controller before a certain controller instance?

ghost commented 8 years ago

@xpbliss Initially was a method that required specific input arguments (if you miss one of them the whole method was ignored):

func (c *App) Initially(http.ResponseWriter, *http.Request, as []string) bool {
}

It was used for passing Request, Response, and current action / controller name from handler function to your controller. Before is a regular action. I.e. it can take any arguments (that are automatically binded) and returns http.Handler as the first result:

func (c *App) Before(name string, page int) http.Handler {
}

Apart from that there were no differences. And "bind" struct tags are used for passing arguments to controller now. That's why Initially was removed and now there is only Before.