go vendor

This commit is contained in:
lealife committed 2017-11-30 19:55:33 +08:00
1 parent 2856da6888
commit 0fb92efbf3
670 files changed
+199010

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
# inetelli G idea.jet.brainz
*.iml
.idea/
+30
View File
@@ -0,0 +1,30 @@
---
title: Modules Overview
section: modules
---
Revel Modules
==================================
## Core Modules
These modules are part of the core revel and a dependancy
- [Static](static/) = Serves static files
- [Jobs](jobs/) = The Cron Jobs runner
- [TestRunner](testrunner/) = Test sweet
## Upcoming
- [csrf](csrf/)
- [pprof](/pprof/)
## Xperimental
- [Auth](/auth/)
- [Db](/db/)
- [Gorm ORM](/orm/gorm)
### Caution
this is a work in progress
+6
View File
@@ -0,0 +1,6 @@
modules/auth
===============
Basic user/auth module
This should be modeled after [flask-security](https://github.com/mattupstate/flask-security)
+78
View File
@@ -0,0 +1,78 @@
package auth
import (
// "errors"
)
var (
Store StorageDriver
)
// Store = gormauth.NewGormAuthDriver()
type UserAuth interface {
// getters/setters implemented by the app-level model
UserId() string
Secret() string
HashedSecret() string
SetHashedSecret(string)
SecretDriver
// // implemented by secret driver
// Authenticate() (bool, error)
}
type SecretDriver interface {
Authenticate() (bool, error)
HashSecret(args ...interface{}) (string, error)
// stuff for documentation
// UserContext is expected in these?
// Secret expects 0 or non-0 arguments
// When no parameter is passed, it acts as a getter
// When one or more parameters are passed, it acts as a setter
// A driver should specify the expected arguments and their meanings
// Register()
// Login()
// Logout()
}
type StorageDriver interface {
Save(user interface{}) error
// Load should take a partially filled struct
// (with values needed to look up)
// and fills in the rest
Load(user interface{}) error
}
// func init() {
// // auth.Store = gorm...
// }
// func (c App) Login(email, password string) {
// u := User {
// Email ...
// }
// good, err := auth.Authenticate(email, password)
// user, err := user_info.GetUserByEmail(email)
// }
// Bycrypt Authenticate() expects a single string argument of the plaintext password
// It returns true on success and false if error or password mismatch
// func Authenticate(attemptedUser UserAuth) (bool, error) {
// // check user in Store
// loadedUser, err := Store.Load(attemptedUser.UserId())
// if err != nil {
// return false, errors.New("User Not Found")
// }
// loadedUser.Authenticate(attemptedUser.Secret())
// // successfully authenticated
// return true, nil
// }
+134
View File
@@ -0,0 +1,134 @@
package auth_test
import (
"errors"
"testing"
"github.com/revel/modules/auth"
"github.com/revel/modules/auth/driver/secret"
)
type User struct {
email string
password string
hashpass string
secret.BcryptAuth // SecurityDriver for testing
}
func NewUser(email, pass string) *User {
u := &User{
email: email,
password: pass,
}
u.UserContext = u
return u
}
func (self *User) UserId() string {
return self.email
}
func (self *User) Secret() string {
return self.password
}
func (self *User) HashedSecret() string {
return self.hashpass
}
func (self *User) SetHashedSecret(hpass string) {
self.hashpass = hpass
}
// func (self *User) Load() string
type TestStore struct {
data map[string]string
}
func (self *TestStore) Save(user interface{}) error {
u, ok := user.(*User)
if !ok {
return errors.New("TestStore.Save() expected arg of type User")
}
hPass, err := u.HashSecret(u.Secret())
if err != nil {
return err
}
self.data[u.UserId()] = hPass
return nil
}
func (self *TestStore) Load(user interface{}) error {
u, ok := user.(*User)
if !ok {
return errors.New("TestStore.Load() expected arg of type User")
}
hpass, ok := self.data[u.UserId()]
if !ok {
return errors.New("Record Not Found")
}
u.SetHashedSecret(hpass)
return nil
}
func TestPasswordHash(t *testing.T) {
auth.Store = &TestStore{
data: make(map[string]string),
}
u := NewUser("demo@domain.com", "demopass")
fail := NewUser("demo@domain.com", "")
var err error
u.hashpass, err = u.HashSecret(u.password)
if err != nil {
t.Errorf("Should have hashed password, get error: %v\n", err)
}
fail.hashpass, err = fail.HashSecret(fail.password)
if err == nil {
t.Errorf("Should have failed hashing\n")
}
}
func TestAuthenticate(t *testing.T) {
auth.Store = &TestStore{
data: make(map[string]string),
}
// user registered a long time ago
u := NewUser("demo@domain.com", "demopass")
err := auth.Store.Save(u)
if err != nil {
t.Errorf("Should have saved user: %v", err)
}
// users now logging in
pass := NewUser("demo@domain.com", "demopass")
fail := NewUser("demo@domain.com", "invalid")
// valid user is now trying to login
// check user in DB
err = auth.Store.Load(pass)
if err != nil {
t.Errorf("Should have loaded pass user: %v\n", err)
}
// check credentials
ok, err := pass.Authenticate()
if !ok || err != nil {
t.Errorf("Should have authenticated user")
}
// invalid user is now trying to login
err = auth.Store.Load(fail)
if err != nil {
t.Errorf("Should have loaded fail user")
}
// this should fail
ok, err = fail.Authenticate()
if ok || err != nil {
t.Errorf("Should have failed to authenticate user: %v\n", err)
}
}
+53
View File
@@ -0,0 +1,53 @@
modules/casbin
======
Casbin authz is an authorization middleware for [Revel](https://github.com/revel/revel), it's based on [https://github.com/casbin/casbin](https://github.com/casbin/casbin).
## Simple Example
```Go
package main
import (
"net/http"
"net/http/httptest"
"github.com/casbin/casbin"
"github.com/revel/revel"
"github.com/revel/modules/auth/casbin"
)
var adapter = casbinauth.NewAdapter(params)
var enforcer = casbin.NewEnforcer("authz_model.conf", adapter)
var casbinModule = casbinauth.NewCasbinModule(enforcer)
var testFilters = []revel.Filter{
casbinModule.AuthzFilter,
func(c *revel.Controller, fc []revel.Filter) {
c.RenderHTML("OK.")
},
}
func main() {
r, _ := http.NewRequest("GET", "/dataset1/resource1", nil)
r.SetBasicAuth("alice", "123")
w := httptest.NewRecorder()
context := revel.NewGoContext(nil)
context.Request.SetRequest(r)
context.Response.SetResponse(w)
c := revel.NewController(context)
testFilters[0](c, testFilters)
}
```
## Documentation
The authorization determines a request based on ``{subject, object, action}``, which means what ``subject`` can perform what ``action`` on what ``object``. In this plugin, the meanings are:
1. ``subject``: the logged-on user name
2. ``object``: the URL path for the web resource like "dataset1/item1"
3. ``action``: HTTP method like GET, POST, PUT, DELETE, or the high-level actions you defined like "read-file", "write-blog"
For how to write authorization policy and other details, please refer to [the Casbin's documentation](https://github.com/casbin/casbin).
+162
View File
@@ -0,0 +1,162 @@
package casbinauthz
import (
"errors"
"github.com/casbin/casbin/model"
"github.com/casbin/casbin/persist"
"github.com/jinzhu/gorm"
"github.com/revel/modules/orm/gorm/app"
)
type Line struct {
PType string `gorm:"size:100"`
V0 string `gorm:"size:100"`
V1 string `gorm:"size:100"`
V2 string `gorm:"size:100"`
V3 string `gorm:"size:100"`
V4 string `gorm:"size:100"`
V5 string `gorm:"size:100"`
}
// Adapter represents the Gorm adapter for policy storage.
type Adapter struct {
driverName string
dataSourceName string
db *gorm.DB
}
// NewAdapter is the constructor for Adapter.
func NewAdapter(params gormdb.DbInfo) *Adapter {
a := &Adapter{}
gormdb.InitDBWithParameters(params)
a.db = gormdb.DB
return a
}
func (a *Adapter) createTable() {
if a.db.HasTable(&Line{}) {
return
}
err := a.db.CreateTable(&Line{}).Error
if err != nil {
panic(err)
}
}
func (a *Adapter) dropTable() {
err := a.db.DropTable(&Line{}).Error
if err != nil {
panic(err)
}
}
func loadPolicyLine(line Line, model model.Model) {
lineText := line.PType
if line.V0 != "" {
lineText += ", " + line.V0
}
if line.V1 != "" {
lineText += ", " + line.V1
}
if line.V2 != "" {
lineText += ", " + line.V2
}
if line.V3 != "" {
lineText += ", " + line.V3
}
if line.V4 != "" {
lineText += ", " + line.V4
}
if line.V5 != "" {
lineText += ", " + line.V5
}
persist.LoadPolicyLine(lineText, model)
}
// LoadPolicy loads policy from database.
func (a *Adapter) LoadPolicy(model model.Model) error {
var lines []Line
err := a.db.Find(&lines).Error
if err != nil {
return err
}
for _, line := range lines {
loadPolicyLine(line, model)
}
return nil
}
func savePolicyLine(ptype string, rule []string) Line {
line := Line{}
line.PType = ptype
if len(rule) > 0 {
line.V0 = rule[0]
}
if len(rule) > 1 {
line.V1 = rule[1]
}
if len(rule) > 2 {
line.V2 = rule[2]
}
if len(rule) > 3 {
line.V3 = rule[3]
}
if len(rule) > 4 {
line.V4 = rule[4]
}
if len(rule) > 5 {
line.V5 = rule[5]
}
return line
}
// SavePolicy saves policy to database.
func (a *Adapter) SavePolicy(model model.Model) error {
a.dropTable()
a.createTable()
for ptype, ast := range model["p"] {
for _, rule := range ast.Policy {
line := savePolicyLine(ptype, rule)
err := a.db.Create(&line).Error
if err != nil {
return err
}
}
}
for ptype, ast := range model["g"] {
for _, rule := range ast.Policy {
line := savePolicyLine(ptype, rule)
err := a.db.Create(&line).Error
if err != nil {
return err
}
}
}
return nil
}
// AddPolicy adds a policy rule to the storage.
func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
return errors.New("not implemented")
}
// RemovePolicy removes a policy rule from the storage.
func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
return errors.New("not implemented")
}
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return errors.New("not implemented")
}
+49
View File
@@ -0,0 +1,49 @@
package casbinauthz
import (
"net/http"
"github.com/casbin/casbin"
"github.com/revel/revel"
)
type CasbinModule struct {
enforcer *casbin.Enforcer
}
func NewCasbinModule(enforcer *casbin.Enforcer) *CasbinModule {
cm := &CasbinModule{}
cm.enforcer = enforcer
return cm
}
// AuthzFilter enables the authorization based on Casbin.
//
// Usage:
// 1) Add `casbin.AuthzFilter` to the app's filters (it must come after the authentication).
// 2) Init the Casbin enforcer.
func (cm *CasbinModule) AuthzFilter(c *revel.Controller, fc []revel.Filter) {
if !CheckPermission(cm.enforcer, c.Request) {
c.Result = c.Forbidden("Access denied by the Authz plugin.")
return
} else {
fc[0](c, fc[1:])
}
}
// GetUserName gets the user name from the request.
// Currently, only HTTP basic authentication is supported
func GetUserName(r *revel.Request) string {
req := r.In.GetRaw().(*http.Request)
username, _, _ := req.BasicAuth()
return username
}
// CheckPermission checks the user/method/path combination from the request.
// Returns true (permission granted) or false (permission forbidden)
func CheckPermission(e *casbin.Enforcer, r *revel.Request) bool {
user := GetUserName(r)
method := r.Method
path := r.URL.Path
return e.Enforce(user, path, method)
}
+14
View File
@@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")
+7
View File
@@ -0,0 +1,7 @@
p, alice, /dataset1/*, GET
p, alice, /dataset1/resource1, POST
p, bob, /dataset2/resource1, *
p, bob, /dataset2/resource2, GET
p, bob, /dataset2/folder1/*, POST
p, dataset1_admin, /dataset1/*, *
g, cathy, dataset1_admin
1 p, alice, /dataset1/*, GET
2 p, alice, /dataset1/resource1, POST
3 p, bob, /dataset2/resource1, *
4 p, bob, /dataset2/resource2, GET
5 p, bob, /dataset2/folder1/*, POST
6 p, dataset1_admin, /dataset1/*, *
7 g, cathy, dataset1_admin
+129
View File
@@ -0,0 +1,129 @@
package casbinauthz
import (
"log"
"net/http"
"net/http/httptest"
"testing"
"github.com/casbin/casbin"
"github.com/casbin/casbin/util"
_ "github.com/go-sql-driver/mysql"
"github.com/revel/revel"
"github.com/revel/modules/orm/gorm/app"
)
var adapter = NewAdapter(DefaultDbParams())
var enforcer = casbin.NewEnforcer("authz_model.conf", adapter)
var casbinModule = NewCasbinModule(enforcer)
var testFilters = []revel.Filter{
casbinModule.AuthzFilter,
func(c *revel.Controller, fc []revel.Filter) {
c.RenderHTML("OK.")
},
}
func DefaultDbParams() gormdb.DbInfo {
params := gormdb.DbInfo{}
params.DbDriver = "mysql"
params.DbHost = "(localhost:3306)"
params.DbUser = "root"
params.DbPassword = ""
params.DbName = "casbin"
return params
}
func testRequest(t *testing.T, user string, path string, method string, code int) {
r, _ := http.NewRequest(method, path, nil)
r.SetBasicAuth(user, "123")
w := httptest.NewRecorder()
context := revel.NewGoContext(nil)
context.Request.SetRequest(r)
context.Response.SetResponse(w)
c := revel.NewController(context)
testFilters[0](c, testFilters)
if c.Response.Status != code {
t.Errorf("%s, %s, %s: %d, supposed to be %d", user, path, method, c.Response.Status, code)
}
}
func testGetPolicy(t *testing.T, e *casbin.Enforcer, res [][]string) {
myRes := e.GetPolicy()
log.Print("Policy: ", myRes)
if !util.Array2DEquals(res, myRes) {
t.Error("Policy: ", myRes, ", supposed to be ", res)
}
}
func initPolicy(t *testing.T) {
// Because the DB is empty at first,
// so we need to load the policy from the file adapter (.CSV) first.
e := casbin.NewEnforcer("authz_model.conf", "authz_policy.csv")
a := NewAdapter(DefaultDbParams())
// This is a trick to save the current policy to the DB.
// We can't call e.SavePolicy() because the adapter in the enforcer is still the file adapter.
// The current policy means the policy in the Casbin enforcer (aka in memory).
err := a.SavePolicy(e.GetModel())
if err != nil {
panic(err)
}
}
func TestBasic(t *testing.T) {
// Initialize some policy in DB.
initPolicy(t)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
// Now the DB has policy, so we can provide a normal use case.
testRequest(t, "alice", "/dataset1/resource1", "GET", 200)
testRequest(t, "alice", "/dataset1/resource1", "POST", 200)
testRequest(t, "alice", "/dataset1/resource2", "GET", 200)
testRequest(t, "alice", "/dataset1/resource2", "POST", 403)
}
func TestPathWildcard(t *testing.T) {
// Initialize some policy in DB.
initPolicy(t)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
// Now the DB has policy, so we can provide a normal use case.
testRequest(t, "bob", "/dataset2/resource1", "GET", 200)
testRequest(t, "bob", "/dataset2/resource1", "POST", 200)
testRequest(t, "bob", "/dataset2/resource1", "DELETE", 200)
testRequest(t, "bob", "/dataset2/resource2", "GET", 200)
testRequest(t, "bob", "/dataset2/resource2", "POST", 403)
testRequest(t, "bob", "/dataset2/resource2", "DELETE", 403)
testRequest(t, "bob", "/dataset2/folder1/item1", "GET", 403)
testRequest(t, "bob", "/dataset2/folder1/item1", "POST", 200)
testRequest(t, "bob", "/dataset2/folder1/item1", "DELETE", 403)
testRequest(t, "bob", "/dataset2/folder1/item2", "GET", 403)
testRequest(t, "bob", "/dataset2/folder1/item2", "POST", 200)
testRequest(t, "bob", "/dataset2/folder1/item2", "DELETE", 403)
}
func TestRBAC(t *testing.T) {
// Initialize some policy in DB.
initPolicy(t)
// Note: you don't need to look at the above code
// if you already have a working DB with policy inside.
// Now the DB has policy, so we can provide a normal use case.
// cathy can access all /dataset1/* resources via all methods because it has the dataset1_admin role.
testRequest(t, "cathy", "/dataset1/item", "GET", 200)
testRequest(t, "cathy", "/dataset1/item", "POST", 200)
testRequest(t, "cathy", "/dataset1/item", "DELETE", 200)
testRequest(t, "cathy", "/dataset2/item", "GET", 403)
testRequest(t, "cathy", "/dataset2/item", "POST", 403)
testRequest(t, "cathy", "/dataset2/item", "DELETE", 403)
}
+27
View File
@@ -0,0 +1,27 @@
/* A basic user authentication module for Revel
list of concerns:
- Separating out the interface and driver
- Removing DB/Storage dependency
- UUID as default identifier?
- how to deal with password/secret or generally, method of authorization
- default {views,controllers,routes} for register/login/logut ?
- reset password in most basic ?
- activation (and other features) in a second / more sophisticated driver
- filter for checking that user is authenticated
I think a driver is made up of 2 parts
data prep and data storage
register and password reset are part of data prep
as is the auth hash method
they don't care how the data is stored
then there is the data store
perhaps each auth user model should instantiate 2 drivers instead of 1?
one for data prep components and one for storage
so the security driver and the storage driver
*/
package auth
+73
View File
@@ -0,0 +1,73 @@
package secret
import (
"errors"
"github.com/revel/modules/auth"
"golang.org/x/crypto/bcrypt"
)
// example implementation of a Revel auth security driver
// This driver should be embedded into your app-level User model
// It expects your User model to have `Password` and `HashedPassword` string fields
//
// Your User model also needs to set itself as the UserContext for the BcryptAuth driver
//
// func NewUser(email, pass string) *User {
// u := &User{
// email: email,
// password: pass,
// }
// u.UserContext = u
// }
//
type BcryptAuth struct {
UserContext auth.UserAuth
}
// Bcrypt Secret() returns the hashed version of the password.
// It expects an argument of type string, which is the plain text password
func (self *BcryptAuth) HashSecret(args ...interface{}) (string, error) {
if auth.Store == nil {
return "", errors.New("Auth module StorageDriver not set")
}
argLen := len(args)
if argLen == 0 {
// we are getting
return string(self.UserContext.HashedSecret()), nil
}
if argLen == 1 {
// we are setting
password, ok := args[0].(string)
if !ok {
return "", errors.New("Wrong argument type provided, expected plaintext password as string")
}
hPass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
self.UserContext.SetHashedSecret(string(hPass))
return self.UserContext.HashedSecret(), nil
}
// bad argument count
return "", errors.New("Too many arguments provided, expected one")
}
// Bycrypt Authenticate() expects a single string argument of the plaintext password
// It returns true on success and false if error or password mismatch
func (self *BcryptAuth) Authenticate() (bool, error) {
// check password
err := bcrypt.CompareHashAndPassword([]byte(self.UserContext.HashedSecret()), []byte(self.UserContext.Secret()))
if err == bcrypt.ErrMismatchedHashAndPassword {
return false, nil
}
if err != nil {
return false, err
}
// successfully authenticated
return true, nil
}
+3
View File
@@ -0,0 +1,3 @@
Mysql Auth driver
==================
@@ -0,0 +1,3 @@
Postgresql Auth driver
==================
+3
View File
@@ -0,0 +1,3 @@
Sqlite Auth driver
==================
+17
View File
@@ -0,0 +1,17 @@
package auth
// var storageDriver auth.StorageDriver // postgres in example
// type AuthUserModel struct {
// userId string
// security *SecurityDriver // bcrypt in example
// }
// func (self *AuthUserModel) UserId() string {
// return self.userId
// }
// func (self *AuthUserModel) Secret() string {
// return self.security.Secret()
// }
+125
View File
@@ -0,0 +1,125 @@
package csrf
import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"html/template"
"io"
"math"
"net/url"
"github.com/revel/revel"
)
// allowMethods are HTTP methods that do NOT require a token
var allowedMethods = map[string]bool{
"GET": true,
"HEAD": true,
"OPTIONS": true,
"TRACE": true,
}
func RandomString(length int) (string, error) {
buffer := make([]byte, int(math.Ceil(float64(length)/2)))
if _, err := io.ReadFull(rand.Reader, buffer); err != nil {
return "", nil
}
str := hex.EncodeToString(buffer)
return str[:length], nil
}
func RefreshToken(c *revel.Controller) {
token, err := RandomString(64)
if err != nil {
panic(err)
}
c.Session["csrf_token"] = token
}
// CsrfFilter enables CSRF request token creation and verification.
//
// Usage:
// 1) Add `csrf.CsrfFilter` to the app's filters (it must come after the revel.SessionFilter).
// 2) Add CSRF fields to a form with the template tag `{{ csrftoken . }}`. The filter adds a function closure to the `ViewArgs` that can pull out the secret and make the token as-needed, caching the value in the request. Ajax support provided through the `X-CSRFToken` header.
func CsrfFilter(c *revel.Controller, fc []revel.Filter) {
token, foundToken := c.Session["csrf_token"]
if !foundToken {
RefreshToken(c)
}
referer, refErr := url.Parse(c.Request.Referer())
if refErr != nil {
c.Result = c.Forbidden("REVEL CSRF: Unable to fetch referer")
return
}
isSameOrigin := sameOrigin(c.Request.URL, referer)
// If the Request method isn't in the white listed methods
if !allowedMethods[c.Request.Method] && !IsExempt(c) {
// Token wasn't present at all
if !foundToken {
c.Result = c.Forbidden("REVEL CSRF: Session token missing.")
return
}
// Referer header is invalid
if refErr != nil {
c.Result = c.Forbidden("REVEL CSRF: HTTP Referer malformed.")
return
}
// Same origin
if !isSameOrigin {
c.Result = c.Forbidden("REVEL CSRF: Same origin mismatch.")
return
}
var requestToken string
// First check for token in post data
if c.Request.Method == "POST" {
requestToken = c.Params.Get("csrftoken")
}
// Then check for token in custom headers, as with AJAX
if requestToken == "" {
requestToken = c.Request.GetHttpHeader("X-CSRFToken")
}
if requestToken == "" || !compareToken(requestToken, token) {
c.Result = c.Forbidden("REVEL CSRF: Invalid token.")
return
}
}
fc[0](c, fc[1:])
// Only add token to ViewArgs if the request is: not AJAX, not missing referer header, and is same origin.
if c.Request.GetHttpHeader("X-CSRFToken") == "" && isSameOrigin {
c.ViewArgs["_csrftoken"] = token
}
}
func compareToken(requestToken, token string) bool {
// ConstantTimeCompare will panic if the []byte aren't the same length
if len(requestToken) != len(token) {
return false
}
return subtle.ConstantTimeCompare([]byte(requestToken), []byte(token)) == 1
}
// Validates same origin policy
func sameOrigin(u1, u2 *url.URL) bool {
return u1.Scheme == u2.Scheme && u1.Host == u2.Host
}
func init() {
revel.TemplateFuncs["csrftoken"] = func(viewArgs map[string]interface{}) template.HTML {
if tokenFunc, ok := viewArgs["_csrftoken"]; !ok {
panic("REVEL CSRF: _csrftoken missing from ViewArgs.")
} else {
return template.HTML(tokenFunc.(string))
}
}
}
+183
View File
@@ -0,0 +1,183 @@
package csrf
import (
"bytes"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
"github.com/revel/revel"
)
func NewTestController(w http.ResponseWriter, r *http.Request) *revel.Controller{
context := revel.NewGoContext(nil)
context.Request.SetRequest(r)
context.Response.SetResponse(w)
c := revel.NewController(context)
return c
}
var testFilters = []revel.Filter{
revel.ParamsFilter,
CsrfFilter,
func(c *revel.Controller, fc []revel.Filter) {
c.RenderHTML("{{ csrftoken . }}")
},
}
func TestTokenInSession(t *testing.T) {
resp := httptest.NewRecorder()
getRequest, _ := http.NewRequest("GET", "http://www.example.com/", nil)
c := NewTestController(resp,getRequest)
c.Session = make(revel.Session)
testFilters[0](c, testFilters)
if _, ok := c.Session["csrf_token"]; !ok {
t.Fatal("token should be present in session")
}
}
func TestPostWithoutToken(t *testing.T) {
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
c := NewTestController(resp,postRequest)
c.Session = make(revel.Session)
testFilters[0](c, testFilters)
if c.Response.Status != 403 {
t.Fatal("post without token should be forbidden")
}
}
func TestNoReferrer(t *testing.T) {
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
c := NewTestController(resp,postRequest)
c.Session = make(revel.Session)
RefreshToken(c)
token := c.Session["csrf_token"]
// make a new request with the token
data := url.Values{}
data.Set("csrftoken", token)
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
cnew := NewTestController(resp,formPostRequest)
// and replace the old request
c.Request = cnew.Request
testFilters[0](c, testFilters)
if c.Response.Status != 403 {
t.Fatal("post without referer should be forbidden")
}
}
func TestRefererHttps(t *testing.T) {
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
c := NewTestController(resp,postRequest)
c.Session = make(revel.Session)
RefreshToken(c)
token := c.Session["csrf_token"]
// make a new request with the token
data := url.Values{}
data.Set("csrftoken", token)
formPostRequest, _ := http.NewRequest("POST", "https://www.example.com/", bytes.NewBufferString(data.Encode()))
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
formPostRequest.Header.Add("Referer", "http://www.example.com/")
cnew := NewTestController(resp,formPostRequest)
// and replace the old request
c.Request = cnew.Request
testFilters[0](c, testFilters)
if c.Response.Status != 403 {
t.Fatal("posts to https should have an https referer")
}
}
func TestHeaderWithToken(t *testing.T) {
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
c := NewTestController(resp,postRequest)
c.Session = make(revel.Session)
RefreshToken(c)
token := c.Session["csrf_token"]
// make a new request with the token
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
formPostRequest.Header.Add("X-CSRFToken", token)
formPostRequest.Header.Add("Referer", "http://www.example.com/")
cnew := NewTestController(resp,formPostRequest)
// and replace the old request
c.Request = cnew.Request
testFilters[0](c, testFilters)
if c.Response.Status == 403 {
t.Fatal("post with http header token should be allowed")
}
}
func TestFormPostWithToken(t *testing.T) {
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/", nil)
c := NewTestController(resp,postRequest)
c.Session = make(revel.Session)
RefreshToken(c)
token := c.Session["csrf_token"]
// make a new request with the token
data := url.Values{}
data.Set("csrftoken", token)
formPostRequest, _ := http.NewRequest("POST", "http://www.example.com/", bytes.NewBufferString(data.Encode()))
formPostRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
formPostRequest.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
formPostRequest.Header.Add("Referer", "http://www.example.com/")
cnew := NewTestController(resp,formPostRequest)
// and replace the old request
c.Request = cnew.Request
testFilters[0](c, testFilters)
if c.Response.Status == 403 {
t.Fatal("form post with token should be allowed")
}
}
func TestNoTokenInArgsWhenCORs(t *testing.T) {
resp := httptest.NewRecorder()
getRequest, _ := http.NewRequest("GET", "http://www.example1.com/", nil)
getRequest.Header.Add("Referer", "http://www.example2.com/")
c := NewTestController(resp,getRequest)
c.Session = make(revel.Session)
testFilters[0](c, testFilters)
if _, ok := c.ViewArgs["_csrftoken"]; ok {
t.Fatal("ViewArgs should not contain token when not same origin")
}
}
+36
View File
@@ -0,0 +1,36 @@
package csrf
import (
"fmt"
"strings"
"github.com/revel/revel"
)
var (
exemptPath = make(map[string]bool)
exemptAction = make(map[string]bool)
)
func MarkExempt(route string) {
if strings.HasPrefix(route, "/") {
// e.g. "/controller/action"
exemptPath[strings.ToLower(route)] = true
} else if routeParts := strings.Split(route, "."); len(routeParts) == 2 {
// e.g. "ControllerName.ActionName"
exemptAction[route] = true
} else {
err := fmt.Sprintf("csrf.MarkExempt() received invalid argument \"%v\". Either provide a path prefixed with \"/\" or controller action in the form of \"ControllerName.ActionName\".", route)
panic(err)
}
}
func IsExempt(c *revel.Controller) bool {
if _, ok := exemptPath[strings.ToLower(c.Request.GetPath())]; ok {
return true
} else if _, ok := exemptAction[c.Action]; ok {
return true
}
return false
}
+55
View File
@@ -0,0 +1,55 @@
package csrf
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/revel/revel"
)
func TestExemptPath(t *testing.T) {
MarkExempt("/Controller/Action")
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
c := NewTestController(resp, postRequest)
c.Session = make(revel.Session)
testFilters[0](c, testFilters)
if c.Response.Status == 403 {
t.Fatal("post to csrf exempt action should pass")
}
}
func TestExemptPathCaseInsensitive(t *testing.T) {
MarkExempt("/Controller/Action")
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/controller/action", nil)
c := NewTestController(resp, postRequest)
c.Session = make(revel.Session)
testFilters[0](c, testFilters)
if c.Response.Status == 403 {
t.Fatal("post to csrf exempt action should pass")
}
}
func TestExemptAction(t *testing.T) {
MarkExempt("Controller.Action")
resp := httptest.NewRecorder()
postRequest, _ := http.NewRequest("POST", "http://www.example.com/Controller/Action", nil)
c := NewTestController(resp, postRequest)
c.Session = make(revel.Session)
c.Action = "Controller.Action"
testFilters[0](c, testFilters)
if c.Response.Status == 403 {
t.Fatal("post to csrf exempt action should pass")
}
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
// Package db module configures a database connection for the application.
//
// Developers use this module by importing and calling db.Init().
// A "Transactional" controller type is provided as a way to import interceptors
// that manage the transaction
//
// In particular, a transaction is begun before each request and committed on
// success. If a panic occurred during the request, the transaction is rolled
// back. (The application may also roll the transaction back itself.)
package db
import (
"database/sql"
"github.com/revel/revel"
)
// Database connection variables
var (
Db *sql.DB
Driver string
Spec string
)
// Init method used to initialize DB module on `OnAppStart`
func Init() {
// Read configuration.
var found bool
if Driver, found = revel.Config.String("db.driver"); !found {
revel.RevelLog.Fatal("db.driver not configured")
}
if Spec, found = revel.Config.String("db.spec"); !found {
revel.RevelLog.Fatal("db.spec not configured")
}
// Open a connection.
var err error
Db, err = sql.Open(Driver, Spec)
if err != nil {
revel.RevelLog.Fatal("Open database connection error", "error", err, "driver", Driver, "spec", Spec)
}
}
// Transactional definition for database transaction
type Transactional struct {
*revel.Controller
Txn *sql.Tx
}
// Begin a transaction
func (c *Transactional) Begin() revel.Result {
txn, err := Db.Begin()
if err != nil {
panic(err)
}
c.Txn = txn
return nil
}
// Rollback if it's still going (must have panicked).
func (c *Transactional) Rollback() revel.Result {
if c.Txn != nil {
if err := c.Txn.Rollback(); err != nil {
if err != sql.ErrTxDone {
panic(err)
}
}
c.Txn = nil
}
return nil
}
// Commit the transaction.
func (c *Transactional) Commit() revel.Result {
if c.Txn != nil {
if err := c.Txn.Commit(); err != nil {
if err != sql.ErrTxDone {
panic(err)
}
}
c.Txn = nil
}
return nil
}
func init() {
revel.InterceptMethod((*Transactional).Begin, revel.BEFORE)
revel.InterceptMethod((*Transactional).Commit, revel.AFTER)
revel.InterceptMethod((*Transactional).Rollback, revel.FINALLY)
}
+220
View File
@@ -0,0 +1,220 @@
---
title: Jobs
github:
labels:
- topic-jobs
- topic-runtime
---
The [`Jobs`](https://godoc.org/github.com/revel/modules/jobs/app/jobs) framework for performing work asynchronously, outside of the
request flow. This may take the form of [recurring tasks](#jobs) that updates cached data
or [one-off tasks](#OneOff) such as sending emails.
## Activation
The [`Jobs`](https://godoc.org/github.com/revel/modules/jobs/app/jobs) framework is included as an optional [module](index.html), and is not enabled by default.
To activate it, add `module.jobs` to the [app.conf](../manual/appconf.html) file:
```ini
module.jobs = github.com/revel/modules/jobs
```
Additionally, in order to access the job monitoring page, you will need to add
this line to the `conf/routes` file, which will insert the `/@jobs` url:
module:jobs
## Options
There are some [configuration settings](../manual/appconf.html#jobs) that tell the framework what sort of limitations
to place on the jobs that it runs. These are listed below with their default values;
- [`jobs.pool = 10`](appconf.html#jobspool) - The number of jobs allowed to run simultaneously
- [`jobs.selfconcurrent = false`](appconf.html#jobsselfconcurrent) - Allow a job to run only if previous instances are done
- [`jobs.acceptproxyaddress = false`](appconf#jobsacceptproxyaddress) - Accept `X-Forwarded-For` header value (which is spoofable) to allow or deny status page access
## Implementing Jobs
To create a Job, implement the [`cron.Job`](https://github.com/robfig/cron/) interface. The
[`Job`](https://godoc.org/github.com/revel/modules/jobs/app/jobs#Job) interface has the following signature:
{% highlight go %}
// https://github.com/robfig/cron/blob/master/cron.go
type Job interface {
Run()
}
{% endhighlight %}
For example:
{% highlight go %}
type MyJob struct {}
func (j MyJob) Run() {
// Do something
}
{% endhighlight %}
## Startup jobs
To run a task on application startup, use
[`revel.OnAppStart()`](https://godoc.org/github.com/revel/revel#OnAppStart) to register a function.
Revel runs these tasks serially, before starting the server. Note that this
functionality does not actually use the jobs module, but it can be used to
submit a job for execution that doesn't block server startup.
{% highlight go %}
func init() {
revel.OnAppStart(func() { jobs.Now(populateCache{}) })
}
{% endhighlight %}
<a name="RecurringJobs"></a>
## Recurring Jobs
Jobs may be scheduled to run on any schedule. There are two options for expressing the schedule:
1. A cron specification
2. A fixed interval
Revel uses the [`cron library`](https://godoc.org/github.com/revel/cron) to parse the
schedule and run the jobs. The library's
[README](https://github.com/revel/cron/blob/master/README.md) provides a detailed
description of the format accepted.
Jobs are generally registered using the
[`revel.OnAppStart()`](https://godoc.org/github.com/revel/revel#OnAppStart) hook, but they may be
registered at any later time as well.
Here are some examples:
{% highlight go %}
import (
"github.com/revel/revel"
"github.com/revel/modules/jobs/app/jobs"
"time"
)
type ReminderEmails struct {
// filtered
}
func (e ReminderEmails) Run() {
// Queries the DB
// Sends some email
}
func init() {
revel.OnAppStart(func() {
jobs.Schedule("0 0 0 * * ?", ReminderEmails{})
jobs.Schedule("@midnight", ReminderEmails{})
jobs.Schedule("@every 24h", ReminderEmails{})
jobs.Every(24 * time.Hour, ReminderEmails{})
})
}
{% endhighlight %}
<a name="NamedSchedules"></a>
## Named schedules
You can [configure schedules ](appconf.html#jobs) in the [`app.conf`](appconf.html) file and reference them anywhere.
This provides an easy way to reuse, and a useful description for crontab specs.
Here is an example **named cron schedule**, in an [`app.conf`](appconf.html) file:
cron.workhours_15m = 0 */15 9-17 ? * MON-FRI
Use the named schedule by referencing it anywhere you would have used a cron spec.
{% highlight go %}
func init() {
revel.OnAppStart(func() {
jobs.Schedule("cron.workhours_15m", ReminderEmails{})
})
}
{% endhighlight %}
<div class="alert alert-warning">
<b>IMPORTANT</b>: The cron schedule's name must begin with <b>cron</b>.
</div>
<a name="OneOff"></a>
## One-off Jobs
Sometimes it is necessary to do something in response to a user action. In these
cases, the jobs module allows you to submit a job to be run a single time.
The only control offered is how long to wait until the job should be run.
{% highlight go %}
type AppController struct { *revel.Controller }
func (c AppController) Action() revel.Result {
// Handle the request.
...
// Send them email asynchronously, right now.
jobs.Now(SendConfirmationEmail{})
// Or, send them email asynchronously after a minute.
jobs.In(time.Minute, SendConfirmationEmail{})
}
{% endhighlight %}
## Registering functions
It is possible to register a `func()` as a job by wrapping it in the [`jobs.Func`](https://godoc.org/github.com/revel/modules/jobs/app/jobs#Func)
type. For example:
{% highlight go %}
func sendReminderEmails() {
// Query the DB
// Send some email
}
func init() {
revel.OnAppStart(func() {
jobs.Schedule("@midnight", jobs.Func(sendReminderEmails))
})
}
{% endhighlight %}
## Job Status
The jobs module provides a status page (`/@jobs` url) that shows:
- a list of the scheduled jobs it knows about
- the current status; **IDLE** or **RUNNING**
- the previous and next run times
<div class="alert alert-info">For security purposes, the status page is restricted to requests that originate
from 127.0.0.1.</div>
![Job Status Page](../img/jobs-status.png)
## Constrained pool size
It is possible to configure the job module to limit the number of jobs that are
allowed to run at the same time. This allows the developer to restrict the
resources that could be potentially in use by asynchronous jobs -- typically
interactive responsiveness is valued above asynchronous processing. When a pool
is full of running jobs, new jobs block to wait for running jobs to complete.
**Implementation Note**: The implementation blocks on a channel receive, which is
implemented to be [FIFO](http://en.wikipedia.org/wiki/FIFO) for waiting goroutines (but not specified/required to be
so). [See here for discussion](https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/CPwv8WlqKag).
## Future areas for development
* Allow access to the job status page with HTTP Basic Authentication credentials
* Allow administrators to run scheduled jobs interactively from the status page
* Provide more visibility into the job runner, e.g. the pool size, the job queue length, etc.
+35
View File
@@ -0,0 +1,35 @@
package controllers
import (
"strings"
"github.com/revel/cron"
"github.com/revel/modules/jobs/app/jobs"
"github.com/revel/revel"
)
type Jobs struct {
*revel.Controller
}
func (c Jobs) Status() revel.Result {
remoteAddress := c.Request.RemoteAddr
if revel.Config.BoolDefault("jobs.acceptproxyaddress", false) {
if proxiedAddress := c.Request.GetHttpHeader("X-Forwarded-For"); proxiedAddress!="" {
remoteAddress = proxiedAddress
}
}
if !strings.HasPrefix(remoteAddress, "127.0.0.1") &&
!strings.HasPrefix(remoteAddress, "::1") &&
!strings.HasPrefix(remoteAddress, "[::1]") {
return c.Forbidden("%s is not local", remoteAddress)
}
entries := jobs.MainCron.Entries()
return c.Render(entries)
}
func init() {
revel.TemplateFuncs["castjob"] = func(job cron.Job) *jobs.Job {
return job.(*jobs.Job)
}
}
+13
View File
@@ -0,0 +1,13 @@
package jobs
import (
"github.com/revel/revel"
)
var jobLog = revel.AppLog
func init() {
revel.RegisterModuleInit(func(m *revel.Module){
jobLog = m.Log
})
}
+67
View File
@@ -0,0 +1,67 @@
package jobs
import (
"reflect"
"runtime/debug"
"sync"
"sync/atomic"
"github.com/revel/cron"
"github.com/revel/revel"
)
type Job struct {
Name string
inner cron.Job
status uint32
running sync.Mutex
}
const UnNamed = "(unnamed)"
func New(job cron.Job) *Job {
name := reflect.TypeOf(job).Name()
if name == "Func" {
name = UnNamed
}
return &Job{
Name: name,
inner: job,
}
}
func (j *Job) Status() string {
if atomic.LoadUint32(&j.status) > 0 {
return "RUNNING"
}
return "IDLE"
}
func (j *Job) Run() {
// If the job panics, just print a stack trace.
// Don't let the whole process die.
defer func() {
if err := recover(); err != nil {
if revelError := revel.NewErrorFromPanic(err); revelError != nil {
jobLog.Error("Job Recovery ", "error", err, "stack", revelError.Stack)
} else {
jobLog.Error("Job Recovery ", "error", err, "stack", string(debug.Stack()))
}
}
}()
if !selfConcurrent {
j.running.Lock()
defer j.running.Unlock()
}
if workPermits != nil {
workPermits <- struct{}{}
defer func() { <-workPermits }()
}
atomic.StoreUint32(&j.status, 1)
defer atomic.StoreUint32(&j.status, 0)
j.inner.Run()
}
+70
View File
@@ -0,0 +1,70 @@
// A job runner for executing scheduled or ad-hoc tasks asynchronously from HTTP requests.
//
// It adds a couple of features on top of the cron package to make it play nicely with Revel:
//
// 1. Protection against job panics. (They print to ERROR instead of take down the process)
//
// 2. (Optional) Limit on the number of jobs that may run simulatenously, to
// limit resource consumption.
//
// 3. (Optional) Protection against multiple instances of a single job running
// concurrently. If one execution runs into the next, the next will be queued.
//
// 4. Cron expressions may be defined in app.conf and are reusable across jobs.
//
// 5. Job status reporting.
package jobs
import (
"strings"
"time"
"github.com/revel/cron"
"github.com/revel/revel"
)
// Callers can use jobs.Func to wrap a raw func.
// (Copying the type to this package makes it more visible)
//
// For example:
// jobs.Schedule("cron.frequent", jobs.Func(myFunc))
type Func func()
func (r Func) Run() { r() }
func Schedule(spec string, job cron.Job) error {
// Look to see if given spec is a key from the Config.
if strings.HasPrefix(spec, "cron.") {
confSpec, found := revel.Config.String(spec)
if !found {
jobLog.Panic("Cron spec not found: " + spec)
}
spec = confSpec
}
sched, err := cron.Parse(spec)
if err != nil {
return err
}
MainCron.Schedule(sched, New(job))
return nil
}
// Run the given job at a fixed interval.
// The interval provided is the time between the job ending and the job being run again.
// The time that the job takes to run is not included in the interval.
func Every(duration time.Duration, job cron.Job) {
MainCron.Schedule(cron.Every(duration), New(job))
}
// Run the given job right now.
func Now(job cron.Job) {
go New(job).Run()
}
// Run the given job once, after the given delay.
func In(duration time.Duration, job cron.Job) {
go func() {
time.Sleep(duration)
New(job).Run()
}()
}
+31
View File
@@ -0,0 +1,31 @@
package jobs
import (
"github.com/revel/cron"
"github.com/revel/revel"
)
const DefaultJobPoolSize = 10
var (
// Singleton instance of the underlying job scheduler.
MainCron *cron.Cron
// This limits the number of jobs allowed to run concurrently.
workPermits chan struct{}
// Is a single job allowed to run concurrently with itself?
selfConcurrent bool
)
func init() {
MainCron = cron.New()
revel.OnAppStart(func() {
if size := revel.Config.IntDefault("jobs.pool", DefaultJobPoolSize); size > 0 {
workPermits = make(chan struct{}, size)
}
selfConcurrent = revel.Config.BoolDefault("jobs.selfconcurrent", false)
MainCron.Start()
jobLog.Info("Go to /@jobs to see job status.")
})
}
+39
View File
@@ -0,0 +1,39 @@
<html>
<head>
<style>
body {
font-size: 12px;
font-family: sans-serif;
}
table {
border-collapse: collapse;
border: none;
}
table td, table th {
padding: 4 10px;
border: none;
}
table tr:nth-child(odd) {
background-color: #f0f0f0;
}
th {
text-align: left;
}
</style>
</head>
<body>
<h1>Scheduled Jobs</h1>
<table>
<tr><th>Name</th><th>Status</th><th>Last run</th><th>Next run</th></tr>
{{range .entries}}
{{$job := castjob .Job}}
<tr>
<td>{{$job.Name}}</td>
<td>{{$job.Status}}</td>
<td>{{if not .Prev.IsZero}}{{.Prev.Format "2006-01-02 15:04:05"}}{{end}}</td>
<td>{{if not .Next.IsZero}}{{.Next.Format "2006-01-02 15:04:05"}}{{end}}</td>
</tr>
{{end}}
</table>
+1
View File
@@ -0,0 +1 @@
GET /@jobs Jobs.Status
+3
View File
@@ -0,0 +1,3 @@
package jobs
// Required for vendoring see golang.org/issue/13832
+80
View File
@@ -0,0 +1,80 @@
modules/gorm
===============
[Gorm](http://jinzhu.me/gorm) module
## Activation
```ini
module.gorm = github.com/revel/modules/orm/gorm
```
## Drivers
* sqlite3
* postgres
* mysql
## Configuration file
```ini
# Database config
db.autoinit=true # default=true
db.driver=sqlite # mysql, postgres, sqlite3
db.host=localhost # Use db.host /tmp/app.db is your driver is sqlite
db.user=dbuser
db.name=dbname
db.password=dbpassword
```
## Example usage with transactions
```go
package controllers
import (
"github.com/revel/revel"
gormc "github.com/revel/modules/gorm/orm/app/controllers"
)
type App struct {
gormc.TxnController
}
type Toy struct {
Name string
}
func (c App) Index() revel.Result {
c.Txn.LogMode(true)
c.Txn.AutoMigrate(&Toy{})
c.Txn.Save(&Toy{Name: "Fidget spinner"})
return c.Render()
}
```
## Example usage without transactions
```go
package controllers
import (
"github.com/revel/revel"
gormc "github.com/revel/modules/gorm/orm/app/controllers"
)
type App struct {
gormc.Controller
}
type Toy struct {
Name string
}
func (c App) Index() revel.Result {
c.DB.LogMode(true)
c.DB.AutoMigrate(&Toy{})
c.DB.Save(&Toy{Name: "Fidget spinner"})
return c.Render()
}
```
+84
View File
@@ -0,0 +1,84 @@
package gormcontroller
import (
"database/sql"
"fmt"
"github.com/jinzhu/gorm"
gormdb "github.com/revel/modules/orm/gorm/app"
"github.com/revel/revel"
)
// Controller is a Revel controller with a pointer to the opened database
type Controller struct {
*revel.Controller
DB *gorm.DB
}
func (c *Controller) setDB() revel.Result {
c.DB = gormdb.DB
return nil
}
// TxnController is a Revel controller with database transaction support (begin, commit and rollback)
type TxnController struct {
*revel.Controller
Txn *gorm.DB
}
// Begin begins a DB transaction
func (c *TxnController) Begin() revel.Result {
txn := gormdb.DB.Begin()
if txn.Error != nil {
c.Log.Panic("Transaction begine error","error",txn.Error)
}
c.Txn = txn
return nil
}
// Commit commits the database transation
func (c *TxnController) Commit() revel.Result {
if c.Txn == nil {
return nil
}
c.Txn.Commit()
if c.Txn.Error != nil && c.Txn.Error != sql.ErrTxDone {
fmt.Println(c.Txn.Error)
panic(c.Txn.Error)
}
c.Txn = nil
return nil
}
// Rollback rolls back the transaction (eg. after a panic)
func (c *TxnController) Rollback() revel.Result {
if c.Txn == nil {
return nil
}
c.Txn.Rollback()
if c.Txn.Error != nil && c.Txn.Error != sql.ErrTxDone {
fmt.Println(c.Txn.Error)
panic(c.Txn.Error)
}
c.Txn = nil
return nil
}
func init() {
revel.OnAppStart(func() {
if revel.Config.BoolDefault("db.autoinit", true) {
gormdb.InitDB()
revel.InterceptMethod((*TxnController).Begin, revel.BEFORE)
revel.InterceptMethod((*TxnController).Commit, revel.AFTER)
revel.InterceptMethod((*TxnController).Rollback, revel.FINALLY)
revel.InterceptMethod((*Controller).setDB, revel.BEFORE)
}
})
}
+72
View File
@@ -0,0 +1,72 @@
package gormdb
// # Database config
// db.driver=sqlite3 # mysql, postgres, sqlite3
// db.host=localhost # Use dbhost /tmp/app.db is your driver is sqlite
// db.user=dbuser
// db.name=dbname
// db.password=dbpassword
import (
"fmt"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql" // mysql package
_ "github.com/jinzhu/gorm/dialects/postgres" // postgres package
_ "github.com/jinzhu/gorm/dialects/sqlite" // mysql package
"github.com/revel/revel"
)
// DB Gorm
var (
DB *gorm.DB
gormLog = revel.AppLog
)
func init() {
revel.RegisterModuleInit(func(m *revel.Module){
gormLog = m.Log
})
}
// InitDB database
func OpenDB(dbDriver string, dbInfo string) {
db, err := gorm.Open(dbDriver, dbInfo)
if err != nil {
gormLog.Fatal("sql.Open failed", "error", err)
}
DB = db
}
type DbInfo struct {
DbDriver string
DbHost string
DbUser string
DbPassword string
DbName string
}
func InitDBWithParameters(params DbInfo) {
dbInfo := ""
switch params.DbDriver {
default:
dbInfo = fmt.Sprintf(params.DbHost)
case "postgres":
dbInfo = fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", params.DbHost, params.DbUser, params.DbName, params.DbPassword)
case "mysql":
dbInfo = fmt.Sprintf("%s:%s@%s/%s?charset=utf8&parseTime=True&loc=Local", params.DbUser, params.DbPassword, params.DbHost, params.DbName)
}
OpenDB(params.DbDriver, dbInfo)
}
func InitDB() {
params := DbInfo{}
params.DbDriver = revel.Config.StringDefault("db.driver", "sqlite3")
params.DbHost = revel.Config.StringDefault("db.host", "localhost")
if params.DbDriver == "sqlite3" && params.DbHost == "localhost" {
params.DbHost = "/tmp/app.db"
}
params.DbUser = revel.Config.StringDefault("db.user", "default")
params.DbPassword = revel.Config.StringDefault("db.password", "")
params.DbName = revel.Config.StringDefault("db.name", "default")
InitDBWithParameters(params)
}
+3
View File
@@ -0,0 +1,3 @@
package gorm
// Required for vendoring see golang.org/issue/13832
+264
View File
@@ -0,0 +1,264 @@
modules/gorp
===============
This composite orm module combines Gorp with Squirrel to give you a complete solution
* [Gorp](https://github.com/go-gorp/gorp) *saves you time, minimizes the drudgery of
getting data in and out of your database, and helps your code focus on algorithms,
not infrastructure*.
* [Squirrel](https://github.com/Masterminds/squirrel) *helps you build SQL queries from
composable parts*
## Activation
```ini
module.gorp = github.com/revel/modules/orm/gorp
```
## Drivers
* sqlite3
* postgres
* mysql
## Configuration file
```ini
# Database config
db.autoinit=true # default=true
db.driver=postgres # mysql, postgres, sqlite3
# The database connection properties individually or use the db.connection
db.host=localhost # Use db.host /tmp/app.db is your driver is sqlite
db.user=dbuser
db.name=dbname
db.password=dbpassword
# Database connection string (host, user, dbname and other params)
db.connection=localhost port=8500 user=user dbname=mydb sslmode=disable password=ack
# If true then the database will be initialized on startup.
db.autoinit=true
```
## Decelerations
A global `Db *DbGorp` object is created in `github.com/revel/modules/gorp/app`.
The `Db` is initialized from the app.conf if `db.autoinit=true`.
```go
// DB Gorp
type DbGorp struct {
Gorp *gorp.DbMap
// The Sql statement builder to use to build select statements
SqlStatementBuilder sq.StatementBuilderType
}
var (
// The database map to use to populate data
Db = &DbGorp{}
)
```
## Usage
If `db.autoinit=true` in app.conf then you can add your tables to Gorp on app start.
Note that the tables are added as a function using `gorp.Db.SetDbInit` - this is for database thread pooling
```go
import (
"github.com/revel/revel"
"github.com/revel/modules/gorp/app"
)
func init() {
revel.OnAppStart(func(){
// Register tables
gorp.Db.SetDbInit(func(dbGorp *gorp.DbGorp) error {
// Register tables
gorp.Db.Map.AddTableWithName(model.MyTable{}, "my_table")
return nil
})
},5)
}
```
### Controller
Controllers, with the `gorpController.Controller` embedded,
have a gorp.DbGorp populated in the `Controller.Db`. This database is
the global one that is created on startup
```go
package controllers
import (
"github.com/revel/revel"
"github.com/revel/modules/orm/gorp/app/controllers"
)
type App struct {
gorpController.Controller
}
type TableRow struct {
Id int `db:"id,int64"`
}
func (c App) Index() revel.Result {
sql,args,_ := c.Db.SqlStatementBuilder.Select("*").From("table").Limit(1).ToSql()
row := &TableRow{}
if err:= c.Db.Map.SelectOne(row,sql,args...); err!=nil {
c.RenderError(err)
}
return c.Render(row)
}
```
### Multiple databases
The gorp module can populate a `DbGorp` object for you from a `gorp.DbInfo` object. So if you don't want
to use the global database (in the gorp module) you can initialize another anywhere in your project.
```go
import (
"github.com/revel/revel"
"github.com/revel/modules/orm/gorp/app"
)
var (
SecondDb = &gorp.DbGorp{}
)
func init() {
revel.OnAppStart(func(){
// Create a DbInfo object with a minimum of a driver and other details
params := gorp.DbInfo{Driver:"postgres",DbUser:revel.Config.StringDefault("seconddb.user", "default")}
secondDb.Info = params
if err:=secondDb.InitDb(true); err!=nil {
revel.Panicf("Second database failed to open %s", err.Error())
}
},0)
}
```
### Multi Channel Connections
This is not connection pooling - this is for distributing work across multiple channels to get a
lot of stuff done fast. It creates a bunch workers and each worker has
its own connection (On the start of the worker a status is sent in case you want to do some prework).
Tasks are sent through the `DbWorkContainer.InputChannel` which distributes the
task to whatever worker is available.
If you are using any tables that requires GORP to have initialized tables you
must register the tables using `gorp.Db.SetDbInit`. This is the only way that this service
can properly initialize the newly thread created GORP instances. Here is an example.
```go
import (
"github.com/revel/revel"
"github.com/revel/modules/gorp/app"
)
func init() {
revel.OnAppStart(func(){
// Register tables
gorp.Db.SetDbInit(func(dbGorp *gorp.DbGorp) error {
// Register tables
gorp.Db.Map.AddTableWithName(model.MyTable{}, "my_table")
return nil
})
},5)
}
```
In order to achieve this there is a `gorp.DbWorkerContainer` which is initialized by
`NewDbWorker(db *DbGorp, callBack DbCallback, numWorkers int) (container *DbWorkerContainer, err error)`
The `DbCallback` can be initialized by
`gorp.MakeCallback(status func(phase WorkerPhase, worker *DbWorker), work func(value interface{}, worker *DbWorker)) DbCallback`
()the status function is optional) or implemented by your structure
Once the `gorp.DbWorkerContainer` is created tasks can be submitted to it by using the
`gorp.DbWorkerContainer.InputChannel<-task` this will call the `gorp.DbCallback.Work` function
passing in an instance of *DbWorker
```go
// Assume sourceDb is a *gorp.DbGorp instance
workerPool := gorp.NewDbWorker(d.Db, gorp.MakeCallback(func(phase gorp.WorkerPhase, worker *gorp.DbWorker) {
// On start initialize some data to be used later
if phase == gorp.Start {
dataList, err := model.FetchData(worker.Db)
worker.SharedData["dataList"] = dataList
} else if phase == gorp.JobLongrunning {
revel.AppLog.Error("Long running process detected", "worker", worker.Id)
}
}, func(work interface{}, worker *gorp.DbWorker) {
dataList := worker.SharedData["dataList"].(model.DataList)
// Whatever is sent into the workerPool.InputChannel<- will be the value
value := work.(*SomeObject)
}), 100)
// Set the timeout for watchdog notifications
workerPool.LongWorkTimeout=300
err := workerPool.Start() // Start a 100 worker threads
if err!=nil {
return err
}
defer workerPool.Close(0) // Close, wait for channels to exit (non zero would exit after timeout)
// var tasks[] a large list of work to be done
tasksBlock := make([]*SomeObject,100)
for i,task := range tasks {
if i>0 && i%100==0 {
workerPool.InputChannel<-taskBlock
tasksBlock = make([]*SomeObject)
}
taskBlock = append(taskBlock, task)
}
workerPool.InputChannel<-taskBlock
// Pool is closed on defer function, it will not return till pool closes
return nil
```
#### Implementation notes
If your "work" is short but there is a lot of it then it is highly recommended you pass in lists of
items to work on. Channels are great at providing an easy way
to move data to and from threads but it is a process of synchronizing between two threads. If you pass
in a single object at a time you pay that cost on every row you pass. If you do it once every 10,000
rows then the cost is minimal.
#### WorkParallel function
There is a handy function called
`WorkParallel(db *DbGorp, tasks []func(worker *DbWorker), returnResults bool, maxNumWorkers int, timeouts int) (err error)`
which makes it simple to do create a group of calls
```go
// Assume sourceDb is a *gorp.DbGorp instance
task := func(query string) func(db *gorp.DbWorker) {
return func(worker *gorp.DbWorker) {
_, e := worker.Db.Map.Exec("ANALYZE " + query)
}
}
gorp.WorkParallel(sourceDb, []func(worker *gorp.DbWorker){
task("history"),
task("summary"),
task("daily"),
task("monthly"),
}, false, 0, 0)
```
#### Returning data from workers
`gorp.DbWorker` contains an OutputChannel which you can send the results back to, you must read
from the output the same number of times that you wrote. The output size is the same size as the
```go
// Assume sourceDb is a *gorp.DbGorp instance
task := func(query string) func(db *gorp.DbWorker) {
return func(worker *gorp.DbWorker) {
result, err := worker.Db.Map.Exec("ANALYZE " + query)
worker.OutputChannel <- []interface{}{result,err}
}
}
r,e:=gorp.WorkParallel(sourceDb, []func(worker *gorp.DbWorker){
task("history"),
task("daily"),
task("monthly"),
},true, 0, 0)
println(r, e)
for _,result := range r {
key,err:= result.([]interface{})[0],result.([]interface{})[1]
fmt.Println("Returned result",key,"error",err)
}
```
#### Watchdog Timeouts
`gorp.DbWorkerContainer` contains a couple of timeout settings (in seconds) used to monitor
the startup and running of the workers for the duration of the container
(this is typically called a watchdog timeout).
- `StartWorkTimeout` If greater then 0 this is the timeout in seconds that it takes to start a worker.
- `LongWorkTimeout` If greater then 0 this is the timeout in seconds that it takes before a
notification is sent to the `DbCallbackImplied.StatusFn func(phase WorkerPhase, worker *DbWorker)`
if a worker runs past X seconds on a single task.
Each worker will have their own watchdog channel and it will send a `gorp.JobLongrunning` and
the `gorp.DBWorker` to the status function so you can log or investigate long running processes
+69
View File
@@ -0,0 +1,69 @@
package gorpController
import (
"database/sql"
"github.com/revel/modules/orm/gorp/app"
"github.com/revel/revel"
)
// Controller definition for database transaction
// This controller is only useful if you intend to use the database instance
// defined in github.com/revel/modules/orm/gorp/app.Db
type Controller struct {
*revel.Controller
Txn *gorp.Transaction
Db *gorp.DbGorp
}
// Begin a transaction
func (c *Controller) Begin() revel.Result {
c.Db = gorp.Db
txn, err := gorp.Db.Begin()
if err != nil {
panic(err)
}
c.Txn = txn
return nil
}
// Rollback if it's still going (must have panicked).
func (c *Controller) Rollback() revel.Result {
if c.Txn != nil {
if err := c.Txn.Rollback(); err != nil {
if err != sql.ErrTxDone {
panic(err)
}
}
c.Txn = nil
}
return nil
}
// Commit the transaction.
func (c *Controller) Commit() revel.Result {
if c.Txn != nil {
if err := c.Txn.Commit(); err != nil {
if err != sql.ErrTxDone {
panic(err)
}
}
c.Txn = nil
}
return nil
}
func init() {
// Run this as soon as possible
revel.OnAppStart(func() {
if revel.Config.BoolDefault("db.autoinit", false) {
if err := gorp.InitDb(gorp.Db); err != nil {
// Force a failure
revel.RevelLog.Panicf("gorp:Unable to initialize database")
}
revel.InterceptMethod((*Controller).Begin, revel.BEFORE)
revel.InterceptMethod((*Controller).Commit, revel.AFTER)
revel.InterceptMethod((*Controller).Rollback, revel.FINALLY)
}
}, 0,
)
}
+168
View File
@@ -0,0 +1,168 @@
package gorp
import (
"database/sql"
sq "gopkg.in/Masterminds/squirrel.v1"
"gopkg.in/gorp.v2"
"github.com/revel/revel/logger"
)
// DB Gorp
type DbGorp struct {
Map *gorp.DbMap
// The Sql statement builder to use to build select statements
SqlStatementBuilder sq.StatementBuilderType
// Database connection information
Info *DbInfo
// The database initialization function
dbInitFn func(dbMap *DbGorp) error
}
type DbInfo struct {
DbDriver string
DbHost string
DbUser string
DbPassword string
DbName string
DbConnection string
Dialect gorp.Dialect
}
// OpenDb database
func (dbGorp *DbGorp) OpenDb() (err error) {
db, err := sql.Open(dbGorp.Info.DbDriver, dbGorp.Info.DbConnection)
if err != nil {
moduleLogger.Fatal("Open Database Error", "error", err)
}
// Create the database map
dbGorp.Map = &gorp.DbMap{Db: db, Dialect: dbGorp.Info.Dialect}
return dbGorp.dbInit()
}
// Create a new database connection and open it from this one
func (dbGorp *DbGorp) CloneDb(open bool) (newDb *DbGorp, err error) {
dbInfo := *dbGorp.Info
newDb = &DbGorp{Info: &dbInfo}
newDb.dbInitFn = dbGorp.dbInitFn
err = newDb.InitDb(open)
return
}
// Close the database connection
func (dbGorp *DbGorp) Begin() (txn *Transaction, err error) {
tx,err := dbGorp.Map.Begin()
if err!=nil {
return
}
txn = &Transaction{tx}
return
}
// Close the database connection
func (dbGorp *DbGorp) Close() (err error) {
if dbGorp.Map.Db != nil {
err = dbGorp.Map.Db.Close()
}
return
}
// Called to perform table registration and anything else that needs to be done on a new connection
func (dbGorp *DbGorp) dbInit() (err error) {
if dbGorp.dbInitFn != nil {
err = dbGorp.dbInitFn(dbGorp)
}
return
}
// Used to specifiy the init function to call when database is initialized
// Calls the init function immediately
func (dbGorp *DbGorp) SetDbInit(dbInitFn func(dbMap *DbGorp) error) (err error) {
dbGorp.dbInitFn = dbInitFn
return dbGorp.dbInit()
}
func (dbGorp *DbGorp) Select(i interface{}, builder sq.SelectBuilder) (l []interface{}, err error) {
query, args, err := builder.ToSql()
if err == nil {
list, err := dbGorp.Map.Select(i, query, args...)
if err != nil && gorp.NonFatalError(err) {
return list, nil
}
if err==sql.ErrNoRows {
err = nil
}
return list, err
}
return
}
func (dbGorp *DbGorp) SelectOne(i interface{}, builder sq.SelectBuilder) (err error) {
query, args, err := builder.ToSql()
if err == nil {
err = dbGorp.Map.SelectOne(i, query, args...)
if err != nil && gorp.NonFatalError(err) {
return nil
}
}
return
}
func (dbGorp *DbGorp) SelectInt(builder sq.SelectBuilder) (i int64, err error) {
query, args, err := builder.ToSql()
if err == nil {
i, err = dbGorp.Map.SelectInt(query, args...)
}
return
}
func (dbGorp *DbGorp) ExecUpdate(builder sq.UpdateBuilder) (r sql.Result, err error) {
query, args, err := builder.ToSql()
if err == nil {
r, err = dbGorp.Map.Exec(query, args...)
}
return
}
func (dbGorp *DbGorp) ExecInsert(builder sq.InsertBuilder) (r sql.Result, err error) {
query, args, err := builder.ToSql()
if err == nil {
r, err = dbGorp.Map.Exec(query, args...)
}
return
}
//
// Shifted some common functions up a level
////
func (dbGorp *DbGorp) Insert(list ...interface{}) error {
return dbGorp.Map.Insert(list...)
}
func (dbGorp *DbGorp) Update(list ...interface{}) (int64, error) {
return dbGorp.Map.Update(list...)
}
func (dbGorp *DbGorp) Get(i interface{}, keys ...interface{}) (interface{}, error) {
return dbGorp.Map.Get(i,keys...)
}
func (dbGorp *DbGorp) Delete(i ...interface{}) (int64, error) {
return dbGorp.Map.Delete(i...)
}
func (dbGorp *DbGorp) TraceOn(log logger.MultiLogger) {
dbGorp.Map.TraceOn("",&simpleTrace{log.New("section","gorp")})
}
func (dbGorp *DbGorp) TraceOff() {
}
type simpleTrace struct {
log logger.MultiLogger
}
func (s *simpleTrace) Printf(format string, v ...interface{}) {
s.log.Infof(format,v...)
}
+70
View File
@@ -0,0 +1,70 @@
package gorp
import (
"fmt"
_ "github.com/jinzhu/gorm/dialects/mysql" // mysql package
_ "github.com/jinzhu/gorm/dialects/postgres" // postgres package
_ "github.com/jinzhu/gorm/dialects/sqlite" // mysql package
"github.com/revel/revel"
sq "gopkg.in/Masterminds/squirrel.v1"
"gopkg.in/gorp.v2"
"github.com/revel/revel/logger"
)
var (
// The database map to use to populate data
Db = &DbGorp{}
moduleLogger logger.MultiLogger
)
func init() {
revel.RegisterModuleInit(func(module *revel.Module){
moduleLogger = module.Log
moduleLogger.Debug("Assigned Logger")
})
}
func (dbResult *DbGorp)InitDb(open bool) (err error) {
dbInfo := dbResult.Info
switch dbInfo.DbDriver {
default:
dbResult.SqlStatementBuilder = sq.StatementBuilder.PlaceholderFormat(sq.Question)
dbInfo.Dialect = gorp.SqliteDialect{}
if len(dbInfo.DbConnection) == 0 {
dbInfo.DbConnection = fmt.Sprintf(dbInfo.DbHost)
}
case "postgres":
dbResult.SqlStatementBuilder = sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
dbInfo.Dialect = gorp.PostgresDialect{}
if len(dbInfo.DbConnection) == 0 {
dbInfo.DbConnection = fmt.Sprintf("host=%s port=8500 user=%s dbname=%s sslmode=disable password=%s", dbInfo.DbHost, dbInfo.DbUser, dbInfo.DbName, dbInfo.DbPassword)
}
case "mysql":
dbResult.SqlStatementBuilder = sq.StatementBuilder.PlaceholderFormat(sq.Question)
dbInfo.Dialect = gorp.MySQLDialect{}
if len(dbInfo.DbConnection) == 0 {
dbInfo.DbConnection = fmt.Sprintf("%s:%s@%s/%s?charset=utf8&parseTime=True&loc=Local", dbInfo.DbUser, dbInfo.DbPassword, dbInfo.DbHost, dbInfo.DbName)
}
}
if open {
err = dbResult.OpenDb()
}
return
}
// Initialize the database from revel.Config
func InitDb(dbResult *DbGorp) (error) {
params := DbInfo{}
params.DbDriver = revel.Config.StringDefault("db.driver", "sqlite3")
params.DbHost = revel.Config.StringDefault("db.host", "localhost")
if params.DbDriver == "sqlite3" && params.DbHost == "localhost" {
params.DbHost = "/tmp/app.db"
}
params.DbUser = revel.Config.StringDefault("db.user", "default")
params.DbPassword = revel.Config.StringDefault("db.password", "")
params.DbName = revel.Config.StringDefault("db.name", "default")
params.DbConnection = revel.Config.StringDefault("db.connection", "")
dbResult.Info = &params
return dbResult.InitDb(true)
}
+82
View File
@@ -0,0 +1,82 @@
package gorp
import (
gorpa "gopkg.in/gorp.v2"
"database/sql"
sq "gopkg.in/Masterminds/squirrel.v1"
)
type (
// This is a small wrapped around gorp.Transaction so you can make use of the builder statements as well
Transaction struct {
Map *gorpa.Transaction
}
)
func (txn *Transaction) Rollback() (err error) {
return txn.Map.Rollback()
}
func (txn *Transaction) Commit() (err error) {
return txn.Map.Commit()
}
func (txn *Transaction) Select(i interface{}, builder sq.SelectBuilder) (l []interface{}, err error) {
query, args, err := builder.ToSql()
if err == nil {
list, err := txn.Map.Select(i, query, args...)
if err != nil && gorpa.NonFatalError(err) {
return list, nil
}
if err==sql.ErrNoRows {
err = nil
}
return list, err
}
return
}
func (txn *Transaction) SelectOne(i interface{}, builder sq.SelectBuilder) (err error) {
query, args, err := builder.ToSql()
if err == nil {
err = txn.Map.SelectOne(i, query, args...)
if err != nil && gorpa.NonFatalError(err) {
return nil
}
}
return
}
func (txn *Transaction) SelectInt(builder sq.SelectBuilder) (i int64, err error) {
query, args, err := builder.ToSql()
if err == nil {
i, err = txn.Map.SelectInt(query, args...)
}
return
}
func (txn *Transaction) ExecUpdate(builder sq.UpdateBuilder) (r sql.Result, err error) {
query, args, err := builder.ToSql()
if err == nil {
r, err = txn.Map.Exec(query, args...)
}
return
}
func (txn *Transaction) ExecInsert(builder sq.InsertBuilder) (r sql.Result, err error) {
query, args, err := builder.ToSql()
if err == nil {
r, err = txn.Map.Exec(query, args...)
}
return
}
// Shifted some common functions up a level
func (txn *Transaction) Insert(list ...interface{}) error {
return txn.Map.Insert(list...)
}
func (txn *Transaction) Update(list ...interface{}) (int64, error) {
return txn.Map.Update(list...)
}
func (txn *Transaction) Get(i interface{}, keys ...interface{}) (interface{}, error) {
return txn.Map.Get(i,keys...)
}
func (txn *Transaction) Delete(i ...interface{}) (int64, error) {
return txn.Map.Delete(i...)
}
+268
View File
@@ -0,0 +1,268 @@
package gorp
import (
"fmt"
"runtime"
"sync"
"time"
)
// The worker container
type DbWorkerContainer struct {
SharedWorker
mutex sync.Mutex
Workers []*DbWorker
NumWorkers int
LongWorkTimeout int64
StartWorkTimeout int64
Db *DbGorp
}
// The timeoutInfo for monitoring long running processes
type timeoutInfo struct {
worker *DbWorker
started time.Time
ended time.Time
state WorkerPhase
}
type DbWorker struct {
Id int
Db *DbGorp
SharedWorker
WorkUnit int
SharedData map[string]interface{}
TimeInfo *timeoutInfo
TimeoutChannel chan *timeoutInfo
}
type SharedWorker struct {
workInfo DbWorkInfo
InputChannel chan interface{}
OutputChannel chan interface{}
ControlChannel chan func() (WorkerPhase, *DbWorker)
}
type DbWorkInfo interface {
Status(phase WorkerPhase, worker *DbWorker)
Work(value interface{}, worker *DbWorker)
}
type DbCallbackImplied struct {
StatusFn func(phase WorkerPhase, worker *DbWorker)
WorkFn func(value interface{}, worker *DbWorker)
}
type WorkerPhase int
const (
Start WorkerPhase = iota
Stop
StartJob
EndJob
JobLongrunning
)
// Creates a container to run the group of workers (up to a max of maxNumWorkers), does not return to all workers are completed)
// If returnResults is true then the task MUST write to the DbWorker.OutputChannel once for every task
func WorkParallel(db *DbGorp, tasks []func(worker *DbWorker), returnResults bool, maxNumWorkers int, timeouts int) (results []interface{}, err error) {
if maxNumWorkers == 0 {
maxNumWorkers = len(tasks)
}
// Create a container with no status callback
container := NewDbWorker(db,
MakeCallback(nil,
func(value interface{}, worker *DbWorker) {
task := value.(func(worker *DbWorker))
task(worker)
}), maxNumWorkers)
err = container.Start()
if err != nil {
return
}
for _, task := range tasks {
container.InputChannel <- task
}
if returnResults {
for range tasks {
result := <-container.OutputChannel
results = append(results, result)
}
}
container.Close(timeouts)
return
}
// This creates a DbWorkerContainer with the number of working threads already started.
// Each working thread has their own database instance running.
func NewDbWorker(db *DbGorp, workInfo DbWorkInfo, numWorkers int) (container *DbWorkerContainer) {
container = &DbWorkerContainer{
SharedWorker: SharedWorker{
InputChannel: make(chan interface{}, numWorkers),
OutputChannel: make(chan interface{}, numWorkers),
ControlChannel: make(chan func() (WorkerPhase, *DbWorker), numWorkers),
workInfo: workInfo,
},
NumWorkers: numWorkers,
Db: db,
StartWorkTimeout: 0,
LongWorkTimeout: 0,
}
return
}
func (container *DbWorkerContainer) Start() (err error) {
for x := 0; x < container.NumWorkers; x++ {
go startWorker(container, container.Db, x)
}
// Make sure all containers are running before returning
for x := 0; x < container.NumWorkers; x++ {
if container.StartWorkTimeout > 0 {
select {
case result := <-container.ControlChannel:
state, source := result()
if state != Start {
container.Close(5)
err = fmt.Errorf("Failed to start workers %d", source)
return
}
case <-time.After(time.Second * time.Duration(container.StartWorkTimeout)):
container.Close(5)
err = fmt.Errorf("Failed to start worker timeout")
return
}
} else {
result := <-container.ControlChannel
state, source := result()
if state != Start {
container.Close(5)
err = fmt.Errorf("Failed to start workers %d", source)
return
}
}
}
return
}
func (container *DbWorkerContainer) Close(timeouts int) (totalWork int, err error) {
close(container.InputChannel)
for x := 0; x < len(container.Workers); x++ {
// Allow close to continue even if a worker does not respond
if timeouts > 0 {
select {
case result := <-container.ControlChannel:
_, worker := result()
totalWork += worker.WorkUnit
case <-time.After(time.Second * time.Duration(timeouts)):
}
} else {
_, worker := (<-container.ControlChannel)()
totalWork += worker.WorkUnit
}
}
close(container.OutputChannel)
return
}
// Called by using "go" to invoke, creates a DBWorker, and starts a watchdog channel
func startWorker(container *DbWorkerContainer, db *DbGorp, id int) {
newDb, _ := db.CloneDb(true)
worker := &DbWorker{
Db: newDb,
Id: id,
SharedData: map[string]interface{}{},
SharedWorker: SharedWorker{
workInfo: container.workInfo,
InputChannel: container.InputChannel,
OutputChannel: container.OutputChannel,
ControlChannel: container.ControlChannel,
},
}
// Close the database after worker has ended (Start returned
defer worker.Db.Close()
container.mutex.Lock()
container.Workers = append(container.Workers, worker)
container.mutex.Unlock()
// Only monitor jobs if Status function defined and a timeout is also defined
if worker.workInfo.Status != nil && container.LongWorkTimeout > 0 {
worker.TimeoutChannel = make(chan *timeoutInfo)
go worker.TimeInfo.start(worker.TimeoutChannel, container.LongWorkTimeout)
}
worker.start()
}
// Starts the worker, continues running until inputchannel is closed
func (worker *DbWorker) start() {
if worker.workInfo.Status != nil {
worker.workInfo.Status(Start, worker)
}
worker.ControlChannel <- func() (WorkerPhase, *DbWorker) { return Start, worker }
for job := range worker.InputChannel {
worker.invoke(job)
}
if worker.workInfo.Status != nil {
worker.workInfo.Status(Stop, worker)
}
worker.ControlChannel <- func() (WorkerPhase, *DbWorker) { return Stop, worker }
if worker.TimeoutChannel != nil {
close(worker.TimeoutChannel)
}
}
// Wrapper to prevent panics from disturbing the channel
func (worker *DbWorker) invoke(job interface{}) {
defer func() {
if err := recover(); err != nil {
trace := make([]byte, 1024)
count := runtime.Stack(trace, true)
moduleLogger.Error("Recover from panic: ", "error", err)
moduleLogger.Error("Stack", "size", count, "trace", string(trace))
}
}()
// Setup the timeout information
if worker.TimeoutChannel != nil {
worker.TimeInfo = &timeoutInfo{worker: worker, started: time.Now(), state: StartJob}
worker.TimeoutChannel <- worker.TimeInfo
}
worker.workInfo.Work(job, worker)
if worker.TimeoutChannel != nil {
worker.TimeInfo.state = EndJob
worker.TimeoutChannel <- worker.TimeInfo
}
}
// A function to return an object that is a valid DbCallback
func MakeCallback(status func(phase WorkerPhase, worker *DbWorker), work func(value interface{}, worker *DbWorker)) DbWorkInfo {
return &DbCallbackImplied{StatusFn: status, WorkFn: work}
}
// Call the status function if available
func (dbCallback *DbCallbackImplied) Status(phase WorkerPhase, worker *DbWorker) {
if dbCallback.StatusFn != nil {
dbCallback.StatusFn(phase, worker)
}
}
// Calls the work function
func (dbCallback *DbCallbackImplied) Work(value interface{}, worker *DbWorker) {
dbCallback.WorkFn(value, worker)
}
//Starts the timeout worker
func (_ *timeoutInfo) start(TimeoutChannel chan *timeoutInfo, timeout int64) {
for j := range TimeoutChannel {
j.started = time.Now()
j.state = StartJob
j.worker.workInfo.Status(j.state, j.worker)
for {
select {
case complete, ok := <-TimeoutChannel:
if !ok {
// Channel closed returning...
return
}
// Received new State, record and loop
complete.worker.workInfo.Status(complete.state, complete.worker)
break
case <-time.After(time.Second * time.Duration(timeout)):
j.worker.workInfo.Status(JobLongrunning, j.worker)
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
package gorp
// Required for vendoring see golang.org/issue/13832
+19
View File
@@ -0,0 +1,19 @@
Revel pprof module
============
#### How to use:
1. Open your app.conf file and add the following line:
`module.pprof=github.com/revel/modules/pprof`
This will enable the pprof module.
2. Next, open your routes file and add:
`module:pprof` **Note:** Do not change these routes. The pprof command-line tool by default assumes these routes.
Congrats! You can now profile your application. To use the web interface, visit `http://<host>:<port>/debug/pprof`. You can also use the `go tool pprof` command to profile your application and get a little deeper. Use the command by running `go tool pprof <binary> http://<host>:<port>`. For example, if you modified the booking sample, you would run: `go tool pprof $GOPATH/bin/booking http://localhost:9000` (assuming you used the default `revel run` arguments.
The command-line tool will take a 30-second CPU profile, and save the results to a temporary file in your `$HOME/pprof` directory. You can reference this file at a later time by using the same command as above, but by specifying the filename instead of the server address.
In order to fully utilize the command-line tool, you may need the graphviz utilities. If you're on OS X, you can install these easily using Homebrew: `brew install graphviz`.
To read more about profiling Go programs, here is some reading material: [The Go Blog](http://blog.golang.org/profiling-go-programs) : [net/pprof package documentation](http://golang.org/pkg/net/http/pprof/)
+39
View File
@@ -0,0 +1,39 @@
package controllers
import (
"github.com/revel/revel"
"net/http"
"net/http/pprof"
)
type Pprof struct {
*revel.Controller
}
// The PprofHandler type makes it easy to call the net/http/pprof handler methods
// since they all have the same method signature
type PprofHandler func(http.ResponseWriter, *http.Request)
func (r PprofHandler) Apply(req *revel.Request, resp *revel.Response) {
r(resp.Out.Server.GetRaw().(http.ResponseWriter), req.In.GetRaw().(*http.Request))
}
func (c Pprof) Profile() revel.Result {
return PprofHandler(pprof.Profile)
}
func (c Pprof) Symbol() revel.Result {
return PprofHandler(pprof.Symbol)
}
func (c Pprof) Cmdline() revel.Result {
return PprofHandler(pprof.Cmdline)
}
func (c Pprof) Trace() revel.Result {
return PprofHandler(pprof.Trace)
}
func (c Pprof) Index() revel.Result {
return PprofHandler(pprof.Index)
}
+6
View File
@@ -0,0 +1,6 @@
GET /debug/pprof/profile Pprof.Profile
GET /pprof/symbol Pprof.Symbol
GET /debug/pprof/cmdline Pprof.Cmdline
GET /debug/pprof/trace Pprof.Trace
GET /debug/pprof/* Pprof.Index
GET /debug/pprof/ Pprof.Index
+3
View File
@@ -0,0 +1,3 @@
package pprof
// Required for vendoring see golang.org/issue/13832
+14
View File
@@ -0,0 +1,14 @@
# Server Engines
These are some of the server engines built by the Revel team.
In order to use an engine you must import the module and set
the configuration option `server.engine` equal to the engine you want.
You may define your own engine by implementing `revel.ServerEngine` and
registering it with revel in an `init`
function `revel.RegisterServerEngine(NAME, <function to return your server instance>)`. See
`revel.GoHttpServer` for an example
### app.conf
- **server.engine** Defaults to `go`. Specify the engine you wish to use
+11
View File
@@ -0,0 +1,11 @@
# FastHTTP
This module is the for [FastHTTP](https://github.com/valyala/fasthttp) server engine.
It does not support WebSockets.
### app.conf
- **server.engine** You must set this to `fasthttp` in order to use this server engine
### Other Notes
All features from that the go HTTP engine is supported by this server engine.
Memory usage is decreased by avoiding memory allocations. This should
increase overall runtime performance and throughput.
@@ -0,0 +1,423 @@
package fasthttp
import (
"bytes"
"fmt"
"github.com/revel/revel"
"github.com/valyala/fasthttp"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
type FastHTTPServer struct {
Server *fasthttp.Server
ServerInit *revel.EngineInit
MaxMultipartSize int64
}
var serverLog = revel.AppLog
func init() {
revel.RegisterServerEngine("fasthttp", func() revel.ServerEngine {
return &FastHTTPServer{}
})
revel.RegisterModuleInit(func(m *revel.Module) {
serverLog = m.Log
})
}
func (f *FastHTTPServer) Init(init *revel.EngineInit) {
f.MaxMultipartSize = int64(revel.Config.IntDefault("server.request.max.multipart.filesize", 32)) << 20 /* 32 MB */
fastHttpContextStack = revel.NewStackLock(revel.Config.IntDefault("server.context.stack", 100),
revel.Config.IntDefault("server.context.maxstack", 200),
func() interface{} { return NewFastHttpContext(f) })
fastHttpMultipartFormStack = revel.NewStackLock(revel.Config.IntDefault("server.form.stack", 100),
revel.Config.IntDefault("server.form.maxstack", 200),
func() interface{} { return &FastHttpMultipartForm{} })
requestHandler := func(ctx *fasthttp.RequestCtx) {
f.RequestHandler(ctx)
}
f.ServerInit = init
f.Server = &fasthttp.Server{
ReadTimeout: time.Duration(revel.Config.IntDefault("http.timeout.read", 0)) * time.Second,
WriteTimeout: time.Duration(revel.Config.IntDefault("http.timeout.write", 0)) * time.Second,
Handler: requestHandler,
}
}
// Handler is assigned in the Init
func (f *FastHTTPServer) Start() {
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Printf("\nListening on fasthttp %s...\n", f.ServerInit.Address)
}()
if revel.HTTPSsl {
if f.ServerInit.Network != "tcp" {
// This limitation is just to reduce complexity, since it is standard
// to terminate SSL upstream when using unix domain sockets.
serverLog.Fatal("SSL is only supported for TCP sockets. Specify a port to listen on.")
}
serverLog.Fatal("Failed to listen https:", "error",
f.Server.ListenAndServeTLS(f.ServerInit.Address, revel.HTTPSslCert, revel.HTTPSslKey))
} else {
listener, err := net.Listen(f.ServerInit.Network, f.ServerInit.Address)
if err != nil {
serverLog.Fatal("Failed to listen http:", "error", err)
}
serverLog.Info("Listening fasthttp ", f.ServerInit.Network, f.ServerInit.Address)
// revel.ERROR.Fatalln("Failed to serve:", f.Server.ListenAndServe(f.ServerInit.Address))
serverLog.Fatal("Server exited", "error", f.Server.Serve(listener))
println("***ENDING ***")
}
}
func (f *FastHTTPServer) RequestHandler(ctx *fasthttp.RequestCtx) {
// TODO this
//if maxRequestSize := int64(revel.Config.IntDefault("http.maxrequestsize", 0)); maxRequestSize > 0 {
// buffer := &bytes.Buffer{}
// err := ctx.Request.ReadLimitBody(buffer,maxRequestSize)
// if err!=nil {
// // Send the error back to the client
// ctx.SetStatusCode(http.StatusRequestEntityTooLarge)
// return
// }
//}
context := fastHttpContextStack.Pop().(*FastHttpContext)
defer func() {
fastHttpContextStack.Push(context)
}()
context.SetContext(ctx)
f.ServerInit.Callback(context)
}
func (f *FastHTTPServer) Event(event int, args interface{}) {
switch event {
case revel.ENGINE_BEFORE_INITIALIZED:
case revel.ENGINE_STARTED:
}
}
func (f *FastHTTPServer) Name() string {
return "fasthttp"
}
func (f *FastHTTPServer) Engine() interface{} {
return f
}
func (g *FastHTTPServer) Stats() map[string]interface{} {
return map[string]interface{}{
"FastHTTP Engine Context": fastHttpContextStack.String(),
"FastHTTP Engine Forms": fastHttpMultipartFormStack.String(),
}
}
type (
FastHttpContext struct {
Request *FastHttpRequest
Response *FastHttpResponse
}
FastHttpRequest struct {
toQuery bool
url *url.URL
query url.Values
Original *fasthttp.RequestCtx
FormParsed bool
form url.Values
MultiFormParsed bool
// WebSocket *websocket.Conn
ParsedForm *FastHttpMultipartForm
header *FastHttpHeader
Engine *FastHTTPServer
}
FastHttpResponse struct {
Original *fasthttp.RequestCtx
header *FastHttpHeader
Writer io.Writer
Engine *FastHTTPServer
}
FastHttpMultipartForm struct {
Form *multipart.Form
}
FastHttpHeader struct {
Source interface{}
isResponse bool
}
FastHttpCookie []byte
)
var (
fastHttpContextStack *revel.SimpleLockStack
fastHttpMultipartFormStack *revel.SimpleLockStack
)
func NewFastHttpContext(instance *FastHTTPServer) *FastHttpContext {
if instance == nil {
instance = &FastHTTPServer{MaxMultipartSize: 32 << 20}
}
c := &FastHttpContext{
Request: &FastHttpRequest{header: &FastHttpHeader{isResponse: false},
Engine: instance},
Response: &FastHttpResponse{header: &FastHttpHeader{isResponse: true},
Engine: instance},
}
c.Response.header.Source = c.Response
c.Request.header.Source = c.Request
return c
}
func (c *FastHttpContext) GetRequest() revel.ServerRequest {
return c.Request
}
func (c *FastHttpContext) GetResponse() revel.ServerResponse {
return c.Response
}
func (c *FastHttpContext) SetContext(context *fasthttp.RequestCtx) {
c.Response.SetContext(context)
c.Request.SetContext(context)
}
func (c *FastHttpContext) Destroy() {
c.Response.Destroy()
c.Request.Destroy()
}
func (r *FastHttpRequest) Get(key int) (value interface{}, err error) {
switch key {
case revel.HTTP_SERVER_HEADER:
value = r.GetHeader()
case revel.HTTP_MULTIPART_FORM:
value, err = r.GetMultipartForm()
case revel.HTTP_QUERY:
value = r.GetQuery()
case revel.HTTP_FORM:
value, err = r.GetForm()
case revel.HTTP_REQUEST_URI:
value = string(r.Original.RequestURI())
case revel.HTTP_REMOTE_ADDR:
value = r.Original.RemoteAddr().String()
case revel.HTTP_METHOD:
value = string(r.Original.Method())
case revel.HTTP_PATH:
value = string(r.Original.Path())
case revel.HTTP_HOST:
value = string(r.Original.Request.Host())
case revel.HTTP_URL:
if r.url == nil {
r.url, _ = url.Parse(string(r.Original.Request.URI().FullURI()))
}
value = r.url
case revel.HTTP_BODY:
value = bytes.NewBuffer(r.Original.Request.Body())
default:
err = revel.ENGINE_UNKNOWN_GET
}
return
}
func (r *FastHttpRequest) Set(key int, value interface{}) bool {
return false
}
func (r *FastHttpRequest) GetQuery() url.Values {
if !r.toQuery {
// Attempt to convert to query
r.query = url.Values{}
r.Original.QueryArgs().VisitAll(func(key, value []byte) {
r.query.Set(string(key), string(value))
})
r.toQuery = true
}
return r.query
}
func (r *FastHttpRequest) GetForm() (url.Values, error) {
if !r.FormParsed {
r.form = url.Values{}
r.Original.PostArgs().VisitAll(func(key, value []byte) {
println("Set value", string(key), string(value))
r.query.Set(string(key), string(value))
})
r.FormParsed = true
}
return r.form, nil
}
func (r *FastHttpRequest) GetMultipartForm() (revel.ServerMultipartForm, error) {
if !r.MultiFormParsed {
// TODO Limit size r.Engine.MaxMultipartSize
form, err := r.Original.MultipartForm()
if err != nil {
return nil, err
}
r.ParsedForm = fastHttpMultipartFormStack.Pop().(*FastHttpMultipartForm)
r.ParsedForm.Form = form
}
return r.ParsedForm, nil
}
func (r *FastHttpRequest) GetHeader() revel.ServerHeader {
return r.header
}
func (r *FastHttpRequest) GetRaw() interface{} {
return r.Original
}
func (r *FastHttpRequest) SetContext(req *fasthttp.RequestCtx) {
r.Original = req
}
func (r *FastHttpRequest) Destroy() {
r.Original = nil
r.FormParsed = false
r.MultiFormParsed = false
r.ParsedForm = nil
r.toQuery = false
}
func (r *FastHttpResponse) Get(key int) (value interface{}, err error) {
switch key {
case revel.HTTP_SERVER_HEADER:
value = r.Header()
case revel.HTTP_STREAM_WRITER:
value = r
case revel.HTTP_WRITER:
value = r.Writer
default:
err = revel.ENGINE_UNKNOWN_GET
}
return
}
func (r *FastHttpResponse) Set(key int, value interface{}) (set bool) {
switch key {
case revel.ENGINE_RESPONSE_STATUS:
r.Header().SetStatus(value.(int))
set = true
case revel.HTTP_WRITER:
r.SetWriter(value.(io.Writer))
set = true
}
return
}
func (r *FastHttpResponse) GetWriter() io.Writer {
return r.Writer
}
func (r *FastHttpResponse) Header() revel.ServerHeader {
return r.header
}
func (r *FastHttpResponse) GetRaw() interface{} {
return r.Original
}
func (r *FastHttpResponse) WriteStream(name string, contentlen int64, modtime time.Time, reader io.Reader) error {
// do a simple io.Copy, we do it directly into the writer which may be configured to be a compressed
// writer
ius := r.Original.Request.Header.Peek("If-Unmodified-Since")
if t, err := http.ParseTime(string(ius)); ius != nil && err == nil && !modtime.IsZero() {
// The Date-Modified header truncates sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if modtime.Before(t.Add(1 * time.Second)) {
h := r.Original.Response.Header
h.Del("Content-Type")
h.Del("Content-Length")
if h.Peek("Etag") != nil {
h.Del("Last-Modified")
}
h.SetStatusCode(http.StatusNotModified)
return nil
}
}
if contentlen != -1 {
r.Original.Response.Header.Set("Content-Length", strconv.FormatInt(contentlen, 10))
}
if _, err := io.Copy(r.Writer, reader); err != nil {
r.Original.Response.Header.SetStatusCode(http.StatusInternalServerError)
return err
} else {
r.Original.Response.Header.SetStatusCode(http.StatusOK)
}
return nil
}
func (r *FastHttpResponse) Destroy() {
if c, ok := r.Writer.(io.Closer); ok {
c.Close()
}
r.Original = nil
r.Writer = nil
}
func (r *FastHttpResponse) SetContext(w *fasthttp.RequestCtx) {
r.Original = w
r.Writer = w.Response.BodyWriter()
}
func (r *FastHttpResponse) SetWriter(writer io.Writer) {
r.Writer = writer
}
func (r *FastHttpHeader) SetCookie(cookie string) {
if r.isResponse {
r.Source.(*FastHttpResponse).Original.Response.Header.Add("Set-Cookie", cookie)
}
}
func (r *FastHttpHeader) GetCookie(key string) (value revel.ServerCookie, err error) {
if !r.isResponse {
var cookie []byte
if cookie = r.Source.(*FastHttpRequest).Original.Request.Header.Cookie(key); cookie != nil {
value = FastHttpCookie(cookie)
} else {
err = http.ErrNoCookie
}
}
return
}
func (r *FastHttpHeader) Set(key string, value string) {
if r.isResponse {
r.Source.(*FastHttpResponse).Original.Response.Header.Set(key, value)
}
}
func (r *FastHttpHeader) Add(key string, value string) {
if r.isResponse {
r.Source.(*FastHttpResponse).Original.Response.Header.Add(key, value)
}
}
func (r *FastHttpHeader) Del(key string) {
if r.isResponse {
r.Source.(*FastHttpResponse).Original.Response.Header.Del(key)
}
}
func (r *FastHttpHeader) Get(key string) (value []string) {
if !r.isResponse {
value = strings.Split(string(r.Source.(*FastHttpRequest).Original.Request.Header.Peek(key)), ",")
} else {
value = strings.Split(string(r.Source.(*FastHttpResponse).Original.Response.Header.Peek(key)), ",")
}
return
}
func (r *FastHttpHeader) SetStatus(statusCode int) {
if r.isResponse {
r.Source.(*FastHttpResponse).Original.Response.SetStatusCode(statusCode)
}
}
func (r FastHttpCookie) GetValue() string {
return string(r)
}
func (f *FastHttpMultipartForm) GetFiles() map[string][]*multipart.FileHeader {
return f.Form.File
}
func (f *FastHttpMultipartForm) GetValues() url.Values {
return url.Values(f.Form.Value)
}
func (f *FastHttpMultipartForm) RemoveAll() error {
return f.Form.RemoveAll()
}
+3
View File
@@ -0,0 +1,3 @@
package fasthttp
// Required for vendoring see golang.org/issue/13832
+15
View File
@@ -0,0 +1,15 @@
# NewRelic
This module is the for [NewRelic](http://newrelic.com) it wraps the Go
http server and inserts a filter to track each requests.
###App.conf
- **server.engine** You must set this to `newrelic` in order to use this server engine
- **server.newrelic.license** The license key for NewRelic
- **server.newrelic.addfilter** Defauls `true` When true a filter is inserted
into `revel.Filters` at position 2. This filter logs every request.
###Other Notes
There is a controller called RelicController which contains a single method
to return a `newrelic.Application` object. If you want to access this object
you can extend that class.
@@ -0,0 +1,18 @@
package controllers
import (
"github.com/newrelic/go-agent"
"github.com/revel/modules/server-engine/newrelic"
"github.com/revel/revel"
)
type RelicController struct {
*revel.Controller
}
func (r *RelicController) GetRelicApplication() newrelic.Application {
if app, ok := revel.CurrentEngine.(*revelnewrelic.ServerNewRelic); ok {
return app.NewRelicApp
}
return nil
}
+3
View File
@@ -0,0 +1,3 @@
package revelnewrelic
// Required for vendoring see golang.org/issue/13832
@@ -0,0 +1,92 @@
package revelnewrelic
import (
"github.com/newrelic/go-agent"
"github.com/revel/revel"
)
type ServerNewRelic struct {
NewRelicConfig *newrelic.Config
NewRelicApp newrelic.Application
revel.GoHttpServer
}
var (
serverLog = revel.AppLog
)
func init() {
revel.RegisterServerEngine("newrelic", func() revel.ServerEngine {
nr := &ServerNewRelic{
GoHttpServer: revel.GoHttpServer{},
}
config := newrelic.NewConfig("Unknown Application", " Unknown Key ")
nr.NewRelicConfig = &config
return nr
})
revel.RegisterModuleInit(func(m *revel.Module) {
serverLog = m.Log
})
}
func (g *ServerNewRelic) Init(init *revel.EngineInit) {
g.GoHttpServer.Init(init)
}
func (nr *ServerNewRelic) Event(event int, args interface{}) {
switch event {
case revel.ENGINE_BEFORE_INITIALIZED:
nr.NewRelicConfig.AppName = revel.Config.StringDefault("app.name", "Uknown App")
license := revel.Config.StringDefault("server.newrelic.license", "")
if license != "" {
nr.NewRelicConfig.License = license
serverLog.Debug("Assigned NewRelic license")
} else {
serverLog.Error("Newrelic license key not assigned, configuration missing 'server.newrelic.license'")
}
addfilter := revel.Config.BoolDefault("server.newrelic.addfilter", true)
if addfilter {
// Inject filter after the router (Normally position 2)
revel.Filters = append(revel.Filters, NewRelicFilter)
copy(revel.Filters[3:], revel.Filters[2:])
revel.Filters[2] = NewRelicFilter
serverLog.Debug("Newrelic filter injected")
}
case revel.ENGINE_STARTED:
// Check to see if configuration is set
// create the application interface
app, err := newrelic.NewApplication(*nr.NewRelicConfig)
if err != nil {
serverLog.Panic("Failed to start NewRelic:", "error", err)
}
nr.NewRelicApp = app
}
nr.GoHttpServer.Event(event, args)
}
func (nr *ServerNewRelic) Name() string {
return "newrelic"
}
func (nr *ServerNewRelic) Engine() interface{} {
return nr
}
// This is a simplistic example of setting up a filter to record all events for the
// webserver as transactions
func NewRelicFilter(c *revel.Controller, fc []revel.Filter) {
if nr, ok := revel.CurrentEngine.Engine().(*ServerNewRelic); ok {
if nr.NewRelicApp != nil {
txn := nr.NewRelicApp.StartTransaction(c.Action,
c.Response.Out.Server.(*revel.GoResponse).Original,
c.Request.In.(*revel.GoRequest).Original)
defer txn.End()
} else {
serverLog.Error("Newrelic application not initialized before filter called")
}
}
fc[0](c, fc[1:])
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package controllers
import (
"os"
fpath "path/filepath"
"strings"
"syscall"
"github.com/revel/revel"
)
// Static file serving controller
type Static struct {
*revel.Controller
}
// Serve method handles requests for files. The supplied prefix may be absolute
// or relative. If the prefix is relative it is assumed to be relative to the
// application directory. The filepath may either be just a file or an
// additional filepath to search for the given file. This response may return
// the following responses in the event of an error or invalid request;
// 403(Forbidden): If the prefix filepath combination results in a directory.
// 404(Not found): If the prefix and filepath combination results in a non-existent file.
// 500(Internal Server Error): There are a few edge cases that would likely indicate some configuration error outside of revel.
//
// Note that when defining routes in routes/conf the parameters must not have
// spaces around the comma.
// Bad: Static.Serve("public/img", "favicon.png")
// Good: Static.Serve("public/img","favicon.png")
//
// Examples:
// Serving a directory
// Route (conf/routes):
// GET /public/{<.*>filepath} Static.Serve("public")
// Request:
// public/js/sessvars.js
// Calls
// Static.Serve("public","js/sessvars.js")
//
// Serving a file
// Route (conf/routes):
// GET /favicon.ico Static.Serve("public/img","favicon.png")
// Request:
// favicon.ico
// Calls:
// Static.Serve("public/img", "favicon.png")
func (c Static) Serve(prefix, filepath string) revel.Result {
// Fix for #503.
prefix = c.Params.Fixed.Get("prefix")
if prefix == "" {
return c.NotFound("")
}
return serve(c, prefix, filepath)
}
// ServeModule method allows modules to serve binary files. The parameters are the same
// as Static.Serve with the additional module name pre-pended to the list of
// arguments.
func (c Static) ServeModule(moduleName, prefix, filepath string) revel.Result {
// Fix for #503.
prefix = c.Params.Fixed.Get("prefix")
if prefix == "" {
return c.NotFound("")
}
var basePath string
for _, module := range revel.Modules {
if module.Name == moduleName {
basePath = module.Path
}
}
absPath := fpath.Join(basePath, fpath.FromSlash(prefix))
return serve(c, absPath, filepath)
}
// This method allows static serving of application files in a verified manner.
func serve(c Static, prefix, filepath string) revel.Result {
var basePath string
if !fpath.IsAbs(prefix) {
basePath = revel.BasePath
}
basePathPrefix := fpath.Join(basePath, fpath.FromSlash(prefix))
fname := fpath.Join(basePathPrefix, fpath.FromSlash(filepath))
// Verify the request file path is within the application's scope of access
if !strings.HasPrefix(fname, basePathPrefix) {
c.Log.Warn("Attempted to read file outside of base path", "file", fname)
return c.NotFound("")
}
// Verify file path is accessible
finfo, err := os.Stat(fname)
if err != nil {
if os.IsNotExist(err) || err.(*os.PathError).Err == syscall.ENOTDIR {
c.Log.Warn("File not found ", "file", fname, "error", err)
return c.NotFound("File not found")
}
c.Log.Error("Error trying to get fileinfo for", "file", fname, "error", err)
return c.RenderError(err)
}
// Disallow directory listing
if finfo.Mode().IsDir() {
c.Log.Warn("Attempted directory listing of ", "dir", fname)
return c.Forbidden("Directory listing not allowed")
}
// Open request file path
file, err := os.Open(fname)
if err != nil {
if os.IsNotExist(err) {
c.Log.Warn("File not found", "file", fname, "error", err)
return c.NotFound("File not found")
}
c.Log.Error("Error opening", "file", fname, "error", err)
return c.RenderError(err)
}
return c.RenderFile(file, revel.Inline)
}
+3
View File
@@ -0,0 +1,3 @@
package static
// Required for vendoring see golang.org/issue/13832
+59
View File
@@ -0,0 +1,59 @@
# Ace
The [ACE](https://github.com/yosssi/ace) Templating Plugin
- Ace templates have full access to the revel.TemplateFuncs, any function
defined in there can be used by the this engine
- Ace files must be identified by using a `shebang` on the first line
(preferred method) or changing the file extension to home.ace.html.
By default the operation of revel assumes just a `.html` extension for
controller responses so it would make more sense to use the shebang
- Ace templates can be set to be case sensitive by setting
`ace.tempate.caseinsensitive=false`, default is not case sensitive. If case sensitivity
is off internal imports must be done using lower case
- All function registered in `revel.TemplateFuncs` are available for use
inside the ace framework
##### Details
Ace is a little different of a templating system, its output is a
standard go template but there is no concept of template sets,
instead you build a composite template using
a *base* template and an *inner* template. The
*inner* template can only contain items like :
```
= content main
h2 Inner Template - Main : {{.Msg}}
= content sub
h3 Inner Template - Sub : {{.Msg}}
```
The base template can contain items like
```
= doctype html
html lang=en
head
meta charset=utf-8
title Ace example
= css
h1 { color: blue; }
body
h1 Base Template : {{.Msg}}
#container.wrapper
= yield main
= yield sub
= include inc .Msg
= javascript
alert('{{.Msg}}');
```
You are allowed to include one *inner* template with the base template,
to do so in revel you can extend your controller from the ace controller
and call `RenderAceTemplate(base ,inner string)` which will insert
the inner template using the outer template.
The ace engine requires that you explicitly set the template type on the
template itself by either using the shebang method on the first line
like `#! ace` or having the file name like `template.ace.html`
either method will work.
+3
View File
@@ -0,0 +1,3 @@
package ace
// Required for vendoring see golang.org/issue/13832
@@ -0,0 +1,13 @@
package controllers
import "github.com/revel/revel"
type AceController struct {
*revel.Controller
}
// Called to render the ace template inner
func (c *AceController) RenderAceTemplate(base, inner string) revel.Result {
c.ViewArgs["ace_inner"] = inner
return c.RenderTemplate(base)
}
+139
View File
@@ -0,0 +1,139 @@
package app
import (
"fmt"
"github.com/revel/revel"
"github.com/yosssi/ace"
"html/template"
"io"
"strings"
)
const ACE_TEMPLATE = "ace"
// Adapter for Go Templates.
type AceTemplate struct {
*template.Template
engine *AceEngine
*revel.TemplateView
File *ace.File
Inner *ace.File
}
// A bit trick of an implementation
// If the arg contains an ace_inner field then that will be used
// to fetch a new template
func (acetmpl AceTemplate) Render(wr io.Writer, arg interface{}) error {
// We can redirect this render to another template if the arguments contain ace_content in them
if argmap, ok := arg.(map[string]interface{}); ok {
if acecontentraw, ok := argmap["ace-inner"]; ok {
acecontent := acecontentraw.(string)
newtemplatename := acetmpl.TemplateName + "-" + acecontent
// Now lookup the template again
if _, ok := acetmpl.engine.templatesByName[newtemplatename]; !ok {
if inner, ok := acetmpl.engine.templatesByName[acecontent]; !ok {
return fmt.Errorf("Inner content %s not found in ace templates", acecontent)
} else {
acetmpl.engine.templatesByName[newtemplatename] = &AceTemplate{
File: acetmpl.File,
Inner: inner.File,
engine: acetmpl.engine,
TemplateView: acetmpl.TemplateView}
}
}
return acetmpl.engine.templatesByName[newtemplatename].renderInternal(wr, arg)
}
}
return acetmpl.renderInternal(wr, arg)
}
func (acetmpl AceTemplate) renderInternal(wr io.Writer, arg interface{}) error {
if acetmpl.Template == nil {
// Compile the template first
if acetmpl.Inner == nil {
acetmpl.Inner = ace.NewFile("", nil)
}
source := ace.NewSource(acetmpl.File, acetmpl.Inner, acetmpl.engine.files)
result, err := ace.ParseSource(source, acetmpl.engine.Options)
if err != nil {
return err
}
if gtemplate, err := ace.CompileResult(acetmpl.TemplateName, result, acetmpl.engine.Options); err != nil {
return err
} else {
acetmpl.Template = gtemplate
}
}
return acetmpl.Execute(wr, arg)
}
type AceEngine struct {
loader *revel.TemplateLoader
templatesByName map[string]*AceTemplate
files []*ace.File
Options *ace.Options
CaseInsensitive bool
}
func (i *AceEngine) ConvertPath(path string) string {
if i.CaseInsensitive {
return strings.ToLower(path)
}
return path
}
func (i *AceEngine) Handles(templateView *revel.TemplateView) bool {
return revel.EngineHandles(i, templateView)
}
func (engine *AceEngine) ParseAndAdd(baseTemplate *revel.TemplateView) error {
// Ace templates must only render views specified for it (no trial and error)
if baseTemplate.EngineType != ACE_TEMPLATE {
return &revel.Error{
Title: "Template Compilation Error",
Path: baseTemplate.FilePath,
Description: "Not correct template for engine",
Line: 1,
SourceLines: baseTemplate.Content(),
}
}
baseTemplate.TemplateName = engine.ConvertPath(baseTemplate.TemplateName)
file := ace.NewFile(baseTemplate.TemplateName, baseTemplate.FileBytes)
engine.files = append(engine.files, file)
engine.templatesByName[baseTemplate.TemplateName] = &AceTemplate{File: file, engine: engine, TemplateView: baseTemplate}
return nil
}
func (engine *AceEngine) Lookup(templateName string) revel.Template {
if tpl, found := engine.templatesByName[engine.ConvertPath(templateName)]; found {
return tpl
}
return nil
}
func (engine *AceEngine) Name() string {
return ACE_TEMPLATE
}
func (engine *AceEngine) Event(action int, i interface{}) {
if action == revel.TEMPLATE_REFRESH_REQUESTED {
engine.templatesByName = map[string]*AceTemplate{}
engine.CaseInsensitive = revel.Config.BoolDefault("ace.template.caseinsensitive", true)
}
}
func init() {
revel.RegisterTemplateLoader(ACE_TEMPLATE, func(loader *revel.TemplateLoader) (revel.TemplateEngine, error) {
return &AceEngine{
loader: loader,
templatesByName: map[string]*AceTemplate{},
Options: &ace.Options{FuncMap: revel.TemplateFuncs},
}, nil
})
}
@@ -0,0 +1,9 @@
= doctype html
html lang=en
head
meta charset=utf-8
title Forbidden
body
h1 Base Template : {{.Error.Title}}
p : {{.Error.Description}}
@@ -0,0 +1,4 @@
{
"title": "{{Error.Title|escapejs}}",
"description": "{{Error.Description|escapejs}}"
}
@@ -0,0 +1,3 @@
{{.Error.Title}}
{{.Error.Description}}
+23
View File
@@ -0,0 +1,23 @@
package tests
import (
"github.com/revel/revel/testing"
)
type ApplicationTest struct {
testing.TestSuite
}
func (t *ApplicationTest) Before() {
println("Set up")
}
func (t *ApplicationTest) TestThatIndexPageWorks() {
t.Get("/")
t.AssertOk()
t.AssertContentType("text/html; charset=utf-8")
}
func (t *ApplicationTest) After() {
println("Tear down")
}
+149
View File
@@ -0,0 +1,149 @@
# Pongo2
The [PONGO2](https://github.com/flosch/pongo2) Templating Plugin
- Pongo2 templates may be identified by a `shebang` on the first line
(preferred method) or changing the file extension to home.pongo2.html.
By default the operation of revel assumes just a `.html` extension for
controller responses so it would make more sense to use the shebang
- Pongo2 templates can be set to be case sensitive by setting
`pongo2.tempate.caseinsensitive=false`, default is not case sensitive. If case sensitivity is
off the templates are compiled using lower case
- Currently the only functions built in are as follows:
- field
- radio
- option
- url
- checkbox
- append
Samples implementation below
##### Details
pongo2 is the successor of [pongo](https://github.com/flosch/pongo), a Django-syntax like templating-language.
Install/update using `go get` (no dependencies required by pongo2):
```
go get -u github.com/flosch/pongo2
```
Please use the [issue tracker](https://github.com/flosch/pongo2/issues) if you're encountering any problems with pongo2 or if you need help with implementing tags or filters ([create a ticket!](https://github.com/flosch/pongo2/issues/new)). If possible, please use [playground](https://www.florian-schlachter.de/pongo2/) to create a short test case on what's wrong and include the link to the snippet in your issue.
**New**: [Try pongo2 out in the pongo2 playground.](https://www.florian-schlachter.de/pongo2/)
```
{%append "moreStyles" "ui-lightness/jquery-ui-1.7.2.custom.css"%}
{%append "moreScripts" "js/jquery-ui-1.7.2.custom.min.js"%}
{% include "header.html" %}
<h1>Book hotel</h1>
<form method="POST" action="{%url "Hotels.Book" hotel.HotelId%}">
<p>
<strong>Name:</strong> {{hotel.Name}}
</p>
<p>
<strong>Address:</strong> {{hotel.Address}}
</p>
<p>
<strong>City:</strong> {{hotel.City}}
</p>
<p>
<strong>State:</strong> {{hotel.State}}
</p>
<p>
<strong>Zip:</strong> {{hotel.Zip}}
</p>
<p>
<strong>Country:</strong> {{hotel.Country}}
</p>
<p>
<strong>Nightly rate:</strong> {{hotel.Price}}
</p>
{%with field = "booking.CheckInDate"|field %}
<p class="{{field.ErrorClass}}">
<strong>Check In Date:</strong>
<input type="text" size="10" name="{{field.Name}}" class="datepicker" value="{{field.Flash}}">
* <span class="error">{{field.Error}}</span>
</p>
{% endwith %}
{%with field = "booking.CheckOutDate"|field %}
<p class="{{field.ErrorClass}}">
<strong>Check Out Date:</strong>
<input type="text" size="10" name="{{field.Name}}" class="datepicker" value="{{field.Flash}}">
* <span class="error">{{field.Error}}</span>
</p>
{% endwith %}
<p>
<strong>Room preference:</strong>
{%with field = "booking.Beds"|field %}
<select name="{{field.Name}}">
{%option field "1" "One king-size bed"%}
{%option field "2" "Two double beds"%}
{%option field "3" "Three beds"%}
</select>
{% endwith %}
</p>
<p>
<strong>Smoking preference:</strong>
{%with field = "booking.Smoking"|field %}
{%radio field "true"%} Smoking
{%radio field "false"%} Non smoking
{% endwith %}
</p>
{%with field = "booking.CardNumber"|field %}
<p class="{{field.ErrorClass}}">
<strong>Credit Card #:</strong>
<input type="text" name="{{field.Name}}" size="16" value="{{field.Flash}}">
* <span class="error">{{field.Error}}</span>
</p>
{% endwith %}
{%with field = "booking.NameOnCard"|field %}
<p class="{{field.ErrorClass}}">
<strong>Credit Card Name:</strong>
<input type="text" name="{{field.Name}}" size="16" value="{{field.Flash}}">
* <span class="error">{{field.Error}}</span>
</p>
{% endwith %}
<p>
<strong>Credit Card Expiry:</strong>
{%with field = "booking.CardExpMonth"|field %}
<select name="{{field.Name}}">
{%option field "1" "Jan"%}
{%option field "2" "Feb"%}
{%option field "3" "Mar"%}
{%option field "4" "Apr"%}
{%option field "5" "May"%}
{%option field "6" "Jun"%}
{%option field "7" "Jul"%}
{%option field "8" "Aug"%}
{%option field "9" "Sep"%}
{%option field "10" "Oct"%}
{%option field "11" "Nov"%}
{%option field "12" "Dec"%}
</select>
{% endwith %}
{%with field = "booking.CardExpYear"|field %}
<select name="{{field.Name}}">
{%option field "2008" "2008"%}
{%option field "2009" "2009"%}
{%option field "2010" "2010"%}
{%option field "2011" "2011"%}
{%option field "2012" "2012"%}
</select>
{% endwith %}
</p>
<p class="buttons">
<input type="submit" value="Proceed">
<a href="{%url "Hotels.Show" hotel.HotelId %}">Cancel</a>
</p>
</form>
<script type="text/javascript" charset="utf-8">
$(function() {
$(".datepicker").datepicker({dateFormat: 'yy-mm-dd'});
});
</script>
{% include "footer.html" %}
``
@@ -0,0 +1,16 @@
package pongo2
import (
p2 "github.com/flosch/pongo2"
"github.com/revel/revel"
)
func init() {
p2.RegisterFilter("field", func(in *p2.Value, param *p2.Value) (out *p2.Value, err *p2.Error) {
if nil == in.Interface() || in.String() == "" {
return nil, &p2.Error{Sender: "field argument must is string"}
}
newField := revel.NewField(in.String(), getContext())
return p2.AsValue(newField), nil
})
}
+184
View File
@@ -0,0 +1,184 @@
package pongo2
import (
"io"
"strings"
p2 "github.com/flosch/pongo2"
"github.com/revel/revel"
"github.com/tylerb/gls"
)
// Adapter for HAML Templates.
type PongoTemplate struct {
template *p2.Template
engine *PongoEngine
*revel.TemplateView
}
type Pongo2BaseTag struct {
field string
}
func (node *Pongo2BaseTag) GetField(ctx *p2.ExecutionContext) (value interface{}, found bool) {
value, found = ctx.Public[node.field]
if !found {
value, found = ctx.Private[node.field]
}
if found {
if wrapped, ok := value.(*p2.Value); ok {
value = wrapped.Interface()
}
}
return
}
type INodeImplied struct {
Exec func(*p2.ExecutionContext, p2.TemplateWriter) *p2.Error
}
func (i *INodeImplied) Execute(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
return i.Exec(ctx, w)
}
func (tmpl PongoTemplate) Name() string {
return tmpl.TemplateName
}
func getContext() map[string]interface{} {
return gls.Get("data").(map[string]interface{})
}
// return a 'revel.Template' from HAML's template.
func (tmpl PongoTemplate) Render(wr io.Writer, arg interface{}) (err error) {
gls.With(gls.Values(map[interface{}]interface{}{"data": arg}), func() {
err = tmpl.template.ExecuteWriter(p2.Context(arg.(map[string]interface{})), wr)
if nil != err {
if e, ok := err.(*p2.Error); ok {
rerr := &revel.Error{
Title: "Template Execution Error",
Path: tmpl.TemplateName,
Description: e.Error(),
Line: e.Line,
}
if revel.DevMode {
rerr.SourceLines = tmpl.Content()
}
err = rerr
}
}
})
return err
}
// There is only a single instance of the PongoEngine initialized
type PongoEngine struct {
loader *revel.TemplateLoader
templateSetBybasePath map[string]*p2.TemplateSet
templates map[string]*PongoTemplate
CaseInsensitive bool
}
func (i *PongoEngine) ConvertPath(path string) string {
if i.CaseInsensitive {
return strings.ToLower(path)
}
return path
}
func (i *PongoEngine) Handles(templateView *revel.TemplateView) bool {
return revel.EngineHandles(i, templateView)
}
func (engine *PongoEngine) ParseAndAdd(baseTemplate *revel.TemplateView) error {
templateSet := engine.templateSetBybasePath[baseTemplate.BasePath]
if nil == templateSet {
templateSet = p2.NewSet(baseTemplate.BasePath, p2.MustNewLocalFileSystemLoader(baseTemplate.BasePath))
engine.templateSetBybasePath[baseTemplate.BasePath] = templateSet
}
tpl, err := templateSet.FromBytes(baseTemplate.FileBytes)
if nil != err {
_, line, description := parsePongo2Error(err)
return &revel.Error{
Title: "Template Compilation Error",
Path: baseTemplate.FilePath,
Description: description,
Line: line,
SourceLines: strings.Split(string(baseTemplate.FileBytes), "\n"),
}
}
baseTemplate.TemplateName = engine.ConvertPath(baseTemplate.TemplateName)
engine.templates[baseTemplate.TemplateName] = &PongoTemplate{
template: tpl,
engine: engine,
TemplateView: baseTemplate}
return nil
}
func (engine *PongoEngine) Name() string {
return "pongo2"
}
func parsePongo2Error(err error) (templateName string, line int, description string) {
pongoError := err.(*p2.Error)
if nil != pongoError {
return pongoError.Filename, pongoError.Line, pongoError.Error()
}
return "Unknown error", 0, err.Error()
}
func (engine *PongoEngine) Lookup(templateName string) revel.Template {
tpl, found := engine.templates[engine.ConvertPath(templateName)]
if !found {
return nil
}
return tpl
}
func (engine *PongoEngine) Event(action int, i interface{}) {
if action == revel.TEMPLATE_REFRESH_REQUESTED {
// At this point all the templates have been passed into the
engine.templateSetBybasePath = map[string]*p2.TemplateSet{}
engine.templates = map[string]*PongoTemplate{}
engine.CaseInsensitive = revel.Config.BoolDefault("pongo2.template.caseinsensitive", true)
}
}
func init() {
revel.RegisterTemplateLoader("pongo2", func(loader *revel.TemplateLoader) (revel.TemplateEngine, error) {
return &PongoEngine{
loader: loader,
templateSetBybasePath: map[string]*p2.TemplateSet{},
templates: map[string]*PongoTemplate{},
}, nil
})
/*
// TODO Dynamically call all the built in functions, PR welcome
for key,templateFunction := range revel.TemplateFuncs {
p2.RegisterTag(key, func(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
evals := []p2.IEvaluator{}
for arguments.Remaining() > 0 {
expr, err := arguments.ParseExpression()
evals = append(evals, expr)
if err != nil {
return nil, err
}
}
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext,w p2.TemplateWriter) *p2.Error {
args := make([]interface{}, len(evals))
for i, ev := range evals {
obj, err := ev.Evaluate(ctx)
if err != nil {
return err
}
args[i] = obj
}
v:= &tagURLForNode{evals}
reflect.MakeFunc ....
return v.Execute(ctx,w)
}}, nil
})
}
*/
}
@@ -0,0 +1,87 @@
package pongo2
import (
p2 "github.com/flosch/pongo2"
"reflect"
)
type tagAppendNode struct {
isPublic bool
name string
objectEvaluators []p2.IEvaluator
}
func (node *tagAppendNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
var values []interface{}
var reflectValues reflect.Value
found := false
if o, found := ctx.Public[node.name]; found && nil != o {
values, _ = o.([]interface{})
if nil == values {
reflectValues = reflect.ValueOf(o)
if reflectValues.Kind() == reflect.Ptr {
reflectValues = reflectValues.Elem()
}
if reflectValues.Kind() != reflect.Slice {
return &p2.Error{Sender: "'" + node.name + "' isn't a slice."}
}
}
}
for _, ev := range node.objectEvaluators {
obj, err := ev.Evaluate(ctx)
if err != nil {
return err
}
if !found || reflectValues.IsNil() {
values = append(values, obj)
} else {
reflectValues = reflect.AppendSlice(reflectValues, reflect.ValueOf(obj))
}
}
if !found || reflectValues.IsNil() {
ctx.Public[node.name] = values
} else {
ctx.Public[node.name] = reflectValues.Interface()
}
return nil
}
// tagURLForParser implements a {% urlfor %} tag.
//
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
func tagAppendParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
var name string
var isPublic bool
// Parse variable name
typeToken := arguments.MatchType(p2.TokenIdentifier)
if typeToken != nil {
name = typeToken.Val
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
name = sToken.Val
} else {
return nil, arguments.Error("Expected an identifier or string.", nil)
}
evals := []p2.IEvaluator{}
for arguments.Remaining() > 0 {
expr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
evals = append(evals, expr)
}
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
node := &tagAppendNode{isPublic, name, evals}
return node.Execute(ctx, w)
}}, nil
}
func init() {
p2.RegisterTag("append", tagAppendParser)
}
@@ -0,0 +1,70 @@
package pongo2
import (
"fmt"
"html"
p2 "github.com/flosch/pongo2"
"github.com/revel/revel"
)
type tagCheckboxNode struct {
value p2.IEvaluator
Pongo2BaseTag
}
func (node *tagCheckboxNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
fieldObj, _ := node.GetField(ctx)
if nil == fieldObj {
return ctx.Error("field '"+node.field+"' tagCheckboxNode is missing.", nil)
}
field, _ := fieldObj.(*revel.Field)
if nil == field {
return ctx.Error(fmt.Sprintf("field '"+node.field+"' isn't Field - %T.", fieldObj), nil)
}
val, err := node.value.Evaluate(ctx)
if err != nil {
return err
}
val_str := val.String()
checked := ""
if field.Flash() == val_str {
checked = " checked"
}
fmt.Fprintf(writer, `<input type="checkbox" name="%s" value="%s"%s>`,
html.EscapeString(field.Name), html.EscapeString(val_str), checked)
return nil
}
// tagURLForParser implements a {% urlfor %} tag.
//
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
func tagCheckboxParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
var field string
typeToken := arguments.MatchType(p2.TokenIdentifier)
if typeToken != nil {
field = typeToken.Val
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
field = sToken.Val
} else {
return nil, arguments.Error("Expected an identifier or string.", nil)
}
expr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
v := tagCheckboxNode{Pongo2BaseTag: Pongo2BaseTag{field: field},
value: expr}
return v.Execute(ctx, w)
}}, nil
}
func init() {
p2.RegisterTag("checkbox", tagCheckboxParser)
}
@@ -0,0 +1,79 @@
package pongo2
import (
"fmt"
"html"
p2 "github.com/flosch/pongo2"
"github.com/revel/revel"
)
type tagOptionNode struct {
value p2.IEvaluator
label string
Pongo2BaseTag
}
func (node *tagOptionNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
fieldObj, _ := node.GetField(ctx)
if nil == fieldObj {
return ctx.Error("field '"+node.field+"' tagOptionNode is missing.", nil)
}
field, _ := fieldObj.(*revel.Field)
if nil == field {
return ctx.Error(fmt.Sprintf("field '"+node.field+"' isn't Field - %T.", fieldObj), nil)
}
val, err := node.value.Evaluate(ctx)
if err != nil {
return err
}
val_str := val.String()
selected := ""
if field.Flash() == val_str || (field.Flash() == "" && field.Value() == val_str) {
selected = " selected"
}
fmt.Fprintf(writer, `<option value="%s"%s>%s</option>`,
html.EscapeString(val_str), selected, html.EscapeString(node.label))
return nil
}
// tagURLForParser implements a {% urlfor %} tag.
//
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
func tagOptionParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
var field string
typeToken := arguments.MatchType(p2.TokenIdentifier)
if typeToken != nil {
field = typeToken.Val
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
field = sToken.Val
} else {
return nil, arguments.Error("Expected an identifier or string.", nil)
}
expr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
var v *tagOptionNode
if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
v = &tagOptionNode{Pongo2BaseTag: Pongo2BaseTag{field: field},
value: expr,
label: sToken.Val}
} else {
return nil, arguments.Error("Expected an string.", nil)
}
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
return v.Execute(ctx, w)
}}, nil
}
func init() {
p2.RegisterTag("option", tagOptionParser)
}
@@ -0,0 +1,69 @@
package pongo2
import (
"fmt"
"html"
p2 "github.com/flosch/pongo2"
"github.com/revel/revel"
)
type tagRadioNode struct {
value p2.IEvaluator
Pongo2BaseTag
}
func (node *tagRadioNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
fieldObj, _ := node.GetField(ctx)
if nil == fieldObj {
return ctx.Error("field '"+node.field+"' tagRadioNode is missing.", nil)
}
field, _ := fieldObj.(*revel.Field)
if nil == field {
return ctx.Error(fmt.Sprintf("field '"+node.field+"' isn't Field - %T.", fieldObj), nil)
}
val, err := node.value.Evaluate(ctx)
if err != nil {
return err
}
val_str := val.String()
checked := ""
if field.Flash() == val_str {
checked = " checked"
}
fmt.Fprintf(writer, `<input type="radio" name="%s" value="%s"%s>`,
html.EscapeString(field.Name), html.EscapeString(val_str), checked)
return nil
}
// tagURLForParser implements a {% urlfor %} tag.
//
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
func tagRadioParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
var field string
typeToken := arguments.MatchType(p2.TokenIdentifier)
if typeToken != nil {
field = typeToken.Val
} else if sToken := arguments.MatchType(p2.TokenString); nil != sToken {
field = sToken.Val
} else {
return nil, arguments.Error("Expected an identifier or string.", nil)
}
expr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
v := &tagRadioNode{Pongo2BaseTag: Pongo2BaseTag{field: field},
value: expr}
return v.Execute(ctx, w)
}}, nil
}
func init() {
p2.RegisterTag("radio", tagRadioParser)
}
+64
View File
@@ -0,0 +1,64 @@
package pongo2
import (
"github.com/revel/revel"
p2 "github.com/flosch/pongo2"
)
type tagURLForNode struct {
objectEvaluators []p2.IEvaluator
}
func (node *tagURLForNode) Execute(ctx *p2.ExecutionContext, writer p2.TemplateWriter) *p2.Error {
args := make([]string, len(node.objectEvaluators))
for i, ev := range node.objectEvaluators {
obj, err := ev.Evaluate(ctx)
if err != nil {
return err
}
args[i] = obj.String()
}
params := make([]interface{}, len(args))
params[0] = args[0]
for i := range params[1:] {
params[i+1] = args[i+1]
}
url, err := revel.ReverseURL(params...)
if nil != err {
return ctx.Error(err.Error(), nil)
}
writer.WriteString(string(url))
return nil
}
// tagURLForParser implements a {% urlfor %} tag.
//
// urlfor takes one argument for the controller, as well as any number of key/value pairs for additional URL data.
// Example: {% urlfor "UserController.View" ":slug" "oal" %}
func tagURLForParser(doc *p2.Parser, start *p2.Token, arguments *p2.Parser) (p2.INodeTag, *p2.Error) {
evals := []p2.IEvaluator{}
for arguments.Remaining() > 0 {
expr, err := arguments.ParseExpression()
evals = append(evals, expr)
if err != nil {
return nil, err
}
}
if len(evals) <= 0 {
return nil, arguments.Error("URL takes one argument for the controller and any number of optional value.", nil)
}
return &INodeImplied{Exec: func(ctx *p2.ExecutionContext, w p2.TemplateWriter) *p2.Error {
v := &tagURLForNode{evals}
return v.Execute(ctx, w)
}}, nil
}
func init() {
p2.RegisterTag("url", tagURLForParser)
}
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Forbidden</title>
</head>
<body>
<h1>
{{Error.Title}}
</h1>
<p>
{{Error.Description}}
</p>
{{end}}
</body>
</html>
@@ -0,0 +1,4 @@
{
"title": "{{Error.Title|escapejs}}",
"description": "{{Error.Description|escapejs}}"
}
@@ -0,0 +1,3 @@
{{Error.Title}}
{{Error.Description}}
@@ -0,0 +1 @@
<forbidden>{{Error.Description}}</forbidden>
@@ -0,0 +1,61 @@
<style type="text/css">
html, body {
margin: 0;
padding: 0;
font-family: Helvetica, Arial, Sans;
background: #EEEEEE;
}
.block {
padding: 20px;
border-bottom: 1px solid #aaa;
}
#header h1 {
font-weight: normal;
font-size: 28px;
margin: 0;
}
#more {
color: #666;
font-size: 80%;
border: none;
}
#header {
background: #FFFFCC;
}
#header p {
color: #333;
}
#routes {
background: #f6f6f6;
}
#routes h2 {
font-weight: normal;
font-size: 18px;
margin: 0 0 10px 0;
}
#routes ol {
}
#routes li {
font-size: 14px;
font-family: monospace;
color: #333;
}
</style>
<div id="header" class="block">
<h1>
{{Error.Title}}
</h1>
<p>
{{Error.Description}}
</p>
</div>
<div id="routes" class="block">
<h2>These routes have been tried, in this order :</h2>
<ol>
{% for route in Router.Routes %}
<li>{{route.Method}} {{route.Path}}{{route.Action}}</li>
{% endfor %}
</ol>
</div>
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Not found</title>
</head>
<body>
{% if DevMode %}
{% include "errors/404-dev.html" %}
{% else %}
<h1>
{{ Error.Title }}
</h1>
<p>
{{ Error.Description}}
</p>
{% endif %}
</body>
</html>
@@ -0,0 +1,4 @@
{
"title": "{{Error.Title|escapejs}}",
"description": "{{Error.Description|escapejs}}"
}
@@ -0,0 +1,3 @@
{{Error.Title}}
{{Error.Description}}
@@ -0,0 +1 @@
<notfound>{{Error.Description}}</notfound>
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Method not allowed</title>
</head>
<body>
<h1>
{{Error.Title}}
</h1>
<p>
{{Error.Description}}
</p>
</body>
</html>
@@ -0,0 +1,4 @@
{
"title": "{{Error.Title|escapejs}}",
"description": "{{Error.Description|escapejs}}"
}
@@ -0,0 +1,3 @@
{{Error.Title}}
{{Error.Description}}
@@ -0,0 +1 @@
<method-not-allowed>{{Error.Description}}</method-not-allowed>
@@ -0,0 +1,131 @@
<style type="text/css">
html, body {
margin: 0;
padding: 0;
font-family: Helvetica, Arial, Sans;
background: #EEEEEE;
}
.block {
padding: 20px;
border-bottom: 1px solid #aaa;
}
#header h1 {
font-weight: normal;
font-size: 28px;
margin: 0;
}
#more {
color: #666;
font-size: 80%;
border: none;
}
#header {
background: #fcd2da;
}
#header p {
color: #333;
}
#source {
background: #f6f6f6;
}
#source h2 {
font-weight: normal;
font-size: 18px;
margin: 0 0 10px 0;
}
#source .lineNumber {
float: left;
display: block;
width: 40px;
text-align: right;
margin-right: 10px;
font-size: 14px;
font-family: monospace;
background: #333;
color: #fff;
}
#source .line {
clear: both;
color: #333;
margin-bottom: 1px;
}
#source pre {
font-size: 14px;
margin: 0;
overflow-x: hidden;
}
#source .error {
color: #c00 !important;
}
#source .error .lineNumber {
background: #c00;
}
#source a {
text-decoration: none;
}
#source a:hover * {
cursor: pointer !important;
}
#source a:hover pre {
background: #FAFFCF !important;
}
#source em {
font-style: normal;
text-decoration: underline;
font-weight: bold;
}
#source strong {
font-style: normal;
font-weight: bold;
}
#stack {
background: #eee;
padding:0 1em 1em;
}
#stack h3 {
font-weight: normal;
}
#stack code {
font-family:monospace;
white-space: pre;
}
</style>
<div id="header" class="block">
<h1>{{Error.Title}}</h1>
<p>
{% if Error.SourceType %}
The {{Error.SourceType}} <strong>{{Error.Path}}</strong> does not compile: <strong>{{Error.Description}}</strong>
{% else %}
{{Error.Description}}
{% endif %}
</p>
</div>
{% if Error.Path %}
<div id="source" class="block">
<h2>In {{Error.Path}}
{% if Error.Line %}
(around {% if Error.Line %}line {{Error.Line}}{% endif %}{% if Error.Column %} column {{Error.Column}}{% endif %})
{% endif %}
</h2>
{% for src in Error.ContextSource %}
<div class="line{% if src.IsError %} error{% endif %}">
<span class="lineNumber">{{src.Line}}:</span>
<pre>{{src.Source}}</pre>
</div>
{% endfor %}
</div>
{% endif %}
{% if Error.Stack %}
<div id="stack">
<h3>Call Stack</h3>
<code>{{Error.Stack}}</code>
</div>
{% endif %}
{% if Error.MetaError %}
<div id="source" class="block">
<h2>Additionally, an error occurred while handling this error.</h2>
<div class="line error">
{{Error.MetaError}}
</div>
</div>
{% endif %}
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Application error</title>
</head>
<body>
{% if DevMode %}
{% include "errors/500-dev.html" %}
{% else %}
<h1>Oops, an error occured</h1>
<p>
This exception has been logged.
</p>
{% endif %}
</body>
</html>
@@ -0,0 +1,4 @@
{
"title": "{{Error.Title|escapejs}}",
"description": "{{Error.Description|escapejs}}"
}
@@ -0,0 +1,13 @@
{{Error.Title}}
{{Error.Description}}
{% if RunMode == "dev" %}
{% if Error.Path %}
----------
In {{Error.Path}} {% if Error.Line %}(around line {{Error.Line}}){% endif %}
{% for src in Error.ContextSource %}
{% if src.IsError %}>{% else %} {% endif %} {{src.Line}}: {{src.Source}}{% endfor %}
{% endif %}
{% endif %}
@@ -0,0 +1,4 @@
<error>
<title>{{Error.Title}}</title>
<description>{{Error.Description}}</description>
</error>
+3
View File
@@ -0,0 +1,3 @@
package pongo2
// Required for vendoring see golang.org/issue/13832
@@ -0,0 +1,327 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package controllers
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"reflect"
"sort"
"strings"
"github.com/revel/revel"
"github.com/revel/revel/testing"
)
// TestRunner is a controller which is used for running application tests in browser.
type TestRunner struct {
*revel.Controller
}
// TestSuiteDesc is used for storing information about a single test suite.
// This structure is required by revel test cmd.
type TestSuiteDesc struct {
Name string
Tests []TestDesc
// Elem is reflect.Type which can be used for accessing methods
// of the test suite.
Elem reflect.Type
}
// TestDesc is used for describing a single test of some test suite.
// This structure is required by revel test cmd.
type TestDesc struct {
Name string
}
// TestSuiteResult stores the results the whole test suite.
// This structure is required by revel test cmd.
type TestSuiteResult struct {
Name string
Passed bool
Results []TestResult
}
// TestResult represents the results of running a single test of some test suite.
// This structure is required by revel test cmd.
type TestResult struct {
Name string
Passed bool
ErrorHTML template.HTML
ErrorSummary string
}
var (
testSuites []TestSuiteDesc // A list of all available tests.
none = []reflect.Value{} // It is used as input for reflect call in a few places.
// registeredTests simplifies the search of test suites by their name.
// "TestSuite.TestName" is used as a key. Value represents index in testSuites.
registeredTests map[string]int
)
/*
Controller's action methods are below.
*/
// Index is an action which renders the full list of available test suites and their tests.
func (c TestRunner) Index() revel.Result {
c.ViewArgs["suiteFound"] = len(testSuites) > 0
return c.Render(testSuites)
}
// Suite method allows user to navigate to individual Test Suite and their tests
func (c TestRunner) Suite(suite string) revel.Result {
var foundTestSuites []TestSuiteDesc
for _, testSuite := range testSuites {
if strings.EqualFold(testSuite.Name, suite) {
foundTestSuites = append(foundTestSuites, testSuite)
}
}
c.ViewArgs["testSuites"] = foundTestSuites
c.ViewArgs["suiteFound"] = len(foundTestSuites) > 0
c.ViewArgs["suiteName"] = suite
return c.RenderTemplate("TestRunner/Index.html")
}
// Run runs a single test, given by the argument.
func (c TestRunner) Run(suite, test string) revel.Result {
// Check whether requested test exists.
suiteIndex, ok := registeredTests[suite+"."+test]
if !ok {
return c.NotFound("Test %s.%s does not exist", suite, test)
}
result := TestResult{Name: test}
// Found the suite, create a new instance and run the named method.
t := testSuites[suiteIndex].Elem
v := reflect.New(t)
func() {
// When the function stops executing try to recover from panic.
defer func() {
if err := recover(); err != nil {
// If panic error is empty, exit.
panicErr := revel.NewErrorFromPanic(err)
if panicErr == nil {
return
}
// Otherwise, prepare and format the response of server if possible.
testSuite := v.Elem().FieldByName("TestSuite").Interface().(testing.TestSuite)
res := formatResponse(testSuite)
// Render the error and save to the result structure.
var buffer bytes.Buffer
tmpl, _ := revel.MainTemplateLoader.TemplateLang("TestRunner/FailureDetail.html", "")
_ = tmpl.Render(&buffer, map[string]interface{}{
"error": panicErr,
"response": res,
"postfix": suite + "_" + test,
})
result.ErrorSummary = errorSummary(panicErr)
result.ErrorHTML = template.HTML(buffer.String())
}
}()
// Initialize the test suite with a NewTestSuite()
testSuiteInstance := v.Elem().FieldByName("TestSuite")
testSuiteInstance.Set(reflect.ValueOf(testing.NewTestSuite()))
// Make sure After method will be executed at the end.
if m := v.MethodByName("After"); m.IsValid() {
defer m.Call(none)
}
// Start from running Before method of test suite if exists.
if m := v.MethodByName("Before"); m.IsValid() {
m.Call(none)
}
// Start the test method itself.
v.MethodByName(test).Call(none)
// No panic means success.
result.Passed = true
}()
return c.RenderJSON(result)
}
// List returns a JSON list of test suites and tests.
// It is used by revel test command line tool.
func (c TestRunner) List() revel.Result {
return c.RenderJSON(testSuites)
}
/*
Below are helper functions.
*/
// describeSuite expects testsuite interface as input parameter
// and returns its description in a form of TestSuiteDesc structure.
func describeSuite(testSuite interface{}) TestSuiteDesc {
t := reflect.TypeOf(testSuite)
// Get a list of methods of the embedded test type.
// It will be used to make sure the same tests are not included in multiple test suites.
super := t.Elem().Field(0).Type
superMethods := map[string]bool{}
for i := 0; i < super.NumMethod(); i++ {
// Save the current method's name.
superMethods[super.Method(i).Name] = true
}
// Get a list of methods on the test suite that take no parameters, return
// no results, and were not part of the embedded type's method set.
var tests []TestDesc
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
mt := m.Type
// Make sure the test method meets the criterias:
// - method of testSuite without input parameters;
// - nothing is returned;
// - has "Test" prefix;
// - doesn't belong to the embedded structure.
methodWithoutParams := (mt.NumIn() == 1 && mt.In(0) == t)
nothingReturned := (mt.NumOut() == 0)
hasTestPrefix := (strings.HasPrefix(m.Name, "Test"))
if methodWithoutParams && nothingReturned && hasTestPrefix && !superMethods[m.Name] {
// Register the test suite's index so we can quickly find it by test's name later.
registeredTests[t.Elem().Name()+"."+m.Name] = len(testSuites)
// Add test to the list of tests.
tests = append(tests, TestDesc{m.Name})
}
}
return TestSuiteDesc{
Name: t.Elem().Name(),
Tests: tests,
Elem: t.Elem(),
}
}
// errorSummary gets an error and returns its summary in human readable format.
func errorSummary(err *revel.Error) (message string) {
expectedPrefix := "(expected)"
actualPrefix := "(actual)"
errDesc := err.Description
//strip the actual/expected stuff to provide more condensed display.
if strings.Index(errDesc, expectedPrefix) == 0 {
errDesc = errDesc[len(expectedPrefix):]
}
if strings.LastIndex(errDesc, actualPrefix) > 0 {
errDesc = errDesc[0 : len(errDesc)-len(actualPrefix)]
}
errFile := err.Path
slashIdx := strings.LastIndex(errFile, "/")
if slashIdx > 0 {
errFile = errFile[slashIdx+1:]
}
message = fmt.Sprintf("%s %s#%d", errDesc, errFile, err.Line)
/*
// If line of error isn't known return the message as is.
if err.Line == 0 {
return
}
// Otherwise, include info about the line number and the relevant
// source code lines.
message += fmt.Sprintf(" (around line %d): ", err.Line)
for _, line := range err.ContextSource() {
if line.IsError {
message += line.Source
}
}
*/
return
}
// formatResponse gets *revel.TestSuite as input parameter and
// transform response related info into a readable format.
func formatResponse(t testing.TestSuite) map[string]string {
if t.Response == nil {
return map[string]string{}
}
// Since Go 1.6 http.Request struct contains `Cancel <-chan struct{}` which
// results in `json: unsupported type: <-chan struct {}`
// So pull out required things for Request and Response
req := map[string]interface{}{
"Method": t.Response.Request.Method,
"URL": t.Response.Request.URL,
"Proto": t.Response.Request.Proto,
"ContentLength": t.Response.Request.ContentLength,
"Header": t.Response.Request.Header,
"Form": t.Response.Request.Form,
"PostForm": t.Response.Request.PostForm,
}
resp := map[string]interface{}{
"Status": t.Response.Status,
"StatusCode": t.Response.StatusCode,
"Proto": t.Response.Proto,
"Header": t.Response.Header,
"ContentLength": t.Response.ContentLength,
"TransferEncoding": t.Response.TransferEncoding,
}
// Beautify the response JSON to make it human readable.
respBytes, err := json.MarshalIndent(
map[string]interface{}{
"Response": resp,
"Request": req,
},
"",
" ")
if err != nil {
fmt.Println(err)
}
// Remove extra new line symbols so they do not take too much space on a result page.
// Allow no more than 1 line break at a time.
body := strings.Replace(string(t.ResponseBody), "\n\n", "\n", -1)
body = strings.Replace(body, "\r\n\r\n", "\r\n", -1)
return map[string]string{
"Headers": string(respBytes),
"Body": strings.TrimSpace(body),
}
}
//sortbySuiteName sorts the testsuites by name.
type sortBySuiteName []interface{}
func (a sortBySuiteName) Len() int { return len(a) }
func (a sortBySuiteName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortBySuiteName) Less(i, j int) bool {
return reflect.TypeOf(a[i]).Elem().Name() < reflect.TypeOf(a[j]).Elem().Name()
}
func init() {
// Every time app is restarted convert the list of available test suites
// provided by the revel testing package into a format which will be used by
// the testrunner module and revel test cmd.
revel.OnAppStart(func() {
// Extracting info about available test suites from revel/testing package.
registeredTests = map[string]int{}
sort.Sort(sortBySuiteName(testing.TestSuites))
for _, testSuite := range testing.TestSuites {
testSuites = append(testSuites, describeSuite(testSuite))
}
})
}
+11
View File
@@ -0,0 +1,11 @@
package app
import (
"github.com/revel/revel"
)
func init() {
revel.OnAppStart(func() {
revel.AppLog.Info("Go to /@tests to run the tests.")
})
}
@@ -0,0 +1,45 @@
<div class="panel panel-default">
<div class="panel-heading">
<b>{{.error.Description}}</b>
</div>
<div class="panel-body">
<ul class="nav nav-tabs" role="tablist">
<li class="active"><a href="#error_{{.postfix}}" role="tab" data-toggle="tab">Error</a></li>
<li><a href="#stack_{{.postfix}}" role="tab" data-toggle="tab">Stack</a></li>
{{if .response}}
<li><a href="#headers_{{.postfix}}" role="tab" data-toggle="tab">Headers</a></li>
<li><a href="#body_{{.postfix}}" role="tab" data-toggle="tab">Response Body</a></li>
{{end}}
</ul>
<div class="tab-content" id="result_{{.postfix}}">
<div class="tab-pane active" id="error_{{.postfix}}">
<div class="panel panel-danger">
<div class="panel-heading">
In {{.error.Path}}{{if .error.Line}} (around {{if .error.Line}}line {{.error.Line}}{{end}}{{if .error.Column}} column {{.error.Column}}{{end}}){{end}}:
</div>
<div class="panel-body">
{{range .error.ContextSource}}
{{if .IsError}}
<pre><code class="go">{{.Source}}</code></pre>
{{end}}
{{end}}
</div>
</div>
</div>
<div class="tab-pane" id="stack_{{.postfix}}">
<pre><code class="bash">{{.error.Stack}}</code></pre>
</div>
{{if .response}}
<div class="tab-pane" id="headers_{{.postfix}}">
<pre><code class="json">{{.response.Headers}}</code></pre>
</div>
<div class="tab-pane" id="body_{{.postfix}}">
<pre><code class="html">{{.response.Body}}</code></pre>
</div>
{{end}}
</div>
</div>
</div>
@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html>
<head>
<title>Revel Test Runner</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link href="{{url `Root`}}/@tests/public/css/bootstrap.min.css" type="text/css" rel="stylesheet"></link>
<link href="{{url `Root`}}/@tests/public/css/github.css" type="text/css" rel="stylesheet"></link>
<script src="{{url `Root`}}/@tests/public/js/jquery-1.9.1.min.js" type="text/javascript"></script>
<script src="{{url `Root`}}/@tests/public/js/bootstrap.min.js" type="text/javascript"></script>
<script src="{{url `Root`}}/@tests/public/js/highlight.pack.js" type="text/javascript"></script>
<style>
header { background-color:#ADD8E6 }
header h1 {margin-top: 10px; margin-bottom:20px;}
header table {margin-bottom: 0px }
td .btn {margin-bottom: 1px; }
button.file-test { margin-bottom: 0px; margin-left: 2px }
table.tests tr { border-bottom: 1px solid #ddd; background-color: #f9f9f9; }
.passed td { background-color: #90EE90 !important; }
.failed td { background-color: #FFB6C1 !important; }
td.result div.panel-default{ display:none; }
td.result > a { color: red; }
td.rightCol, td.leftCol { width: 40px; }
pre { font-size:10px; white-space: pre; }
.panel-heading {
padding: 10px 5px 8px 5px
}
.name { width: 35%; }
.w100 { width: 100%; }
.logo, .logo:hover { text-decoration: none; color: inherit;}
.pnt-triangle { color: #777; font-size: 18px;}
.panel-group .panel-heading+.panel-collapse>.panel-body { border-top: none;}
</style>
</head>
<body>
<header>
<div class="container">
<h1 class="pull-left">
<a href="/@tests" class="logo">Test Runner</a> <small>- Run your application's tests here.</small>
</h1>
<div style="margin-top:16px" class="pull-right">
<button class="btn btn-success {{if not .suiteFound}}disabled{{end}}" all-tests="">Run All Tests</button>
<div><a class="small" href="#" id="allTestResults"></a></div>
</div>
</div>
</header>
<div class="panel-group container">
{{if not .suiteFound}}
<div style="margin-top:20px;padding:15px;" class="panel panel-default">
<span style="font-weight:bold;color:#777;">Suite "{{.suiteName}}" is not found.</span>
</div>
{{end}}
{{range .testSuites}}
{{ $testFile := .Name }}
<div style="margin-top:20px;" class="panel panel-default">
<div class="panel-heading collapseLnk" style="cursor:pointer" data-toggle="collapse" data-target="#{{.Name}}">
<button id="suite{{.Name}}" class="btn btn-xs btn-success" test-file="{{.Name}}">Run</button>
<span class="h5">&nbsp;<a href="/@tests/{{ .Name }}">{{.Name}}</a></span>
<span id="pointTriangle{{.Name}}" class="pnt-triangle pull-right" data-tri-open="open">&#9660</span>
</div>
<div id="{{.Name}}" class="panel-collapse collapse in">
<table class="panel-body table table-condensed tests" suite="{{.Name}}">
{{range .Tests}}
<tr id="testRow{{.Name}}">
<td class="leftCol"><button data-test-file="{{$testFile}}" test="{{.Name}}" class="leftbutton btn btn-success btn-xs">Run</button></td>
<td class="name">{{ .Name }}</td>
<td class="result"><a href="#"></a></td>
<td class="rightCol"><button data-test-file="{{$testFile}}" test="{{.Name}}" class="pull-right btn btn-success btn-xs">Run</button></td>
</tr>
{{end}}
</table>
</div>
</div>
{{end}}
</div>
<script>
var passCount = 0;
var failCount = 0;
var buttons = [];
var running;
$(function() {
var divId, visible;
var oneOpened = false;
var panels = $("div.panel-collapse");
//close any panels that were previously closed.
panels.each(function() {
divId = "#" + $(this).attr("id");
visible = parseBoolean(localStorage.getItem("testrunner_" + divId));
if (visible) {
togglePntTriangle(divId, visible);
oneOpened = visible;
} else {
$(divId).css("height", "0").removeClass("in"); //this is the way bootstrap does it.
$("div[data-target=" + divId + "]").addClass("collapsed");
togglePntTriangle(divId, visible);
}
});
if (!oneOpened && panels.length > 0) {
divId = "#" + panels[0].id;
$(divId).collapse('show');
togglePntTriangle(divId, true);
}
});
$(".fileTestLnk").click(function() {
var tableId = $(this).attr("href");
$(tableId).toggle();
return false;
});
$("#allTestResults").click(function() {
var badRows = $("tr.failed");
if (badRows.length >= 0) {
badRows[0].scrollIntoView();
}
return false;
});
$("button[test]").click(function() {
$("#allTestResults").text("");
var button = $(this).addClass("disabled").text("Running");
$(this).closest("tr").removeClass("passed").removeClass("failed");
addToQueue(button);
});
$("td.result a").click(function() { //show/hide the extended error div
$(this).siblings().toggle();
return false;
});
$("button[test-file]").click(function() {
$("#allTestResults").text("");
var testfile = $(this).attr('test-file');
$("button").each(function() {
if ($(this).data("test-file") == testfile)
$(this).click();
});
return false;
});
$("button[all-tests]").click(function() {
$("tr").removeClass("passed").removeClass("failed");
passCount = 0;
failCount = 0;
var button = $(this).addClass("disabled").text("Running");
$("button.leftbutton[test]").click();
});
$("div.collapseLnk").click(function() {
var tableId = $(this).data("target");
var visible = !$(tableId).is(":visible");
localStorage.setItem("testrunner_" + tableId, visible);
togglePntTriangle(tableId, visible);
});
function togglePntTriangle(suiteId, visible) {
if (suiteId.charAt(0) == '#') {
suiteId = suiteId.substring(1);
}
$('#pointTriangle' + suiteId).html(visible ? '&#9660' : '&#9654');
}
function parseBoolean(str) {
return /^true$/i.test(str);
}
function addToQueue(button) {
buttons.push(button);
if (!running) {
running = true;
nextTest();
}
}
function nextTest() {
if (buttons.length == 0) {
running = false;
} else {
var next = buttons.shift();
runTest(next);
}
}
function runTest(button) {
var suite = button.parents("table").attr("suite");
var test = button.attr("test");
var row = button.parents("tr");
var resultCell = row.children(".result");
$("a", resultCell).text("");
$("div.panel-default", resultCell).remove();
$.ajax({
dataType: "json",
url: "{{url `Root`}}/@tests/"+suite+"/"+test,
success: function(result) {
row.attr("class", result.Passed ? "passed" : "failed");
if (result.Passed) {
passCount++;
} else {
console.log("fail:", result.Name);
failCount++;
var resultLnk = $("a", resultCell);
$(resultLnk).text(result.ErrorSummary);
resultCell.append(result.ErrorHTML);
var pnlDiv = row.closest("div");
if ($(pnlDiv).hasClass("in") == false) {
$("#link-" + suite).click();
}
$("#result_" + suite + "_" + test + " pre code").each(function(i, block) {
hljs.highlightBlock(block);
});
}
button.removeClass("disabled").text("Run");
var runAllBut = $("button[all-tests]");
if (buttons.length == 0 && runAllBut.hasClass("disabled")) {
runAllBut.removeClass("disabled").text("Run All Tests");
var resMsg = passCount + " passed, " + failCount + " failed.";
$("#allTestResults").text(resMsg);
}
nextTest();
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<title>Revel Test Runner</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style>
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333333;
background-color: #ffffff;
}
header { padding:20px 0; }
header.passed { background-color: #90EE90 !important; }
header.failed { background-color: #FFB6C1 !important; }
table { margin-top: 20px; padding: 8px; line-height: 20px; }
td { vertical-align: top; padding-right:20px; }
a { color: #0088cc; }
.container { margin-left: auto; margin-right: auto; width: 940px; overflow: hidden; }
.result h2 { font-size: 16px; border-bottom: 1px solid #f0f0f0; padding-bottom: 0.2em; }
.result.failed b { font-weight:bold; color: #C00; font-size: 14px; }
.result.failed h2 { color: #C00; }
.result .info { font-size: 12px; }
.result .info pre { overflow: auto; background-color: #f0f0f0; width: 100%; max-height: 500px; }
</style>
</head>
<body>
<header class="{{if .Passed}}passed{{else}}failed{{end}}">
<div class="container">
<h1>{{.Name}}</h1>
<p>{{if .Passed}}PASSED{{else}}FAILED{{end}}</p>
</div>
</header>
<div class="container">
{{range .Results}}
<div class="result {{if .Passed}}passed{{else}}failed{{end}}">
<div><h2>{{.Name}}</h2></div>
<div class="info">{{if .ErrorHTML}}{{.ErrorHTML}}{{else}}PASSED{{end}}</div>
</div>
{{end}}
</div>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
GET /@tests TestRunner.Index
GET /@tests.list TestRunner.List
GET /@tests/public/*filepath Static.ServeModule(testrunner,public)
GET /@tests/:suite TestRunner.Suite
GET /@tests/:suite/:test TestRunner.Run
@@ -0,0 +1,5 @@
/*!
* Bootstrap v3.2.0 (http://getbootstrap.com)
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*//*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-Line truncated
+127
View File
@@ -0,0 +1,127 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
-webkit-text-size-adjust: none;
}
.hljs-comment,
.hljs-template_comment,
.diff .hljs-header,
.hljs-javadoc {
color: #998;
font-style: italic;
}
.hljs-keyword,
.css .rule .hljs-keyword,
.hljs-winutils,
.javascript .hljs-title,
.nginx .hljs-title,
.hljs-subst,
.hljs-request,
.hljs-status {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-hexcolor,
.ruby .hljs-constant {
color: #008080;
}
.hljs-string,
.hljs-tag .hljs-value,
.hljs-phpdoc,
.hljs-dartdoc,
.tex .hljs-formula {
color: #d14;
}
.hljs-title,
.hljs-id,
.scss .hljs-preprocessor {
color: #900;
font-weight: bold;
}
.javascript .hljs-title,
.hljs-list .hljs-keyword,
.hljs-subst {
font-weight: normal;
}
.hljs-class .hljs-title,
.hljs-type,
.vhdl .hljs-literal,
.tex .hljs-command {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-tag .hljs-title,
.hljs-rules .hljs-property,
.django .hljs-tag .hljs-keyword {
color: #000080;
font-weight: normal;
}
.hljs-attribute,
.hljs-variable,
.lisp .hljs-body {
color: #008080;
}
.hljs-regexp {
color: #009926;
}
.hljs-symbol,
.ruby .hljs-symbol .hljs-string,
.lisp .hljs-keyword,
.clojure .hljs-keyword,
.scheme .hljs-keyword,
.tex .hljs-special,
.hljs-prompt {
color: #990073;
}
.hljs-built_in {
color: #0086b3;
}
.hljs-preprocessor,
.hljs-pragma,
.hljs-pi,
.hljs-doctype,
.hljs-shebang,
.hljs-cdata {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.diff .hljs-change {
background: #0086b3;
}
.hljs-chunk {
color: #aaa;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Loaded 100 of 670 files, more files were not shown because too many files have changed in this diff. Show more