├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cmd ├── kocha-build │ ├── main.go │ ├── main_test.go │ ├── skeleton │ │ └── build │ │ │ ├── builder.go.tmpl │ │ │ └── main.go.tmpl │ ├── testdata │ │ ├── app │ │ │ ├── controller │ │ │ │ └── root.go │ │ │ └── view │ │ │ │ ├── layout │ │ │ │ └── app.html │ │ │ │ └── root.html │ │ ├── config │ │ │ ├── app.go │ │ │ └── routes.go │ │ ├── main.go │ │ └── public │ │ │ └── robots.txt │ └── testutil_test.go ├── kocha-generate │ ├── kocha-generate-controller │ │ ├── main.go │ │ ├── main_test.go │ │ └── skeleton │ │ │ └── controller │ │ │ ├── controller.go.tmpl │ │ │ └── view.html.tmpl │ ├── kocha-generate-model │ │ ├── genmai.go │ │ ├── genmai_test.go │ │ ├── main.go │ │ ├── main_test.go │ │ ├── model.go │ │ └── skeleton │ │ │ └── model │ │ │ └── genmai │ │ │ ├── config.go.tmpl │ │ │ └── genmai.go.tmpl │ ├── kocha-generate-unit │ │ ├── main.go │ │ ├── main_test.go │ │ └── skeleton │ │ │ └── unit │ │ │ └── unit.go.tmpl │ ├── main.go │ └── main_test.go ├── kocha-new │ ├── main.go │ ├── main_test.go │ └── skeleton │ │ └── new │ │ ├── app │ │ ├── controller │ │ │ └── root.go.tmpl │ │ └── view │ │ │ ├── error │ │ │ ├── 404.html.tmpl.tmpl │ │ │ └── 500.html.tmpl.tmpl │ │ │ ├── layout │ │ │ └── app.html.tmpl.tmpl │ │ │ └── root.html.tmpl.tmpl │ │ ├── config │ │ └── app.go.tmpl │ │ ├── main.go.tmpl │ │ └── public │ │ └── robots.txt ├── kocha-run │ ├── main.go │ └── main_test.go └── kocha │ └── main.go ├── controller.go ├── controller_test.go ├── event.go ├── event ├── event.go ├── event_test.go ├── memory │ ├── queue.go │ └── queue_test.go └── payload.go ├── flash.go ├── flash_test.go ├── kocha.go ├── kocha_bench_test.go ├── kocha_test.go ├── log.go ├── log ├── entry.go ├── formatter.go ├── formatter_test.go ├── level_string.go ├── logger.go └── logger_test.go ├── middleware.go ├── middleware_test.go ├── param.go ├── param_test.go ├── request.go ├── request_test.go ├── resource.go ├── resource_test.go ├── response.go ├── response_test.go ├── router.go ├── router_test.go ├── session.go ├── session_test.go ├── template.go ├── template_test.go ├── testdata ├── app │ └── view │ │ ├── another_delims.html │ │ ├── date.html │ │ ├── def_tmpl.html │ │ ├── error │ │ ├── 400.html │ │ ├── 404.html │ │ ├── 500.html │ │ └── 500.json │ │ ├── json.json │ │ ├── layout │ │ ├── another_layout.html │ │ ├── application.html │ │ ├── application.json │ │ └── sub.html │ │ ├── post_test.html │ │ ├── root.html │ │ ├── teapot.html │ │ ├── teapot.json │ │ ├── test_tmpl1.html │ │ ├── testctrlr.html │ │ ├── testctrlr.js │ │ ├── testctrlr.json │ │ ├── testctrlr_ctx.html │ │ └── user.html └── public │ ├── robots.txt │ └── test.js ├── testfixtures_test.go ├── unit.go └── util ├── util.go └── util_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | vendor/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | - 1.9 6 | - tip 7 | 8 | install: 9 | - go get -u -v github.com/mattn/go-sqlite3 10 | - go get -u -v ./... 11 | 12 | script: 13 | - go test $(go list ./... | grep -v /vendor/) 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Kocha v0.7.0 2 | 3 | This release contains the incompatible changes with previous releases. 4 | Also the feature freeze until 1.0.0 release. 5 | 6 | ## New features: 7 | 8 | * request: Add Context.Request.IsXHR 9 | * render: Add Context.Format 10 | * template: Add `join` template func 11 | * template: Add `flash` template func 12 | * template: Template action delimiters now can be changed 13 | * log: Add RawFormatter 14 | * misc: Add ErrorWithLine 15 | 16 | ## Incompatible changes: 17 | 18 | * cli: Move to `cmd` directory 19 | * template: `{{define "content"}}` on each templates are no longer required 20 | * template: Suffix of template file changed to .tmpl 21 | * render: kocha.Render* back to kocha.Context.Render* and signatures are changed 22 | * kocha: Rename SettingEnv to Getenv 23 | * middleware: Several features are now implemented as the middlewares 24 | * middleware: Change interface signature 25 | 26 | ## Other changes: 27 | 28 | * log: Output to console will be coloring 29 | * Some bugfix 30 | 31 | # Kocha v0.6.1 32 | 33 | ## Changes 34 | 35 | * [bugfix] Fix a problem that reloading process of `kocha run' doesn't work 36 | 37 | # Kocha v0.6.0 38 | 39 | This release is an incompatible with previous releases. 40 | 41 | ## New features 42 | 43 | * feature: add middleware for Flash messaging 44 | * cli: CLI now can append the user-defined subcommands like git 45 | * session: add Get, Set and Del API 46 | 47 | ## Incompatible changes 48 | 49 | * all: names of packages in an application to change to singular name 50 | * log: logger is fully redesigned 51 | * template: Remove `date' template function 52 | * renderer: Move kocha.Context.Render* to kocha.Render* 53 | * context: Change Errors() method to the Errors field 54 | * middleware: Middleware.After will be called in the reverse of the order in which Middleware.Before are called 55 | * controller: controller types are fully redesigned 56 | * controller: remove NewErrorController() 57 | * router: Route.dispatch won't create an instance of Controller for each dispatching 58 | * middleware: processing of ResponseContentTypeMiddleware moves to core 59 | 60 | ## Other changes 61 | 62 | * session: codec.MsgpackHandle won't be created on each call 63 | * all: refactoring 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Naoya Inada 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kocha [![Build Status](https://travis-ci.org/naoina/kocha.svg?branch=master)](https://travis-ci.org/naoina/kocha) 2 | 3 | A convenient web application framework for [Go](http://golang.org/) 4 | 5 | **NOTE: Kocha is still under development, so API might be changed in future. If you still want to use the current version of Kocha, use of a version control such as [gopkg.in](http://labix.org/gopkg.in) is highly recommended.** 6 | 7 | ## Features 8 | 9 | * Batteries included 10 | * All configurations are in Go's syntax 11 | * Generate an All-In-One binary 12 | * Compatible with `net/http` 13 | 14 | ## Requirement 15 | 16 | * Go 1.4 or later 17 | 18 | ## Getting started 19 | 20 | 1. install the framework: 21 | 22 | go get -u github.com/naoina/kocha 23 | 24 | And command-line tool 25 | 26 | go get -u github.com/naoina/kocha/cmd/... 27 | 28 | 2. Create a new application: 29 | 30 | kocha new myapp 31 | 32 | Where "myapp" is the application name. 33 | 34 | 3. Change directory and run the application: 35 | 36 | cd myapp 37 | kocha run 38 | 39 | or 40 | 41 | cd myapp 42 | go build -o myapp 43 | ./myapp 44 | 45 | ## Documentation 46 | 47 | See http://naoina.github.io/kocha/ for more information. 48 | 49 | ## License 50 | 51 | Kocha is licensed under the MIT 52 | -------------------------------------------------------------------------------- /cmd/kocha-build/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "text/template" 14 | "time" 15 | 16 | "github.com/naoina/kocha" 17 | "github.com/naoina/kocha/util" 18 | ) 19 | 20 | type buildCommand struct { 21 | option struct { 22 | All bool `short:"a" long:"all"` 23 | Tag string `short:"t" long:"tag"` 24 | Help bool `short:"h" long:"help"` 25 | } 26 | } 27 | 28 | func (c *buildCommand) Name() string { 29 | return "kocha build" 30 | } 31 | 32 | func (c *buildCommand) Usage() string { 33 | return fmt.Sprintf(`Usage: %s [OPTIONS] [IMPORT_PATH] 34 | 35 | Build your application. 36 | 37 | Options: 38 | -a, --all make the true all-in-one binary 39 | -t, --tag=TAG specify version tag 40 | -h, --help display this help and exit 41 | 42 | `, c.Name()) 43 | } 44 | 45 | func (c *buildCommand) Option() interface{} { 46 | return &c.option 47 | } 48 | 49 | func (c *buildCommand) Run(args []string) (err error) { 50 | var appDir string 51 | var dir string 52 | if len(args) > 0 { 53 | appDir = args[0] 54 | dir, err = util.FindAbsDir(appDir) 55 | if err != nil { 56 | return err 57 | } 58 | } else { 59 | dir, err = os.Getwd() 60 | if err != nil { 61 | return err 62 | } 63 | appDir, err = util.FindAppDir() 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | appName := filepath.Base(dir) 69 | configPkg, err := getPackage(path.Join(appDir, "config")) 70 | if err != nil { 71 | return fmt.Errorf(`cannot import "%s": %v`, path.Join(appDir, "config"), err) 72 | } 73 | var dbImportPath string 74 | if dbPkg, err := getPackage(path.Join(appDir, "db")); err == nil { 75 | dbImportPath = dbPkg.ImportPath 76 | } 77 | var migrationImportPath string 78 | if migrationPkg, err := getPackage(path.Join(appDir, "db", "migration")); err == nil { 79 | migrationImportPath = migrationPkg.ImportPath 80 | } 81 | tmpDir, err := filepath.Abs("tmp") 82 | if err != nil { 83 | return err 84 | } 85 | if err := os.Mkdir(tmpDir, 0755); err != nil && !os.IsExist(err) { 86 | return fmt.Errorf("failed to create directory: %v", err) 87 | } 88 | _, filename, _, _ := runtime.Caller(0) 89 | baseDir := filepath.Dir(filename) 90 | skeletonDir := filepath.Join(baseDir, "skeleton", "build") 91 | mainTemplate, err := ioutil.ReadFile(filepath.Join(skeletonDir, "main.go"+util.TemplateSuffix)) 92 | if err != nil { 93 | return err 94 | } 95 | mainFilePath := filepath.ToSlash(filepath.Join(tmpDir, "main.go")) 96 | builderFilePath := filepath.ToSlash(filepath.Join(tmpDir, "builder.go")) 97 | file, err := os.Create(builderFilePath) 98 | if err != nil { 99 | return fmt.Errorf("failed to create file: %v", err) 100 | } 101 | defer file.Close() 102 | builderTemplatePath := filepath.ToSlash(filepath.Join(skeletonDir, "builder.go"+util.TemplateSuffix)) 103 | t := template.Must(template.ParseFiles(builderTemplatePath)) 104 | var resources map[string]string 105 | if c.option.All { 106 | resources = collectResourcePaths(filepath.Join(dir, kocha.StaticDir)) 107 | } 108 | tag, err := c.detectVersionTag() 109 | if err != nil { 110 | return err 111 | } 112 | data := map[string]interface{}{ 113 | "configImportPath": configPkg.ImportPath, 114 | "dbImportPath": dbImportPath, 115 | "migrationImportPath": migrationImportPath, 116 | "mainTemplate": string(mainTemplate), 117 | "mainFilePath": mainFilePath, 118 | "resources": resources, 119 | "version": tag, 120 | } 121 | if err := t.Execute(file, data); err != nil { 122 | return fmt.Errorf("failed to write file: %v", err) 123 | } 124 | file.Close() 125 | execName := appName 126 | if runtime.GOOS == "windows" { 127 | execName += ".exe" 128 | } 129 | if err := execCmdWithHostEnv("go", "run", builderFilePath); err != nil { 130 | return err 131 | } 132 | // To avoid to become a dynamic linked binary. 133 | // See https://github.com/golang/go/issues/9344 134 | execPath := filepath.Join(dir, execName) 135 | execArgs := []string{"build", "-o", execPath, "-tags", "netgo", "-installsuffix", "netgo"} 136 | // On Linux, works fine. On Windows, doesn't work. 137 | // On other platforms, not tested. 138 | if runtime.GOOS == "linux" { 139 | execArgs = append(execArgs, "-ldflags", `-extldflags "-static"`) 140 | } 141 | execArgs = append(execArgs, mainFilePath) 142 | if err := execCmd("go", execArgs...); err != nil { 143 | return err 144 | } 145 | if err := os.RemoveAll(tmpDir); err != nil { 146 | return err 147 | } 148 | if err := util.PrintEnv(dir); err != nil { 149 | return err 150 | } 151 | fmt.Printf("build all-in-one binary to %v\n", execPath) 152 | util.PrintGreen("Build successful!\n") 153 | return nil 154 | } 155 | 156 | func getPackage(importPath string) (*build.Package, error) { 157 | return build.Import(importPath, "", build.FindOnly) 158 | } 159 | 160 | func execCmd(cmd string, args ...string) error { 161 | command := exec.Command(cmd, args...) 162 | if msg, err := command.CombinedOutput(); err != nil { 163 | return fmt.Errorf("build failed: %v\n%v", err, string(msg)) 164 | } 165 | return nil 166 | } 167 | 168 | func execCmdWithHostEnv(cmd string, args ...string) (err error) { 169 | targetGOOS, targetGOARCH := os.Getenv("GOOS"), os.Getenv("GOARCH") 170 | for name, env := range map[string]string{ 171 | "GOOS": runtime.GOOS, 172 | "GOARCH": runtime.GOARCH, 173 | } { 174 | if err := os.Setenv(name, env); err != nil { 175 | return err 176 | } 177 | } 178 | defer func() { 179 | for name, env := range map[string]string{ 180 | "GOOS": targetGOOS, 181 | "GOARCH": targetGOARCH, 182 | } { 183 | if e := os.Setenv(name, env); e != nil { 184 | if err != nil { 185 | err = e 186 | } 187 | } 188 | } 189 | }() 190 | return execCmd(cmd, args...) 191 | } 192 | 193 | func collectResourcePaths(root string) map[string]string { 194 | result := make(map[string]string) 195 | filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 196 | if err != nil { 197 | return err 198 | } 199 | if info.Name()[0] == '.' { 200 | if info.IsDir() { 201 | return filepath.SkipDir 202 | } 203 | return nil 204 | } 205 | if info.IsDir() { 206 | return nil 207 | } 208 | rel, err := filepath.Rel(root, path) 209 | if err != nil { 210 | return err 211 | } 212 | result[rel] = filepath.ToSlash(path) 213 | return nil 214 | }) 215 | return result 216 | } 217 | 218 | func (c *buildCommand) detectVersionTag() (string, error) { 219 | if c.option.Tag != "" { 220 | return c.option.Tag, nil 221 | } 222 | var repo string 223 | for _, dir := range []string{".git", ".hg"} { 224 | if info, err := os.Stat(dir); err == nil && info.IsDir() { 225 | repo = dir 226 | break 227 | } 228 | } 229 | version := time.Now().Format(time.RFC1123Z) 230 | switch repo { 231 | case ".git": 232 | bin, err := exec.LookPath("git") 233 | if err != nil { 234 | fmt.Fprintf(os.Stderr, "%s: WARNING: git repository found, but `git` command not found. use \"%s\" as version\n", c.Name(), version) 235 | break 236 | } 237 | line, err := exec.Command(bin, "rev-parse", "HEAD").Output() 238 | if err != nil { 239 | return "", fmt.Errorf("unexpected error: %v\nplease specify the version using '--tag' option to avoid the this error", err) 240 | } 241 | version = strings.TrimSpace(string(line)) 242 | case ".hg": 243 | bin, err := exec.LookPath("hg") 244 | if err != nil { 245 | fmt.Fprintf(os.Stderr, "%s: WARNING: hg repository found, but `hg` command not found. use \"%s\" as version\n", c.Name(), version) 246 | break 247 | } 248 | line, err := exec.Command(bin, "identify").Output() 249 | if err != nil { 250 | return "", fmt.Errorf("unexpected error: %v\nplease specify version using '--tag' option to avoid the this error", err) 251 | } 252 | version = strings.TrimSpace(string(line)) 253 | } 254 | if version == "" { 255 | // Probably doesn't reach here. 256 | version = time.Now().Format(time.RFC1123Z) 257 | fmt.Fprintf(os.Stderr, `%s: WARNING: version is empty, use "%s" as version`, c.Name(), version) 258 | } 259 | return version, nil 260 | } 261 | 262 | func main() { 263 | util.RunCommand(&buildCommand{}) 264 | } 265 | -------------------------------------------------------------------------------- /cmd/kocha-build/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "reflect" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func Test_buildCommand_Name(t *testing.T) { 17 | c := &buildCommand{} 18 | var actual interface{} = c.Name() 19 | var expect interface{} = "kocha build" 20 | if !reflect.DeepEqual(actual, expect) { 21 | t.Errorf(`%T.Name() => %#v; want %#v`, c, actual, expect) 22 | } 23 | } 24 | 25 | func Test_buildCommand_Run_withNoENVGiven(t *testing.T) { 26 | c := &buildCommand{} 27 | args := []string{} 28 | err := c.Run(args) 29 | actual := err.Error() 30 | expect := "cannot import " 31 | if !strings.HasPrefix(actual, expect) { 32 | t.Errorf(`%T.Run(%#v) => %#v; want %#v`, c, args, actual, expect) 33 | } 34 | } 35 | 36 | func Test_buildCommand_Run(t *testing.T) { 37 | tempDir, err := ioutil.TempDir("", "Test_buildCommandRun") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | defer os.RemoveAll(tempDir) 42 | appName := "testappname" 43 | dstPath := filepath.Join(tempDir, "src", appName) 44 | _, filename, _, _ := runtime.Caller(0) 45 | baseDir := filepath.Dir(filename) 46 | testdataDir := filepath.Join(baseDir, "testdata") 47 | if err := copyAll(testdataDir, dstPath); err != nil { 48 | t.Fatal(err) 49 | } 50 | if err := os.Chdir(dstPath); err != nil { 51 | t.Fatal(err) 52 | } 53 | origGOPATH := build.Default.GOPATH 54 | defer func() { 55 | build.Default.GOPATH = origGOPATH 56 | os.Setenv("GOPATH", origGOPATH) 57 | }() 58 | build.Default.GOPATH = tempDir + string(filepath.ListSeparator) + build.Default.GOPATH 59 | os.Setenv("GOPATH", build.Default.GOPATH) 60 | f, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModePerm) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | defer f.Close() 65 | oldStdout, oldStderr := os.Stdout, os.Stderr 66 | os.Stdout, os.Stderr = f, f 67 | defer func() { 68 | os.Stdout, os.Stderr = oldStdout, oldStderr 69 | }() 70 | c := &buildCommand{} 71 | args := []string{} 72 | err = c.Run(args) 73 | var actual interface{} = err 74 | var expect interface{} = nil 75 | if !reflect.DeepEqual(actual, expect) { 76 | t.Errorf(`%T.Run(%#v) => %#v; want %#v`, c, args, actual, expect) 77 | } 78 | 79 | tmpDir := filepath.Join(dstPath, "tmp") 80 | if _, err := os.Stat(tmpDir); err == nil { 81 | t.Errorf("Expect %v was removed, but exists", tmpDir) 82 | } 83 | 84 | execName := appName 85 | if runtime.GOOS == "windows" { 86 | execName += ".exe" 87 | } 88 | execPath := filepath.Join(dstPath, execName) 89 | if _, err := os.Stat(execPath); err != nil { 90 | t.Fatalf("Expect %v is exists, but not exists", execName) 91 | } 92 | 93 | output, err := exec.Command(execPath, "-v").CombinedOutput() 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | actual = string(output) 98 | expect = fmt.Sprintf("%s version ", execName) 99 | if !strings.HasPrefix(actual.(string), expect.(string)) { 100 | t.Errorf("Expect starts with %v, but %v", expect, actual) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /cmd/kocha-build/skeleton/build/builder.go.tmpl: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED BY kocha build 2 | // DO NOT EDIT THIS FILE 3 | package main 4 | 5 | import ( 6 | "github.com/naoina/kocha" 7 | "github.com/naoina/kocha/util" 8 | "io/ioutil" 9 | config "{{.configImportPath}}" 10 | "os" 11 | "text/template" 12 | ) 13 | 14 | const ( 15 | mainTemplate = {{.mainTemplate|printf "%q"}} 16 | ) 17 | 18 | func main() { 19 | funcMap := template.FuncMap{ 20 | "goString": util.GoString, 21 | } 22 | t := template.Must(template.New("main").Funcs(funcMap).Parse(mainTemplate)) 23 | file, err := os.Create("{{.mainFilePath}}") 24 | if err != nil { 25 | panic(err) 26 | } 27 | defer file.Close() 28 | app, err := kocha.New(config.AppConfig) 29 | if err != nil { 30 | panic(err) 31 | } 32 | res := map[string]string{ 33 | {{range $name, $path := .resources}} 34 | "{{$name}}": "{{$path}}", 35 | {{end}} 36 | } 37 | resources := make(map[string]string) 38 | for name, path := range res { 39 | buf, err := ioutil.ReadFile(path) 40 | if err != nil { 41 | panic(err) 42 | } 43 | resources[name] = util.Gzip(string(buf)) 44 | } 45 | data := map[string]interface{}{ 46 | "app": app, 47 | "configImportPath": "{{.configImportPath}}", 48 | "dbImportPath": "{{.dbImportPath}}", 49 | "migrationImportPath": "{{.migrationImportPath}}", 50 | "resources": resources, 51 | "version": "{{.version}}", 52 | } 53 | if err := t.Execute(file, data); err != nil { 54 | panic(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/kocha-build/skeleton/build/main.go.tmpl: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED BY kocha build 2 | // DO NOT EDIT THIS FILE 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | config "{{.configImportPath}}" 9 | {{if and .dbImportPath .migrationImportPath}} 10 | db "{{.dbImportPath}}" 11 | migration "{{.migrationImportPath}}" 12 | {{end}} 13 | "os" 14 | "path/filepath" 15 | 16 | "github.com/naoina/kocha" 17 | {{if .resources}} 18 | "github.com/naoina/kocha/util" 19 | {{end}} 20 | ) 21 | 22 | const Version = "{{.version}}" 23 | 24 | func main() { 25 | progName := filepath.Base(os.Args[0]) 26 | showVersion := flag.Bool("v", false, "show version") 27 | flag.Usage = func() { 28 | fmt.Fprintf(os.Stderr, "usage: %s [-v] [migrate [-db confname] [-n n] {up|down}]\n", progName) 29 | os.Exit(1) 30 | } 31 | flag.Parse() 32 | if *showVersion { 33 | fmt.Printf("%s version %s\n", progName, Version) 34 | os.Exit(0) 35 | } 36 | migrate() 37 | config.AppConfig.ResourceSet = {{.app.ResourceSet|goString}} 38 | if len(config.AppConfig.RouteTable) != {{.app.Config.RouteTable|len}} { 39 | fmt.Fprintf(os.Stderr, "abort: length of config.AppConfig.RouteTable is mismatched between build-time and run-time") 40 | os.Exit(1) 41 | } 42 | {{range $name, $data := .resources}} 43 | config.AppConfig.ResourceSet.Add("{{$name}}", util.Gunzip({{$data|printf "%q"}})) 44 | {{end}} 45 | if err := kocha.Run(config.AppConfig); err != nil { 46 | panic(err) 47 | } 48 | } 49 | 50 | func migrate() { 51 | if flag.NArg() > 0 { 52 | switch flag.Arg(0) { 53 | case "migrate": 54 | {{if and .dbImportPath .migrationImportPath}} 55 | fs := flag.NewFlagSet("migrate", flag.ExitOnError) 56 | dbconf := fs.String("db", "default", "name of a database config") 57 | n := fs.Int("n", -1, "number of migrations to be run") 58 | if err := fs.Parse(flag.Args()[1:]); err != nil { 59 | panic(err) 60 | } 61 | config, found := db.DatabaseMap[*dbconf] 62 | if !found { 63 | fmt.Fprintf(os.Stderr, "abort: database config `%v' is undefined\n", *dbconf) 64 | flag.Usage() 65 | } 66 | var err error 67 | mig := kocha.Migrate(config, &migration.Migration{}) 68 | switch fs.Arg(0) { 69 | case "up": 70 | err = mig.Up(*n) 71 | case "down": 72 | err = mig.Down(*n) 73 | default: 74 | flag.Usage() 75 | } 76 | if err != nil { 77 | panic(err) 78 | } 79 | os.Exit(0) 80 | {{else}} 81 | fmt.Fprintf(os.Stderr, "abort: `migrate' is unsupported in this app binary\n") 82 | os.Exit(1) 83 | {{end}} 84 | default: 85 | flag.Usage() 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cmd/kocha-build/testdata/app/controller/root.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/naoina/kocha" 5 | ) 6 | 7 | type Root struct { 8 | *kocha.DefaultController 9 | } 10 | 11 | func (ro *Root) GET(c *kocha.Context) error { 12 | return c.Render(nil) 13 | } 14 | -------------------------------------------------------------------------------- /cmd/kocha-build/testdata/app/view/layout/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to Kocha 8 | 9 | 10 | {{yield .}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /cmd/kocha-build/testdata/app/view/root.html: -------------------------------------------------------------------------------- 1 |

Welcome to Kocha

2 | -------------------------------------------------------------------------------- /cmd/kocha-build/testdata/config/app.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/naoina/kocha" 10 | "github.com/naoina/kocha/log" 11 | ) 12 | 13 | var ( 14 | AppName = "testappname" 15 | AppConfig = &kocha.Config{ 16 | Addr: kocha.Getenv("KOCHA_ADDR", "127.0.0.1:9100"), 17 | AppPath: rootPath, 18 | AppName: AppName, 19 | DefaultLayout: "app", 20 | Template: &kocha.Template{ 21 | PathInfo: kocha.TemplatePathInfo{ 22 | Name: AppName, 23 | Paths: []string{ 24 | filepath.Join(rootPath, "app", "view"), 25 | }, 26 | }, 27 | FuncMap: kocha.TemplateFuncMap{}, 28 | }, 29 | 30 | // Logger settings. 31 | Logger: &kocha.LoggerConfig{ 32 | Writer: os.Stdout, 33 | Formatter: &log.LTSVFormatter{}, 34 | Level: log.INFO, 35 | }, 36 | 37 | Middlewares: []kocha.Middleware{ 38 | &kocha.RequestLoggingMiddleware{}, 39 | &kocha.SessionMiddleware{ 40 | Name: "testappname_session", 41 | Store: &kocha.SessionCookieStore{ 42 | // AUTO-GENERATED Random keys. DO NOT EDIT. 43 | SecretKey: "a1uApKUdSnugB7TbvDZ5GfkGEJOUd3qbV0dpJ5Bqmc4=", 44 | SigningKey: "jlPLiRWBaLCUg4PJ5u/Ncg==", 45 | }, 46 | 47 | // Expiration of session cookie, in seconds, from now. 48 | // Persistent if -1, For not specify, set 0. 49 | CookieExpires: time.Duration(90) * time.Hour * 24, 50 | 51 | // Expiration of session data, in seconds, from now. 52 | // Perssitent if -1, For not specify, set 0. 53 | SessionExpires: time.Duration(90) * time.Hour * 24, 54 | HttpOnly: false, 55 | }, 56 | &kocha.DispatchMiddleware{}, 57 | }, 58 | 59 | MaxClientBodySize: 1024 * 1024 * 10, // 10MB 60 | } 61 | 62 | _, configFileName, _, _ = runtime.Caller(0) 63 | rootPath = filepath.Dir(filepath.Join(configFileName, "..")) 64 | ) 65 | -------------------------------------------------------------------------------- /cmd/kocha-build/testdata/config/routes.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testappname/app/controller" 5 | 6 | "github.com/naoina/kocha" 7 | ) 8 | 9 | type RouteTable kocha.RouteTable 10 | 11 | var routes = RouteTable{ 12 | { 13 | Name: "root", 14 | Path: "/", 15 | Controller: &controller.Root{}, 16 | }, 17 | } 18 | 19 | func init() { 20 | AppConfig.RouteTable = kocha.RouteTable(append(routes, RouteTable{ 21 | { 22 | Name: "static", 23 | Path: "/*path", 24 | Controller: &kocha.StaticServe{}, 25 | }, 26 | }...)) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /cmd/kocha-build/testdata/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testappname/config" 5 | 6 | "github.com/naoina/kocha" 7 | ) 8 | 9 | func main() { 10 | kocha.Run(config.AppConfig) 11 | } 12 | -------------------------------------------------------------------------------- /cmd/kocha-build/testdata/public/robots.txt: -------------------------------------------------------------------------------- 1 | # User-Agent: * 2 | # Disallow: / 3 | -------------------------------------------------------------------------------- /cmd/kocha-build/testutil_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func copyAll(srcPath, destPath string) error { 11 | return filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { 12 | if err != nil { 13 | return err 14 | } 15 | dest := filepath.Join(destPath, strings.TrimPrefix(path, srcPath)) 16 | if info.IsDir() { 17 | err := os.MkdirAll(filepath.Join(dest), 0755) 18 | return err 19 | } 20 | src, err := ioutil.ReadFile(path) 21 | if err != nil { 22 | return err 23 | } 24 | return ioutil.WriteFile(dest, src, 0644) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-controller/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/naoina/kocha/util" 10 | ) 11 | 12 | type generateControllerCommand struct { 13 | option struct { 14 | Help bool `short:"h" long:"help"` 15 | } 16 | } 17 | 18 | func (c *generateControllerCommand) Name() string { 19 | return "kocha generate controller" 20 | } 21 | 22 | func (c *generateControllerCommand) Usage() string { 23 | return fmt.Sprintf(`Usage: %s [OPTIONS] NAME 24 | 25 | Generate the skeleton files of controller. 26 | 27 | Options: 28 | -h, --help display this help and exit 29 | 30 | `, c.Name()) 31 | } 32 | 33 | func (c *generateControllerCommand) Option() interface{} { 34 | return &c.option 35 | } 36 | 37 | // Run generates the controller templates. 38 | func (c *generateControllerCommand) Run(args []string) error { 39 | if len(args) < 1 || args[0] == "" { 40 | return fmt.Errorf("no NAME given") 41 | } 42 | name := args[0] 43 | camelCaseName := util.ToCamelCase(name) 44 | snakeCaseName := util.ToSnakeCase(name) 45 | receiverName := strings.ToLower(name) 46 | if len(receiverName) > 1 { 47 | receiverName = receiverName[:2] 48 | } else { 49 | receiverName = receiverName[:1] 50 | } 51 | data := map[string]interface{}{ 52 | "Name": camelCaseName, 53 | "Receiver": receiverName, 54 | } 55 | if err := util.CopyTemplate( 56 | filepath.Join(skeletonDir("controller"), "controller.go"+util.TemplateSuffix), 57 | filepath.Join("app", "controller", snakeCaseName+".go"), data); err != nil { 58 | return err 59 | } 60 | if err := util.CopyTemplate( 61 | filepath.Join(skeletonDir("controller"), "view.html"+util.TemplateSuffix), 62 | filepath.Join("app", "view", snakeCaseName+".html"), data); err != nil { 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | func skeletonDir(name string) string { 69 | _, filename, _, _ := runtime.Caller(0) 70 | baseDir := filepath.Dir(filename) 71 | return filepath.Join(baseDir, "skeleton", name) 72 | } 73 | 74 | func main() { 75 | util.RunCommand(&generateControllerCommand{}) 76 | } 77 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-controller/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func Test_generateControllerCommand_Run_withNoNAMEGiven(t *testing.T) { 13 | c := &generateControllerCommand{} 14 | args := []string{} 15 | err := c.Run(args) 16 | var actual interface{} = err 17 | var expect interface{} = fmt.Errorf("no NAME given") 18 | if !reflect.DeepEqual(actual, expect) { 19 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 20 | } 21 | } 22 | 23 | func Test_generateControllerCommand_Run(t *testing.T) { 24 | tempdir, err := ioutil.TempDir("", "Test_controllerGeneratorGenerate") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | defer os.RemoveAll(tempdir) 29 | os.Chdir(tempdir) 30 | controllerPath := filepath.Join("app", "controller") 31 | if err := os.MkdirAll(controllerPath, 0755); err != nil { 32 | t.Fatal(err) 33 | } 34 | viewPath := filepath.Join("app", "view") 35 | if err := os.MkdirAll(viewPath, 0755); err != nil { 36 | t.Fatal(err) 37 | } 38 | configPath := filepath.Join("config") 39 | if err := os.MkdirAll(configPath, 0755); err != nil { 40 | t.Fatal(err) 41 | } 42 | routeFileContent := `package config 43 | import ( 44 | "../app/controller" 45 | "github/naoina/kocha" 46 | ) 47 | type RouteTable kocha.RouteTable 48 | var routes = RouteTable{ 49 | { 50 | Name: "root", 51 | Path: "/", 52 | Controller: &controller.Root{}, 53 | }, 54 | } 55 | func Routes() RouteTable { 56 | return append(routes, RouteTable{}...) 57 | } 58 | ` 59 | if err := ioutil.WriteFile(filepath.Join(configPath, "routes.go"), []byte(routeFileContent), 0644); err != nil { 60 | t.Fatal(err) 61 | } 62 | f, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModePerm) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | defer f.Close() 67 | oldStdout, oldStderr := os.Stdout, os.Stderr 68 | os.Stdout, os.Stderr = f, f 69 | defer func() { 70 | os.Stdout, os.Stderr = oldStdout, oldStderr 71 | }() 72 | c := &generateControllerCommand{} 73 | args := []string{"app_controller"} 74 | err = c.Run(args) 75 | var actual interface{} = err 76 | var expect interface{} = nil 77 | if !reflect.DeepEqual(actual, expect) { 78 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 79 | } 80 | 81 | for _, v := range []string{ 82 | filepath.Join(controllerPath, "app_controller.go"), 83 | filepath.Join(viewPath, "app_controller.html"), 84 | } { 85 | if _, err := os.Stat(v); os.IsNotExist(err) { 86 | t.Errorf("generate(%#v); file %#v is not exists; want exists", args, v) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-controller/skeleton/controller/controller.go.tmpl: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/naoina/kocha" 5 | ) 6 | 7 | type {{.Name}} struct { 8 | *kocha.DefaultController 9 | } 10 | 11 | func ({{.Receiver}} *{{.Name}}) GET(c *kocha.Context) error { 12 | // FIXME: auto-generated by kocha 13 | return c.Render(map[string]interface{}{ 14 | "ControllerName": "{{.Name}}", 15 | }) 16 | } 17 | 18 | func ({{.Receiver}} *{{.Name}}) POST(c *kocha.Context) error { 19 | // FIXME: auto-generated by kocha 20 | return c.Render(map[string]interface{}{ 21 | "ControllerName": "{{.Name}}", 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-controller/skeleton/controller/view.html.tmpl: -------------------------------------------------------------------------------- 1 |

