Skip to content

Commit

Permalink
Implemented field binding
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Kucherenko committed Feb 10, 2016
1 parent 3b0dc52 commit 8d7d66a
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 27 deletions.
6 changes: 3 additions & 3 deletions internal/skeleton/assets/handlers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ type tApp struct {

// New allocates (github.com/colegion/goal/internal/skeleton/controllers).App controller,
// initializes its parents; then returns the controller.
func (t tApp) New() *contr.App {
func (t tApp) New(w http.ResponseWriter, r *http.Request, ctr, act string) *contr.App {
c := &contr.App{}
c.Controllers = Controllers.New()
c.Controllers = Controllers.New(w, r, ctr, act)
return c
}

Expand Down Expand Up @@ -90,7 +90,7 @@ func (t tApp) Finally(c *contr.App, w http.ResponseWriter, r *http.Request, a []
// Index is an action that renders a home page.
func (t tApp) Index(w http.ResponseWriter, r *http.Request) {
var h http.Handler
c := App.New()
c := App.New(w, r, "App", "Index")
defer func() {
if h != nil {
h.ServeHTTP(w, r)
Expand Down
8 changes: 4 additions & 4 deletions internal/skeleton/assets/handlers/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ type tControllers struct {

// New allocates (github.com/colegion/goal/internal/skeleton/controllers).Controllers controller,
// initializes its parents; then returns the controller.
func (t tControllers) New() *contr.Controllers {
func (t tControllers) New(w http.ResponseWriter, r *http.Request, ctr, act string) *contr.Controllers {
c := &contr.Controllers{}
c.Requests = c0.Requests.New()
c.Templates = c1.Templates.New()
c.Sessions = c2.Sessions.New()
c.Requests = c0.Requests.New(w, r, ctr, act)
c.Templates = c1.Templates.New(w, r, ctr, act)
c.Sessions = c2.Sessions.New(w, r, ctr, act)
return c
}

Expand Down
8 changes: 4 additions & 4 deletions internal/skeleton/assets/handlers/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ type tErrors struct {

// New allocates (github.com/colegion/goal/internal/skeleton/controllers).Errors controller,
// initializes its parents; then returns the controller.
func (t tErrors) New() *contr.Errors {
func (t tErrors) New(w http.ResponseWriter, r *http.Request, ctr, act string) *contr.Errors {
c := &contr.Errors{}
c.Controllers = Controllers.New()
c.Controllers = Controllers.New(w, r, ctr, act)
return c
}

Expand Down Expand Up @@ -88,7 +88,7 @@ func (t tErrors) Finally(c *contr.Errors, w http.ResponseWriter, r *http.Request
// NotFound prints an error 404 "Page Not Found" message.
func (t tErrors) NotFound(w http.ResponseWriter, r *http.Request) {
var h http.Handler
c := Errors.New()
c := Errors.New(w, r, "Errors", "NotFound")
defer func() {
if h != nil {
h.ServeHTTP(w, r)
Expand Down Expand Up @@ -120,7 +120,7 @@ func (t tErrors) NotFound(w http.ResponseWriter, r *http.Request) {
// InternalError displays an error 500 "Internal Server Error" message.
func (t tErrors) InternalError(w http.ResponseWriter, r *http.Request) {
var h http.Handler
c := Errors.New()
c := Errors.New(w, r, "Errors", "InternalError")
defer func() {
if h != nil {
h.ServeHTTP(w, r)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type tRequests struct {

// New allocates (github.com/colegion/contrib/controllers/requests).Requests controller,
// then returns it.
func (t tRequests) New() *contr.Requests {
func (t tRequests) New(w http.ResponseWriter, r *http.Request, ctr, act string) *contr.Requests {
c := &contr.Requests{}
return c
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type tSessions struct {

// New allocates (github.com/colegion/contrib/controllers/sessions).Sessions controller,
// then returns it.
func (t tSessions) New() *contr.Sessions {
func (t tSessions) New(w http.ResponseWriter, r *http.Request, ctr, act string) *contr.Sessions {
c := &contr.Sessions{}
return c
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type tTemplates struct {

// New allocates (github.com/colegion/contrib/controllers/templates).Templates controller,
// then returns it.
func (t tTemplates) New() *contr.Templates {
func (t tTemplates) New(w http.ResponseWriter, r *http.Request, ctr, act string) *contr.Templates {
c := &contr.Templates{}
return c
}
Expand Down Expand Up @@ -72,7 +72,7 @@ func (t tTemplates) Finally(c *contr.Templates, w http.ResponseWriter, r *http.R
// and renders it using data from Context.
func (t tTemplates) RenderTemplate(w http.ResponseWriter, r *http.Request) {
var h http.Handler
c := Templates.New()
c := Templates.New(w, r, "Templates", "RenderTemplate")
defer func() {
if h != nil {
h.ServeHTTP(w, r)
Expand Down Expand Up @@ -112,7 +112,7 @@ func (t tTemplates) RenderTemplate(w http.ResponseWriter, r *http.Request) {
// default.pattern = %s/%s.tpl
func (t tTemplates) Render(w http.ResponseWriter, r *http.Request) {
var h http.Handler
c := Templates.New()
c := Templates.New(w, r, "Templates", "Render")
defer func() {
if h != nil {
h.ServeHTTP(w, r)
Expand Down Expand Up @@ -145,7 +145,7 @@ func (t tTemplates) Render(w http.ResponseWriter, r *http.Request) {
// and returns a handler for user's redirect using 303 status code.
func (t tTemplates) Redirect(w http.ResponseWriter, r *http.Request) {
var h http.Handler
c := Templates.New()
c := Templates.New(w, r, "Templates", "Redirect")
defer func() {
if h != nil {
h.ServeHTTP(w, r)
Expand Down
13 changes: 9 additions & 4 deletions tools/generate/handlers/handlers.go.template
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ type t<@.ctx.name> struct {
// New allocates (<@.ctx.import>).<@.ctx.name> controller,<@if .ctx.parents>
// initializes its parents; then returns the controller.<@else>
// then returns it.<@end>
func (t t<@.ctx.name>) New() *contr.<@.ctx.name> {
c := &contr.<@.ctx.name>{}<@range $i, $v := .ctx.parents>
c.<@$v.Name> = <@$v.Package "."><@$v.Name>.New()<@end>
func (t t<@.ctx.name>) New(w http.ResponseWriter, r *http.Request, ctr, act string) *contr.<@.ctx.name> {
c := &contr.<@.ctx.name>{<@range $i, $v := .ctx.controller.Fields>
<@if eq $v.Type "response"><@$v.Name>: w,<@end>
<@if eq $v.Type "request"><@$v.Name>: r,<@end>
<@if eq $v.Type "controller"><@$v.Name>: ctr,<@end>
<@if eq $v.Type "action"><@$v.Name>: act,<@end>
<@end>}<@range $i, $v := .ctx.parents>
c.<@$v.Name> = <@$v.Package "."><@$v.Name>.New(w, r, ctr, act)<@end>
return c
}

Expand Down Expand Up @@ -98,7 +103,7 @@ func (t t<@.ctx.name>) <@.ctx.finally>(c *contr.<@.ctx.name>, w http.ResponseWri
// in appropriate order.<@template "printComments" dict (set "comments" $f.Comments)>
func (t t<@$.ctx.name>) <@$f.Name>(w http.ResponseWriter, r *http.Request) {
var h http.Handler
c := <@$.ctx.name>.New()
c := <@$.ctx.name>.New(w, r, "<@$.ctx.name>", "<@$f.Name>")
defer func() {
if h != nil {
h.ServeHTTP(w, r)
Expand Down
96 changes: 90 additions & 6 deletions tools/generate/handlers/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

import (
"fmt"
"go/ast"
"strings"

a "github.com/colegion/goal/internal/action"
Expand Down Expand Up @@ -37,6 +38,12 @@ type parent struct {
Name string // Name of the structure, e.g. "Template".
}

// field represents a field of a structure that must be automatically binded.
type field struct {
Name string // Name of the field, e.g. "Request".
Type string // Type of the binding, e.g. "request" or "action".
}

// controller is a type that represents application controller,
// a structure that has actions.
type controller struct {
Expand All @@ -49,6 +56,7 @@ type controller struct {
Comments reflect.Comments // A group of comments right above the controller declaration.
File string // Name of the file where this controller is located.
Parents parents // A list of embedded structs that should be parsed.
Fields []field // A list of fields that require binding.
}

// Package returns a unique package name that may be used in templates
Expand Down Expand Up @@ -101,13 +109,85 @@ func (ps packages) processPackage(importPath string) {
}
}

// scanAnonEmbStructs expects a package and an index of structure in that package.
// It scans the structure looking for fields that are anonymously embedded types.
// If those types are from other packages, they are processed as well.
// As a result a list of all found types in a form of []parent is returned.
func (ps packages) scanAnonEmbStructs(pkg *reflect.Package, i int) (prs []parent) {
// needBindingField gets a package, an index of struct and index of field
// in the struct. The field is checked whether it has a reserved tag
// and it is of correct type.
func (ps packages) needBindingField(pkg *reflect.Package, i, j int) *field {
f := &field{}
switch t := pkg.Structs[i].Fields[j]; t.Tag {
case `bind:"response"`:
// Make sure "http" package is imported.
n, ok := pkg.Imports.Name(pkg.Structs[i].File, "net/http")
if !ok || t.Type.String() != fmt.Sprintf("%s.ResponseWriter", n) {
log.Warn.Printf(
`Field "%s" in controller "%s" cannot be binded. Response must be of type "(net/http).ResponseWriter".`,
t.Name, pkg.Structs[i].Name,
)
return nil
}
f.Name = t.Name
f.Type = "response"
case `bind:"request"`:
// Make sure "http" package is imported.
n, ok := pkg.Imports.Name(pkg.Structs[i].File, "net/http")
if !ok || t.Type.String() != fmt.Sprintf("*%s.Request", n) {
log.Warn.Printf(
`Field "%s" in controller "%s" cannot be binded. Request must be of type "*(net/http).Request".`,
t.Name, pkg.Structs[i].Name,
)
return nil
}
f.Name = t.Name
f.Type = "request"
case `bind:"controller"`:
if t.Type.String() != "string" {
log.Warn.Printf(
`Field "%s" in controller "%s" cannot be binded. Controller name must be of type "string".`,
t.Name, pkg.Structs[i].Name,
)
return nil
}
f.Name = t.Name
f.Type = "controller"
case `bind:"action"`:
if t.Type.String() != "string" {
log.Warn.Printf(
`Field "%s" in controller "%s" cannot be binded. Action name must be of type "string".`,
t.Name, pkg.Structs[i].Name,
)
return nil
}
f.Name = t.Name
f.Type = "action"
default:
return nil
}
if !ast.IsExported(f.Name) {
log.Warn.Printf(
`Field "%s" in controller "%s" must be public in order to be binded.`,
f.Name, pkg.Structs[i].Name,
)
return nil
}
log.Trace.Printf(`Field %s will be binded to "%s".`, f.Name, f.Type)
return f
}

// scanFields expects a package and an index of structure in that package.
// It scans the structure looking for two kinds of fields:
// anonymously embedded types and named fields with special tags.
// Every anonymously embedded type is checked recursively regarding being a controller.
// As a result a list of all found fields with the tags and
// types in a form of []parent are returned.
func (ps packages) scanFields(pkg *reflect.Package, i int) (fs []field, prs []parent) {
// Iterating over fields of the structure.
for j := range pkg.Structs[i].Fields {
// Check whether the field requires binding.
if f := ps.needBindingField(pkg, i, j); f != nil {
fs = append(fs, *f)
continue
}

// Make sure current field is embedded anonymously,
// i.e. there is no arg name.
if pkg.Structs[i].Fields[j].Name != "" {
Expand Down Expand Up @@ -194,6 +274,9 @@ func (ps packages) extractControllers(pkg *reflect.Package) controllers {
continue
}

// Parse parent controllers and fields that require binding.
fs, prs := ps.scanFields(pkg, i)

// Add a new controller to the list of results.
cs.data[pkg.Structs[i].Name] = controller{
Actions: as[0],
Expand All @@ -204,7 +287,8 @@ func (ps packages) extractControllers(pkg *reflect.Package) controllers {

Comments: pkg.Structs[i].Comments,
File: pkg.Structs[i].File,
Parents: ps.scanAnonEmbStructs(pkg, i),
Parents: prs,
Fields: fs,
}
}
return cs
Expand Down
22 changes: 22 additions & 0 deletions tools/generate/handlers/packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ func assertDeepEqualController(c1, c2 *controller) {
if err := reflect.AssertEqualFunc(c1.Finally, c2.Finally); err != nil {
log.Error.Panic(err)
}
log.Trace.Println("Fields...")
if !r.DeepEqual(c1.Fields, c2.Fields) {
log.Error.Panicf(`Fields %v and %v are not equal.`, c1.Fields, c2.Fields)
}
}

func assertDeepEqualControllers(cs1, cs2 controllers) {
Expand Down Expand Up @@ -344,6 +348,24 @@ var ps = packages{
},
},
},
Fields: []field{
{
Name: "R",
Type: "request",
},
{
Name: "W",
Type: "response",
},
{
Name: "A",
Type: "action",
},
{
Name: "C",
Type: "controller",
},
},

Comments: []string{
"// Controller is a struct that should be embedded into every controller",
Expand Down
6 changes: 6 additions & 0 deletions tools/generate/handlers/testdata/controllers/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ type App struct {
*Controller
*NotController
*NotController1

// Fields with incorrect types.
a http.ResponseWriter `bind:"request"`
b string `bind:"response"`
c int `bind:"action"`
d int `bind:"controller"`
}

// NotController is not a controller as it doesn't have methods.
Expand Down
8 changes: 8 additions & 0 deletions tools/generate/handlers/testdata/controllers/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ type Controller struct {

testing.M
Test testing.M

R *h.Request `bind:"request"`
W h.ResponseWriter `bind:"response"`
A string `bind:"action"`
C string `bind:"controller"`

// r is not exported and thus must be ignored.
r *h.Request `bind:"request"`
}

// Before is a magic method that is executed before every request.
Expand Down

0 comments on commit 8d7d66a

Please sign in to comment.