This is {{.Name}}

2 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-model/genmai.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | 7 | "github.com/naoina/kocha/util" 8 | ) 9 | 10 | // GenmaiModelType implements ModelTyper interface. 11 | type GenmaiModelType struct{} 12 | 13 | // FieldTypeMap returns type map for Genmai ORM. 14 | func (mt *GenmaiModelType) FieldTypeMap() map[string]ModelFieldType { 15 | return map[string]ModelFieldType{ 16 | "int": ModelFieldType{"int", nil}, 17 | "integer": ModelFieldType{"int", nil}, 18 | "int8": ModelFieldType{"int8", nil}, 19 | "byte": ModelFieldType{"int8", nil}, 20 | "int16": ModelFieldType{"int16", nil}, 21 | "smallint": ModelFieldType{"int16", nil}, 22 | "int32": ModelFieldType{"int32", nil}, 23 | "int64": ModelFieldType{"int64", nil}, 24 | "bigint": ModelFieldType{"int64", nil}, 25 | "string": ModelFieldType{"string", nil}, 26 | "text": ModelFieldType{"string", []string{`size:"65533"`}}, 27 | "mediumtext": ModelFieldType{"string", []string{`size:"16777216"`}}, 28 | "longtext": ModelFieldType{"string", []string{`size:"4294967295"`}}, 29 | "bytea": ModelFieldType{"[]byte", nil}, 30 | "blob": ModelFieldType{"[]byte", nil}, 31 | "mediumblob": ModelFieldType{"[]byte", []string{`size:"65533"`}}, 32 | "longblob": ModelFieldType{"[]byte", []string{`size:"4294967295"`}}, 33 | "bool": ModelFieldType{"bool", nil}, 34 | "boolean": ModelFieldType{"bool", nil}, 35 | "float": ModelFieldType{"genmai.Float64", nil}, 36 | "float64": ModelFieldType{"genmai.Float64", nil}, 37 | "double": ModelFieldType{"genmai.Float64", nil}, 38 | "real": ModelFieldType{"genmai.Float64", nil}, 39 | "date": ModelFieldType{"time.Time", nil}, 40 | "time": ModelFieldType{"time.Time", nil}, 41 | "datetime": ModelFieldType{"time.Time", nil}, 42 | "timestamp": ModelFieldType{"time.Time", nil}, 43 | "decimal": ModelFieldType{"genmai.Rat", nil}, 44 | "numeric": ModelFieldType{"genmai.Rat", nil}, 45 | } 46 | } 47 | 48 | // TemplatePath returns paths that templates of Genmai ORM for model generation. 49 | func (mt *GenmaiModelType) TemplatePath() (templatePath string, configTemplatePath string) { 50 | templatePath = filepath.Join(skeletonDir("model"), "genmai", "genmai.go"+util.TemplateSuffix) 51 | configTemplatePath = filepath.Join(skeletonDir("model"), "genmai", "config.go"+util.TemplateSuffix) 52 | return templatePath, configTemplatePath 53 | } 54 | 55 | func skeletonDir(name string) string { 56 | _, filename, _, _ := runtime.Caller(0) 57 | baseDir := filepath.Dir(filename) 58 | return filepath.Join(baseDir, "skeleton", name) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-model/genmai_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path/filepath" 5 | "reflect" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/naoina/kocha/util" 10 | ) 11 | 12 | func TestGenmaiModelType_FieldTypeMap(t *testing.T) { 13 | m := map[string]ModelFieldType{ 14 | "int": ModelFieldType{"int", nil}, 15 | "integer": ModelFieldType{"int", nil}, 16 | "int8": ModelFieldType{"int8", nil}, 17 | "byte": ModelFieldType{"int8", nil}, 18 | "int16": ModelFieldType{"int16", nil}, 19 | "smallint": ModelFieldType{"int16", nil}, 20 | "int32": ModelFieldType{"int32", nil}, 21 | "int64": ModelFieldType{"int64", nil}, 22 | "bigint": ModelFieldType{"int64", nil}, 23 | "string": ModelFieldType{"string", nil}, 24 | "text": ModelFieldType{"string", []string{`size:"65533"`}}, 25 | "mediumtext": ModelFieldType{"string", []string{`size:"16777216"`}}, 26 | "longtext": ModelFieldType{"string", []string{`size:"4294967295"`}}, 27 | "bytea": ModelFieldType{"[]byte", nil}, 28 | "blob": ModelFieldType{"[]byte", nil}, 29 | "mediumblob": ModelFieldType{"[]byte", []string{`size:"65533"`}}, 30 | "longblob": ModelFieldType{"[]byte", []string{`size:"4294967295"`}}, 31 | "bool": ModelFieldType{"bool", nil}, 32 | "boolean": ModelFieldType{"bool", nil}, 33 | "float": ModelFieldType{"genmai.Float64", nil}, 34 | "float64": ModelFieldType{"genmai.Float64", nil}, 35 | "double": ModelFieldType{"genmai.Float64", nil}, 36 | "real": ModelFieldType{"genmai.Float64", nil}, 37 | "date": ModelFieldType{"time.Time", nil}, 38 | "time": ModelFieldType{"time.Time", nil}, 39 | "datetime": ModelFieldType{"time.Time", nil}, 40 | "timestamp": ModelFieldType{"time.Time", nil}, 41 | "decimal": ModelFieldType{"genmai.Rat", nil}, 42 | "numeric": ModelFieldType{"genmai.Rat", nil}, 43 | } 44 | actual := (&GenmaiModelType{}).FieldTypeMap() 45 | expected := m 46 | if !reflect.DeepEqual(actual, expected) { 47 | t.Errorf("(&GenmaiModelType{}).FieldTypeMap() => %#v, want %#v", actual, expected) 48 | } 49 | } 50 | 51 | func TestGenmaiModelType_TemplatePath(t *testing.T) { 52 | _, filename, _, _ := runtime.Caller(0) 53 | basepath := filepath.Dir(filename) 54 | path1, path2 := (&GenmaiModelType{}).TemplatePath() 55 | actual := path1 56 | expected := filepath.Join(basepath, "skeleton", "model", "genmai", "genmai.go"+util.TemplateSuffix) 57 | if !reflect.DeepEqual(actual, expected) { 58 | t.Errorf("(&GenmaiModelType{}).TemplatePath() => %#v, $, want %#v, $", actual, expected) 59 | } 60 | actual = path2 61 | expected = filepath.Join(basepath, "skeleton", "model", "genmai", "config.go"+util.TemplateSuffix) 62 | if !reflect.DeepEqual(actual, expected) { 63 | t.Errorf("(&GenmaiModelType{}).TemplatePath() => $, %#v, want $, %#v", actual, expected) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-model/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/naoina/kocha/util" 10 | ) 11 | 12 | const ( 13 | defaultORM = "genmai" 14 | ) 15 | 16 | var modelTypeMap = map[string]ModelTyper{ 17 | "genmai": &GenmaiModelType{}, 18 | } 19 | 20 | type generateModelCommand struct { 21 | option struct { 22 | ORM string `short:"o" long:"orm"` 23 | Help bool `short:"h" long:"help"` 24 | } 25 | } 26 | 27 | func (c *generateModelCommand) Name() string { 28 | return "kocha generate model" 29 | } 30 | 31 | func (c *generateModelCommand) Usage() string { 32 | return fmt.Sprintf(`Usage: %s [OPTIONS] NAME [[field:type]...] 33 | 34 | Options: 35 | -o, --orm=ORM ORM to be used for a model [default: "%s"] 36 | -h, --help display this help and exit 37 | 38 | `, c.Name(), defaultORM) 39 | } 40 | 41 | func (c *generateModelCommand) Option() interface{} { 42 | return &c.option 43 | } 44 | 45 | // Run generates model templates. 46 | func (c *generateModelCommand) Run(args []string) error { 47 | if len(args) < 1 || args[0] == "" { 48 | return fmt.Errorf("no NAME given") 49 | } 50 | name := args[0] 51 | if c.option.ORM == "" { 52 | c.option.ORM = defaultORM 53 | } 54 | mt, exists := modelTypeMap[c.option.ORM] 55 | if !exists { 56 | return fmt.Errorf("unsupported ORM: `%v'", c.option.ORM) 57 | } 58 | m := mt.FieldTypeMap() 59 | var fields []modelField 60 | for _, arg := range args[1:] { 61 | input := strings.Split(arg, ":") 62 | if len(input) != 2 { 63 | return fmt.Errorf("invalid argument format is specified: `%v'", arg) 64 | } 65 | name, t := input[0], input[1] 66 | if name == "" { 67 | return fmt.Errorf("field name isn't specified: `%v'", arg) 68 | } 69 | if t == "" { 70 | return fmt.Errorf("field type isn't specified: `%v'", arg) 71 | } 72 | ft, found := m[t] 73 | if !found { 74 | return fmt.Errorf("unsupported field type: `%v'", t) 75 | } 76 | fields = append(fields, modelField{ 77 | Name: util.ToCamelCase(name), 78 | Type: ft.Name, 79 | Column: util.ToSnakeCase(name), 80 | OptionTags: ft.OptionTags, 81 | }) 82 | } 83 | camelCaseName := util.ToCamelCase(name) 84 | snakeCaseName := util.ToSnakeCase(name) 85 | data := map[string]interface{}{ 86 | "Name": camelCaseName, 87 | "Fields": fields, 88 | } 89 | templatePath, configTemplatePath := mt.TemplatePath() 90 | if err := util.CopyTemplate(templatePath, filepath.Join("app", "model", snakeCaseName+".go"), data); err != nil { 91 | return err 92 | } 93 | initPath := filepath.Join("db", "config.go") 94 | if _, err := os.Stat(initPath); os.IsNotExist(err) { 95 | if err := util.CopyTemplate(configTemplatePath, initPath, nil); err != nil { 96 | return err 97 | } 98 | } 99 | return nil 100 | } 101 | 102 | type modelField struct { 103 | Name string 104 | Type string 105 | Column string 106 | OptionTags []string 107 | } 108 | 109 | func main() { 110 | util.RunCommand(&generateModelCommand{}) 111 | } 112 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-model/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func Test_generateModelCommand_Run(t *testing.T) { 13 | // test for no arguments. 14 | func() { 15 | c := &generateModelCommand{} 16 | args := []string{} 17 | err := c.Run(args) 18 | var actual interface{} = err 19 | var expect interface{} = fmt.Errorf("no NAME given") 20 | if !reflect.DeepEqual(actual, expect) { 21 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 22 | } 23 | }() 24 | 25 | // test for unsupported ORM. 26 | func() { 27 | c := &generateModelCommand{} 28 | c.option.ORM = "invalid" 29 | args := []string{"app_model"} 30 | err := c.Run(args) 31 | var actual interface{} = err 32 | var expect interface{} = fmt.Errorf("unsupported ORM: `invalid'") 33 | if !reflect.DeepEqual(actual, expect) { 34 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 35 | } 36 | }() 37 | 38 | // test for invalid argument formats. 39 | func() { 40 | c := &generateModelCommand{} 41 | c.option.ORM = "" 42 | for _, v := range []struct { 43 | arg string 44 | expect interface{} 45 | }{ 46 | {"field", fmt.Errorf("invalid argument format is specified: `field'")}, 47 | {":", fmt.Errorf("field name isn't specified: `:'")}, 48 | {"field:", fmt.Errorf("field type isn't specified: `field:'")}, 49 | {":type", fmt.Errorf("field name isn't specified: `:type'")}, 50 | {":string", fmt.Errorf("field name isn't specified: `:string'")}, 51 | {"", fmt.Errorf("invalid argument format is specified: `'")}, 52 | } { 53 | func() { 54 | args := []string{"app_model", v.arg} 55 | err := c.Run(args) 56 | var actual interface{} = err 57 | var expect interface{} = v.expect 58 | if !reflect.DeepEqual(actual, expect) { 59 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 60 | } 61 | }() 62 | } 63 | }() 64 | 65 | // test for default ORM. 66 | func() { 67 | tempdir, err := ioutil.TempDir("", "TestModelGeneratorGenerate") 68 | if err != nil { 69 | panic(err) 70 | } 71 | defer os.RemoveAll(tempdir) 72 | os.Chdir(tempdir) 73 | f, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModePerm) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | defer f.Close() 78 | oldStdout, oldStderr := os.Stdout, os.Stderr 79 | os.Stdout, os.Stderr = f, f 80 | defer func() { 81 | os.Stdout, os.Stderr = oldStdout, oldStderr 82 | }() 83 | c := &generateModelCommand{} 84 | args := []string{"app_model"} 85 | err = c.Run(args) 86 | var actual interface{} = err 87 | var expect interface{} = nil 88 | if !reflect.DeepEqual(actual, expect) { 89 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 90 | } 91 | 92 | expect = filepath.Join("app", "model", "app_model.go") 93 | if _, err := os.Stat(expect.(string)); os.IsNotExist(err) { 94 | t.Errorf("generate(%#v); file %#v is not exists; want exists", args, expect) 95 | } 96 | }() 97 | } 98 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-model/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // ModelTyper is an interface for a model type. 4 | type ModelTyper interface { 5 | // FieldTypeMap returns type map for ORM. 6 | FieldTypeMap() map[string]ModelFieldType 7 | 8 | // TemplatePath returns paths that templates of ORM for model generation. 9 | TemplatePath() (templatePath string, configTemplatePath string) 10 | } 11 | 12 | type ModelFieldType struct { 13 | Name string 14 | OptionTags []string 15 | } 16 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-model/skeleton/model/genmai/config.go.tmpl: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/naoina/genmai" 8 | "github.com/naoina/kocha" 9 | 10 | _ "github.com/go-sql-driver/mysql" 11 | // _ "github.com/lib/pq" 12 | _ "github.com/mattn/go-sqlite3" 13 | ) 14 | 15 | var DatabaseMap = map[string]struct { 16 | Driver string 17 | DSN string 18 | }{ 19 | "default": { 20 | Driver: kocha.Getenv("KOCHA_DB_DRIVER", "sqlite3"), 21 | DSN: kocha.Getenv("KOCHA_DB_DSN", filepath.Join("db", "db.sqlite3")), 22 | }, 23 | } 24 | 25 | var dbMap = make(map[string]*genmai.DB) 26 | 27 | func Get(name string) *genmai.DB { 28 | return dbMap[name] 29 | } 30 | 31 | func init() { 32 | for name, dbconf := range DatabaseMap { 33 | var d genmai.Dialect 34 | switch dbconf.Driver { 35 | case "mysql": 36 | d = &genmai.MySQLDialect{} 37 | case "postgres": 38 | d = &genmai.PostgresDialect{} 39 | case "sqlite3": 40 | d = &genmai.SQLite3Dialect{} 41 | default: 42 | panic(fmt.Errorf("kocha: genmai: unsupported driver type: %v", dbconf.Driver)) 43 | } 44 | db, err := genmai.New(d, dbconf.DSN) 45 | if err != nil { 46 | panic(err) 47 | } 48 | dbMap[name] = db 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-model/skeleton/model/genmai/genmai.go.tmpl: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/naoina/genmai" 5 | ) 6 | 7 | type {{.Name}} struct { 8 | Id int64 `db:"pk" json:"id"` 9 | {{range .Fields}}{{.Name}} {{.Type}} `{{range .OptionTags}}{{.}} {{end}}json:"{{.Column}}"` 10 | {{end}} 11 | genmai.TimeStamp 12 | } 13 | 14 | func (m *{{.Name}}) BeforeInsert() error { 15 | // FIXME: This method is auto-generated by Kocha. 16 | // You can remove this method if unneeded. 17 | return m.TimeStamp.BeforeInsert() 18 | } 19 | 20 | func (m *{{.Name}}) AfterInsert() error { 21 | // FIXME: This method is auto-generated by Kocha. 22 | // You can remove this method if unneeded. 23 | return nil 24 | } 25 | 26 | func (m *{{.Name}}) BeforeUpdate() error { 27 | // FIXME: This method is auto-generated by Kocha. 28 | // You can remove this method if unneeded. 29 | return m.TimeStamp.BeforeUpdate() 30 | } 31 | 32 | func (m *{{.Name}}) AfterUpdate() error { 33 | // FIXME: This method is auto-generated by Kocha. 34 | // You can remove this method if unneeded. 35 | return nil 36 | } 37 | 38 | func (m *{{.Name}}) BeforeDelete() error { 39 | // FIXME: This method is auto-generated by Kocha. 40 | // You can remove this method if unneeded. 41 | return nil 42 | } 43 | 44 | func (m *{{.Name}}) AfterDelete() error { 45 | // FIXME: This method is auto-generated by Kocha. 46 | // You can remove this method if unneeded. 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-unit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "path/filepath" 7 | "runtime" 8 | 9 | "github.com/naoina/kocha/util" 10 | ) 11 | 12 | type generateUnitCommand struct { 13 | option struct { 14 | Help bool `short:"h" long:"help"` 15 | } 16 | } 17 | 18 | func (c *generateUnitCommand) Name() string { 19 | return "kocha generate unit" 20 | } 21 | 22 | func (c *generateUnitCommand) Usage() string { 23 | return fmt.Sprintf(`Usage: %s [OPTIONS] NAME 24 | 25 | Generate the skeleton files of unit. 26 | 27 | Options: 28 | -h, --help display this help and exit 29 | 30 | `, c.Name()) 31 | } 32 | 33 | func (c *generateUnitCommand) Option() interface{} { 34 | return &c.option 35 | } 36 | 37 | // Run generates unit skeleton files. 38 | func (c *generateUnitCommand) Run(args []string) error { 39 | if len(args) < 1 || args[0] == "" { 40 | return fmt.Errorf("no NAME given") 41 | } 42 | name := args[0] 43 | camelCaseName := util.ToCamelCase(name) 44 | snakeCaseName := util.ToSnakeCase(name) 45 | data := map[string]interface{}{ 46 | "Name": camelCaseName, 47 | } 48 | if err := util.CopyTemplate( 49 | filepath.Join(skeletonDir("unit"), "unit.go"+util.TemplateSuffix), 50 | filepath.Join("app", "unit", snakeCaseName+".go"), data); err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | 56 | func skeletonDir(name string) string { 57 | _, filename, _, _ := runtime.Caller(0) 58 | baseDir := filepath.Dir(filename) 59 | return filepath.Join(baseDir, "skeleton", name) 60 | } 61 | 62 | func main() { 63 | util.RunCommand(&generateUnitCommand{}) 64 | } 65 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-unit/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func Test_generateUnitCommand_Run(t *testing.T) { 13 | func() { 14 | c := &generateUnitCommand{} 15 | args := []string{} 16 | err := c.Run(args) 17 | var actual interface{} = err 18 | var expect interface{} = fmt.Errorf("no NAME given") 19 | if !reflect.DeepEqual(actual, expect) { 20 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 21 | } 22 | }() 23 | 24 | func() { 25 | tempdir, err := ioutil.TempDir("", "TestGenerateUnit") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | defer os.RemoveAll(tempdir) 30 | os.Chdir(tempdir) 31 | f, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModePerm) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer f.Close() 36 | oldStdout, oldStderr := os.Stdout, os.Stderr 37 | os.Stdout, os.Stderr = f, f 38 | defer func() { 39 | os.Stdout, os.Stderr = oldStdout, oldStderr 40 | }() 41 | c := &generateUnitCommand{} 42 | args := []string{"app_unit"} 43 | err = c.Run(args) 44 | var actual interface{} = err 45 | var expect interface{} = nil 46 | if !reflect.DeepEqual(actual, expect) { 47 | t.Errorf(`generate(%#v) => %#v; want %#v`, args, actual, expect) 48 | } 49 | 50 | expect = filepath.Join("app", "unit", "app_unit.go") 51 | if _, err := os.Stat(expect.(string)); os.IsNotExist(err) { 52 | t.Errorf("generate(%#v); file %#v is not exists; want exists", args, expect) 53 | } 54 | }() 55 | } 56 | -------------------------------------------------------------------------------- /cmd/kocha-generate/kocha-generate-unit/skeleton/unit/unit.go.tmpl: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | type {{.Name}}Unit struct{} 4 | 5 | func (u *{{.Name}}Unit) ActiveIf() bool { 6 | // FIXME: auto-generated by Kocha. 7 | return true 8 | } 9 | -------------------------------------------------------------------------------- /cmd/kocha-generate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | "go/build" 11 | 12 | "github.com/naoina/kocha/util" 13 | ) 14 | 15 | const ( 16 | generatorPrefix = "kocha-generate-" 17 | ) 18 | 19 | type generateCommand struct { 20 | option struct { 21 | Help bool `short:"h" long:"help"` 22 | } 23 | } 24 | 25 | func (c *generateCommand) Name() string { 26 | return "kocha generate" 27 | } 28 | 29 | func (c *generateCommand) Usage() string { 30 | return fmt.Sprintf(`Usage: %s [OPTIONS] GENERATOR [argument...] 31 | 32 | Generate the skeleton files. 33 | 34 | Generators: 35 | controller 36 | migration 37 | model 38 | unit 39 | 40 | Options: 41 | -h, --help display this help and exit 42 | 43 | `, c.Name()) 44 | } 45 | 46 | func (c *generateCommand) Option() interface{} { 47 | return &c.option 48 | } 49 | 50 | // Run execute the process for `generate` command. 51 | func (c *generateCommand) Run(args []string) error { 52 | if len(args) < 1 || args[0] == "" { 53 | return fmt.Errorf("no GENERATOR given") 54 | } 55 | name := args[0] 56 | var paths []string 57 | for _, dir := range build.Default.SrcDirs() { 58 | paths = append(paths, filepath.Clean(filepath.Join(filepath.Dir(dir), "bin"))) 59 | } 60 | paths = append(paths, filepath.SplitList(os.Getenv("PATH"))...) 61 | if err := os.Setenv("PATH", strings.Join(paths, string(filepath.ListSeparator))); err != nil { 62 | return err 63 | } 64 | filename, err := exec.LookPath(generatorPrefix + name) 65 | if err != nil { 66 | return fmt.Errorf("could not found generator: %s", name) 67 | } 68 | cmd := exec.Command(filename, args[1:]...) 69 | cmd.Stdin = os.Stdin 70 | cmd.Stdout = os.Stdout 71 | cmd.Stderr = os.Stderr 72 | return cmd.Run() 73 | } 74 | 75 | func main() { 76 | util.RunCommand(&generateCommand{}) 77 | } 78 | -------------------------------------------------------------------------------- /cmd/kocha-generate/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func Test_generateCommand_Run_withNoAPPPATHGiven(t *testing.T) { 10 | c := &generateCommand{} 11 | args := []string{} 12 | err := c.Run(args) 13 | var actual interface{} = err 14 | var expect interface{} = fmt.Errorf("no GENERATOR given") 15 | if !reflect.DeepEqual(actual, expect) { 16 | t.Errorf(`run(%#v) => %#v; want %#v`, args, actual, expect) 17 | } 18 | } 19 | 20 | func Test_generateCommand_Run_withUnknownGenerator(t *testing.T) { 21 | c := &generateCommand{} 22 | args := []string{"unknown"} 23 | err := c.Run(args) 24 | var actual interface{} = err 25 | var expect interface{} = fmt.Errorf("could not found generator: unknown") 26 | if !reflect.DeepEqual(actual, expect) { 27 | t.Errorf(`run(%#v) => %#v; want %#v`, args, actual, expect) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/kocha-new/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "go/build" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/naoina/kocha/util" 13 | ) 14 | 15 | type newCommand struct { 16 | option struct { 17 | Help bool `short:"h" long:"help"` 18 | } 19 | } 20 | 21 | func (c *newCommand) Name() string { 22 | return "kocha new" 23 | } 24 | 25 | func (c *newCommand) Usage() string { 26 | return fmt.Sprintf(`Usage: %s [OPTIONS] APP_PATH 27 | 28 | Create a new application. 29 | 30 | Options: 31 | -h, --help display this help and exit 32 | 33 | `, c.Name()) 34 | } 35 | 36 | func (c *newCommand) Option() interface{} { 37 | return &c.option 38 | } 39 | 40 | func (c *newCommand) Run(args []string) error { 41 | if len(args) < 1 || args[0] == "" { 42 | return fmt.Errorf("no APP_PATH given") 43 | } 44 | appPath := args[0] 45 | dstBasePath := filepath.Join(filepath.SplitList(build.Default.GOPATH)[0], "src", appPath) 46 | _, filename, _, _ := runtime.Caller(0) 47 | baseDir := filepath.Dir(filename) 48 | skeletonDir := filepath.Join(baseDir, "skeleton", "new") 49 | if _, err := os.Stat(filepath.Join(dstBasePath, "config", "app.go")); err == nil { 50 | return fmt.Errorf("Kocha application is already exists") 51 | } 52 | data := map[string]interface{}{ 53 | "appName": filepath.Base(appPath), 54 | "appPath": appPath, 55 | "secretKey": fmt.Sprintf("%q", base64.StdEncoding.EncodeToString(util.GenerateRandomKey(32))), // AES-256 56 | "signedKey": fmt.Sprintf("%q", base64.StdEncoding.EncodeToString(util.GenerateRandomKey(16))), 57 | } 58 | return filepath.Walk(skeletonDir, func(path string, info os.FileInfo, err error) error { 59 | if err != nil { 60 | return err 61 | } 62 | if info.IsDir() { 63 | return nil 64 | } 65 | dstPath := filepath.Join(dstBasePath, strings.TrimSuffix(strings.TrimPrefix(path, skeletonDir), util.TemplateSuffix)) 66 | dstDir := filepath.Dir(dstPath) 67 | dirCreated, err := mkdirAllIfNotExists(dstDir) 68 | if err != nil { 69 | return fmt.Errorf("failed to create directory: %v", err) 70 | } 71 | if dirCreated { 72 | util.PrintCreateDirectory(dstDir) 73 | } 74 | return util.CopyTemplate(path, dstPath, data) 75 | }) 76 | } 77 | 78 | func mkdirAllIfNotExists(dstDir string) (created bool, err error) { 79 | if _, err := os.Stat(dstDir); os.IsNotExist(err) { 80 | if err := os.MkdirAll(dstDir, 0755); err != nil { 81 | return false, err 82 | } 83 | return true, nil 84 | } 85 | return false, nil 86 | } 87 | 88 | func main() { 89 | util.RunCommand(&newCommand{}) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/kocha-new/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "sort" 11 | "testing" 12 | ) 13 | 14 | func Test_newCommand_Run_withNoAPPPATHGiven(t *testing.T) { 15 | c := &newCommand{} 16 | args := []string{} 17 | err := c.Run(args) 18 | var actual interface{} = err 19 | var expect interface{} = fmt.Errorf("no APP_PATH given") 20 | if !reflect.DeepEqual(actual, expect) { 21 | t.Errorf(`run(%#v) => %#v; want %#v`, args, actual, expect) 22 | } 23 | } 24 | 25 | func Test_newCommand_Run_withAlreadyExists(t *testing.T) { 26 | tempdir, err := ioutil.TempDir("", "TestRunWithAlreadyExists") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer os.RemoveAll(tempdir) 31 | appPath := filepath.Base(tempdir) 32 | dstPath := filepath.Join(tempdir, "src", appPath) 33 | configDir := filepath.Join(dstPath, "config") 34 | if err := os.MkdirAll(configDir, 0755); err != nil { 35 | t.Fatal(err) 36 | } 37 | if err := ioutil.WriteFile(filepath.Join(configDir, "app.go"), nil, 0644); err != nil { 38 | t.Fatal(err) 39 | } 40 | origGOPATH := build.Default.GOPATH 41 | defer func() { 42 | build.Default.GOPATH = origGOPATH 43 | }() 44 | build.Default.GOPATH = tempdir + string(filepath.ListSeparator) + build.Default.GOPATH 45 | c := &newCommand{} 46 | args := []string{appPath} 47 | err = c.Run(args) 48 | var actual interface{} = err 49 | var expect interface{} = fmt.Errorf("Kocha application is already exists") 50 | if !reflect.DeepEqual(actual, expect) { 51 | t.Errorf(`run(%#v) => %#v; want %#v`, args, actual, expect) 52 | } 53 | } 54 | 55 | func Test_newCommand_Run(t *testing.T) { 56 | tempdir, err := ioutil.TempDir("", "Test_newRun") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer os.RemoveAll(tempdir) 61 | appPath := filepath.Base(tempdir) 62 | dstPath := filepath.Join(tempdir, "src", appPath) 63 | f, err := os.OpenFile(os.DevNull, os.O_WRONLY, os.ModePerm) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | defer f.Close() 68 | oldStdout, oldStderr := os.Stdout, os.Stderr 69 | os.Stdout, os.Stderr = f, f 70 | defer func() { 71 | os.Stdout, os.Stderr = oldStdout, oldStderr 72 | }() 73 | origGOPATH := build.Default.GOPATH 74 | defer func() { 75 | build.Default.GOPATH = origGOPATH 76 | }() 77 | build.Default.GOPATH = tempdir + string(filepath.ListSeparator) + build.Default.GOPATH 78 | c := &newCommand{} 79 | args := []string{appPath} 80 | err = c.Run(args) 81 | var actual interface{} = err 82 | var expect interface{} = nil 83 | if !reflect.DeepEqual(actual, expect) { 84 | t.Errorf(`run(%#v) => %#v; want %#v`, args, actual, expect) 85 | } 86 | 87 | var actuals []string 88 | filepath.Walk(dstPath, func(path string, info os.FileInfo, err error) error { 89 | if err != nil { 90 | panic(err) 91 | } 92 | if info.IsDir() { 93 | return nil 94 | } 95 | actuals = append(actuals, path) 96 | return nil 97 | }) 98 | expects := []string{ 99 | filepath.Join("main.go"), 100 | filepath.Join("app", "controller", "root.go"), 101 | filepath.Join("app", "view", "error", "404.html.tmpl"), 102 | filepath.Join("app", "view", "error", "500.html.tmpl"), 103 | filepath.Join("app", "view", "layout", "app.html.tmpl"), 104 | filepath.Join("app", "view", "root.html.tmpl"), 105 | filepath.Join("config", "app.go"), 106 | filepath.Join("public", "robots.txt"), 107 | } 108 | sort.Strings(actuals) 109 | sort.Strings(expects) 110 | for i, _ := range actuals { 111 | actual := actuals[i] 112 | expected := filepath.Join(dstPath, expects[i]) 113 | if !reflect.DeepEqual(actual, expected) { 114 | t.Errorf(`run(%#v); generated file => %#v; want %#v`, args, actual, expected) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/app/controller/root.go.tmpl: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/naoina/kocha" 5 | ) 6 | 7 | type Root struct { 8 | *kocha.DefaultController 9 | } 10 | 11 | func (r *Root) GET(c *kocha.Context) error { 12 | return c.Render(map[string]interface{}{ 13 | "ControllerName": "Root", 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/app/view/error/404.html.tmpl.tmpl: -------------------------------------------------------------------------------- 1 |

404 Not Found

2 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/app/view/error/500.html.tmpl.tmpl: -------------------------------------------------------------------------------- 1 |

500 Internal Server Error

2 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/app/view/layout/app.html.tmpl.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to Kocha 8 | 9 | 10 | {{"{{"}}yield .{{"}}"}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/app/view/root.html.tmpl.tmpl: -------------------------------------------------------------------------------- 1 |

Welcome to Kocha

2 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/config/app.go.tmpl: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/naoina/kocha" 10 | "github.com/naoina/kocha/log" 11 | ) 12 | 13 | var ( 14 | AppName = "{{.appName}}" 15 | AppConfig = &kocha.Config{ 16 | Addr: kocha.Getenv("KOCHA_ADDR", "127.0.0.1:9100"), 17 | AppPath: rootPath, 18 | AppName: AppName, 19 | DefaultLayout: "app", 20 | Template: &kocha.Template{ 21 | PathInfo: kocha.TemplatePathInfo { 22 | Name: AppName, 23 | Paths: []string{ 24 | filepath.Join(rootPath, "app", "view"), 25 | }, 26 | }, 27 | FuncMap: kocha.TemplateFuncMap{}, 28 | }, 29 | 30 | // Logger settings. 31 | Logger: &kocha.LoggerConfig{ 32 | Writer: os.Stdout, 33 | Formatter: &log.LTSVFormatter{}, 34 | Level: log.INFO, 35 | }, 36 | 37 | // Middlewares. 38 | Middlewares: []kocha.Middleware{ 39 | &kocha.RequestLoggingMiddleware{}, 40 | &kocha.PanicRecoverMiddleware{}, 41 | &kocha.FormMiddleware{}, 42 | &kocha.SessionMiddleware{ 43 | Name: "{{.appName}}_session", 44 | Store: &kocha.SessionCookieStore{ 45 | // AUTO-GENERATED Random keys. DO NOT EDIT. 46 | SecretKey: {{.secretKey}}, 47 | SigningKey: {{.signedKey}}, 48 | }, 49 | 50 | // Expiration of session cookie, in seconds, from now. 51 | // Persistent if -1, For not specify, set 0. 52 | CookieExpires: time.Duration(90) * time.Hour * 24, 53 | 54 | // Expiration of session data, in seconds, from now. 55 | // Perssitent if -1, For not specify, set 0. 56 | SessionExpires: time.Duration(90) * time.Hour * 24, 57 | HttpOnly: false, 58 | }, 59 | &kocha.FlashMiddleware{}, 60 | &kocha.DispatchMiddleware{}, 61 | }, 62 | 63 | MaxClientBodySize: 1024 * 1024 * 10, // 10MB 64 | } 65 | 66 | _, configFileName, _, _ = runtime.Caller(0) 67 | rootPath = filepath.Dir(filepath.Join(configFileName, "..")) 68 | ) 69 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/main.go.tmpl: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "{{.appPath}}/app/controller" 5 | "{{.appPath}}/config" 6 | 7 | "github.com/naoina/kocha" 8 | ) 9 | 10 | func main() { 11 | config.AppConfig.RouteTable = kocha.RouteTable{ 12 | { 13 | Name: "root", 14 | Path: "/", 15 | Controller: &controller.Root{}, 16 | }, 17 | { 18 | Name: "static", 19 | Path: "/*path", 20 | Controller: &kocha.StaticServe{}, 21 | }, 22 | } 23 | if err := kocha.Run(config.AppConfig); err != nil { 24 | panic(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/kocha-new/skeleton/new/public/robots.txt: -------------------------------------------------------------------------------- 1 | # User-Agent: * 2 | # Disallow: / 3 | -------------------------------------------------------------------------------- /cmd/kocha-run/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | 10 | "github.com/naoina/kocha/util" 11 | "github.com/naoina/miyabi" 12 | "gopkg.in/fsnotify.v1" 13 | ) 14 | 15 | type runCommand struct { 16 | option struct { 17 | Help bool `short:"h" long:"help"` 18 | } 19 | } 20 | 21 | func (c *runCommand) Name() string { 22 | return "kocha run" 23 | } 24 | 25 | func (c *runCommand) Usage() string { 26 | return fmt.Sprintf(`Usage: %s [OPTIONS] [IMPORT_PATH] 27 | 28 | Run the your application. 29 | 30 | Options: 31 | -h, --help display this help and exit 32 | 33 | `, c.Name()) 34 | } 35 | 36 | func (c *runCommand) Option() interface{} { 37 | return &c.option 38 | } 39 | 40 | func (c *runCommand) Run(args []string) (err error) { 41 | var basedir string 42 | var importPath string 43 | if len(args) > 0 { 44 | importPath = args[0] 45 | basedir, err = util.FindAbsDir(importPath) 46 | if err != nil { 47 | c, err := execCmd("go", "get", "-v", importPath) 48 | if err != nil { 49 | return err 50 | } 51 | if err := c.Wait(); err != nil { 52 | c.Process.Kill() 53 | return err 54 | } 55 | basedir, err = util.FindAbsDir(importPath) 56 | if err != nil { 57 | return err 58 | } 59 | } 60 | } else { 61 | basedir, err = os.Getwd() 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | execName := filepath.Base(basedir) 67 | if runtime.GOOS == "windows" { 68 | execName += ".exe" 69 | } 70 | if err := util.PrintEnv(basedir); err != nil { 71 | return err 72 | } 73 | fmt.Println("Starting...") 74 | var cmd *exec.Cmd 75 | for { 76 | if cmd != nil { 77 | if err := cmd.Process.Signal(miyabi.ShutdownSignal); err != nil { 78 | cmd.Process.Kill() 79 | } 80 | if err := cmd.Wait(); err != nil { 81 | fmt.Fprintln(os.Stderr, err) 82 | } 83 | } 84 | newCmd, err := runApp(basedir, execName, importPath) 85 | if err != nil { 86 | fmt.Fprint(os.Stderr, err) 87 | } 88 | fmt.Println() 89 | cmd = newCmd 90 | if err := watchApp(basedir, execName); err != nil { 91 | if err := cmd.Process.Signal(miyabi.ShutdownSignal); err != nil { 92 | cmd.Process.Kill() 93 | } 94 | return err 95 | } 96 | fmt.Println("\nRestarting...") 97 | } 98 | } 99 | 100 | func runApp(basedir, execName, importPath string) (*exec.Cmd, error) { 101 | execPath := filepath.Join(basedir, execName) 102 | execArgs := []string{"build", "-o", execPath} 103 | // if runtime.GOARCH == "amd64" { 104 | // execArgs = append(execArgs, "-race") 105 | // } 106 | execArgs = append(execArgs, importPath) 107 | c, err := execCmd("go", execArgs...) 108 | if err != nil { 109 | return nil, err 110 | } 111 | if err := c.Wait(); err != nil { 112 | c.Process.Kill() 113 | return nil, err 114 | } 115 | c, err = execCmd(execPath) 116 | if err != nil { 117 | c.Process.Kill() 118 | } 119 | return c, err 120 | } 121 | 122 | func watchApp(basedir, execName string) error { 123 | watcher, err := fsnotify.NewWatcher() 124 | if err != nil { 125 | return err 126 | } 127 | defer watcher.Close() 128 | watchFunc := func(path string, info os.FileInfo, err error) error { 129 | if err != nil { 130 | return err 131 | } 132 | if info.Name()[0] == '.' { 133 | if info.IsDir() { 134 | return filepath.SkipDir 135 | } 136 | return nil 137 | } 138 | if err := watcher.Add(path); err != nil { 139 | return err 140 | } 141 | return nil 142 | } 143 | for _, path := range []string{ 144 | "app", "config", "main.go", 145 | } { 146 | if err := filepath.Walk(filepath.Join(basedir, path), watchFunc); err != nil { 147 | return err 148 | } 149 | } 150 | select { 151 | case <-watcher.Events: 152 | case err := <-watcher.Errors: 153 | return err 154 | } 155 | return nil 156 | } 157 | 158 | func execCmd(name string, args ...string) (*exec.Cmd, error) { 159 | cmd := exec.Command(name, args...) 160 | cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr 161 | if err := cmd.Start(); err != nil { 162 | return nil, err 163 | } 164 | return cmd, nil 165 | } 166 | 167 | func main() { 168 | util.RunCommand(&runCommand{}) 169 | } 170 | -------------------------------------------------------------------------------- /cmd/kocha-run/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_runCommand_Run(t *testing.T) { 6 | // The below tests do not end because run() have an infinite loop. 7 | // Any ideas? 8 | 9 | // func() { 10 | // tempDir, err := ioutil.TempDir("", "Test_runCommand_Run") 11 | // if err != nil { 12 | // t.Fatal(err) 13 | // } 14 | // defer os.RemoveAll(tempDir) 15 | // if err := os.Chdir(tempDir); err != nil { 16 | // t.Fatal(err) 17 | // } 18 | // if err := ioutil.WriteFile(filepath.Join(tempDir, "dev.go"), []byte(` 19 | // package main 20 | // func main() { panic("expected panic") } 21 | // `), 0644); err != nil { 22 | // t.Fatal(err) 23 | // } 24 | // cmd := &runCommand{} 25 | // flags := flag.NewFlagSet("testflags", flag.ExitOnError) 26 | // cmd.DefineFlags(flags) 27 | // flags.Parse([]string{}) 28 | // defer func() { 29 | // if err := recover(); err == nil { 30 | // t.Error("Expect panic, but not occurred") 31 | // } 32 | // }() 33 | // cmd.Run() 34 | // }() 35 | 36 | // func() { 37 | // tempDir, err := ioutil.TempDir("", "Test_runCommand_Run") 38 | // if err != nil { 39 | // t.Fatal(err) 40 | // } 41 | // defer os.RemoveAll(tempDir) 42 | // if err := os.Chdir(tempDir); err != nil { 43 | // t.Fatal(err) 44 | // } 45 | // if err := ioutil.WriteFile(filepath.Join(tempDir, "dev.go"), []byte(` 46 | // package main 47 | // func main() {} 48 | // `), 0644); err != nil { 49 | // t.Fatal(err) 50 | // } 51 | // cmd := &runCommand{} 52 | // flags := flag.NewFlagSet("testflags", flag.ExitOnError) 53 | // cmd.DefineFlags(flags) 54 | // flags.Parse([]string{}) 55 | // cmd.Run() 56 | // binName := filepath.Base(tempDir) 57 | // if _, err := os.Stat(filepath.Join(tempDir, binName)); err != nil { 58 | // t.Error("Expect %v is exists, but not", binName) 59 | // } 60 | // }() 61 | } 62 | -------------------------------------------------------------------------------- /cmd/kocha/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/naoina/kocha/util" 12 | ) 13 | 14 | const ( 15 | commandPrefix = "kocha-" 16 | ) 17 | 18 | var ( 19 | aliases = map[string]string{ 20 | "g": "generate", 21 | "b": "build", 22 | } 23 | ) 24 | 25 | type kochaCommand struct { 26 | option struct { 27 | Help bool `short:"h" long:"help"` 28 | } 29 | } 30 | 31 | func (c *kochaCommand) Name() string { 32 | return filepath.Base(os.Args[0]) 33 | } 34 | 35 | func (c *kochaCommand) Usage() string { 36 | return fmt.Sprintf(`Usage: %s [OPTIONS] COMMAND [argument...] 37 | 38 | Commands: 39 | new create a new application 40 | generate generate files (alias: "g") 41 | build build your application (alias: "b") 42 | run run the your application 43 | migrate run the migrations 44 | 45 | Options: 46 | -h, --help display this help and exit 47 | 48 | `, c.Name()) 49 | } 50 | 51 | func (c *kochaCommand) Option() interface{} { 52 | return &c.option 53 | } 54 | 55 | // run runs the subcommand specified by the argument. 56 | // run is the launcher of another command actually. It will find a subcommand 57 | // from $GOROOT/bin, $GOPATH/bin and $PATH, and then run it. 58 | // If subcommand is not found, run prints the usage and exit. 59 | func (c *kochaCommand) Run(args []string) error { 60 | if len(args) < 1 || args[0] == "" { 61 | return fmt.Errorf("no COMMAND given") 62 | } 63 | var paths []string 64 | for _, dir := range build.Default.SrcDirs() { 65 | paths = append(paths, filepath.Clean(filepath.Join(filepath.Dir(dir), "bin"))) 66 | } 67 | paths = append(paths, filepath.SplitList(os.Getenv("PATH"))...) 68 | if err := os.Setenv("PATH", strings.Join(paths, string(filepath.ListSeparator))); err != nil { 69 | return err 70 | } 71 | name := args[0] 72 | if n, exists := aliases[name]; exists { 73 | name = n 74 | } 75 | filename, err := exec.LookPath(commandPrefix + name) 76 | if err != nil { 77 | return fmt.Errorf("command not found: %s", name) 78 | } 79 | cmd := exec.Command(filename, args[1:]...) 80 | cmd.Stdin = os.Stdin 81 | cmd.Stdout = os.Stdout 82 | cmd.Stderr = os.Stderr 83 | return cmd.Run() 84 | } 85 | 86 | func main() { 87 | util.RunCommand(&kochaCommand{}) 88 | } 89 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "strconv" 7 | 8 | "github.com/naoina/kocha/event" 9 | ) 10 | 11 | // EventHandlerMap represents a map of event handlers. 12 | type EventHandlerMap map[event.Queue]map[string]func(app *Application, args ...interface{}) error 13 | 14 | // Evevnt represents the event. 15 | type Event struct { 16 | // HandlerMap is a map of queue/handlers. 17 | HandlerMap EventHandlerMap 18 | 19 | // WorkersPerQueue is a number of workers per queue. 20 | // The default value is taken from GOMAXPROCS. 21 | // If value of GOMAXPROCS is invalid, set to 1. 22 | WorkersPerQueue int 23 | 24 | // ErrorHandler is the handler for error. 25 | // If you want to use your own error handler, please set to ErrorHandler. 26 | ErrorHandler func(err interface{}) 27 | 28 | e *event.Event 29 | app *Application 30 | } 31 | 32 | // Trigger emits the event. 33 | // The name is an event name that is defined in e.HandlerMap. 34 | // If args given, they will be passed to event handler that is defined in e.HandlerMap. 35 | func (e *Event) Trigger(name string, args ...interface{}) error { 36 | return e.e.Trigger(name, args...) 37 | } 38 | 39 | func (e *Event) addHandler(name string, queueName string, handler func(app *Application, args ...interface{}) error) error { 40 | return e.e.AddHandler(name, queueName, func(args ...interface{}) error { 41 | return handler(e.app, args...) 42 | }) 43 | } 44 | 45 | func (e *Event) build(app *Application) (*Event, error) { 46 | if e == nil { 47 | e = &Event{} 48 | } 49 | e.e = event.New() 50 | for queue, handlerMap := range e.HandlerMap { 51 | queueName := reflect.TypeOf(queue).String() 52 | if err := e.e.RegisterQueue(queueName, queue); err != nil { 53 | return nil, err 54 | } 55 | for name, handler := range handlerMap { 56 | if err := e.addHandler(name, queueName, handler); err != nil { 57 | return nil, err 58 | } 59 | } 60 | } 61 | n := e.WorkersPerQueue 62 | if n < 1 { 63 | if n, _ = strconv.Atoi(os.Getenv("GOMAXPROCS")); n < 1 { 64 | n = 1 65 | } 66 | } 67 | e.e.SetWorkersPerQueue(n) 68 | e.e.ErrorHandler = e.ErrorHandler 69 | return e, nil 70 | } 71 | 72 | func (e *Event) start() { 73 | e.e.Start() 74 | } 75 | 76 | func (e *Event) stop() { 77 | e.e.Stop() 78 | } 79 | -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | ) 8 | 9 | var ( 10 | // DefaultEvent is the default event and is used by AddHandler, Trigger, AddQueue, Start and Stop. 11 | DefaultEvent = New() 12 | 13 | // ErrDone represents that a queue is finished. 14 | ErrDone = errors.New("queue is done") 15 | 16 | // ErrNotExist is passed to ErrorHandler if handler not exists. 17 | ErrNotExist = errors.New("handler not exist") 18 | ) 19 | 20 | // AddHandler is shorthand of the DefaultEvent.AddHandler. 21 | func AddHandler(name string, queueName string, handler func(args ...interface{}) error) error { 22 | return DefaultEvent.AddHandler(name, queueName, handler) 23 | } 24 | 25 | // Trigger is shorthand of the DefaultEvent.Trigger. 26 | func Trigger(name string, args ...interface{}) error { 27 | return DefaultEvent.Trigger(name, args...) 28 | } 29 | 30 | // RegisterQueue is shorthand of the DefaultEvent.RegisterQueue. 31 | func RegisterQueue(name string, queue Queue) error { 32 | return DefaultEvent.RegisterQueue(name, queue) 33 | } 34 | 35 | // Start is shorthand of the DefaultEvent.Start. 36 | func Start() { 37 | DefaultEvent.Start() 38 | } 39 | 40 | // Stop is shorthand of the DefaultEvent.Stop. 41 | func Stop() { 42 | DefaultEvent.Stop() 43 | } 44 | 45 | // Event represents an Event. 46 | type Event struct { 47 | // ErrorHandler is the error handler. 48 | // If you want to use your own error handler, set ErrorHandler. 49 | ErrorHandler func(err interface{}) 50 | 51 | workersPerQueue int 52 | queues map[string]Queue 53 | handlerQueues map[string]map[string][]handlerFunc 54 | workers []*worker 55 | wg struct{ enqueue, dequeue sync.WaitGroup } 56 | } 57 | 58 | // New returns a new Event. 59 | func New() *Event { 60 | return &Event{ 61 | workersPerQueue: 1, 62 | queues: make(map[string]Queue), 63 | handlerQueues: make(map[string]map[string][]handlerFunc), 64 | wg: struct{ enqueue, dequeue sync.WaitGroup }{}, 65 | } 66 | } 67 | 68 | // AddHandler adds handlers that related to name and queue. 69 | // The name is an event name such as "log.error" that will be used for Trigger. 70 | // The queueName is a name of queue registered by RegisterQueue in advance. 71 | // If you add handler by name that has already been added, handler will associated 72 | // to that name additionally. 73 | // If queue of queueName still hasn't been registered, it returns error. 74 | func (e *Event) AddHandler(name string, queueName string, handler func(args ...interface{}) error) error { 75 | queue := e.queues[queueName] 76 | if queue == nil { 77 | return fmt.Errorf("kocha: event: queue `%s' isn't registered", queueName) 78 | } 79 | if _, exist := e.handlerQueues[name]; !exist { 80 | e.handlerQueues[name] = make(map[string][]handlerFunc) 81 | } 82 | hq := e.handlerQueues[name] 83 | hq[queueName] = append(hq[queueName], handler) 84 | return nil 85 | } 86 | 87 | // Trigger emits the event. 88 | // The name is an event name. It must be added in advance using AddHandler. 89 | // If Trigger called by not added name, it returns error. 90 | // If args are given, they will be passed to handlers added by AddHandler. 91 | func (e *Event) Trigger(name string, args ...interface{}) error { 92 | hq, exist := e.handlerQueues[name] 93 | if !exist { 94 | return fmt.Errorf("kocha: event: handler `%s' isn't added", name) 95 | } 96 | e.triggerAll(hq, name, args...) 97 | return nil 98 | } 99 | 100 | // RegisterQueue makes a background queue available by the provided name. 101 | // If queue is already registerd or if queue nil, it panics. 102 | func (e *Event) RegisterQueue(name string, queue Queue) error { 103 | if queue == nil { 104 | return fmt.Errorf("kocha: event: Register queue is nil") 105 | } 106 | if _, exist := e.queues[name]; exist { 107 | return fmt.Errorf("kocha: event: Register queue `%s' is already registered", name) 108 | } 109 | e.queues[name] = queue 110 | return nil 111 | } 112 | 113 | func (e *Event) triggerAll(hq map[string][]handlerFunc, name string, args ...interface{}) { 114 | e.wg.enqueue.Add(len(hq)) 115 | for queueName := range hq { 116 | queue := e.queues[queueName] 117 | go func() { 118 | defer e.wg.enqueue.Done() 119 | defer func() { 120 | if err := recover(); err != nil { 121 | if e.ErrorHandler != nil { 122 | e.ErrorHandler(err) 123 | } 124 | } 125 | }() 126 | if err := e.enqueue(queue, payload{name, args}); err != nil { 127 | panic(err) 128 | } 129 | }() 130 | } 131 | } 132 | 133 | // alias. 134 | type handlerFunc func(args ...interface{}) error 135 | 136 | func (e *Event) enqueue(queue Queue, pld payload) error { 137 | var data string 138 | if err := pld.encode(&data); err != nil { 139 | return err 140 | } 141 | return queue.Enqueue(data) 142 | } 143 | 144 | // Start starts background event workers. 145 | // By default, workers per queue is 1. To set the workers per queue, use 146 | // SetWorkersPerQueue before Start calls. 147 | func (e *Event) Start() { 148 | for name, queue := range e.queues { 149 | for i := 0; i < e.workersPerQueue; i++ { 150 | worker := e.newWorker(name, queue.New(e.workersPerQueue)) 151 | e.workers = append(e.workers, worker) 152 | go worker.start() 153 | } 154 | } 155 | } 156 | 157 | // SetWorkersPerQueue sets the number of workers per queue. 158 | // It must be called before Start calls. 159 | func (e *Event) SetWorkersPerQueue(n int) { 160 | if n < 1 { 161 | n = 1 162 | } 163 | e.workersPerQueue = n 164 | } 165 | 166 | // Stop wait for all workers to complete. 167 | func (e *Event) Stop() { 168 | e.wg.enqueue.Wait() 169 | defer func() { 170 | e.workers = nil 171 | }() 172 | defer e.wg.dequeue.Wait() 173 | for _, worker := range e.workers { 174 | worker.stop() 175 | } 176 | } 177 | 178 | type worker struct { 179 | queueName string 180 | queue Queue 181 | e *Event 182 | } 183 | 184 | func (e *Event) newWorker(queueName string, queue Queue) *worker { 185 | return &worker{ 186 | queueName: queueName, 187 | queue: queue, 188 | e: e, 189 | } 190 | } 191 | 192 | func (w *worker) start() { 193 | var done bool 194 | for !done { 195 | func() { 196 | defer func() { 197 | if err := recover(); err != nil { 198 | if w.e.ErrorHandler != nil { 199 | w.e.ErrorHandler(err) 200 | } 201 | } 202 | }() 203 | if err := w.run(); err != nil { 204 | if err == ErrDone { 205 | done = true 206 | return 207 | } 208 | panic(err) 209 | } 210 | }() 211 | } 212 | } 213 | 214 | func (w *worker) run() (err error) { 215 | w.e.wg.dequeue.Add(1) 216 | defer w.e.wg.dequeue.Done() 217 | pld, err := w.dequeue() 218 | if err != nil { 219 | return err 220 | } 221 | hq, exist := w.e.handlerQueues[pld.Name] 222 | if !exist { 223 | return ErrNotExist 224 | } 225 | w.runAll(hq, pld) 226 | return nil 227 | } 228 | 229 | func (w *worker) runAll(hq map[string][]handlerFunc, pld payload) { 230 | for queueName, handlers := range hq { 231 | if w.queueName != queueName { 232 | continue 233 | } 234 | w.e.wg.dequeue.Add(len(handlers)) 235 | for _, h := range handlers { 236 | go func(handler handlerFunc) { 237 | defer w.e.wg.dequeue.Done() 238 | if err := handler(pld.Args...); err != nil { 239 | if w.e.ErrorHandler != nil { 240 | w.e.ErrorHandler(err) 241 | } 242 | } 243 | }(h) 244 | } 245 | } 246 | } 247 | 248 | func (w *worker) dequeue() (pld payload, err error) { 249 | data, err := w.queue.Dequeue() 250 | if err != nil { 251 | return pld, err 252 | } 253 | if err := pld.decode(data); err != nil { 254 | return pld, err 255 | } 256 | return pld, nil 257 | } 258 | 259 | func (w *worker) stop() { 260 | w.queue.Stop() 261 | } 262 | 263 | // Queue is the interface that must be implemeted by background event queue. 264 | type Queue interface { 265 | // New returns a new Queue to launch the workers. 266 | // You can use an argument n as a hint when you create a new queue. 267 | // n is the number of workers per queue. 268 | New(n int) Queue 269 | 270 | // Enqueue add data to the queue. 271 | Enqueue(data string) error 272 | 273 | // Dequeue returns the data that fetch from the queue. 274 | // It will return ErrDone as err when Stop is called. 275 | Dequeue() (data string, err error) 276 | 277 | // Stop wait for Enqueue and/or Dequeue to complete then will stop a queue. 278 | Stop() 279 | } 280 | -------------------------------------------------------------------------------- /event/event_test.go: -------------------------------------------------------------------------------- 1 | package event_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/naoina/kocha/event" 11 | ) 12 | 13 | const ( 14 | queueName = "fakeQueue" 15 | ) 16 | 17 | var stopped []struct{} 18 | 19 | type fakeQueue struct { 20 | c chan string 21 | done chan struct{} 22 | } 23 | 24 | func (q *fakeQueue) New(n int) event.Queue { 25 | return q 26 | } 27 | 28 | func (q *fakeQueue) Enqueue(data string) error { 29 | q.c <- data 30 | return nil 31 | } 32 | 33 | func (q *fakeQueue) Dequeue() (string, error) { 34 | select { 35 | case data := <-q.c: 36 | return data, nil 37 | case <-q.done: 38 | return "", event.ErrDone 39 | } 40 | } 41 | 42 | func (q *fakeQueue) Stop() { 43 | stopped = append(stopped, struct{}{}) 44 | close(q.done) 45 | } 46 | 47 | func TestDefaultEvent(t *testing.T) { 48 | actual := event.DefaultEvent 49 | expect := event.New() 50 | if !reflect.DeepEqual(actual, expect) { 51 | t.Errorf(`DefaultEvent => %#v; want %#v`, actual, expect) 52 | } 53 | } 54 | 55 | func TestEvent_AddHandler(t *testing.T) { 56 | e := event.New() 57 | e.RegisterQueue(queueName, &fakeQueue{c: make(chan string), done: make(chan struct{})}) 58 | 59 | handlerName := "testAddHandler" 60 | for _, v := range []struct { 61 | queueName string 62 | expect error 63 | }{ 64 | {"unknownQueue", fmt.Errorf("kocha: event: queue `unknownQueue' isn't registered")}, 65 | {queueName, nil}, 66 | {queueName, nil}, // testcase for override. 67 | } { 68 | actual := e.AddHandler(handlerName, v.queueName, func(args ...interface{}) error { 69 | return nil 70 | }) 71 | expect := v.expect 72 | if !reflect.DeepEqual(actual, expect) { 73 | t.Errorf("AddHandler(%q, %q, func) => %#v, want %#v", handlerName, v.queueName, actual, expect) 74 | } 75 | } 76 | } 77 | 78 | func TestEvent_Trigger(t *testing.T) { 79 | e := event.New() 80 | e.RegisterQueue(queueName, &fakeQueue{c: make(chan string), done: make(chan struct{})}) 81 | e.Start() 82 | defer e.Stop() 83 | 84 | handlerName := "unknownHandler" 85 | var expect interface{} = fmt.Errorf("kocha: event: handler `unknownHandler' isn't added") 86 | if err := e.Trigger(handlerName); err == nil { 87 | t.Errorf("Trigger(%q) => %#v, want %#v", handlerName, err, expect) 88 | } 89 | 90 | handlerName = "testTrigger" 91 | var actual string 92 | timer := make(chan struct{}) 93 | if err := e.AddHandler(handlerName, queueName, func(args ...interface{}) error { 94 | defer func() { 95 | timer <- struct{}{} 96 | }() 97 | actual += fmt.Sprintf("|call %s(%v)", handlerName, args) 98 | return nil 99 | }); err != nil { 100 | t.Fatal(err) 101 | } 102 | for i := 1; i <= 2; i++ { 103 | if err := e.Trigger(handlerName); err != nil { 104 | t.Errorf("Trigger(%#v) => %#v, want nil", handlerName, err) 105 | } 106 | select { 107 | case <-timer: 108 | case <-time.After(3 * time.Second): 109 | t.Fatalf("Trigger(%q) has try to call handler but hasn't been called within 3 seconds", handlerName) 110 | } 111 | expected := strings.Repeat("|call testTrigger([])", i) 112 | if !reflect.DeepEqual(actual, expected) { 113 | t.Errorf("Trigger(%q) has try to call handler, actual => %#v, want %#v", handlerName, actual, expected) 114 | } 115 | } 116 | 117 | handlerName = "testTriggerWithArgs" 118 | actual = "" 119 | if err := e.AddHandler(handlerName, queueName, func(args ...interface{}) error { 120 | defer func() { 121 | timer <- struct{}{} 122 | }() 123 | actual += fmt.Sprintf("|call %s(%v)", handlerName, args) 124 | return nil 125 | }); err != nil { 126 | t.Fatal(err) 127 | } 128 | for i := 1; i <= 2; i++ { 129 | if err := e.Trigger(handlerName, 1, true, "arg"); err != nil { 130 | t.Errorf("Trigger(%q) => %#v, want nil", handlerName, err) 131 | } 132 | select { 133 | case <-timer: 134 | case <-time.After(3 * time.Second): 135 | t.Fatalf("Trigger(%q) has try to call handler but hasn't been called within 3 seconds", handlerName) 136 | } 137 | expected := strings.Repeat("|call testTriggerWithArgs([1 true arg])", i) 138 | if !reflect.DeepEqual(actual, expected) { 139 | t.Errorf("Trigger(%q) has try to call handler, actual => %#v, want %#v", handlerName, actual, expected) 140 | } 141 | } 142 | 143 | handlerName = "testTriggerWithMultipleHandlers" 144 | actual = "" 145 | actual2 := "" 146 | timer2 := make(chan struct{}) 147 | if err := e.AddHandler(handlerName, queueName, func(args ...interface{}) error { 148 | defer func() { 149 | timer <- struct{}{} 150 | }() 151 | actual += fmt.Sprintf("|call1 %s(%v)", handlerName, args) 152 | return nil 153 | }); err != nil { 154 | t.Fatal(err) 155 | } 156 | if err := e.AddHandler(handlerName, queueName, func(args ...interface{}) error { 157 | defer func() { 158 | timer2 <- struct{}{} 159 | }() 160 | actual2 += fmt.Sprintf("|call2 %s(%v)", handlerName, args) 161 | return nil 162 | }); err != nil { 163 | t.Fatal(err) 164 | } 165 | for i := 1; i <= 2; i++ { 166 | if err := e.Trigger(handlerName); err != nil { 167 | t.Errorf("Trigger(%q) => %#v, want nil", handlerName, err) 168 | } 169 | select { 170 | case <-timer: 171 | case <-time.After(3 * time.Second): 172 | t.Fatalf("Trigger(%q) has try to call handler but hasn't been called within 3 seconds", handlerName) 173 | } 174 | expected := strings.Repeat("|call1 testTriggerWithMultipleHandlers([])", i) 175 | if !reflect.DeepEqual(actual, expected) { 176 | t.Errorf("Trigger(%q) has try to call handler, actual => %#v, want %#v", handlerName, actual, expected) 177 | } 178 | select { 179 | case <-timer2: 180 | case <-time.After(3 * time.Second): 181 | t.Fatalf("Trigger(%q) has try to call handler but hasn't been called within 3 seconds", handlerName) 182 | } 183 | expected = strings.Repeat("|call2 testTriggerWithMultipleHandlers([])", i) 184 | if !reflect.DeepEqual(actual2, expected) { 185 | t.Errorf("Trigger(%q) has try to call handler, actual => %#v, want %#v", handlerName, actual2, expected) 186 | } 187 | } 188 | } 189 | 190 | func TestEvent_RegisterQueue(t *testing.T) { 191 | e := event.New() 192 | for _, v := range []struct { 193 | name string 194 | queue event.Queue 195 | expect error 196 | }{ 197 | {"test_queue", nil, fmt.Errorf("kocha: event: Register queue is nil")}, 198 | {"test_queue", &fakeQueue{}, nil}, 199 | {"test_queue", &fakeQueue{}, fmt.Errorf("kocha: event: Register queue `test_queue' is already registered")}, 200 | } { 201 | actual := e.RegisterQueue(v.name, v.queue) 202 | expect := v.expect 203 | if !reflect.DeepEqual(actual, expect) { 204 | t.Errorf(`Event.RegisterQueue(%q, %#v) => %#v; want %#v`, v.name, v.queue, actual, expect) 205 | } 206 | } 207 | } 208 | 209 | func TestEvent_Stop(t *testing.T) { 210 | e := event.New() 211 | e.RegisterQueue(queueName, &fakeQueue{c: make(chan string), done: make(chan struct{})}) 212 | e.Start() 213 | defer e.Stop() 214 | 215 | stopped = nil 216 | actual := len(stopped) 217 | expected := 0 218 | if !reflect.DeepEqual(actual, expected) { 219 | t.Errorf("len(stopped) before Stop => %#v, want %#v", actual, expected) 220 | } 221 | e.Stop() 222 | actual = len(stopped) 223 | expected = 1 224 | if !reflect.DeepEqual(actual, expected) { 225 | t.Errorf("len(stopped) after Stop => %#v, want %#v", actual, expected) 226 | } 227 | } 228 | 229 | func TestEvent_ErrorHandler(t *testing.T) { 230 | e := event.New() 231 | e.RegisterQueue(queueName, &fakeQueue{c: make(chan string), done: make(chan struct{})}) 232 | e.Start() 233 | defer e.Stop() 234 | 235 | handlerName := "testErrorHandler" 236 | expected := fmt.Errorf("testErrorHandlerError") 237 | if err := e.AddHandler(handlerName, queueName, func(args ...interface{}) error { 238 | return expected 239 | }); err != nil { 240 | t.Fatal(err) 241 | } 242 | called := make(chan struct{}) 243 | e.ErrorHandler = func(err interface{}) { 244 | if !reflect.DeepEqual(err, expected) { 245 | t.Errorf("ErrorHandler called with %#v, want %#v", err, expected) 246 | } 247 | called <- struct{}{} 248 | } 249 | if err := e.Trigger(handlerName); err != nil { 250 | t.Fatal(err) 251 | } 252 | select { 253 | case <-called: 254 | case <-time.After(3 * time.Second): 255 | t.Errorf("ErrorHandler hasn't been called within 3 seconds") 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /event/memory/queue.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import "github.com/naoina/kocha/event" 4 | 5 | // EventQueue implements the Queue interface. 6 | // This doesn't require the external storages such as Redis. 7 | // Note that EventQueue isn't persistent, this means that queued data may be 8 | // lost by crash, shutdown or status of not running. 9 | // If you want to do use a persistent queue, please use another Queue 10 | // implementation that supports persistence. 11 | // Also queue won't be shared between different servers but will be shared 12 | // between other workers in same server. 13 | type EventQueue struct { 14 | c chan string 15 | done chan struct{} 16 | exit chan struct{} 17 | } 18 | 19 | // New returns a new EventQueue. 20 | func (q *EventQueue) New(n int) event.Queue { 21 | if q.c == nil { 22 | q.c = make(chan string, n) 23 | } 24 | if q.done == nil { 25 | q.done = make(chan struct{}) 26 | } 27 | if q.exit == nil { 28 | q.exit = make(chan struct{}) 29 | } 30 | return &EventQueue{ 31 | c: q.c, 32 | done: q.done, 33 | exit: q.exit, 34 | } 35 | } 36 | 37 | // Enqueue adds data to queue. 38 | func (q *EventQueue) Enqueue(data string) error { 39 | q.c <- data 40 | return nil 41 | } 42 | 43 | // Dequeue returns the data that fetch from queue. 44 | func (q *EventQueue) Dequeue() (data string, err error) { 45 | select { 46 | case data = <-q.c: 47 | return data, nil 48 | case <-q.done: 49 | defer func() { 50 | q.exit <- struct{}{} 51 | }() 52 | return "", event.ErrDone 53 | } 54 | } 55 | 56 | // Stop wait for Dequeue to complete then will stop a queue. 57 | func (q *EventQueue) Stop() { 58 | q.done <- struct{}{} 59 | <-q.exit 60 | } 61 | -------------------------------------------------------------------------------- /event/memory/queue_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/naoina/kocha/event" 8 | ) 9 | 10 | func TestEventQueue(t *testing.T) { 11 | e := event.New() 12 | if err := e.RegisterQueue("memory", &EventQueue{}); err != nil { 13 | t.Fatal(err) 14 | } 15 | e.Start() 16 | defer e.Stop() 17 | 18 | handlerName := "testEventQueueHandler" 19 | called := make(chan struct{}) 20 | if err := e.AddHandler(handlerName, "memory", func(args ...interface{}) error { 21 | called <- struct{}{} 22 | return nil 23 | }); err != nil { 24 | t.Fatal(err) 25 | } 26 | if err := e.Trigger(handlerName); err != nil { 27 | t.Errorf("event.Trigger(%q) => %#v, want nil", handlerName, err) 28 | } 29 | select { 30 | case <-called: 31 | case <-time.After(3 * time.Second): 32 | t.Errorf("event.Trigger(%q) has try to call handler but hasn't been called within 3 seconds", handlerName) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /event/payload.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "encoding/json" 4 | 5 | type payload struct { 6 | Name string `json:"name"` 7 | Args []interface{} `json:"args"` 8 | } 9 | 10 | func (p *payload) encode(dest *string) error { 11 | buf, err := json.Marshal(p) 12 | if err != nil { 13 | return err 14 | } 15 | *dest = string(buf) 16 | return nil 17 | } 18 | 19 | func (p *payload) decode(src string) error { 20 | if err := json.Unmarshal([]byte(src), &p); err != nil { 21 | return err 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /flash.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | // Flash represents a container of flash messages. 4 | // Flash is for the one-time messaging between requests. It useful for 5 | // implementing the Post/Redirect/Get pattern. 6 | type Flash map[string]FlashData 7 | 8 | // Get gets a value associated with the given key. 9 | // If there is the no value associated with the key, Get returns "". 10 | func (f Flash) Get(key string) string { 11 | if f == nil { 12 | return "" 13 | } 14 | data, exists := f[key] 15 | if !exists { 16 | return "" 17 | } 18 | data.Loaded = true 19 | f[key] = data 20 | return data.Data 21 | } 22 | 23 | // Set sets the value associated with key. 24 | // It replaces the existing value associated with key. 25 | func (f Flash) Set(key, value string) { 26 | if f == nil { 27 | return 28 | } 29 | data := f[key] 30 | data.Loaded = false 31 | data.Data = value 32 | f[key] = data 33 | } 34 | 35 | // Len returns a length of the dataset. 36 | func (f Flash) Len() int { 37 | return len(f) 38 | } 39 | 40 | // deleteLoaded delete the loaded data. 41 | func (f Flash) deleteLoaded() { 42 | for k, v := range f { 43 | if v.Loaded { 44 | delete(f, k) 45 | } 46 | } 47 | } 48 | 49 | // FlashData represents a data of flash messages. 50 | type FlashData struct { 51 | Data string // flash message. 52 | Loaded bool // whether the message was loaded. 53 | } 54 | -------------------------------------------------------------------------------- /flash_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/naoina/kocha" 8 | ) 9 | 10 | func TestFlash(t *testing.T) { 11 | f := kocha.Flash{} 12 | key := "test_key" 13 | var actual interface{} = f.Get(key) 14 | var expected interface{} = "" 15 | if !reflect.DeepEqual(actual, expected) { 16 | t.Errorf(`Flash.Get(%#v) => %#v; want %#v`, key, actual, expected) 17 | } 18 | actual = f.Len() 19 | expected = 0 20 | if !reflect.DeepEqual(actual, expected) { 21 | t.Errorf(`Flash.Len() => %#v; want %#v`, actual, expected) 22 | } 23 | 24 | value := "test_value" 25 | f.Set(key, value) 26 | actual = f.Len() 27 | expected = 1 28 | if !reflect.DeepEqual(actual, expected) { 29 | t.Errorf(`Flash.Set(%#v, %#v); Flash.Len() => %#v; want %#v`, key, value, actual, expected) 30 | } 31 | 32 | actual = f.Get(key) 33 | expected = value 34 | if !reflect.DeepEqual(actual, expected) { 35 | t.Errorf(`Flash.Set(%#v, %#v); Flash.Get(%#v) => %#v; want %#v`, key, value, key, actual, expected) 36 | } 37 | 38 | key2 := "test_key2" 39 | value2 := "test_value2" 40 | f.Set(key2, value2) 41 | actual = f.Len() 42 | expected = 2 43 | if !reflect.DeepEqual(actual, expected) { 44 | t.Errorf(`Flash.Set(%#v, %#v); Flash.Set(%#v, %#v); Flash.Len() => %#v; want %#v`, key, value, key2, value2, actual, expected) 45 | } 46 | 47 | actual = f.Get(key2) 48 | expected = value2 49 | if !reflect.DeepEqual(actual, expected) { 50 | t.Errorf(`Flash.Set(%#v, %#v); Flash.Set(%#v, %#v); Flash.Get(%#v) => %#v; want %#v`, key, value, key2, value2, key2, actual, expected) 51 | } 52 | } 53 | 54 | func TestFlash_Get_withNil(t *testing.T) { 55 | f := kocha.Flash(nil) 56 | key := "test_key" 57 | var actual interface{} = f.Get(key) 58 | var expected interface{} = "" 59 | if !reflect.DeepEqual(actual, expected) { 60 | t.Errorf(`Flash.Get(%#v) => %#v; want %#v`, key, actual, expected) 61 | } 62 | 63 | actual = f.Len() 64 | expected = 0 65 | if !reflect.DeepEqual(actual, expected) { 66 | t.Errorf(`Flash.Get(%#v); Flash.Len() => %#v; want %#v`, key, actual, expected) 67 | } 68 | } 69 | 70 | func TestFlash_Set_withNil(t *testing.T) { 71 | f := kocha.Flash(nil) 72 | key := "test_key" 73 | value := "test_value" 74 | f.Set(key, value) 75 | actual := f.Len() 76 | expected := 0 77 | if !reflect.DeepEqual(actual, expected) { 78 | t.Errorf(`Flash.Set(%#v, %#v); Flash.Len() => %#v; want %#v`, key, value, actual, expected) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /kocha.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "reflect" 9 | "runtime" 10 | "sync" 11 | 12 | "github.com/joho/godotenv" 13 | "github.com/naoina/kocha/log" 14 | "github.com/naoina/miyabi" 15 | ) 16 | 17 | const ( 18 | // DefaultHttpAddr is the default listen address. 19 | DefaultHttpAddr = "127.0.0.1:9100" 20 | 21 | // DefaultMaxClientBodySize is the maximum size of HTTP request body. 22 | // This can be overridden by setting Config.MaxClientBodySize. 23 | DefaultMaxClientBodySize = 1024 * 1024 * 10 // 10MB 24 | 25 | // StaticDir is the directory of the static files. 26 | StaticDir = "public" 27 | ) 28 | 29 | var ( 30 | nullMiddlewareNext = func() error { 31 | return nil 32 | } 33 | 34 | bufPool = &sync.Pool{ 35 | New: func() interface{} { 36 | return new(bytes.Buffer) 37 | }, 38 | } 39 | ) 40 | 41 | // Run starts Kocha app. 42 | // This will launch the HTTP server by using github.com/naoina/miyabi. 43 | // If you want to use other HTTP server that compatible with net/http such as 44 | // http.ListenAndServe, you can use New. 45 | func Run(config *Config) error { 46 | app, err := New(config) 47 | if err != nil { 48 | return err 49 | } 50 | pid := os.Getpid() 51 | miyabi.ServerState = func(state miyabi.State) { 52 | switch state { 53 | case miyabi.StateStart: 54 | fmt.Printf("Listening on %s\n", app.Config.Addr) 55 | fmt.Printf("Server PID: %d\n", pid) 56 | case miyabi.StateRestart: 57 | app.Logger.Warn("kocha: graceful restarted") 58 | case miyabi.StateShutdown: 59 | app.Logger.Warn("kocha: graceful shutdown") 60 | } 61 | } 62 | server := &miyabi.Server{ 63 | Addr: config.Addr, 64 | Handler: app, 65 | } 66 | app.Event.start() 67 | defer app.Event.stop() 68 | return server.ListenAndServe() 69 | } 70 | 71 | // Application represents a Kocha app. 72 | // This implements the http.Handler interface. 73 | type Application struct { 74 | // Config is a configuration of an application. 75 | Config *Config 76 | 77 | // Router is an HTTP request router of an application. 78 | Router *Router 79 | 80 | // Template is template sets of an application. 81 | Template *Template 82 | 83 | // Logger is an application logger. 84 | Logger log.Logger 85 | 86 | // Event is an interface of the event system. 87 | Event *Event 88 | 89 | // ResourceSet is set of resource of an application. 90 | ResourceSet ResourceSet 91 | 92 | failedUnits map[string]struct{} 93 | mu sync.RWMutex 94 | } 95 | 96 | // New returns a new Application that configured by config. 97 | func New(config *Config) (*Application, error) { 98 | app := &Application{ 99 | Config: config, 100 | failedUnits: make(map[string]struct{}), 101 | } 102 | if app.Config.Addr == "" { 103 | config.Addr = DefaultHttpAddr 104 | } 105 | if app.Config.MaxClientBodySize < 1 { 106 | config.MaxClientBodySize = DefaultMaxClientBodySize 107 | } 108 | if err := app.validateMiddlewares(); err != nil { 109 | return nil, err 110 | } 111 | if err := app.buildResourceSet(); err != nil { 112 | return nil, err 113 | } 114 | if err := app.buildTemplate(); err != nil { 115 | return nil, err 116 | } 117 | if err := app.buildRouter(); err != nil { 118 | return nil, err 119 | } 120 | if err := app.buildLogger(); err != nil { 121 | return nil, err 122 | } 123 | if err := app.buildEvent(); err != nil { 124 | return nil, err 125 | } 126 | return app, nil 127 | } 128 | 129 | // ServeHTTP implements the http.Handler.ServeHTTP. 130 | func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) { 131 | c := newContext() 132 | c.Layout = app.Config.DefaultLayout 133 | c.Request = newRequest(r) 134 | c.Response = newResponse() 135 | c.App = app 136 | c.Errors = make(map[string][]*ParamError) 137 | defer c.reuse() 138 | defer func() { 139 | if err := c.Response.writeTo(w); err != nil { 140 | app.Logger.Error(err) 141 | } 142 | }() 143 | if err := app.wrapMiddlewares(c)(); err != nil { 144 | app.Logger.Error(err) 145 | c.Response.reset() 146 | http.Error(c.Response, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 147 | } 148 | } 149 | 150 | // Invoke invokes newFunc. 151 | // It invokes newFunc but will behave to fallback. 152 | // When unit.ActiveIf returns false or any errors occurred in invoking, it invoke the defaultFunc if defaultFunc isn't nil. 153 | // Also if any errors occurred at least once, next invoking will always invoke the defaultFunc. 154 | func (app *Application) Invoke(unit Unit, newFunc func(), defaultFunc func()) { 155 | name := reflect.TypeOf(unit).String() 156 | defer func() { 157 | if err := recover(); err != nil { 158 | if err != ErrInvokeDefault { 159 | app.logStackAndError(err) 160 | app.mu.Lock() 161 | app.failedUnits[name] = struct{}{} 162 | app.mu.Unlock() 163 | } 164 | if defaultFunc != nil { 165 | defaultFunc() 166 | } 167 | } 168 | }() 169 | app.mu.RLock() 170 | _, failed := app.failedUnits[name] 171 | app.mu.RUnlock() 172 | if failed || !unit.ActiveIf() { 173 | panic(ErrInvokeDefault) 174 | } 175 | newFunc() 176 | } 177 | 178 | func (app *Application) buildRouter() (err error) { 179 | app.Router, err = app.Config.RouteTable.buildRouter() 180 | return err 181 | } 182 | 183 | func (app *Application) buildResourceSet() error { 184 | app.ResourceSet = app.Config.ResourceSet 185 | return nil 186 | } 187 | 188 | func (app *Application) buildTemplate() (err error) { 189 | app.Template, err = app.Config.Template.build(app) 190 | return err 191 | } 192 | 193 | func (app *Application) buildLogger() error { 194 | if app.Config.Logger == nil { 195 | app.Config.Logger = &LoggerConfig{} 196 | } 197 | if app.Config.Logger.Writer == nil { 198 | app.Config.Logger.Writer = os.Stdout 199 | } 200 | if app.Config.Logger.Formatter == nil { 201 | app.Config.Logger.Formatter = &log.LTSVFormatter{} 202 | } 203 | app.Logger = log.New(app.Config.Logger.Writer, app.Config.Logger.Formatter, app.Config.Logger.Level) 204 | return nil 205 | } 206 | 207 | func (app *Application) buildEvent() (err error) { 208 | app.Event, err = app.Config.Event.build(app) 209 | return err 210 | } 211 | 212 | func (app *Application) validateMiddlewares() error { 213 | for _, m := range app.Config.Middlewares { 214 | if v, ok := m.(Validator); ok { 215 | if err := v.Validate(); err != nil { 216 | return err 217 | } 218 | } 219 | } 220 | return nil 221 | } 222 | 223 | func (app *Application) wrapMiddlewares(c *Context) func() error { 224 | wrapped := nullMiddlewareNext 225 | for i := len(app.Config.Middlewares) - 1; i >= 0; i-- { 226 | f, next := app.Config.Middlewares[i].Process, wrapped 227 | wrapped = func() error { 228 | return f(app, c, next) 229 | } 230 | } 231 | return wrapped 232 | } 233 | 234 | func (app *Application) logStackAndError(err interface{}) { 235 | buf := make([]byte, 4096) 236 | n := runtime.Stack(buf, false) 237 | app.Logger.Errorf("%v\n%s", err, buf[:n]) 238 | } 239 | 240 | // Config represents a application-scope configuration. 241 | type Config struct { 242 | Addr string // listen address, DefaultHttpAddr if empty. 243 | AppPath string // root path of the application. 244 | AppName string // name of the application. 245 | DefaultLayout string // name of the default layout. 246 | Template *Template // template config. 247 | RouteTable RouteTable // routing config. 248 | Middlewares []Middleware // middlewares. 249 | Logger *LoggerConfig // logger config. 250 | Event *Event // event config. 251 | MaxClientBodySize int64 // maximum size of request body, DefaultMaxClientBodySize if 0 252 | 253 | ResourceSet ResourceSet 254 | } 255 | 256 | // Getenv is similar to os.Getenv. 257 | // However, Getenv returns def value if the variable is not present, and 258 | // sets def to environment variable. 259 | func Getenv(key, def string) string { 260 | env := os.Getenv(key) 261 | if env != "" { 262 | return env 263 | } 264 | os.Setenv(key, def) 265 | return def 266 | } 267 | 268 | func init() { 269 | _ = godotenv.Load() 270 | } 271 | -------------------------------------------------------------------------------- /kocha_bench_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/naoina/kocha" 8 | ) 9 | 10 | type nullResponseWriter struct { 11 | header http.Header 12 | } 13 | 14 | func newNullResponseWriter() *nullResponseWriter { 15 | return &nullResponseWriter{ 16 | header: make(http.Header), 17 | } 18 | } 19 | 20 | func (w *nullResponseWriter) Header() http.Header { 21 | return w.header 22 | } 23 | 24 | func (w *nullResponseWriter) Write(b []byte) (int, error) { 25 | return len(b), nil 26 | } 27 | 28 | func (w *nullResponseWriter) WriteHeader(n int) { 29 | } 30 | 31 | func BenchmarkServeHTTP(b *testing.B) { 32 | app := kocha.NewTestApp() 33 | b.ResetTimer() 34 | for i := 0; i < b.N; i++ { 35 | req, err := http.NewRequest("GET", "/", nil) 36 | if err != nil { 37 | b.Fatal(err) 38 | } 39 | w := newNullResponseWriter() 40 | app.ServeHTTP(w, req) 41 | } 42 | } 43 | 44 | func BenchmarkServeHTTP_WithParams(b *testing.B) { 45 | app := kocha.NewTestApp() 46 | b.ResetTimer() 47 | for i := 0; i < b.N; i++ { 48 | req, err := http.NewRequest("GET", "/user/123", nil) 49 | if err != nil { 50 | b.Fatal(err) 51 | } 52 | w := newNullResponseWriter() 53 | app.ServeHTTP(w, req) 54 | } 55 | } 56 | 57 | func BenchmarkNew(b *testing.B) { 58 | app := kocha.NewTestApp() 59 | config := app.Config 60 | b.ResetTimer() 61 | for i := 0; i < b.N; i++ { 62 | if _, err := kocha.New(config); err != nil { 63 | b.Fatal(err) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/naoina/kocha/log" 7 | ) 8 | 9 | // LoggerConfig represents the configuration of the logger. 10 | type LoggerConfig struct { 11 | Writer io.Writer // output destination for the logger. 12 | Formatter log.Formatter // formatter for log entry. 13 | Level log.Level // log level. 14 | } 15 | -------------------------------------------------------------------------------- /log/entry.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sync" 8 | "time" 9 | 10 | "github.com/naoina/kocha/util" 11 | ) 12 | 13 | // Entry represents a log entry. 14 | type Entry struct { 15 | Level Level // log level. 16 | Time time.Time // time of the log event. 17 | Message string // log message (optional). 18 | Fields Fields // extra fields of the log entry (optional). 19 | } 20 | 21 | // entryLogger implements the Logger interface. 22 | type entryLogger struct { 23 | entry *Entry 24 | logger *logger 25 | mu sync.Mutex 26 | } 27 | 28 | func newEntryLogger(logger *logger) *entryLogger { 29 | return &entryLogger{ 30 | logger: logger, 31 | entry: &Entry{}, 32 | } 33 | } 34 | 35 | func (l *entryLogger) Debug(v ...interface{}) { 36 | if l.logger.Level() <= DEBUG { 37 | l.Output(DEBUG, fmt.Sprint(v...)) 38 | } 39 | } 40 | 41 | func (l *entryLogger) Debugf(format string, v ...interface{}) { 42 | if l.logger.Level() <= DEBUG { 43 | l.Output(DEBUG, fmt.Sprintf(format, v...)) 44 | } 45 | } 46 | 47 | func (l *entryLogger) Debugln(v ...interface{}) { 48 | if l.logger.Level() <= DEBUG { 49 | l.Output(DEBUG, fmt.Sprint(v...)) 50 | } 51 | } 52 | 53 | func (l *entryLogger) Info(v ...interface{}) { 54 | if l.logger.Level() <= INFO { 55 | l.Output(INFO, fmt.Sprint(v...)) 56 | } 57 | } 58 | 59 | func (l *entryLogger) Infof(format string, v ...interface{}) { 60 | if l.logger.Level() <= INFO { 61 | l.Output(INFO, fmt.Sprintf(format, v...)) 62 | } 63 | } 64 | 65 | func (l *entryLogger) Infoln(v ...interface{}) { 66 | if l.logger.Level() <= INFO { 67 | l.Output(INFO, fmt.Sprint(v...)) 68 | } 69 | } 70 | 71 | func (l *entryLogger) Warn(v ...interface{}) { 72 | if l.logger.Level() <= WARN { 73 | l.Output(WARN, fmt.Sprint(v...)) 74 | } 75 | } 76 | 77 | func (l *entryLogger) Warnf(format string, v ...interface{}) { 78 | if l.logger.Level() <= WARN { 79 | l.Output(WARN, fmt.Sprintf(format, v...)) 80 | } 81 | } 82 | 83 | func (l *entryLogger) Warnln(v ...interface{}) { 84 | if l.logger.Level() <= WARN { 85 | l.Output(WARN, fmt.Sprint(v...)) 86 | } 87 | } 88 | 89 | func (l *entryLogger) Error(v ...interface{}) { 90 | if l.logger.Level() <= ERROR { 91 | l.Output(ERROR, fmt.Sprint(v...)) 92 | } 93 | } 94 | 95 | func (l *entryLogger) Errorf(format string, v ...interface{}) { 96 | if l.logger.Level() <= ERROR { 97 | l.Output(ERROR, fmt.Sprintf(format, v...)) 98 | } 99 | } 100 | 101 | func (l *entryLogger) Errorln(v ...interface{}) { 102 | if l.logger.Level() <= ERROR { 103 | l.Output(ERROR, fmt.Sprint(v...)) 104 | } 105 | } 106 | 107 | func (l *entryLogger) Fatal(v ...interface{}) { 108 | l.Output(FATAL, fmt.Sprint(v...)) 109 | os.Exit(1) 110 | } 111 | 112 | func (l *entryLogger) Fatalf(format string, v ...interface{}) { 113 | l.Output(FATAL, fmt.Sprintf(format, v...)) 114 | os.Exit(1) 115 | } 116 | 117 | func (l *entryLogger) Fatalln(v ...interface{}) { 118 | l.Output(FATAL, fmt.Sprint(v...)) 119 | os.Exit(1) 120 | } 121 | 122 | func (l *entryLogger) Panic(v ...interface{}) { 123 | s := fmt.Sprint(v...) 124 | l.Output(PANIC, s) 125 | panic(s) 126 | } 127 | 128 | func (l *entryLogger) Panicf(format string, v ...interface{}) { 129 | s := fmt.Sprintf(format, v...) 130 | l.Output(PANIC, s) 131 | panic(s) 132 | } 133 | 134 | func (l *entryLogger) Panicln(v ...interface{}) { 135 | s := fmt.Sprint(v...) 136 | l.Output(PANIC, s) 137 | panic(s) 138 | } 139 | 140 | func (l *entryLogger) Print(v ...interface{}) { 141 | l.Output(NONE, fmt.Sprint(v...)) 142 | } 143 | 144 | func (l *entryLogger) Printf(format string, v ...interface{}) { 145 | l.Output(NONE, fmt.Sprintf(format, v...)) 146 | } 147 | 148 | func (l *entryLogger) Println(v ...interface{}) { 149 | l.Output(NONE, fmt.Sprint(v...)) 150 | } 151 | 152 | func (l *entryLogger) Output(level Level, message string) { 153 | l.logger.mu.Lock() 154 | defer l.logger.mu.Unlock() 155 | l.entry.Level = level 156 | l.entry.Time = util.Now() 157 | l.entry.Message = message 158 | l.logger.buf.Reset() 159 | format := Formatter.Format 160 | if int(level) < len(l.logger.formatFuncs) { 161 | format = l.logger.formatFuncs[level] 162 | } 163 | if err := format(l.logger.formatter, &l.logger.buf, l.entry); err != nil { 164 | fmt.Fprintf(os.Stderr, "kocha: log: %v\n", err) 165 | } 166 | l.logger.buf.WriteByte('\n') 167 | if _, err := io.Copy(l.logger.out, &l.logger.buf); err != nil { 168 | fmt.Fprintf(os.Stderr, "kocha: log: failed to write log: %v\n", err) 169 | } 170 | } 171 | 172 | func (l *entryLogger) With(fields Fields) Logger { 173 | l.mu.Lock() 174 | l.entry.Fields = fields 175 | l.mu.Unlock() 176 | return l 177 | } 178 | 179 | func (l *entryLogger) Level() Level { 180 | return l.logger.Level() 181 | } 182 | 183 | func (l *entryLogger) SetLevel(level Level) { 184 | l.logger.SetLevel(level) 185 | } 186 | -------------------------------------------------------------------------------- /log/formatter.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "time" 8 | ) 9 | 10 | // Formatter is an interface that formatter for a log entry. 11 | type Formatter interface { 12 | // Format formats a log entry. 13 | // Format writes formatted entry to the w. 14 | Format(w io.Writer, entry *Entry) error 15 | } 16 | 17 | // RawFormatter is a formatter that doesn't format. 18 | // RawFormatter doesn't output the almost fields of the entry except the 19 | // Message. 20 | type RawFormatter struct{} 21 | 22 | // Format outputs entry.Message. 23 | func (f *RawFormatter) Format(w io.Writer, entry *Entry) error { 24 | _, err := io.WriteString(w, entry.Message) 25 | return err 26 | } 27 | 28 | // LTSVFormatter is the formatter of Labeled Tab-separated Values. 29 | // See http://ltsv.org/ for more details. 30 | type LTSVFormatter struct { 31 | } 32 | 33 | // Format formats an entry to Labeled Tab-separated Values format. 34 | func (f *LTSVFormatter) Format(w io.Writer, entry *Entry) error { 35 | var buf bytes.Buffer 36 | fmt.Fprintf(&buf, "level:%v", entry.Level) 37 | if !entry.Time.IsZero() { 38 | fmt.Fprintf(&buf, "\ttime:%v", entry.Time.Format(time.RFC3339Nano)) 39 | } 40 | if entry.Message != "" { 41 | fmt.Fprintf(&buf, "\tmessage:%v", entry.Message) 42 | } 43 | for _, k := range entry.Fields.OrderedKeys() { 44 | fmt.Fprintf(&buf, "\t%v:%v", k, entry.Fields.Get(k)) 45 | } 46 | _, err := io.Copy(w, &buf) 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /log/formatter_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/naoina/kocha/log" 10 | ) 11 | 12 | func TestRawFormatter_Format(t *testing.T) { 13 | now := time.Now() 14 | for _, v := range []struct { 15 | entry *log.Entry 16 | expect string 17 | }{ 18 | {&log.Entry{ 19 | Level: log.DEBUG, 20 | Time: now, 21 | Message: "test_raw_log1", 22 | Fields: log.Fields{ 23 | "first": 1, 24 | "second": "2", 25 | "third": "san", 26 | }, 27 | }, "test_raw_log1"}, 28 | {&log.Entry{ 29 | Message: "test_raw_log2", 30 | Level: log.INFO, 31 | }, "test_raw_log2"}, 32 | } { 33 | var buf bytes.Buffer 34 | formatter := &log.RawFormatter{} 35 | if err := formatter.Format(&buf, v.entry); err != nil { 36 | t.Errorf(`RawFormatter.Format(&buf, %#v) => %#v; want %#v`, v.entry, err, nil) 37 | } 38 | actual := buf.String() 39 | expect := v.expect 40 | if !reflect.DeepEqual(actual, expect) { 41 | t.Errorf(`RawFormatter.Format(&buf, %#v) => %#v; want %#v`, v.entry, actual, expect) 42 | } 43 | } 44 | } 45 | 46 | func TestLTSVFormatter_Format(t *testing.T) { 47 | now := time.Now() 48 | for _, v := range []struct { 49 | entry *log.Entry 50 | expected string 51 | }{ 52 | {&log.Entry{ 53 | Level: log.DEBUG, 54 | Time: now, 55 | Message: "test_ltsv_log1", 56 | Fields: log.Fields{ 57 | "first": 1, 58 | "second": "2", 59 | "third": "san", 60 | }, 61 | }, "level:DEBUG\ttime:" + now.Format(time.RFC3339Nano) + "\tmessage:test_ltsv_log1\tfirst:1\tsecond:2\tthird:san"}, 62 | {&log.Entry{ 63 | Level: log.INFO, 64 | Time: now, 65 | Message: "test_ltsv_log2", 66 | }, "level:INFO\ttime:" + now.Format(time.RFC3339Nano) + "\tmessage:test_ltsv_log2"}, 67 | {&log.Entry{ 68 | Level: log.WARN, 69 | Time: now, 70 | }, "level:WARN\ttime:" + now.Format(time.RFC3339Nano)}, 71 | {&log.Entry{ 72 | Level: log.ERROR, 73 | Time: now, 74 | }, "level:ERROR\ttime:" + now.Format(time.RFC3339Nano)}, 75 | {&log.Entry{ 76 | Level: log.FATAL, 77 | }, "level:FATAL"}, 78 | {&log.Entry{}, "level:NONE"}, 79 | } { 80 | var buf bytes.Buffer 81 | formatter := &log.LTSVFormatter{} 82 | if err := formatter.Format(&buf, v.entry); err != nil { 83 | t.Errorf(`LTSVFormatter.Format(&buf, %#v) => %#v; want %#v`, v.entry, err, nil) 84 | } 85 | actual := buf.String() 86 | expected := v.expected 87 | if !reflect.DeepEqual(actual, expected) { 88 | t.Errorf(`LTSVFormatter.Format(&buf, %#v); buf => %#v; want %#v`, v.entry, actual, expected) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /log/level_string.go: -------------------------------------------------------------------------------- 1 | // generated by stringer -type Level; DO NOT EDIT 2 | 3 | package log 4 | 5 | import "fmt" 6 | 7 | const _Level_name = "NONEDEBUGINFOWARNERRORFATALPANIC" 8 | 9 | var _Level_index = [...]uint8{0, 4, 9, 13, 17, 22, 27, 32} 10 | 11 | func (i Level) String() string { 12 | if i+1 >= Level(len(_Level_index)) { 13 | return fmt.Sprintf("Level(%d)", i) 14 | } 15 | return _Level_name[_Level_index[i]:_Level_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "sort" 8 | "sync" 9 | "sync/atomic" 10 | 11 | "github.com/mattn/go-colorable" 12 | "github.com/mattn/go-isatty" 13 | ) 14 | 15 | // Logger is the interface that logger. 16 | type Logger interface { 17 | // Debug calls Logger.Output to print to the logger with DEBUG level. 18 | // Arguments are handled in the manner of fmt.Print. 19 | // If the current log level is upper than DEBUG, it won't be the output. 20 | Debug(v ...interface{}) 21 | 22 | // Debugf calls Logger.Output to print to the logger with DEBUG level. 23 | // Arguments are handled in the manner of fmt.Printf. 24 | // If the current log level is upper than DEBUG, it won't be the output. 25 | Debugf(format string, v ...interface{}) 26 | 27 | // Debugln calls Logger.Output to print to the logger with DEBUG level. 28 | // Arguments are handled in the manner of fmt.Println. 29 | // If the current log level is upper than DEBUG, it won't be the output. 30 | Debugln(v ...interface{}) 31 | 32 | // Info calls Logger.Output to print to the logger with INFO level. 33 | // Arguments are handled in the manner of fmt.Print. 34 | // If the current log level is upper than INFO, it won't be the output. 35 | Info(v ...interface{}) 36 | 37 | // Infof calls Logger.Output to print to the logger with INFO level. 38 | // Arguments are handled in the manner of fmt.Printf. 39 | // If the current log level is upper than INFO, it won't be the output. 40 | Infof(format string, v ...interface{}) 41 | 42 | // Infoln calls Logger.Output to print to the logger with INFO level. 43 | // Arguments are handled in the manner of fmt.Println. 44 | // If the current log level is upper than INFO, it won't be the output. 45 | Infoln(v ...interface{}) 46 | 47 | // Warn calls Logger.Output to print to the logger with WARN level. 48 | // Arguments are handled in the manner of fmt.Print. 49 | // If the current log level is upper than WARN, it won't be the output. 50 | Warn(v ...interface{}) 51 | 52 | // Warnf calls Logger.Output to print to the logger with WARN level. 53 | // Arguments are handled in the manner of fmt.Printf. 54 | // If the current log level is upper than WARN, it won't be the output. 55 | Warnf(format string, v ...interface{}) 56 | 57 | // Warnln calls Logger.Output to print to the logger with WARN level. 58 | // Arguments are handled in the manner of fmt.Println. 59 | // If the current log level is upper than WARN, it won't be the output. 60 | Warnln(v ...interface{}) 61 | 62 | // Error calls Logger.Output to print to the logger with ERROR level. 63 | // Arguments are handled in the manner of fmt.Print. 64 | // If the current log level is upper than ERROR, it won't be the output. 65 | Error(v ...interface{}) 66 | 67 | // Errorf calls Logger.Output to print to the logger with ERROR level. 68 | // Arguments are handled in the manner of fmt.Printf. 69 | // If the current log level is upper than ERROR, it won't be the output. 70 | Errorf(format string, v ...interface{}) 71 | 72 | // Errorln calls Logger.Output to print to the logger with ERROR level. 73 | // Arguments are handled in the manner of fmt.Println. 74 | // If the current log level is upper than ERROR, it won't be the output. 75 | Errorln(v ...interface{}) 76 | 77 | // Fatal calls Logger.Output to print to the logger with FATAL level. 78 | // Arguments are handled in the manner of fmt.Print. 79 | // Also calls os.Exit(1) after the output. 80 | Fatal(v ...interface{}) 81 | 82 | // Fatalf calls Logger.Output to print to the logger with FATAL level. 83 | // Arguments are handled in the manner of fmt.Printf. 84 | // Also calls os.Exit(1) after the output. 85 | Fatalf(format string, v ...interface{}) 86 | 87 | // Fatalln calls Logger.Output to print to the logger with FATAL level. 88 | // Arguments are handled in the manner of fmt.Println. 89 | // Also calls os.Exit(1) after the output. 90 | Fatalln(v ...interface{}) 91 | 92 | // Panic calls Logger.Output to print to the logger with PANIC level. 93 | // Arguments are handled in the manner of fmt.Print. 94 | // Also calls panic() after the output. 95 | Panic(v ...interface{}) 96 | 97 | // Panicf calls Logger.Output to print to the logger with PANIC level. 98 | // Arguments are handled in the manner of fmt.Printf. 99 | // Also calls panic() after the output. 100 | Panicf(format string, v ...interface{}) 101 | 102 | // Panicln calls Logger.Output to print to the logger with PANIC level. 103 | // Arguments are handled in the manner of fmt.Println. 104 | // Also calls panic() after the output. 105 | Panicln(v ...interface{}) 106 | 107 | // Print calls Logger.Output to print to the logger with NONE level. 108 | // Arguments are handled in the manner of fmt.Print. 109 | Print(v ...interface{}) 110 | 111 | // Printf calls Logger.Output to print to the logger with NONE level. 112 | // Arguments are handled in the manner of fmt.Printf. 113 | Printf(format string, v ...interface{}) 114 | 115 | // Println calls Logger.Output to print to the logger with NONE level. 116 | // Arguments are handled in the manner of fmt.Println. 117 | Println(v ...interface{}) 118 | 119 | // Output writes the output for a logging event with the given level. 120 | // The given message will be format by Formatter. Also a newline is appended 121 | // to the message before the output. 122 | Output(level Level, message string) 123 | 124 | // With returns a new Logger with fields. 125 | With(fields Fields) Logger 126 | 127 | // Level returns the current log level. 128 | Level() Level 129 | 130 | // SetLevel sets the log level. 131 | SetLevel(level Level) 132 | } 133 | 134 | // New creates a new Logger. 135 | func New(out io.Writer, formatter Formatter, level Level) Logger { 136 | l := &logger{ 137 | out: out, 138 | formatter: formatter, 139 | formatFuncs: plainFormats, 140 | level: level, 141 | } 142 | if w, ok := out.(*os.File); ok && isatty.IsTerminal(w.Fd()) { 143 | switch w { 144 | case os.Stdout: 145 | l.out = colorable.NewColorableStdout() 146 | l.formatFuncs = coloredFormats 147 | case os.Stderr: 148 | l.out = colorable.NewColorableStderr() 149 | l.formatFuncs = coloredFormats 150 | } 151 | } 152 | return l 153 | } 154 | 155 | // logger implements the Logger interface. 156 | type logger struct { 157 | out io.Writer 158 | formatter Formatter 159 | formatFuncs [7]formatFunc 160 | level Level 161 | fields Fields 162 | buf bytes.Buffer 163 | mu sync.Mutex 164 | } 165 | 166 | func (l *logger) Debug(v ...interface{}) { 167 | if l.Level() <= DEBUG { 168 | newEntryLogger(l).Debug(v...) 169 | } 170 | } 171 | 172 | func (l *logger) Debugf(format string, v ...interface{}) { 173 | if l.Level() <= DEBUG { 174 | newEntryLogger(l).Debugf(format, v...) 175 | } 176 | } 177 | 178 | func (l *logger) Debugln(v ...interface{}) { 179 | if l.Level() <= DEBUG { 180 | newEntryLogger(l).Debugln(v...) 181 | } 182 | } 183 | 184 | func (l *logger) Info(v ...interface{}) { 185 | if l.Level() <= INFO { 186 | newEntryLogger(l).Info(v...) 187 | } 188 | } 189 | 190 | func (l *logger) Infof(format string, v ...interface{}) { 191 | if l.Level() <= INFO { 192 | newEntryLogger(l).Infof(format, v...) 193 | } 194 | } 195 | 196 | func (l *logger) Infoln(v ...interface{}) { 197 | if l.Level() <= INFO { 198 | newEntryLogger(l).Infoln(v...) 199 | } 200 | } 201 | 202 | func (l *logger) Warn(v ...interface{}) { 203 | if l.Level() <= WARN { 204 | newEntryLogger(l).Warn(v...) 205 | } 206 | } 207 | 208 | func (l *logger) Warnf(format string, v ...interface{}) { 209 | if l.Level() <= WARN { 210 | newEntryLogger(l).Warnf(format, v...) 211 | } 212 | } 213 | 214 | func (l *logger) Warnln(v ...interface{}) { 215 | if l.Level() <= WARN { 216 | newEntryLogger(l).Warnln(v...) 217 | } 218 | } 219 | 220 | func (l *logger) Error(v ...interface{}) { 221 | if l.Level() <= ERROR { 222 | newEntryLogger(l).Error(v...) 223 | } 224 | } 225 | 226 | func (l *logger) Errorf(format string, v ...interface{}) { 227 | if l.Level() <= ERROR { 228 | newEntryLogger(l).Errorf(format, v...) 229 | } 230 | } 231 | 232 | func (l *logger) Errorln(v ...interface{}) { 233 | if l.Level() <= ERROR { 234 | newEntryLogger(l).Errorln(v...) 235 | } 236 | } 237 | 238 | func (l *logger) Fatal(v ...interface{}) { 239 | newEntryLogger(l).Fatal(v...) 240 | } 241 | 242 | func (l *logger) Fatalf(format string, v ...interface{}) { 243 | newEntryLogger(l).Fatalf(format, v...) 244 | } 245 | 246 | func (l *logger) Fatalln(v ...interface{}) { 247 | newEntryLogger(l).Fatalln(v...) 248 | } 249 | 250 | func (l *logger) Panic(v ...interface{}) { 251 | newEntryLogger(l).Panic(v...) 252 | } 253 | 254 | func (l *logger) Panicf(format string, v ...interface{}) { 255 | newEntryLogger(l).Panicf(format, v...) 256 | } 257 | 258 | func (l *logger) Panicln(v ...interface{}) { 259 | newEntryLogger(l).Panicln(v...) 260 | } 261 | 262 | func (l *logger) Print(v ...interface{}) { 263 | newEntryLogger(l).Print(v...) 264 | } 265 | 266 | func (l *logger) Printf(format string, v ...interface{}) { 267 | newEntryLogger(l).Printf(format, v...) 268 | } 269 | 270 | func (l *logger) Println(v ...interface{}) { 271 | newEntryLogger(l).Println(v...) 272 | } 273 | 274 | func (l *logger) Output(level Level, message string) { 275 | newEntryLogger(l).Output(level, message) 276 | } 277 | 278 | func (l *logger) With(fields Fields) Logger { 279 | return newEntryLogger(l).With(fields) 280 | } 281 | 282 | func (l *logger) Level() Level { 283 | return Level(atomic.LoadUint32((*uint32)(&l.level))) 284 | } 285 | 286 | func (l *logger) SetLevel(level Level) { 287 | atomic.StoreUint32((*uint32)(&l.level), uint32(level)) 288 | } 289 | 290 | // Level represents a log level. 291 | type Level uint32 292 | 293 | // The log levels. 294 | const ( 295 | NONE Level = iota 296 | DEBUG 297 | INFO 298 | WARN 299 | ERROR 300 | FATAL 301 | PANIC 302 | ) 303 | 304 | type formatFunc func(f Formatter, w io.Writer, entry *Entry) error 305 | 306 | func makeFormat(esc string) formatFunc { 307 | return func(f Formatter, w io.Writer, entry *Entry) error { 308 | io.WriteString(w, esc) 309 | err := f.Format(w, entry) 310 | io.WriteString(w, "\x1b[0m") 311 | return err 312 | } 313 | } 314 | 315 | var coloredFormats = [...]formatFunc{ 316 | Formatter.Format, // NONE 317 | Formatter.Format, // DEBUG 318 | Formatter.Format, // INFO 319 | makeFormat("\x1b[33m"), // WARN 320 | makeFormat("\x1b[31m"), // ERROR 321 | makeFormat("\x1b[31m"), // FATAL 322 | makeFormat("\x1b[31m"), // PANIC 323 | } 324 | 325 | var plainFormats = [...]formatFunc{ 326 | Formatter.Format, // NONE 327 | Formatter.Format, // DEBUG 328 | Formatter.Format, // INFO 329 | Formatter.Format, // WARN 330 | Formatter.Format, // ERROR 331 | Formatter.Format, // FATAL 332 | Formatter.Format, // PANIC 333 | } 334 | 335 | // Fields represents the key-value pairs in a log Entry. 336 | type Fields map[string]interface{} 337 | 338 | // Get returns a value associated with the given key. 339 | func (f Fields) Get(key string) interface{} { 340 | return f[key] 341 | } 342 | 343 | // OrderedKeys returns the keys of f that sorted in increasing order. 344 | // This is used if you need the consistent map iteration order. 345 | // See also http://golang.org/doc/go1.3#map 346 | func (f Fields) OrderedKeys() (keys []string) { 347 | for k := range f { 348 | keys = append(keys, k) 349 | } 350 | sort.Strings(keys) 351 | return keys 352 | } 353 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/naoina/kocha/log" 11 | "github.com/naoina/kocha/util" 12 | "github.com/ugorji/go/codec" 13 | ) 14 | 15 | // Middleware is the interface that middleware. 16 | type Middleware interface { 17 | Process(app *Application, c *Context, next func() error) error 18 | } 19 | 20 | // Validator is the interface to validate the middleware. 21 | type Validator interface { 22 | // Validate validates the middleware. 23 | // Validate will be called in boot-time of the application. 24 | Validate() error 25 | } 26 | 27 | // PanicRecoverMiddleware is a middleware to recover a panic where occurred in request sequence. 28 | type PanicRecoverMiddleware struct{} 29 | 30 | func (m *PanicRecoverMiddleware) Process(app *Application, c *Context, next func() error) (err error) { 31 | defer func() { 32 | defer func() { 33 | if perr := recover(); perr != nil { 34 | app.logStackAndError(perr) 35 | err = fmt.Errorf("%v", perr) 36 | } 37 | }() 38 | if err != nil { 39 | app.Logger.Errorf("%+v", err) 40 | goto ERROR 41 | } else if perr := recover(); perr != nil { 42 | app.logStackAndError(perr) 43 | goto ERROR 44 | } 45 | return 46 | ERROR: 47 | c.Response.reset() 48 | if err = internalServerErrorController.GET(c); err != nil { 49 | app.logStackAndError(err) 50 | } 51 | }() 52 | return next() 53 | } 54 | 55 | // FormMiddleware is a middleware to parse a form data from query string and/or request body. 56 | type FormMiddleware struct{} 57 | 58 | // Process implements the Middleware interface. 59 | func (m *FormMiddleware) Process(app *Application, c *Context, next func() error) error { 60 | c.Request.Body = http.MaxBytesReader(c.Response, c.Request.Body, app.Config.MaxClientBodySize) 61 | if err := c.Request.ParseMultipartForm(app.Config.MaxClientBodySize); err != nil && err != http.ErrNotMultipart { 62 | return err 63 | } 64 | c.Params = c.newParams() 65 | return next() 66 | } 67 | 68 | // SessionMiddleware is a middleware to process a session. 69 | type SessionMiddleware struct { 70 | // Name of cookie (key) 71 | Name string 72 | 73 | // Implementation of session store 74 | Store SessionStore 75 | 76 | // Expiration of session cookie, in seconds, from now. (not session expiration) 77 | // 0 is for persistent. 78 | CookieExpires time.Duration 79 | 80 | // Expiration of session data, in seconds, from now. (not cookie expiration) 81 | // 0 is for persistent. 82 | SessionExpires time.Duration 83 | HttpOnly bool 84 | ExpiresKey string 85 | } 86 | 87 | func (m *SessionMiddleware) Process(app *Application, c *Context, next func() error) error { 88 | if err := m.before(app, c); err != nil { 89 | return err 90 | } 91 | if err := next(); err != nil { 92 | return err 93 | } 94 | return m.after(app, c) 95 | } 96 | 97 | // Validate validates configuration of the session. 98 | func (m *SessionMiddleware) Validate() error { 99 | if m == nil { 100 | return fmt.Errorf("kocha: session: middleware is nil") 101 | } 102 | if m.Store == nil { 103 | return fmt.Errorf("kocha: session: because Store is nil, session cannot be used") 104 | } 105 | if m.Name == "" { 106 | return fmt.Errorf("kocha: session: Name must be specified") 107 | } 108 | if m.ExpiresKey == "" { 109 | m.ExpiresKey = "_kocha._sess._expires" 110 | } 111 | if v, ok := m.Store.(Validator); ok { 112 | return v.Validate() 113 | } 114 | return nil 115 | } 116 | 117 | func (m *SessionMiddleware) before(app *Application, c *Context) (err error) { 118 | defer func() { 119 | switch err.(type) { 120 | case nil: 121 | // do nothing. 122 | case ErrSession: 123 | app.Logger.Info(err) 124 | default: 125 | app.Logger.Errorf("%+v", err) 126 | } 127 | if c.Session == nil { 128 | c.Session = make(Session) 129 | } 130 | err = nil 131 | }() 132 | cookie, err := c.Request.Cookie(m.Name) 133 | if err != nil { 134 | return NewErrSession("new session") 135 | } 136 | sess, err := m.Store.Load(cookie.Value) 137 | if err != nil { 138 | return err 139 | } 140 | expiresStr, ok := sess[m.ExpiresKey] 141 | if !ok { 142 | return fmt.Errorf("expires value not found") 143 | } 144 | expires, err := strconv.ParseInt(expiresStr, 10, 64) 145 | if err != nil { 146 | return err 147 | } 148 | if expires < util.Now().Unix() { 149 | return NewErrSession("session has been expired") 150 | } 151 | c.Session = sess 152 | return nil 153 | } 154 | 155 | func (m *SessionMiddleware) after(app *Application, c *Context) (err error) { 156 | expires, _ := m.expiresFromDuration(m.SessionExpires) 157 | c.Session[m.ExpiresKey] = strconv.FormatInt(expires.Unix(), 10) 158 | cookie := m.newSessionCookie(app, c) 159 | cookie.Value, err = m.Store.Save(c.Session) 160 | if err != nil { 161 | return err 162 | } 163 | c.Response.SetCookie(cookie) 164 | return nil 165 | } 166 | 167 | func (m *SessionMiddleware) newSessionCookie(app *Application, c *Context) *http.Cookie { 168 | expires, maxAge := m.expiresFromDuration(m.CookieExpires) 169 | return &http.Cookie{ 170 | Name: m.Name, 171 | Value: "", 172 | Path: "/", 173 | Expires: expires, 174 | MaxAge: maxAge, 175 | Secure: c.Request.IsSSL(), 176 | HttpOnly: m.HttpOnly, 177 | } 178 | } 179 | 180 | func (m *SessionMiddleware) expiresFromDuration(d time.Duration) (expires time.Time, maxAge int) { 181 | switch d { 182 | case -1: 183 | // persistent 184 | expires = util.Now().UTC().AddDate(20, 0, 0) 185 | case 0: 186 | expires = time.Time{} 187 | default: 188 | expires = util.Now().UTC().Add(d) 189 | maxAge = int(d.Seconds()) 190 | } 191 | return expires, maxAge 192 | } 193 | 194 | // Flash messages processing middleware. 195 | type FlashMiddleware struct{} 196 | 197 | func (m *FlashMiddleware) Process(app *Application, c *Context, next func() error) error { 198 | if err := m.before(app, c); err != nil { 199 | return err 200 | } 201 | if err := next(); err != nil { 202 | return err 203 | } 204 | return m.after(app, c) 205 | } 206 | 207 | func (m *FlashMiddleware) before(app *Application, c *Context) error { 208 | if c.Session == nil { 209 | app.Logger.Error("kocha: FlashMiddleware hasn't been added after SessionMiddleware; it cannot be used") 210 | return nil 211 | } 212 | c.Flash = Flash{} 213 | if flash := c.Session["_flash"]; flash != "" { 214 | if err := codec.NewDecoderBytes([]byte(flash), codecHandler).Decode(&c.Flash); err != nil { 215 | // make a new Flash instance because there is a possibility that 216 | // garbage data is set to c.Flash by in-place decoding of Decode(). 217 | c.Flash = Flash{} 218 | return fmt.Errorf("kocha: flash: unexpected error in decode process: %v", err) 219 | } 220 | } 221 | return nil 222 | } 223 | 224 | func (m *FlashMiddleware) after(app *Application, c *Context) error { 225 | if c.Session == nil { 226 | return nil 227 | } 228 | if c.Flash.deleteLoaded(); c.Flash.Len() == 0 { 229 | delete(c.Session, "_flash") 230 | return nil 231 | } 232 | buf := bufPool.Get().(*bytes.Buffer) 233 | defer func() { 234 | buf.Reset() 235 | bufPool.Put(buf) 236 | }() 237 | if err := codec.NewEncoder(buf, codecHandler).Encode(c.Flash); err != nil { 238 | return fmt.Errorf("kocha: flash: unexpected error in encode process: %v", err) 239 | } 240 | c.Session["_flash"] = buf.String() 241 | return nil 242 | } 243 | 244 | // Request logging middleware. 245 | type RequestLoggingMiddleware struct{} 246 | 247 | func (m *RequestLoggingMiddleware) Process(app *Application, c *Context, next func() error) error { 248 | defer func() { 249 | app.Logger.With(log.Fields{ 250 | "method": c.Request.Method, 251 | "uri": c.Request.RequestURI, 252 | "protocol": c.Request.Proto, 253 | "status": c.Response.StatusCode, 254 | }).Info() 255 | }() 256 | return next() 257 | } 258 | 259 | // DispatchMiddleware is a middleware to dispatch handler. 260 | // DispatchMiddleware should be set to last of middlewares because doesn't call other middlewares after DispatchMiddleware. 261 | type DispatchMiddleware struct{} 262 | 263 | // Process implements the Middleware interface. 264 | func (m *DispatchMiddleware) Process(app *Application, c *Context, next func() error) error { 265 | name, handler, params, found := app.Router.dispatch(c.Request) 266 | if !found { 267 | handler = (&ErrorController{ 268 | StatusCode: http.StatusNotFound, 269 | }).GET 270 | } 271 | c.Name = name 272 | if c.Params == nil { 273 | c.Params = c.newParams() 274 | } 275 | for _, param := range params { 276 | c.Params.Add(param.Name, param.Value) 277 | } 278 | return handler(c) 279 | } 280 | -------------------------------------------------------------------------------- /param.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "path/filepath" 9 | "reflect" 10 | "runtime" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/naoina/kocha/util" 17 | ) 18 | 19 | var ( 20 | ErrInvalidFormat = errors.New("invalid format") 21 | ErrUnsupportedFieldType = errors.New("unsupported field type") 22 | 23 | paramsPool = &sync.Pool{ 24 | New: func() interface{} { 25 | return &Params{} 26 | }, 27 | } 28 | ) 29 | 30 | // ParamError indicates that a field has error. 31 | type ParamError struct { 32 | Name string 33 | Err error 34 | } 35 | 36 | // NewParamError returns a new ParamError. 37 | func NewParamError(name string, err error) *ParamError { 38 | return &ParamError{ 39 | Name: name, 40 | Err: err, 41 | } 42 | } 43 | 44 | func (e *ParamError) Error() string { 45 | return fmt.Sprintf("%v is %v", e.Name, e.Err) 46 | } 47 | 48 | var formTimeFormats = []string{ 49 | "2006-01-02 15:04:05", 50 | "2006/01/02 15:04:05", 51 | "2006-01-02T15:04:05", 52 | "2006-01-02 15:04", 53 | "2006/01/02 15:04", 54 | "2006-01-02T15:04", 55 | "2006-01-02", 56 | "2006/01/02", 57 | "20060102150405", 58 | "200601021504", 59 | "20060102", 60 | } 61 | 62 | // Params represents a form values. 63 | type Params struct { 64 | c *Context 65 | url.Values 66 | prefix string 67 | } 68 | 69 | func newParams(c *Context, values url.Values, prefix string) *Params { 70 | p := paramsPool.Get().(*Params) 71 | p.c = c 72 | p.Values = values 73 | p.prefix = prefix 74 | return p 75 | } 76 | 77 | // From returns a new Params that has prefix made from given name and children. 78 | func (params *Params) From(name string, children ...string) *Params { 79 | return newParams(params.c, params.Values, params.prefixedName(name, children...)) 80 | } 81 | 82 | // Bind binds form values of fieldNames to obj. 83 | // obj must be a pointer of struct. If obj isn't a pointer of struct, it returns error. 84 | // Note that it in the case of errors due to a form value binding error, no error is returned. 85 | // Binding errors will set to map of returned from Controller.Errors(). 86 | func (params *Params) Bind(obj interface{}, fieldNames ...string) error { 87 | rvalue := reflect.ValueOf(obj) 88 | if rvalue.Kind() != reflect.Ptr { 89 | return fmt.Errorf("kocha: Bind: first argument must be a pointer, but %v", rvalue.Type().Kind()) 90 | } 91 | for rvalue.Kind() == reflect.Ptr { 92 | rvalue = rvalue.Elem() 93 | } 94 | if rvalue.Kind() != reflect.Struct { 95 | return fmt.Errorf("kocha: Bind: first argument must be a pointer of struct, but %T", obj) 96 | } 97 | rtype := rvalue.Type() 98 | for _, name := range fieldNames { 99 | index := params.findFieldIndex(rtype, name, nil) 100 | if len(index) < 1 { 101 | _, filename, line, _ := runtime.Caller(1) 102 | params.c.App.Logger.Warnf( 103 | "kocha: Bind: %s:%s: field name `%s' given, but %s.%s is undefined", 104 | filepath.Base(filename), line, name, rtype.Name(), util.ToCamelCase(name)) 105 | continue 106 | } 107 | fname := params.prefixedName(params.prefix, name) 108 | values, found := params.Values[fname] 109 | if !found { 110 | continue 111 | } 112 | field := rvalue.FieldByIndex(index) 113 | for field.Kind() == reflect.Ptr { 114 | field = field.Elem() 115 | } 116 | value, err := params.parse(field.Interface(), values[0]) 117 | if err != nil { 118 | params.c.Errors[name] = append(params.c.Errors[name], NewParamError(name, err)) 119 | } 120 | field.Set(reflect.ValueOf(value)) 121 | } 122 | return nil 123 | } 124 | 125 | func (params *Params) prefixedName(prefix string, names ...string) string { 126 | if prefix != "" { 127 | names = append([]string{prefix}, names...) 128 | } 129 | return strings.Join(names, ".") 130 | } 131 | 132 | type embeddefFieldInfo struct { 133 | field reflect.StructField 134 | name string 135 | index []int 136 | } 137 | 138 | func (params *Params) findFieldIndex(rtype reflect.Type, name string, index []int) []int { 139 | var embeddedFieldInfos []*embeddefFieldInfo 140 | for i := 0; i < rtype.NumField(); i++ { 141 | field := rtype.Field(i) 142 | if util.IsUnexportedField(field) { 143 | continue 144 | } 145 | if field.Anonymous { 146 | embeddedFieldInfos = append(embeddedFieldInfos, &embeddefFieldInfo{field, name, append(index, i)}) 147 | continue 148 | } 149 | if field.Name == util.ToCamelCase(name) { 150 | return append(index, i) 151 | } 152 | } 153 | for _, fi := range embeddedFieldInfos { 154 | if index := params.findFieldIndex(fi.field.Type, fi.name, fi.index); len(index) > 0 { 155 | return index 156 | } 157 | } 158 | return nil 159 | } 160 | 161 | func (params *Params) parse(fv interface{}, vStr string) (value interface{}, err error) { 162 | switch t := fv.(type) { 163 | case sql.Scanner: 164 | err = t.Scan(vStr) 165 | case time.Time: 166 | for _, format := range formTimeFormats { 167 | if value, err = time.Parse(format, vStr); err == nil { 168 | break 169 | } 170 | } 171 | case string: 172 | value = vStr 173 | case bool: 174 | value, err = strconv.ParseBool(vStr) 175 | case int, int8, int16, int32, int64: 176 | if value, err = strconv.ParseInt(vStr, 10, 0); err == nil { 177 | value = reflect.ValueOf(value).Convert(reflect.TypeOf(t)).Interface() 178 | } 179 | case uint, uint8, uint16, uint32, uint64: 180 | if value, err = strconv.ParseUint(vStr, 10, 0); err == nil { 181 | value = reflect.ValueOf(value).Convert(reflect.TypeOf(t)).Interface() 182 | } 183 | case float32, float64: 184 | if value, err = strconv.ParseFloat(vStr, 0); err == nil { 185 | value = reflect.ValueOf(value).Convert(reflect.TypeOf(t)).Interface() 186 | } 187 | default: 188 | params.c.App.Logger.Warnf("kocha: Bind: unsupported field type: %T", t) 189 | err = ErrUnsupportedFieldType 190 | } 191 | if err != nil { 192 | if err != ErrUnsupportedFieldType { 193 | params.c.App.Logger.Warnf("kocha: Bind: %v", err) 194 | err = ErrInvalidFormat 195 | } 196 | return nil, err 197 | } 198 | return value, nil 199 | } 200 | 201 | func (params *Params) reuse() { 202 | if params != nil { 203 | paramsPool.Put(params) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /param_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/naoina/kocha" 9 | ) 10 | 11 | func TestFromParams_Bind(t *testing.T) { 12 | func() { 13 | type User struct{} 14 | p := &kocha.Params{Values: url.Values{}} 15 | user := User{} 16 | err := p.From("user").Bind(user) 17 | if err == nil { 18 | t.Errorf("From(%#v).Bind(%#v) => %#v, want error", "user", user, err) 19 | } 20 | }() 21 | 22 | func() { 23 | p := &kocha.Params{Values: url.Values{}} 24 | var s string 25 | err := p.From("user").Bind(&s) 26 | if err == nil { 27 | t.Errorf("From.Bind(%#v) => %#v, want error", s, err) 28 | } 29 | }() 30 | 31 | func() { 32 | type User struct { 33 | Name string 34 | Age int 35 | } 36 | p := &kocha.Params{Values: url.Values{}} 37 | user := &User{} 38 | err := p.From("user").Bind(user) 39 | if err != nil { 40 | t.Errorf("From.Bind(%#v) => %#v, want nil", user, err) 41 | } 42 | 43 | actual := user 44 | expected := &User{} 45 | if !reflect.DeepEqual(actual, expected) { 46 | t.Errorf("%T => %#v, want %q", user, actual, expected) 47 | } 48 | }() 49 | 50 | func() { 51 | type User struct { 52 | Name string 53 | Age int 54 | Address string 55 | } 56 | p := &kocha.Params{Values: url.Values{ 57 | "user.name": {"naoina"}, 58 | "user.age": {"17"}, 59 | "admin.name": {"administrator"}, 60 | }} 61 | user := &User{} 62 | err := p.From("user").Bind(user, "name", "age") 63 | if err != nil { 64 | t.Errorf("From.Bind(%#v) => %#v, want nil", user, err) 65 | } 66 | 67 | actual := user 68 | expected := &User{ 69 | Name: "naoina", 70 | Age: 17, 71 | Address: "", 72 | } 73 | if !reflect.DeepEqual(actual, expected) { 74 | t.Errorf("%T => %#v, want %#v", user, actual, expected) 75 | } 76 | }() 77 | 78 | func() { 79 | type User struct { 80 | Name string 81 | } 82 | type Admin struct { 83 | User 84 | Name string 85 | } 86 | p := &kocha.Params{Values: url.Values{ 87 | "user.name": {"naoina"}, 88 | }} 89 | admin := &Admin{} 90 | err := p.From("user").Bind(admin, "name") 91 | if err != nil { 92 | t.Errorf("From.Bind(%#v) => %#v, want nil", admin, err) 93 | } 94 | 95 | actual := admin 96 | expected := &Admin{ 97 | Name: "naoina", 98 | } 99 | if !reflect.DeepEqual(actual, expected) { 100 | t.Errorf("%T => %#v, want %#v", admin, actual, expected) 101 | } 102 | }() 103 | } 104 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | var requestPool = &sync.Pool{ 11 | New: func() interface{} { 12 | return &Request{} 13 | }, 14 | } 15 | 16 | // Request represents a request. 17 | type Request struct { 18 | *http.Request 19 | 20 | // RemoteAddr is similar to http.Request.RemoteAddr, but IP only. 21 | RemoteAddr string 22 | } 23 | 24 | // newRequest returns a new Request that given a *http.Request. 25 | func newRequest(req *http.Request) *Request { 26 | r := requestPool.Get().(*Request) 27 | r.Request = req 28 | r.RemoteAddr = remoteAddr(req) 29 | return r 30 | } 31 | 32 | // Scheme returns current scheme of HTTP connection. 33 | func (r *Request) Scheme() string { 34 | switch { 35 | case r.Header.Get("Https") == "on", r.Header.Get("X-Forwarded-Ssl") == "on": 36 | return "https" 37 | case r.Header.Get("X-Forwarded-Scheme") != "": 38 | return r.Header.Get("X-Forwarded-Scheme") 39 | case r.Header.Get("X-Forwarded-Proto") != "": 40 | return strings.Split(r.Header.Get("X-Forwarded-Proto"), ",")[0] 41 | } 42 | return "http" 43 | } 44 | 45 | // IsSSL returns whether the current connection is secure. 46 | func (r *Request) IsSSL() bool { 47 | return r.Scheme() == "https" 48 | } 49 | 50 | // IsXHR returns whether the XHR request. 51 | func (r *Request) IsXHR() bool { 52 | return r.Header.Get("X-Requested-With") == "XMLHttpRequest" 53 | } 54 | 55 | func (r *Request) reuse() { 56 | requestPool.Put(r) 57 | } 58 | 59 | func remoteAddr(r *http.Request) string { 60 | if addr := r.Header.Get("X-Forwarded-For"); addr != "" { 61 | return strings.TrimSpace(addr[strings.LastIndex(addr, ",")+1:]) 62 | } 63 | host, _, err := net.SplitHostPort(r.RemoteAddr) 64 | if err != nil { 65 | return r.RemoteAddr 66 | } 67 | return host 68 | } 69 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestRequest_RemoteAddr(t *testing.T) { 10 | for _, v := range []struct { 11 | header string 12 | value string 13 | expect string 14 | }{ 15 | {"X-Forwarded-For", "192.168.0.1", "192.168.0.1"}, 16 | {"X-Forwarded-For", "192.168.0.1, 192.168.0.2, 192.168.0.3", "192.168.0.3"}, 17 | {"X-Forwarded-For", "", "127.0.0.1"}, 18 | } { 19 | r := &http.Request{Header: make(http.Header), RemoteAddr: "127.0.0.1:12345"} 20 | r.Header.Set(v.header, v.value) 21 | req := newRequest(r) 22 | actual := req.RemoteAddr 23 | expect := v.expect 24 | if !reflect.DeepEqual(actual, expect) { 25 | t.Errorf(`Request.RemoteAddr with "%v: %v" => %#v; want %#v`, v.header, v.value, actual, expect) 26 | } 27 | } 28 | } 29 | 30 | func TestRequest_Scheme(t *testing.T) { 31 | for _, v := range []struct { 32 | header string 33 | value string 34 | expect string 35 | }{ 36 | {"HTTPS", "on", "https"}, 37 | {"X-Forwarded-SSL", "on", "https"}, 38 | {"X-Forwarded-Scheme", "file", "file"}, 39 | {"X-Forwarded-Proto", "gopher", "gopher"}, 40 | {"X-Forwarded-Proto", "https, http, file", "https"}, 41 | } { 42 | req := &Request{Request: &http.Request{Header: make(http.Header)}} 43 | req.Header.Set(v.header, v.value) 44 | actual := req.Scheme() 45 | expect := v.expect 46 | if !reflect.DeepEqual(actual, expect) { 47 | t.Errorf(`Request.Scheme() with "%v: %v" => %#v; want %#v`, v.header, v.value, actual, expect) 48 | } 49 | } 50 | } 51 | 52 | func TestRequest_IsSSL(t *testing.T) { 53 | req := &Request{Request: &http.Request{Header: make(http.Header)}} 54 | actual := req.IsSSL() 55 | expected := false 56 | if !reflect.DeepEqual(actual, expected) { 57 | t.Errorf("Expect %v, but %v", expected, actual) 58 | } 59 | 60 | req.Header.Set("HTTPS", "on") 61 | actual = req.IsSSL() 62 | expected = true 63 | if !reflect.DeepEqual(actual, expected) { 64 | t.Errorf("Expect %v, but %v", expected, actual) 65 | } 66 | } 67 | 68 | func TestRequest_IsXHR(t *testing.T) { 69 | r, err := http.NewRequest("GET", "/", nil) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | req := &Request{Request: r} 74 | actual := req.IsXHR() 75 | expect := false 76 | if !reflect.DeepEqual(actual, expect) { 77 | t.Errorf(`Request.IsXHR() => %#v; want %#v`, actual, expect) 78 | } 79 | 80 | req.Request.Header.Set("X-Requested-With", "XMLHttpRequest") 81 | actual = req.IsXHR() 82 | expect = true 83 | if !reflect.DeepEqual(actual, expect) { 84 | t.Errorf(`Request.IsXHR() with "X-Requested-With: XMLHttpRequest" header => %#v; want %#v`, actual, expect) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | // ResourceSet represents a set of pre-loaded resources. 4 | type ResourceSet map[string]interface{} 5 | 6 | // Add adds pre-loaded resource. 7 | func (rs *ResourceSet) Add(name string, data interface{}) { 8 | if *rs == nil { 9 | *rs = ResourceSet{} 10 | } 11 | (*rs)[name] = data 12 | } 13 | 14 | // Get gets pre-loaded resource by name. 15 | func (rs ResourceSet) Get(name string) interface{} { 16 | return rs[name] 17 | } 18 | -------------------------------------------------------------------------------- /resource_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/naoina/kocha" 8 | ) 9 | 10 | func TestResourceSet_Add(t *testing.T) { 11 | rs := kocha.ResourceSet{} 12 | for _, v := range []struct { 13 | name string 14 | data interface{} 15 | }{ 16 | {"text1", "test1"}, 17 | {"text2", "test2"}, 18 | } { 19 | rs.Add(v.name, v.data) 20 | actual := rs[v.name] 21 | expected := v.data 22 | if !reflect.DeepEqual(actual, expected) { 23 | t.Errorf(`ResourceSet.Add("%#v", %#v) => %#v; want %#v`, v.name, v.data, actual, expected) 24 | } 25 | } 26 | } 27 | 28 | func TestResourceSet_Get(t *testing.T) { 29 | rs := kocha.ResourceSet{} 30 | for _, v := range []struct { 31 | name string 32 | data interface{} 33 | }{ 34 | {"text1", "test1"}, 35 | {"text2", "test2"}, 36 | } { 37 | rs[v.name] = v.data 38 | actual := rs.Get(v.name) 39 | expected := v.data 40 | if !reflect.DeepEqual(actual, expected) { 41 | t.Errorf(`ResourceSet.Get("%#v") => %#v; want %#v`, v.name, actual, expected) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "sync" 8 | ) 9 | 10 | var ( 11 | _ http.ResponseWriter = &Response{} 12 | 13 | responsePool = &sync.Pool{ 14 | New: func() interface{} { 15 | return &Response{} 16 | }, 17 | } 18 | ) 19 | 20 | // Response represents a response. 21 | type Response struct { 22 | http.ResponseWriter 23 | 24 | ContentType string 25 | StatusCode int 26 | 27 | cookies []*http.Cookie 28 | resp *httptest.ResponseRecorder 29 | } 30 | 31 | // newResponse returns a new Response that responds to rw. 32 | func newResponse() *Response { 33 | r := responsePool.Get().(*Response) 34 | r.reset() 35 | r.ContentType = "" 36 | r.cookies = r.cookies[:0] 37 | return r 38 | } 39 | 40 | // Cookies returns a slice of *http.Cookie. 41 | func (r *Response) Cookies() []*http.Cookie { 42 | return r.cookies 43 | } 44 | 45 | // SetCookie adds a Set-Cookie header to the response. 46 | func (r *Response) SetCookie(cookie *http.Cookie) { 47 | r.cookies = append(r.cookies, cookie) 48 | http.SetCookie(r, cookie) 49 | } 50 | 51 | func (r *Response) writeTo(w http.ResponseWriter) error { 52 | for key, values := range r.Header() { 53 | for _, v := range values { 54 | w.Header().Add(key, v) 55 | } 56 | } 57 | w.WriteHeader(r.resp.Code) 58 | _, err := io.Copy(w, r.resp.Body) 59 | responsePool.Put(r) 60 | return err 61 | } 62 | 63 | func (r *Response) reset() { 64 | r.StatusCode = http.StatusOK 65 | r.resp = httptest.NewRecorder() 66 | r.ResponseWriter = r.resp 67 | } 68 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/naoina/kocha" 10 | ) 11 | 12 | func TestResponse_Cookies(t *testing.T) { 13 | w := httptest.NewRecorder() 14 | res := &kocha.Response{ResponseWriter: w} 15 | actual := res.Cookies() 16 | expected := []*http.Cookie(nil) 17 | if !reflect.DeepEqual(actual, expected) { 18 | t.Errorf(`Response.Cookies() => %#v; want %#v`, actual, expected) 19 | } 20 | 21 | cookie := &http.Cookie{Name: "fox", Value: "dog"} 22 | res.SetCookie(cookie) 23 | actual = res.Cookies() 24 | expected = []*http.Cookie{cookie} 25 | if !reflect.DeepEqual(actual, expected) { 26 | t.Errorf(`Response.Cookies() => %#v; want %#v`, actual, expected) 27 | } 28 | } 29 | 30 | func TestResponse_SetCookie(t *testing.T) { 31 | w := httptest.NewRecorder() 32 | res := &kocha.Response{ 33 | ResponseWriter: w, 34 | } 35 | cookie := &http.Cookie{ 36 | Name: "testCookie", 37 | Value: "testCookieValue", 38 | } 39 | res.SetCookie(cookie) 40 | actual := w.Header().Get("Set-Cookie") 41 | expected := cookie.String() 42 | if !reflect.DeepEqual(actual, expected) { 43 | t.Errorf(`Response.SetCookie(%#v) => %#v; want %#v`, cookie, actual, expected) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "strings" 8 | 9 | "github.com/naoina/denco" 10 | "github.com/naoina/kocha/util" 11 | ) 12 | 13 | // The routing table. 14 | type RouteTable []*Route 15 | 16 | func (rt RouteTable) buildRouter() (*Router, error) { 17 | router := &Router{routeTable: rt} 18 | if err := router.buildForward(); err != nil { 19 | return nil, err 20 | } 21 | if err := router.buildReverse(); err != nil { 22 | return nil, err 23 | } 24 | return router, nil 25 | } 26 | 27 | // Router represents a router of kocha. 28 | type Router struct { 29 | forward *denco.Router 30 | reverse map[string]*Route 31 | routeTable RouteTable 32 | } 33 | 34 | func (router *Router) dispatch(req *Request) (name string, handler requestHandler, params denco.Params, found bool) { 35 | path := util.NormPath(req.URL.Path) 36 | data, params, found := router.forward.Lookup(path) 37 | if !found { 38 | return "", nil, nil, false 39 | } 40 | route := data.(*Route) 41 | handler, found = route.dispatch(req.Method) 42 | return route.Name, handler, params, found 43 | } 44 | 45 | // buildForward builds forward router. 46 | func (router *Router) buildForward() error { 47 | records := make([]denco.Record, len(router.routeTable)) 48 | for i, route := range router.routeTable { 49 | records[i] = denco.NewRecord(route.Path, route) 50 | } 51 | router.forward = denco.New() 52 | return router.forward.Build(records) 53 | } 54 | 55 | // buildReverse builds reverse router. 56 | func (router *Router) buildReverse() error { 57 | router.reverse = make(map[string]*Route) 58 | for _, route := range router.routeTable { 59 | router.reverse[route.Name] = route 60 | for i := 0; i < len(route.Path); i++ { 61 | if c := route.Path[i]; c == denco.ParamCharacter || c == denco.WildcardCharacter { 62 | next := denco.NextSeparator(route.Path, i+1) 63 | route.paramNames = append(route.paramNames, route.Path[i:next]) 64 | i = next 65 | } 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | // Reverse returns path of route by name and any params. 72 | func (router *Router) Reverse(name string, v ...interface{}) (string, error) { 73 | route := router.reverse[name] 74 | if route == nil { 75 | types := make([]string, len(v)) 76 | for i, value := range v { 77 | types[i] = reflect.TypeOf(value).Name() 78 | } 79 | return "", fmt.Errorf("kocha: no match route found: %v (%v)", name, strings.Join(types, ", ")) 80 | } 81 | return route.reverse(v...) 82 | } 83 | 84 | // Route represents a route. 85 | type Route struct { 86 | Name string 87 | Path string 88 | Controller Controller 89 | 90 | paramNames []string 91 | } 92 | 93 | func (route *Route) dispatch(method string) (handler requestHandler, found bool) { 94 | switch strings.ToUpper(method) { 95 | case "GET": 96 | if h, ok := route.Controller.(Getter); ok { 97 | return h.GET, true 98 | } 99 | case "POST": 100 | if h, ok := route.Controller.(Poster); ok { 101 | return h.POST, true 102 | } 103 | case "PUT": 104 | if h, ok := route.Controller.(Putter); ok { 105 | return h.PUT, true 106 | } 107 | case "DELETE": 108 | if h, ok := route.Controller.(Deleter); ok { 109 | return h.DELETE, true 110 | } 111 | case "HEAD": 112 | if h, ok := route.Controller.(Header); ok { 113 | return h.HEAD, true 114 | } 115 | case "PATCH": 116 | if h, ok := route.Controller.(Patcher); ok { 117 | return h.PATCH, true 118 | } 119 | } 120 | return nil, false 121 | } 122 | 123 | // ParamNames returns names of the path parameters. 124 | func (route *Route) ParamNames() []string { 125 | return route.paramNames 126 | } 127 | 128 | func (r *Route) reverse(v ...interface{}) (string, error) { 129 | switch vlen, nlen := len(v), len(r.paramNames); { 130 | case vlen < nlen: 131 | return "", fmt.Errorf("kocha: too few arguments: %v (controller is %T)", r.Name, r.Controller) 132 | case vlen > nlen: 133 | return "", fmt.Errorf("kocha: too many arguments: %v (controller is %T)", r.Name, r.Controller) 134 | case vlen+nlen == 0: 135 | return r.Path, nil 136 | } 137 | var oldnew []string 138 | for i := 0; i < len(v); i++ { 139 | oldnew = append(oldnew, r.paramNames[i], fmt.Sprint(v[i])) 140 | } 141 | replacer := strings.NewReplacer(oldnew...) 142 | path := replacer.Replace(r.Path) 143 | return util.NormPath(path), nil 144 | } 145 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/naoina/kocha" 9 | ) 10 | 11 | func TestRouter_Reverse(t *testing.T) { 12 | app := kocha.NewTestApp() 13 | for _, v := range []struct { 14 | name string 15 | args []interface{} 16 | expect string 17 | }{ 18 | {"root", []interface{}{}, "/"}, 19 | {"user", []interface{}{77}, "/user/77"}, 20 | {"date", []interface{}{2013, 10, 26, "naoina"}, "/2013/10/26/user/naoina"}, 21 | {"static", []interface{}{"/hoge.png"}, "/static/hoge.png"}, 22 | {"static", []interface{}{"hoge.png"}, "/static/hoge.png"}, 23 | } { 24 | r, err := app.Router.Reverse(v.name, v.args...) 25 | if err != nil { 26 | t.Errorf(`Router.Reverse(%#v, %#v) => (_, %#v); want (_, %#v)`, v.name, v.args, err, err) 27 | continue 28 | } 29 | actual := r 30 | expect := v.expect 31 | if !reflect.DeepEqual(actual, expect) { 32 | t.Errorf(`Router.Reverse(%#v, %#v) => (%#v, %#v); want (%#v, %#v)`, v.name, v.args, actual, err, expect, err) 33 | } 34 | } 35 | } 36 | 37 | func TestRouter_Reverse_withUnknownRouteName(t *testing.T) { 38 | app := kocha.NewTestApp() 39 | name := "unknown" 40 | _, err := app.Router.Reverse(name) 41 | actual := err 42 | expect := fmt.Errorf("kocha: no match route found: %s ()", name) 43 | if !reflect.DeepEqual(actual, expect) { 44 | t.Errorf("Router.Reverse(%#v) => (_, %#v); want (_, %#v)", name, actual, expect) 45 | } 46 | } 47 | 48 | func TestRouter_Reverse_withFewArguments(t *testing.T) { 49 | app := kocha.NewTestApp() 50 | name := "user" 51 | _, err := app.Router.Reverse(name) 52 | actual := err 53 | expect := fmt.Errorf("kocha: too few arguments: %s (controller is %T)", name, &kocha.FixtureUserTestCtrl{}) 54 | if !reflect.DeepEqual(actual, expect) { 55 | t.Errorf(`Router.Reverse(%#v) => (_, %#v); want (_, %#v)`, name, actual, expect) 56 | } 57 | } 58 | 59 | func TestRouter_Reverse_withManyArguments(t *testing.T) { 60 | app := kocha.NewTestApp() 61 | name := "user" 62 | args := []interface{}{77, 100} 63 | _, err := app.Router.Reverse(name, args...) 64 | actual := err 65 | expect := fmt.Errorf("kocha: too many arguments: %s (controller is %T)", name, &kocha.FixtureUserTestCtrl{}) 66 | if !reflect.DeepEqual(actual, expect) { 67 | t.Errorf(`Router.Reverse(%#v, %#v) => (_, %#v); want (_, %#v)`, name, args, actual, expect) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/hmac" 8 | "crypto/rand" 9 | "crypto/sha512" 10 | "encoding/base64" 11 | "errors" 12 | "fmt" 13 | "io" 14 | 15 | "github.com/ugorji/go/codec" 16 | ) 17 | 18 | // SessionStore is the interface that session store. 19 | type SessionStore interface { 20 | Save(sess Session) (key string, err error) 21 | Load(key string) (sess Session, err error) 22 | } 23 | 24 | // Session represents a session data store. 25 | type Session map[string]string 26 | 27 | // Get gets a value associated with the given key. 28 | // If there is the no value associated with the given key, Get returns "". 29 | func (sess Session) Get(key string) string { 30 | return sess[key] 31 | } 32 | 33 | // Set sets the value associated with the key. 34 | // If replaces the existing value associated with the key. 35 | func (sess Session) Set(key, value string) { 36 | sess[key] = value 37 | } 38 | 39 | // Del deletes the value associated with the key. 40 | func (sess Session) Del(key string) { 41 | delete(sess, key) 42 | } 43 | 44 | // Clear clear the all session data. 45 | func (sess Session) Clear() { 46 | for k, _ := range sess { 47 | delete(sess, k) 48 | } 49 | } 50 | 51 | type ErrSession struct { 52 | msg string 53 | } 54 | 55 | func (e ErrSession) Error() string { 56 | return e.msg 57 | } 58 | 59 | func NewErrSession(msg string) error { 60 | return ErrSession{ 61 | msg: msg, 62 | } 63 | } 64 | 65 | // Implementation of cookie store. 66 | // 67 | // This session store will be a session save to client-side cookie. 68 | // Session cookie for save is encoded, encrypted and signed. 69 | type SessionCookieStore struct { 70 | // key for the encryption. 71 | SecretKey string 72 | 73 | // Key for the cookie singing. 74 | SigningKey string 75 | } 76 | 77 | var codecHandler = &codec.MsgpackHandle{} 78 | 79 | // Save saves and returns the key of session cookie. 80 | // Actually, key is session cookie data itself. 81 | func (store *SessionCookieStore) Save(sess Session) (key string, err error) { 82 | buf := bufPool.Get().(*bytes.Buffer) 83 | defer func() { 84 | buf.Reset() 85 | bufPool.Put(buf) 86 | }() 87 | if err := codec.NewEncoder(buf, codecHandler).Encode(sess); err != nil { 88 | return "", err 89 | } 90 | encrypted, err := store.encrypt(buf.Bytes()) 91 | if err != nil { 92 | return "", err 93 | } 94 | return store.encode(store.sign(encrypted)), nil 95 | } 96 | 97 | // Load returns the session data that extract from cookie value. 98 | // The key is stored session cookie value. 99 | func (store *SessionCookieStore) Load(key string) (sess Session, err error) { 100 | decoded, err := store.decode(key) 101 | if err != nil { 102 | return nil, err 103 | } 104 | unsigned, err := store.verify(decoded) 105 | if err != nil { 106 | return nil, err 107 | } 108 | decrypted, err := store.decrypt(unsigned) 109 | if err != nil { 110 | return nil, err 111 | } 112 | if err := codec.NewDecoderBytes(decrypted, codecHandler).Decode(&sess); err != nil { 113 | return nil, err 114 | } 115 | return sess, nil 116 | } 117 | 118 | // Validate validates SecretKey size. 119 | func (store *SessionCookieStore) Validate() error { 120 | b, err := base64.StdEncoding.DecodeString(store.SecretKey) 121 | if err != nil { 122 | return err 123 | } 124 | store.SecretKey = string(b) 125 | b, err = base64.StdEncoding.DecodeString(store.SigningKey) 126 | if err != nil { 127 | return err 128 | } 129 | store.SigningKey = string(b) 130 | switch len(store.SecretKey) { 131 | case 16, 24, 32: 132 | return nil 133 | } 134 | return fmt.Errorf("kocha: session: %T.SecretKey size must be 16, 24 or 32, but %v", *store, len(store.SecretKey)) 135 | } 136 | 137 | // encrypt returns encrypted data by AES-256-CBC. 138 | func (store *SessionCookieStore) encrypt(buf []byte) ([]byte, error) { 139 | block, err := aes.NewCipher([]byte(store.SecretKey)) 140 | if err != nil { 141 | return nil, err 142 | } 143 | aead, err := cipher.NewGCM(block) 144 | if err != nil { 145 | return nil, err 146 | } 147 | iv := make([]byte, aead.NonceSize(), len(buf)+aead.NonceSize()) 148 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 149 | return nil, err 150 | } 151 | encrypted := aead.Seal(nil, iv, buf, nil) 152 | return append(iv, encrypted...), nil 153 | } 154 | 155 | // decrypt returns decrypted data from crypted data by AES-256-CBC. 156 | func (store *SessionCookieStore) decrypt(buf []byte) ([]byte, error) { 157 | block, err := aes.NewCipher([]byte(store.SecretKey)) 158 | if err != nil { 159 | return nil, err 160 | } 161 | aead, err := cipher.NewGCM(block) 162 | if err != nil { 163 | return nil, err 164 | } 165 | iv := buf[:aead.NonceSize()] 166 | decrypted := buf[aead.NonceSize():] 167 | if _, err := aead.Open(decrypted[:0], iv, decrypted, nil); err != nil { 168 | return nil, err 169 | } 170 | return decrypted, nil 171 | } 172 | 173 | // encode returns encoded string by Base64 with URLEncoding. 174 | // However, encoded string will stripped the padding character of Base64. 175 | func (store *SessionCookieStore) encode(src []byte) string { 176 | buf := make([]byte, base64.URLEncoding.EncodedLen(len(src))) 177 | base64.URLEncoding.Encode(buf, src) 178 | for { 179 | if buf[len(buf)-1] != '=' { 180 | break 181 | } 182 | buf = buf[:len(buf)-1] 183 | } 184 | return string(buf) 185 | } 186 | 187 | // decode returns decoded data from encoded data by Base64 with URLEncoding. 188 | func (store *SessionCookieStore) decode(src string) ([]byte, error) { 189 | size := len(src) 190 | rem := (4 - size%4) % 4 191 | buf := make([]byte, size+rem) 192 | copy(buf, src) 193 | for i := 0; i < rem; i++ { 194 | buf[size+i] = '=' 195 | } 196 | n, err := base64.URLEncoding.Decode(buf, buf) 197 | if err != nil { 198 | return nil, err 199 | } 200 | return buf[:n], nil 201 | } 202 | 203 | // sign returns signed data. 204 | func (store *SessionCookieStore) sign(src []byte) []byte { 205 | sign := store.hash(src) 206 | return append(sign, src...) 207 | } 208 | 209 | // verify verify signed data and returns unsigned data if valid. 210 | func (store *SessionCookieStore) verify(src []byte) (unsigned []byte, err error) { 211 | if len(src) <= sha512.Size256 { 212 | return nil, errors.New("kocha: session cookie value too short") 213 | } 214 | sign := src[:sha512.Size256] 215 | unsigned = src[sha512.Size256:] 216 | if !hmac.Equal(store.hash(unsigned), sign) { 217 | return nil, errors.New("kocha: session cookie verification failed") 218 | } 219 | return unsigned, nil 220 | } 221 | 222 | // hash returns hashed data by HMAC-SHA512/256. 223 | func (store *SessionCookieStore) hash(src []byte) []byte { 224 | hash := hmac.New(sha512.New512_256, []byte(store.SigningKey)) 225 | hash.Write(src) 226 | return hash.Sum(nil) 227 | } 228 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "testing/quick" 10 | 11 | "github.com/naoina/kocha" 12 | ) 13 | 14 | func TestSession(t *testing.T) { 15 | sess := make(kocha.Session) 16 | key := "test_key" 17 | var actual interface{} = sess.Get(key) 18 | var expected interface{} = "" 19 | if !reflect.DeepEqual(actual, expected) { 20 | t.Errorf(`Session.Get(%#v) => %#v; want %#v`, key, actual, expected) 21 | } 22 | actual = len(sess) 23 | expected = 0 24 | if !reflect.DeepEqual(actual, expected) { 25 | t.Errorf(`len(Session) => %#v; want %#v`, actual, expected) 26 | } 27 | 28 | value := "test_value" 29 | sess.Set(key, value) 30 | actual = sess.Get(key) 31 | expected = value 32 | if !reflect.DeepEqual(actual, expected) { 33 | t.Errorf(`Session.Set(%#v, %#v); Session.Get(%#v) => %#v; want %#v`, key, value, key, actual, expected) 34 | } 35 | actual = len(sess) 36 | expected = 1 37 | if !reflect.DeepEqual(actual, expected) { 38 | t.Errorf(`Session.Set(%#v, %#v); len(Session) => %#v; want %#v`, key, value, actual, expected) 39 | } 40 | 41 | key2 := "test_key2" 42 | value2 := "test_value2" 43 | sess.Set(key2, value2) 44 | actual = sess.Get(key2) 45 | expected = value2 46 | if !reflect.DeepEqual(actual, expected) { 47 | t.Errorf(`Session.Set(%#v, %#v); Session.Get(%#v) => %#v; want %#v`, key2, value2, key2, actual, expected) 48 | } 49 | actual = len(sess) 50 | expected = 2 51 | if !reflect.DeepEqual(actual, expected) { 52 | t.Errorf(`Session.Set(%#v, %#v); len(Session) => %#v; want %#v`, key2, value2, actual, expected) 53 | } 54 | 55 | value3 := "test_value3" 56 | sess.Set(key, value3) 57 | actual = sess.Get(key) 58 | expected = value3 59 | if !reflect.DeepEqual(actual, expected) { 60 | t.Errorf(`Session.Set(%#v, %#v); Session.Get(%#v) => %#v; want %#v`, key, value3, key, actual, expected) 61 | } 62 | actual = len(sess) 63 | expected = 2 64 | if !reflect.DeepEqual(actual, expected) { 65 | t.Errorf(`Session.Set(%#v, %#v); len(Session) => %#v; want %#v`, key, value3, actual, expected) 66 | } 67 | 68 | sess.Clear() 69 | for _, key := range []string{key, key2} { 70 | actual = sess.Get(key) 71 | expected = "" 72 | if !reflect.DeepEqual(actual, expected) { 73 | t.Errorf(`Session.Clear(); Session.Get(%#v) => %#v; want %#v`, key, actual, expected) 74 | } 75 | } 76 | actual = len(sess) 77 | expected = 0 78 | if !reflect.DeepEqual(actual, expected) { 79 | t.Errorf(`Session.Clear(); len(Session) => %#v; want %#v`, actual, expected) 80 | } 81 | } 82 | 83 | func TestSession_Get(t *testing.T) { 84 | sess := make(kocha.Session) 85 | key := "test_key" 86 | var actual interface{} = sess.Get(key) 87 | var expected interface{} = "" 88 | if !reflect.DeepEqual(actual, expected) { 89 | t.Errorf(`Session.Get(%#v) => %#v; want %#v`, key, actual, expected) 90 | } 91 | 92 | value := "test_value" 93 | sess[key] = value 94 | actual = sess.Get(key) 95 | expected = value 96 | if !reflect.DeepEqual(actual, expected) { 97 | t.Errorf(`Session.Get(%#v) => %#v; want %#v`, key, actual, expected) 98 | } 99 | 100 | delete(sess, key) 101 | actual = sess.Get(key) 102 | expected = "" 103 | if !reflect.DeepEqual(actual, expected) { 104 | t.Errorf(`Session.Get(%#v) => %#v; want %#v`, key, actual, expected) 105 | } 106 | } 107 | 108 | func TestSession_Set(t *testing.T) { 109 | sess := make(kocha.Session) 110 | key := "test_key" 111 | var actual interface{} = sess[key] 112 | var expected interface{} = "" 113 | if !reflect.DeepEqual(actual, expected) { 114 | t.Errorf(`Session[%#v] => %#v; want %#v`, key, actual, expected) 115 | } 116 | 117 | value := "test_value" 118 | sess.Set(key, value) 119 | actual = sess[key] 120 | expected = value 121 | if !reflect.DeepEqual(actual, expected) { 122 | t.Errorf(`Session.Set(%#v, %#v); Session[%#v] => %#v; want %#v`, key, value, key, actual, expected) 123 | } 124 | 125 | value2 := "test_value2" 126 | sess.Set(key, value2) 127 | actual = sess[key] 128 | expected = value2 129 | if !reflect.DeepEqual(actual, expected) { 130 | t.Errorf(`Session.Set(%#v, %#v); Session[%#v] => %#v; want %#v`, key, value2, key, actual, expected) 131 | } 132 | } 133 | 134 | func TestSession_Del(t *testing.T) { 135 | sess := make(kocha.Session) 136 | key := "test_key" 137 | value := "test_value" 138 | sess[key] = value 139 | var actual interface{} = sess[key] 140 | var expected interface{} = value 141 | if !reflect.DeepEqual(actual, expected) { 142 | t.Errorf(`Session[%#v] => %#v; want %#v`, key, actual, expected) 143 | } 144 | 145 | sess.Del(key) 146 | actual = sess[key] 147 | expected = "" 148 | if !reflect.DeepEqual(actual, expected) { 149 | t.Errorf(`Session.Del(%#v); Session[%#v] => %#v; want %#v`, key, key, actual, expected) 150 | } 151 | } 152 | 153 | func Test_Session_Clear(t *testing.T) { 154 | sess := make(kocha.Session) 155 | sess["hoge"] = "foo" 156 | sess["bar"] = "baz" 157 | actual := len(sess) 158 | expected := 2 159 | if !reflect.DeepEqual(actual, expected) { 160 | t.Errorf("Expect %v, but %v", expected, actual) 161 | } 162 | sess.Clear() 163 | actual = len(sess) 164 | expected = 0 165 | if !reflect.DeepEqual(actual, expected) { 166 | t.Errorf("Expect %v, but %v", expected, actual) 167 | } 168 | } 169 | 170 | func Test_SessionCookieStore(t *testing.T) { 171 | if err := quick.Check(func(k, v string) bool { 172 | expected := make(kocha.Session) 173 | expected[k] = v 174 | store := kocha.NewTestSessionCookieStore() 175 | r, err := store.Save(expected) 176 | if err != nil { 177 | t.Fatal(err) 178 | } 179 | actual, err := store.Load(r) 180 | if err != nil { 181 | t.Fatal(err) 182 | } 183 | return reflect.DeepEqual(actual, expected) 184 | }, nil); err != nil { 185 | t.Error(err) 186 | } 187 | 188 | func() { 189 | store := kocha.NewTestSessionCookieStore() 190 | key := "invalid" 191 | _, err := store.Load(key) 192 | actual := err 193 | expect := fmt.Errorf("kocha: session cookie value too short") 194 | if !reflect.DeepEqual(actual, expect) { 195 | t.Errorf(`SessionCookieStore.Load(%#v) => _, %#v; want %#v`, key, actual, expect) 196 | } 197 | }() 198 | } 199 | 200 | func Test_SessionCookieStore_Validate(t *testing.T) { 201 | // tests for validate the key size. 202 | for _, keySize := range []int{16, 24, 32} { 203 | store := &kocha.SessionCookieStore{ 204 | SecretKey: base64.StdEncoding.EncodeToString([]byte(strings.Repeat("a", keySize))), 205 | SigningKey: base64.StdEncoding.EncodeToString([]byte("a")), 206 | } 207 | if err := store.Validate(); err != nil { 208 | t.Errorf("Expect key size %v is valid, but returned error: %v", keySize, err) 209 | } 210 | } 211 | // boundary tests 212 | for _, keySize := range []int{15, 17, 23, 25, 31, 33} { 213 | store := &kocha.SessionCookieStore{ 214 | SecretKey: base64.StdEncoding.EncodeToString([]byte(strings.Repeat("a", keySize))), 215 | SigningKey: base64.StdEncoding.EncodeToString([]byte("a")), 216 | } 217 | if err := store.Validate(); err == nil { 218 | t.Errorf("Expect key size %v is invalid, but doesn't returned error", keySize) 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/naoina/kocha/util" 15 | ) 16 | 17 | const ( 18 | LayoutDir = "layout" 19 | ErrorTemplateDir = "error" 20 | 21 | layoutPath = LayoutDir + string(filepath.Separator) 22 | ) 23 | 24 | // TemplatePathInfo represents an information of template paths. 25 | type TemplatePathInfo struct { 26 | Name string // name of the application. 27 | Paths []string // directory paths of the template files. 28 | } 29 | 30 | type templateKey struct { 31 | appName string 32 | name string 33 | format string 34 | isLayout bool 35 | } 36 | 37 | func (k templateKey) String() string { 38 | p := k.name 39 | if k.isLayout { 40 | p = filepath.Join(LayoutDir, p) 41 | } 42 | return fmt.Sprintf("%s:%s.%s", k.appName, p, k.format) 43 | } 44 | 45 | // Template represents the templates information. 46 | type Template struct { 47 | PathInfo TemplatePathInfo // information of location of template paths. 48 | FuncMap TemplateFuncMap // same as template.FuncMap. 49 | LeftDelim string // left action delimiter. 50 | RightDelim string // right action delimiter. 51 | 52 | m map[templateKey]*template.Template 53 | app *Application 54 | } 55 | 56 | // Get gets a parsed template. 57 | func (t *Template) Get(appName, layout, name, format string) (*template.Template, error) { 58 | key := templateKey{ 59 | appName: appName, 60 | format: format, 61 | isLayout: layout != "", 62 | } 63 | if key.isLayout { 64 | key.name = layout 65 | } else { 66 | key.name = name 67 | } 68 | tmpl, exists := t.m[key] 69 | if !exists { 70 | return nil, fmt.Errorf("kocha: template not found: %s", key) 71 | } 72 | return tmpl, nil 73 | } 74 | 75 | func (t *Template) build(app *Application) (*Template, error) { 76 | if t == nil { 77 | t = &Template{} 78 | } 79 | t.app = app 80 | if t.LeftDelim == "" { 81 | t.LeftDelim = "{{" 82 | } 83 | if t.RightDelim == "" { 84 | t.RightDelim = "}}" 85 | } 86 | t, err := t.buildFuncMap() 87 | if err != nil { 88 | return nil, err 89 | } 90 | t, err = t.buildTemplateMap() 91 | if err != nil { 92 | return nil, err 93 | } 94 | return t, nil 95 | } 96 | 97 | func (t *Template) buildFuncMap() (*Template, error) { 98 | m := TemplateFuncMap{ 99 | "yield": t.yield, 100 | "in": t.in, 101 | "url": t.url, 102 | "nl2br": t.nl2br, 103 | "raw": t.raw, 104 | "invoke_template": t.invokeTemplate, 105 | "flash": t.flash, 106 | "join": t.join, 107 | } 108 | for name, fn := range t.FuncMap { 109 | m[name] = fn 110 | } 111 | t.FuncMap = m 112 | return t, nil 113 | } 114 | 115 | // buildTemplateMap returns templateMap constructed from templateSet. 116 | func (t *Template) buildTemplateMap() (*Template, error) { 117 | info := t.PathInfo 118 | var templatePaths map[string]map[string]map[string]string 119 | if data := t.app.ResourceSet.Get("_kocha_template_paths"); data != nil { 120 | if paths, ok := data.(map[string]map[string]map[string]string); ok { 121 | templatePaths = paths 122 | } 123 | } 124 | if templatePaths == nil { 125 | templatePaths = map[string]map[string]map[string]string{ 126 | info.Name: make(map[string]map[string]string), 127 | } 128 | for _, rootPath := range info.Paths { 129 | if err := t.collectTemplatePaths(templatePaths[info.Name], rootPath); err != nil { 130 | return nil, err 131 | } 132 | } 133 | t.app.ResourceSet.Add("_kocha_template_paths", templatePaths) 134 | } 135 | t.m = map[templateKey]*template.Template{} 136 | l := len(t.LeftDelim) + len("$ := .Data") + len(t.RightDelim) 137 | buf := bytes.NewBuffer(append(append(append(make([]byte, 0, l), t.LeftDelim...), "$ := .Data"...), t.RightDelim...)) 138 | for appName, templates := range templatePaths { 139 | if err := t.buildAppTemplateSet(buf, l, t.m, appName, templates); err != nil { 140 | return nil, err 141 | } 142 | } 143 | return t, nil 144 | } 145 | 146 | // TemplateFuncMap is an alias of templete.FuncMap. 147 | type TemplateFuncMap template.FuncMap 148 | 149 | func (t *Template) collectTemplatePaths(templatePaths map[string]map[string]string, templateDir string) error { 150 | return filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error { 151 | if err != nil { 152 | return err 153 | } 154 | if info.IsDir() { 155 | return nil 156 | } 157 | baseName, err := filepath.Rel(templateDir, path) 158 | if err != nil { 159 | return err 160 | } 161 | name := strings.TrimSuffix(baseName, util.TemplateSuffix) 162 | ext := filepath.Ext(name) 163 | if _, exists := templatePaths[ext]; !exists { 164 | templatePaths[ext] = make(map[string]string) 165 | } 166 | templatePaths[ext][name] = path 167 | return nil 168 | }) 169 | } 170 | 171 | func (t *Template) buildAppTemplateSet(buf *bytes.Buffer, l int, m map[templateKey]*template.Template, appName string, templates map[string]map[string]string) error { 172 | for ext, templateInfos := range templates { 173 | tmpl := template.New("") 174 | for name, path := range templateInfos { 175 | buf.Truncate(l) 176 | var body string 177 | if data := t.app.ResourceSet.Get(path); data != nil { 178 | if b, ok := data.(string); ok { 179 | buf.WriteString(b) 180 | body = buf.String() 181 | } 182 | } else { 183 | f, err := os.Open(path) 184 | if err != nil { 185 | return err 186 | } 187 | _, err = io.Copy(buf, f) 188 | f.Close() 189 | if err != nil { 190 | return err 191 | } 192 | body = buf.String() 193 | t.app.ResourceSet.Add(path, body) 194 | } 195 | if _, err := tmpl.New(name).Delims(t.LeftDelim, t.RightDelim).Funcs(template.FuncMap(t.FuncMap)).Parse(body); err != nil { 196 | return err 197 | } 198 | } 199 | for _, t := range tmpl.Templates() { 200 | key := templateKey{ 201 | appName: appName, 202 | name: strings.TrimSuffix(t.Name(), ext), 203 | format: ext[1:], // truncate the leading dot. 204 | } 205 | if strings.HasPrefix(key.name, layoutPath) { 206 | key.isLayout = true 207 | key.name = key.name[len(layoutPath):] 208 | } 209 | m[key] = t 210 | } 211 | } 212 | return nil 213 | } 214 | 215 | func (t *Template) yield(c *Context) (template.HTML, error) { 216 | tmpl, err := t.Get(t.app.Config.AppName, "", c.Name, c.Format) 217 | if err != nil { 218 | return "", err 219 | } 220 | buf := bufPool.Get().(*bytes.Buffer) 221 | defer func() { 222 | buf.Reset() 223 | bufPool.Put(buf) 224 | }() 225 | if err := tmpl.Execute(buf, c); err != nil { 226 | return "", err 227 | } 228 | return template.HTML(buf.String()), nil 229 | } 230 | 231 | // in is for "in" template function. 232 | func (t *Template) in(a, b interface{}) (bool, error) { 233 | v := reflect.ValueOf(a) 234 | switch v.Kind() { 235 | case reflect.Slice, reflect.Array, reflect.String: 236 | if v.IsNil() { 237 | return false, nil 238 | } 239 | for i := 0; i < v.Len(); i++ { 240 | if v.Index(i).Interface() == b { 241 | return true, nil 242 | } 243 | } 244 | default: 245 | return false, fmt.Errorf("valid types are slice, array and string, got `%s'", v.Kind()) 246 | } 247 | return false, nil 248 | } 249 | 250 | // url is for "url" template function. 251 | func (t *Template) url(name string, v ...interface{}) (string, error) { 252 | return t.app.Router.Reverse(name, v...) 253 | } 254 | 255 | // nl2br is for "nl2br" template function. 256 | func (t *Template) nl2br(text string) template.HTML { 257 | return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "
", -1)) 258 | } 259 | 260 | // raw is for "raw" template function. 261 | func (t *Template) raw(text string) template.HTML { 262 | return template.HTML(text) 263 | } 264 | 265 | // invokeTemplate is for "invoke_template" template function. 266 | func (t *Template) invokeTemplate(unit Unit, tmplName, defTmplName string, ctx ...*Context) (html template.HTML, err error) { 267 | var c *Context 268 | switch len(ctx) { 269 | case 0: // do nothing. 270 | case 1: 271 | c = ctx[0] 272 | default: 273 | return "", fmt.Errorf("number of context must be 0 or 1") 274 | } 275 | t.app.Invoke(unit, func() { 276 | if html, err = t.readPartialTemplate(tmplName, c); err != nil { 277 | // TODO: logging error. 278 | panic(ErrInvokeDefault) 279 | } 280 | }, func() { 281 | html, err = t.readPartialTemplate(defTmplName, c) 282 | }) 283 | return html, err 284 | } 285 | 286 | // flash is for "flash" template function. 287 | // This is a shorthand for {{.Flash.Get "success"}} in template. 288 | func (t *Template) flash(c *Context, key string) string { 289 | return c.Flash.Get(key) 290 | } 291 | 292 | // join is for "join" template function. 293 | func (t *Template) join(a interface{}, sep string) (string, error) { 294 | v := reflect.ValueOf(a) 295 | switch v.Kind() { 296 | case reflect.Slice, reflect.Array: 297 | // do nothing. 298 | default: 299 | return "", fmt.Errorf("valid types of first argument are slice or array, got `%s'", v.Kind()) 300 | } 301 | if v.Len() == 0 { 302 | return "", nil 303 | } 304 | buf := append(make([]byte, 0, v.Len()*2-1), fmt.Sprint(v.Index(0).Interface())...) 305 | for i := 1; i < v.Len(); i++ { 306 | buf = append(append(buf, sep...), fmt.Sprint(v.Index(i).Interface())...) 307 | } 308 | return string(buf), nil 309 | } 310 | 311 | func (t *Template) readPartialTemplate(name string, c *Context) (template.HTML, error) { 312 | tmpl, err := t.Get(t.app.Config.AppName, "", name, "html") 313 | if err != nil { 314 | return "", err 315 | } 316 | buf := bufPool.Get().(*bytes.Buffer) 317 | defer func() { 318 | buf.Reset() 319 | bufPool.Put(buf) 320 | }() 321 | if err := tmpl.Execute(buf, c); err != nil { 322 | return "", err 323 | } 324 | return template.HTML(buf.String()), nil 325 | } 326 | 327 | func errorTemplateName(code int) string { 328 | return filepath.Join(ErrorTemplateDir, strconv.Itoa(code)) 329 | } 330 | -------------------------------------------------------------------------------- /template_test.go: -------------------------------------------------------------------------------- 1 | package kocha_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/naoina/kocha" 16 | ) 17 | 18 | func TestTemplate_FuncMap_in_withInvalidType(t *testing.T) { 19 | app := kocha.NewTestApp() 20 | funcMap := template.FuncMap(app.Template.FuncMap) 21 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{in 1 1}}`)) 22 | var buf bytes.Buffer 23 | if err := tmpl.Execute(&buf, nil); err == nil { 24 | t.Errorf("Expect errors, but no errors") 25 | } 26 | } 27 | 28 | func TestTemplate_FuncMap_in(t *testing.T) { 29 | app := kocha.NewTestApp() 30 | funcMap := template.FuncMap(app.Template.FuncMap) 31 | var buf bytes.Buffer 32 | for _, v := range []struct { 33 | Arr interface{} 34 | Sep interface{} 35 | expect string 36 | err error 37 | }{ 38 | {[]string{"b", "a", "c"}, "a", "true", nil}, 39 | {[]string{"ab", "b", "c"}, "a", "false", nil}, 40 | {nil, "a", "", fmt.Errorf("valid types are slice, array and string, got `invalid'")}, 41 | } { 42 | buf.Reset() 43 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{in .Arr .Sep}}`)) 44 | err := tmpl.Execute(&buf, v) 45 | if !strings.HasSuffix(fmt.Sprint(err), fmt.Sprint(v.err)) { 46 | t.Errorf(`{{in %#v %#v}}; error has "%v"; want "%v"`, v.Arr, v.Sep, err, v.err) 47 | } 48 | actual := buf.String() 49 | expect := v.expect 50 | if !reflect.DeepEqual(actual, expect) { 51 | t.Errorf(`{{in %#v %#v}} => %#v; want %#v`, v.Arr, v.Sep, actual, expect) 52 | } 53 | } 54 | } 55 | 56 | func TestTemplate_FuncMap_url(t *testing.T) { 57 | app := kocha.NewTestApp() 58 | funcMap := template.FuncMap(app.Template.FuncMap) 59 | 60 | func() { 61 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{url "root"}}`)) 62 | var buf bytes.Buffer 63 | if err := tmpl.Execute(&buf, nil); err != nil { 64 | panic(err) 65 | } 66 | actual := buf.String() 67 | expected := "/" 68 | if !reflect.DeepEqual(actual, expected) { 69 | t.Errorf("Expect %q, but %q", expected, actual) 70 | } 71 | }() 72 | 73 | func() { 74 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{url "user" 713}}`)) 75 | var buf bytes.Buffer 76 | if err := tmpl.Execute(&buf, nil); err != nil { 77 | panic(err) 78 | } 79 | actual := buf.String() 80 | expected := "/user/713" 81 | if !reflect.DeepEqual(actual, expected) { 82 | t.Errorf("Expect %v, but %v", expected, actual) 83 | } 84 | }() 85 | } 86 | 87 | func TestTemplate_FuncMap_nl2br(t *testing.T) { 88 | app := kocha.NewTestApp() 89 | funcMap := template.FuncMap(app.Template.FuncMap) 90 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{nl2br "a\nb\nc\n"}}`)) 91 | var buf bytes.Buffer 92 | if err := tmpl.Execute(&buf, nil); err != nil { 93 | panic(err) 94 | } 95 | actual := buf.String() 96 | expected := "a
b
c
" 97 | if !reflect.DeepEqual(actual, expected) { 98 | t.Errorf("Expect %q, but %q", expected, actual) 99 | } 100 | } 101 | 102 | func TestTemplate_FuncMap_raw(t *testing.T) { 103 | app := kocha.NewTestApp() 104 | funcMap := template.FuncMap(app.Template.FuncMap) 105 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{raw "\n
"}}`)) 106 | var buf bytes.Buffer 107 | if err := tmpl.Execute(&buf, nil); err != nil { 108 | panic(err) 109 | } 110 | actual := buf.String() 111 | expected := "\n
" 112 | if !reflect.DeepEqual(actual, expected) { 113 | t.Errorf("Expect %q, but %q", expected, actual) 114 | } 115 | } 116 | 117 | func TestTemplate_FuncMap_invokeTemplate(t *testing.T) { 118 | // test that if ActiveIf returns true. 119 | func() { 120 | app := kocha.NewTestApp() 121 | funcMap := template.FuncMap(app.Template.FuncMap) 122 | c := &kocha.Context{ 123 | Data: map[interface{}]interface{}{ 124 | "unit": &testUnit{"test1", true, 0}, 125 | "ctx": "testctx1", 126 | }, 127 | } 128 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{invoke_template .Data.unit "test_tmpl1" "def_tmpl" $}}`)) 129 | var buf bytes.Buffer 130 | if err := tmpl.Execute(&buf, c); err != nil { 131 | t.Error(err) 132 | } 133 | actual := buf.String() 134 | expected := "test_tmpl1: testctx1\n" 135 | if !reflect.DeepEqual(actual, expected) { 136 | t.Errorf("Expect %q, but %q", expected, actual) 137 | } 138 | }() 139 | 140 | // test that if ActiveIf returns false. 141 | func() { 142 | app := kocha.NewTestApp() 143 | funcMap := template.FuncMap(app.Template.FuncMap) 144 | c := &kocha.Context{ 145 | Data: map[interface{}]interface{}{ 146 | "unit": &testUnit{"test2", false, 0}, 147 | "ctx": "testctx2", 148 | }, 149 | } 150 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{invoke_template .Data.unit "test_tmpl1" "def_tmpl" $}}`)) 151 | var buf bytes.Buffer 152 | if err := tmpl.Execute(&buf, c); err != nil { 153 | t.Error(err) 154 | } 155 | actual := buf.String() 156 | expected := "def_tmpl: testctx2\n" 157 | if !reflect.DeepEqual(actual, expected) { 158 | t.Errorf("Expect %q, but %q", expected, actual) 159 | } 160 | }() 161 | 162 | // test that unknown template. 163 | func() { 164 | app := kocha.NewTestApp() 165 | funcMap := template.FuncMap(app.Template.FuncMap) 166 | c := &kocha.Context{ 167 | Data: map[interface{}]interface{}{ 168 | "unit": &testUnit{"test3", true, 0}, 169 | "ctx": "testctx3", 170 | }, 171 | } 172 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{invoke_template .Data.unit "unknown_tmpl" "def_tmpl" $}}`)) 173 | var buf bytes.Buffer 174 | if err := tmpl.Execute(&buf, c); err != nil { 175 | t.Error(err) 176 | } 177 | actual := buf.String() 178 | expected := "def_tmpl: testctx3\n" 179 | if !reflect.DeepEqual(actual, expected) { 180 | t.Errorf("Expect %q, but %q", expected, actual) 181 | } 182 | }() 183 | 184 | // test that unknown templates. 185 | func() { 186 | app := kocha.NewTestApp() 187 | funcMap := template.FuncMap(app.Template.FuncMap) 188 | c := &kocha.Context{ 189 | Data: map[interface{}]interface{}{ 190 | "unit": &testUnit{"test4", true, 0}, 191 | "ctx": "testctx4", 192 | }, 193 | } 194 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{invoke_template .Data.unit "unknown_tmpl" "unknown_def_tmpl" $}}`)) 195 | var buf bytes.Buffer 196 | if err := tmpl.Execute(&buf, c); !strings.HasSuffix(err.Error(), "template not found: appname:unknown_def_tmpl.html") { 197 | t.Error(err) 198 | } 199 | }() 200 | 201 | // test that unknown default template. 202 | func() { 203 | app := kocha.NewTestApp() 204 | funcMap := template.FuncMap(app.Template.FuncMap) 205 | c := &kocha.Context{ 206 | Data: map[interface{}]interface{}{ 207 | "unit": &testUnit{"test5", true, 0}, 208 | "ctx": "testctx5", 209 | }, 210 | } 211 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{invoke_template .Data.unit "test_tmpl1" "unknown" $}}`)) 212 | var buf bytes.Buffer 213 | if err := tmpl.Execute(&buf, c); err != nil { 214 | t.Error(err) 215 | } 216 | }() 217 | 218 | // test that single context. 219 | func() { 220 | app := kocha.NewTestApp() 221 | funcMap := template.FuncMap(app.Template.FuncMap) 222 | c := &kocha.Context{ 223 | Data: map[interface{}]interface{}{ 224 | "unit": &testUnit{"test6", true, 0}, 225 | "ctx": "testctx6", 226 | }, 227 | } 228 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{invoke_template .Data.unit "test_tmpl1" "def_tmpl" $}}`)) 229 | var buf bytes.Buffer 230 | if err := tmpl.Execute(&buf, c); err != nil { 231 | t.Error(err) 232 | } 233 | actual := buf.String() 234 | expected := "test_tmpl1: testctx6\n" 235 | if !reflect.DeepEqual(actual, expected) { 236 | t.Errorf("Expect %q, but %q", expected, actual) 237 | } 238 | }() 239 | 240 | // test that too many contexts. 241 | func() { 242 | app := kocha.NewTestApp() 243 | funcMap := template.FuncMap(app.Template.FuncMap) 244 | c := &kocha.Context{ 245 | Data: map[interface{}]interface{}{ 246 | "unit": &testUnit{"test7", true, 0}, 247 | "ctx": "testctx7", 248 | }, 249 | } 250 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{invoke_template .Data.unit "test_tmpl1" "def_tmpl" $ $}}`)) 251 | var buf bytes.Buffer 252 | if err := tmpl.Execute(&buf, c); !strings.HasSuffix(err.Error(), "number of context must be 0 or 1") { 253 | t.Error(err) 254 | } 255 | }() 256 | } 257 | 258 | func TestTemplateFuncMap_flash(t *testing.T) { 259 | c := newTestContext("testctrlr", "") 260 | funcMap := template.FuncMap(c.App.Template.FuncMap) 261 | for _, v := range []struct { 262 | key string 263 | expect string 264 | }{ 265 | {"", ""}, 266 | {"success", "test succeeded"}, 267 | {"success", "test successful"}, 268 | {"error", "test failed"}, 269 | {"error", "test failure"}, 270 | } { 271 | c.Flash = kocha.Flash{} 272 | c.Flash.Set(v.key, v.expect) 273 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(fmt.Sprintf(`{{flash . "unknown"}}{{flash . "%s"}}`, v.key))) 274 | var buf bytes.Buffer 275 | if err := tmpl.Execute(&buf, c); err != nil { 276 | t.Error(err) 277 | continue 278 | } 279 | actual := buf.String() 280 | expect := v.expect 281 | if !reflect.DeepEqual(actual, expect) { 282 | t.Errorf(`{{flash . %#v}} => %#v; want %#v`, v.key, actual, expect) 283 | } 284 | } 285 | } 286 | 287 | func TestTemplateFuncMap_join(t *testing.T) { 288 | app := kocha.NewTestApp() 289 | funcMap := template.FuncMap(app.Template.FuncMap) 290 | tmpl := template.Must(template.New("test").Funcs(funcMap).Parse(`{{join .Arr .Sep}}`)) 291 | var buf bytes.Buffer 292 | for _, v := range []struct { 293 | Arr interface{} 294 | Sep string 295 | expect string 296 | }{ 297 | {[]int{1, 2, 3}, "&", "1&2&3"}, 298 | {[2]uint{12, 34}, " and ", "12 and 34"}, 299 | {[]string{"alice", "bob", "carol"}, ", ", "alice, bob, carol"}, 300 | {[]string(nil), "|", ""}, 301 | {[]bool{}, " or ", ""}, 302 | {[]interface{}{"1", 2, "three", uint32(4)}, "-", "1-2-three-4"}, 303 | {[]string{"あ", "い", "う", "え", "お"}, "_", "あ_い_う_え_お"}, 304 | {[]string{"a", "b", "c"}, "∧", "a∧b∧c"}, 305 | } { 306 | buf.Reset() 307 | if err := tmpl.Execute(&buf, v); err != nil { 308 | t.Error(err) 309 | continue 310 | } 311 | actual := buf.String() 312 | expect := v.expect 313 | if !reflect.DeepEqual(actual, expect) { 314 | t.Errorf(`{{join %#v %#v}} => %#v; want %#v`, v.Arr, v.Sep, actual, expect) 315 | } 316 | } 317 | } 318 | 319 | func TestTemplate_Get(t *testing.T) { 320 | app := kocha.NewTestApp() 321 | func() { 322 | for _, v := range []struct { 323 | appName string 324 | layout string 325 | ctrlrName string 326 | format string 327 | }{ 328 | {"appname", "application", "testctrlr", "html"}, 329 | {"appname", "", "testctrlr", "js"}, 330 | {"appname", "another_layout", "testctrlr", "html"}, 331 | } { 332 | tmpl, err := app.Template.Get(v.appName, v.layout, v.ctrlrName, v.format) 333 | var actual interface{} = err 334 | var expect interface{} = nil 335 | if !reflect.DeepEqual(actual, expect) { 336 | t.Fatalf(`Template.Get(%#v, %#v, %#v, %#v) => %T, %#v, want *template.Template, %#v`, v.appName, v.layout, v.ctrlrName, v.format, tmpl, actual, expect) 337 | } 338 | } 339 | }() 340 | 341 | func() { 342 | for _, v := range []struct { 343 | appName string 344 | layout string 345 | ctrlrName string 346 | format string 347 | expectErr error 348 | }{ 349 | {"unknownAppName", "app", "test_tmpl1", "html", fmt.Errorf("kocha: template not found: unknownAppName:%s", filepath.Join("layout", "app.html"))}, 350 | {"testAppName", "app", "unknown_tmpl1", "html", fmt.Errorf("kocha: template not found: testAppName:%s", filepath.Join("layout", "app.html"))}, 351 | {"testAppName", "app", "test_tmpl1", "xml", fmt.Errorf("kocha: template not found: testAppName:%s", filepath.Join("layout", "app.xml"))}, 352 | {"testAppName", "", "test_tmpl1", "xml", fmt.Errorf("kocha: template not found: testAppName:test_tmpl1.xml")}, 353 | } { 354 | tmpl, err := app.Template.Get(v.appName, v.layout, v.ctrlrName, v.format) 355 | actual := tmpl 356 | expect := (*template.Template)(nil) 357 | actualErr := err 358 | expectErr := v.expectErr 359 | if !reflect.DeepEqual(actual, expect) || !reflect.DeepEqual(actualErr, expectErr) { 360 | t.Errorf(`Template.Get(%#v, %#v, %#v, %#v) => %#v, %#v, ; want %#v, %#v`, v.appName, v.layout, v.ctrlrName, v.format, actual, actualErr, expect, expectErr) 361 | } 362 | } 363 | }() 364 | } 365 | 366 | func TestTemplateDelims(t *testing.T) { 367 | app, err := kocha.New(&kocha.Config{ 368 | AppPath: "testdata", 369 | AppName: "appname", 370 | DefaultLayout: "", 371 | Template: &kocha.Template{ 372 | PathInfo: kocha.TemplatePathInfo{ 373 | Name: "appname", 374 | Paths: []string{ 375 | filepath.Join("testdata", "app", "view"), 376 | }, 377 | }, 378 | LeftDelim: "{%", 379 | RightDelim: "%}", 380 | }, 381 | RouteTable: []*kocha.Route{ 382 | { 383 | Name: "another_delims", 384 | Path: "/", 385 | Controller: &kocha.FixtureAnotherDelimsTestCtrl{ 386 | Ctx: "test_other_delims_ctx", 387 | }, 388 | }, 389 | }, 390 | Middlewares: []kocha.Middleware{ 391 | &kocha.DispatchMiddleware{}, 392 | }, 393 | Logger: &kocha.LoggerConfig{ 394 | Writer: ioutil.Discard, 395 | }, 396 | }) 397 | if err != nil { 398 | t.Fatal(err) 399 | } 400 | req, err := http.NewRequest("GET", "/", nil) 401 | if err != nil { 402 | t.Fatal(err) 403 | } 404 | w := httptest.NewRecorder() 405 | app.ServeHTTP(w, req) 406 | var actual interface{} = w.Code 407 | var expect interface{} = 200 408 | if !reflect.DeepEqual(actual, expect) { 409 | t.Errorf(`GET / status => %#v; want %#v`, actual, expect) 410 | } 411 | actual = w.Body.String() 412 | expect = "This is other delims: test_other_delims_ctx\n" 413 | if !reflect.DeepEqual(actual, expect) { 414 | t.Errorf(`GET / => %#v; want %#v`, actual, expect) 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /testdata/app/view/another_delims.html: -------------------------------------------------------------------------------- 1 | This is other delims: {% $ %} 2 | -------------------------------------------------------------------------------- /testdata/app/view/date.html: -------------------------------------------------------------------------------- 1 | This is date {{$.name}}: {{$.year}}-{{$.month}}-{{$.day}} 2 | -------------------------------------------------------------------------------- /testdata/app/view/def_tmpl.html: -------------------------------------------------------------------------------- 1 | def_tmpl: {{$.ctx}} 2 | -------------------------------------------------------------------------------- /testdata/app/view/error/400.html: -------------------------------------------------------------------------------- 1 | 400 error 2 | -------------------------------------------------------------------------------- /testdata/app/view/error/404.html: -------------------------------------------------------------------------------- 1 | 404 template not found 2 | -------------------------------------------------------------------------------- /testdata/app/view/error/500.html: -------------------------------------------------------------------------------- 1 | 500 error 2 | -------------------------------------------------------------------------------- /testdata/app/view/error/500.json: -------------------------------------------------------------------------------- 1 | {"error":500} 2 | -------------------------------------------------------------------------------- /testdata/app/view/json.json: -------------------------------------------------------------------------------- 1 | {"tmpl5":"json"} 2 | -------------------------------------------------------------------------------- /testdata/app/view/layout/another_layout.html: -------------------------------------------------------------------------------- 1 | Another layout 2 | -------------------------------------------------------------------------------- /testdata/app/view/layout/application.html: -------------------------------------------------------------------------------- 1 | This is layout 2 | {{yield .}} 3 | -------------------------------------------------------------------------------- /testdata/app/view/layout/application.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "application", 3 | {{yield .}} 4 | } 5 | -------------------------------------------------------------------------------- /testdata/app/view/layout/sub.html: -------------------------------------------------------------------------------- 1 | This is sub 2 | {{yield .}} 3 | -------------------------------------------------------------------------------- /testdata/app/view/post_test.html: -------------------------------------------------------------------------------- 1 | {{$}} 2 | -------------------------------------------------------------------------------- /testdata/app/view/root.html: -------------------------------------------------------------------------------- 1 | This is root 2 | -------------------------------------------------------------------------------- /testdata/app/view/teapot.html: -------------------------------------------------------------------------------- 1 | I'm a tea pot 2 | -------------------------------------------------------------------------------- /testdata/app/view/teapot.json: -------------------------------------------------------------------------------- 1 | {"status":418, "text":"I'm a tea pot"} 2 | -------------------------------------------------------------------------------- /testdata/app/view/test_tmpl1.html: -------------------------------------------------------------------------------- 1 | test_tmpl1: {{$.ctx}} 2 | -------------------------------------------------------------------------------- /testdata/app/view/testctrlr.html: -------------------------------------------------------------------------------- 1 | tmpl 2 | -------------------------------------------------------------------------------- /testdata/app/view/testctrlr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | (function() { 3 | })(); 4 | -------------------------------------------------------------------------------- /testdata/app/view/testctrlr.json: -------------------------------------------------------------------------------- 1 | {"tmpl2":"content"} 2 | -------------------------------------------------------------------------------- /testdata/app/view/testctrlr_ctx.html: -------------------------------------------------------------------------------- 1 | tmpl_ctx: {{$}} 2 | -------------------------------------------------------------------------------- /testdata/app/view/user.html: -------------------------------------------------------------------------------- 1 | This is user {{$.id}} 2 | -------------------------------------------------------------------------------- /testdata/public/robots.txt: -------------------------------------------------------------------------------- 1 | # User-Agent: * 2 | # Disallow: / 3 | -------------------------------------------------------------------------------- /testdata/public/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | console.log("hello, kocha") 3 | -------------------------------------------------------------------------------- /testfixtures_test.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "path/filepath" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | func NewTestApp() *Application { 13 | config := &Config{ 14 | AppPath: "testdata", 15 | AppName: "appname", 16 | DefaultLayout: "application", 17 | Template: &Template{ 18 | PathInfo: TemplatePathInfo{ 19 | Name: "appname", 20 | Paths: []string{ 21 | filepath.Join("testdata", "app", "view"), 22 | }, 23 | }, 24 | }, 25 | RouteTable: RouteTable{ 26 | { 27 | Name: "root", 28 | Path: "/", 29 | Controller: &FixtureRootTestCtrl{}, 30 | }, 31 | { 32 | Name: "user", 33 | Path: "/user/:id", 34 | Controller: &FixtureUserTestCtrl{}, 35 | }, 36 | { 37 | Name: "date", 38 | Path: "/:year/:month/:day/user/:name", 39 | Controller: &FixtureDateTestCtrl{}, 40 | }, 41 | { 42 | Name: "error", 43 | Path: "/error", 44 | Controller: &FixtureErrorTestCtrl{}, 45 | }, 46 | { 47 | Name: "json", 48 | Path: "/json", 49 | Controller: &FixtureJsonTestCtrl{}, 50 | }, 51 | { 52 | Name: "teapot", 53 | Path: "/teapot", 54 | Controller: &FixtureTeapotTestCtrl{}, 55 | }, 56 | { 57 | Name: "panic_in_render", 58 | Path: "/panic_in_render", 59 | Controller: &FixturePanicInRenderTestCtrl{}, 60 | }, 61 | { 62 | Name: "static", 63 | Path: "/static/*path", 64 | Controller: &StaticServe{}, 65 | }, 66 | { 67 | Name: "post_test", 68 | Path: "/post_test", 69 | Controller: &FixturePostTestCtrl{}, 70 | }, 71 | { 72 | Name: "error_controller_test", 73 | Path: "/error_controller_test", 74 | Controller: &ErrorController{ 75 | StatusCode: http.StatusBadGateway, 76 | }, 77 | }, 78 | }, 79 | Logger: &LoggerConfig{ 80 | Writer: ioutil.Discard, 81 | }, 82 | Middlewares: []Middleware{&DispatchMiddleware{}}, 83 | MaxClientBodySize: DefaultMaxClientBodySize, 84 | } 85 | app, err := New(config) 86 | if err != nil { 87 | panic(err) 88 | } 89 | return app 90 | } 91 | 92 | func NewTestSessionCookieStore() *SessionCookieStore { 93 | return &SessionCookieStore{ 94 | SecretKey: "abcdefghijklmnopqrstuvwxyzABCDEF", 95 | SigningKey: "abcdefghijklmn", 96 | } 97 | } 98 | 99 | type OrderedOutputMap map[string]interface{} 100 | 101 | func (m OrderedOutputMap) String() string { 102 | keys := make([]string, 0, len(m)) 103 | for key, _ := range m { 104 | keys = append(keys, key) 105 | } 106 | sort.Strings(keys) 107 | outputs := make([]string, 0, len(keys)) 108 | for _, key := range keys { 109 | outputs = append(outputs, fmt.Sprintf("%s:%s", key, m[key])) 110 | } 111 | return fmt.Sprintf("map[%v]", strings.Join(outputs, " ")) 112 | } 113 | 114 | func (m OrderedOutputMap) GoString() string { 115 | keys := make([]string, 0, len(m)) 116 | for key, _ := range m { 117 | keys = append(keys, key) 118 | } 119 | sort.Strings(keys) 120 | for i, key := range keys { 121 | keys[i] = fmt.Sprintf("%#v:%#v", key, m[key]) 122 | } 123 | return fmt.Sprintf("map[string]interface{}{%v}", strings.Join(keys, ", ")) 124 | } 125 | 126 | type FixturePanicInRenderTestCtrl struct { 127 | *DefaultController 128 | } 129 | 130 | func (ctrl *FixturePanicInRenderTestCtrl) GET(c *Context) error { 131 | return c.RenderXML(map[interface{}]interface{}{}) // Context is unsupported type in XML. 132 | } 133 | 134 | type FixtureUserTestCtrl struct { 135 | *DefaultController 136 | } 137 | 138 | func (ctrl *FixtureUserTestCtrl) GET(c *Context) error { 139 | return c.Render(map[interface{}]interface{}{ 140 | "id": c.Params.Get("id"), 141 | }) 142 | } 143 | 144 | type FixtureDateTestCtrl struct { 145 | DefaultController 146 | } 147 | 148 | func (ctrl *FixtureDateTestCtrl) GET(c *Context) error { 149 | return c.Render(map[interface{}]interface{}{ 150 | "year": c.Params.Get("year"), 151 | "month": c.Params.Get("month"), 152 | "day": c.Params.Get("day"), 153 | "name": c.Params.Get("name"), 154 | }) 155 | } 156 | 157 | type FixtureErrorTestCtrl struct { 158 | DefaultController 159 | } 160 | 161 | func (ctrl *FixtureErrorTestCtrl) GET(c *Context) error { 162 | panic("panic test") 163 | } 164 | 165 | type FixtureJsonTestCtrl struct { 166 | DefaultController 167 | } 168 | 169 | func (ctrl *FixtureJsonTestCtrl) GET(c *Context) error { 170 | c.Response.ContentType = "application/json" 171 | return c.Render(nil) 172 | } 173 | 174 | type FixtureRootTestCtrl struct { 175 | *DefaultController 176 | } 177 | 178 | func (ctrl *FixtureRootTestCtrl) GET(c *Context) error { 179 | return c.Render(nil) 180 | } 181 | 182 | type FixtureTeapotTestCtrl struct { 183 | DefaultController 184 | } 185 | 186 | func (ctrl *FixtureTeapotTestCtrl) GET(c *Context) error { 187 | c.Response.StatusCode = http.StatusTeapot 188 | return c.Render(nil) 189 | } 190 | 191 | type FixtureInvalidReturnValueTypeTestCtrl struct { 192 | *DefaultController 193 | } 194 | 195 | func (ctrl *FixtureInvalidReturnValueTypeTestCtrl) GET(c *Context) string { 196 | return "" 197 | } 198 | 199 | type FixturePostTestCtrl struct { 200 | *DefaultController 201 | } 202 | 203 | func (ctrl *FixturePostTestCtrl) POST(c *Context) error { 204 | m := OrderedOutputMap{} 205 | for k, v := range c.Params.Values { 206 | m[k] = v 207 | } 208 | return c.Render(map[interface{}]interface{}{"params": m}) 209 | } 210 | 211 | type FixtureAnotherDelimsTestCtrl struct { 212 | *DefaultController 213 | Ctx string 214 | } 215 | 216 | func (ctrl *FixtureAnotherDelimsTestCtrl) GET(c *Context) error { 217 | return c.Render(ctrl.Ctx) 218 | } 219 | -------------------------------------------------------------------------------- /unit.go: -------------------------------------------------------------------------------- 1 | package kocha 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvokeDefault = errors.New("invoke default") 7 | ) 8 | 9 | // Unit is an interface that Unit for FeatureToggle. 10 | type Unit interface { 11 | // ActiveIf returns whether the Unit is active. 12 | ActiveIf() bool 13 | } 14 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "go/build" 7 | "go/format" 8 | "html/template" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "regexp" 14 | "sort" 15 | "strings" 16 | "testing" 17 | "testing/quick" 18 | ) 19 | 20 | type orderedOutputMap map[string]interface{} 21 | 22 | func (m orderedOutputMap) String() string { 23 | keys := make([]string, 0, len(m)) 24 | for key, _ := range m { 25 | keys = append(keys, key) 26 | } 27 | sort.Strings(keys) 28 | outputs := make([]string, 0, len(keys)) 29 | for _, key := range keys { 30 | outputs = append(outputs, fmt.Sprintf("%s:%s", key, m[key])) 31 | } 32 | return fmt.Sprintf("map[%v]", strings.Join(outputs, " ")) 33 | } 34 | 35 | func (m orderedOutputMap) GoString() string { 36 | keys := make([]string, 0, len(m)) 37 | for key, _ := range m { 38 | keys = append(keys, key) 39 | } 40 | sort.Strings(keys) 41 | for i, key := range keys { 42 | keys[i] = fmt.Sprintf("%#v:%#v", key, m[key]) 43 | } 44 | return fmt.Sprintf("map[string]interface{}{%v}", strings.Join(keys, ", ")) 45 | } 46 | 47 | func Test_NormPath(t *testing.T) { 48 | for v, expected := range map[string]string{ 49 | "/": "/", 50 | "/path": "/path", 51 | "/path/": "/path/", 52 | "//path//": "/path/", 53 | "/path/to": "/path/to", 54 | "/path/to///": "/path/to/", 55 | } { 56 | actual := NormPath(v) 57 | if !reflect.DeepEqual(actual, expected) { 58 | t.Errorf("%v: Expect %v, but %v", v, expected, actual) 59 | } 60 | } 61 | } 62 | 63 | func TestGoString(t *testing.T) { 64 | re := regexp.MustCompile(`^/path/to/([^/]+)(?:\.html)?$`) 65 | actual := GoString(re) 66 | expected := `regexp.MustCompile("^/path/to/([^/]+)(?:\\.html)?$")` 67 | if !reflect.DeepEqual(actual, expected) { 68 | t.Errorf("Expect %v, but %v", expected, actual) 69 | } 70 | 71 | tmpl := template.Must(template.New("test").Parse(`foo{{.name}}bar`)) 72 | actual = GoString(tmpl) 73 | expected = `template.Must(template.New("test").Funcs(kocha.TemplateFuncs).Parse(util.Gunzip("\x1f\x8b\b\x00\x00\x00\x00\x00\x02\xffJ\xcbϯ\xae\xd6\xcbK\xccM\xad\xadMJ,\x02\x04\x00\x00\xff\xff4%\x83\xb6\x0f\x00\x00\x00")))` 74 | if !reflect.DeepEqual(actual, expected) { 75 | t.Errorf("Expect %v, but %v", expected, actual) 76 | } 77 | 78 | actual = GoString(testGoString{}) 79 | expected = "gostring" 80 | if !reflect.DeepEqual(actual, expected) { 81 | t.Errorf("Expect %v, but %v", expected, actual) 82 | } 83 | 84 | actual = GoString(nil) 85 | expected = "nil" 86 | if !reflect.DeepEqual(actual, expected) { 87 | t.Errorf("Expect %v, but %v", expected, actual) 88 | } 89 | 90 | var ptr *int 91 | actual = GoString(ptr) 92 | expected = "nil" 93 | if !reflect.DeepEqual(actual, expected) { 94 | t.Errorf("Expect %v, but %v", expected, actual) 95 | } 96 | 97 | n := 10 98 | nptr := &n 99 | actual = GoString(nptr) 100 | expected = "10" 101 | if !reflect.DeepEqual(actual, expected) { 102 | t.Errorf("Expect %v, but %v", expected, actual) 103 | } 104 | 105 | aBuf, err := format.Source([]byte(GoString(struct { 106 | Name, path string 107 | Route orderedOutputMap 108 | G *testGoString 109 | }{ 110 | Name: "foo", 111 | path: "path", 112 | Route: orderedOutputMap{ 113 | "first": "Tokyo", 114 | "second": "Kyoto", 115 | "third": []int{10, 11, 20}, 116 | }, 117 | G: &testGoString{}, 118 | }))) 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | eBuf, err := format.Source([]byte(` 123 | struct { 124 | Name string 125 | path string 126 | Route util.orderedOutputMap 127 | G *util.testGoString 128 | }{ 129 | 130 | G: gostring, 131 | 132 | Name: "foo", 133 | 134 | Route: map[string]interface{}{"first": "Tokyo", "second": "Kyoto", "third": []int{10, 11, 20}}, 135 | }`)) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | actual = string(aBuf) 140 | expected = string(eBuf) 141 | if !reflect.DeepEqual(actual, expected) { 142 | t.Errorf("Expect %q, but %q", expected, actual) 143 | } 144 | } 145 | 146 | type testGoString struct{} 147 | 148 | func (g testGoString) GoString() string { return "gostring" } 149 | 150 | func Test_Gzip(t *testing.T) { 151 | actual := base64.StdEncoding.EncodeToString([]byte(Gzip("kocha"))) 152 | expected := "H4sIAAAAAAAC/8rOT85IBAQAAP//DJOFlgUAAAA=" 153 | if !reflect.DeepEqual(actual, expected) { 154 | t.Errorf("Expect %v, but %v", expected, actual) 155 | } 156 | 157 | // reversibility test 158 | gz := Gzip("kocha") 159 | actual = Gunzip(gz) 160 | expected = "kocha" 161 | if !reflect.DeepEqual(actual, expected) { 162 | t.Errorf("Expect %v, but %v", expected, actual) 163 | } 164 | } 165 | 166 | func TestGunzip(t *testing.T) { 167 | actual := Gunzip("\x1f\x8b\b\x00\x00\tn\x88\x02\xff\xca\xceO\xceH\x04\x04\x00\x00\xff\xff\f\x93\x85\x96\x05\x00\x00\x00") 168 | expected := "kocha" 169 | if !reflect.DeepEqual(actual, expected) { 170 | t.Errorf("Expect %v, but %v", expected, actual) 171 | } 172 | 173 | // reversibility test 174 | raw := Gunzip("\x1f\x8b\b\x00\x00\tn\x88\x02\xff\xca\xceO\xceH\x04\x04\x00\x00\xff\xff\f\x93\x85\x96\x05\x00\x00\x00") 175 | actual = base64.StdEncoding.EncodeToString([]byte(Gzip(raw))) 176 | expected = "H4sIAAAAAAAC/8rOT85IBAQAAP//DJOFlgUAAAA=" 177 | if !reflect.DeepEqual(actual, expected) { 178 | t.Errorf("Expect %v, but %v", expected, actual) 179 | } 180 | } 181 | 182 | func TestFindAppDir(t *testing.T) { 183 | tempDir, err := ioutil.TempDir("", "TestFindAppDir") 184 | if err != nil { 185 | t.Fatal(err) 186 | } 187 | defer os.RemoveAll(tempDir) 188 | origGOPATH := build.Default.GOPATH 189 | defer func() { 190 | build.Default.GOPATH = origGOPATH 191 | os.Setenv("GOPATH", origGOPATH) 192 | }() 193 | build.Default.GOPATH = tempDir + string(filepath.ListSeparator) + build.Default.GOPATH 194 | os.Setenv("GOPATH", build.Default.GOPATH) 195 | myappPath := filepath.Join(tempDir, "src", "path", "to", "myapp") 196 | if err := os.MkdirAll(myappPath, 0755); err != nil { 197 | t.Fatal(err) 198 | } 199 | if err := os.Chdir(myappPath); err != nil { 200 | t.Fatal(err) 201 | } 202 | actual, err := FindAppDir() 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | expected := "path/to/myapp" 207 | if !reflect.DeepEqual(actual, expected) { 208 | t.Errorf("FindAppDir() => %q, want %q", actual, expected) 209 | } 210 | } 211 | 212 | func TestIsUnexportedField(t *testing.T) { 213 | // test for bug case older than Go1.3. 214 | func() { 215 | type b struct{} 216 | type C struct { 217 | b 218 | } 219 | v := reflect.TypeOf(C{}).Field(0) 220 | actual := IsUnexportedField(v) 221 | expected := true 222 | if !reflect.DeepEqual(actual, expected) { 223 | t.Errorf("IsUnexportedField(%#v) => %v, want %v", v, actual, expected) 224 | } 225 | }() 226 | 227 | // test for correct case. 228 | func() { 229 | type B struct{} 230 | type C struct { 231 | B 232 | } 233 | v := reflect.TypeOf(C{}).Field(0) 234 | actual := IsUnexportedField(v) 235 | expected := false 236 | if !reflect.DeepEqual(actual, expected) { 237 | t.Errorf("IsUnexportedField(%#v) => %v, want %v", v, actual, expected) 238 | } 239 | }() 240 | } 241 | 242 | func TestGenerateRandomKey(t *testing.T) { 243 | if err := quick.Check(func(length uint16) bool { 244 | already := make([][]byte, 0, 100) 245 | for i := 0; i < 100; i++ { 246 | buf := GenerateRandomKey(int(length)) 247 | for _, v := range already { 248 | if !reflect.DeepEqual(buf, v) { 249 | return false 250 | } 251 | } 252 | } 253 | return true 254 | }, &quick.Config{MaxCount: 10}); err != nil { 255 | t.Error(err) 256 | } 257 | } 258 | --------------------------------------------------------------------------------