diff --git a/src/app/app.go b/src/app/app.go deleted file mode 100644 index 512c9c7..0000000 --- a/src/app/app.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - app "app/server" - "os" - "runtime" -) - -func main() { - runtime.GOMAXPROCS(runtime.NumCPU()) - app.Run(os.Args) -} diff --git a/src/app/client/components/homepage/index.js b/src/app/client/components/homepage/index.js index ce3e146..94d0a58 100644 --- a/src/app/client/components/homepage/index.js +++ b/src/app/client/components/homepage/index.js @@ -19,13 +19,6 @@ export default class Homepage extends Component {

Please take a look at usage page.

- {/* -

- -   - -

- */} ; } diff --git a/src/app/main.go b/src/app/main.go new file mode 100644 index 0000000..90745f8 --- /dev/null +++ b/src/app/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "app/server" + "os" + "runtime" + + "github.com/codegangsta/cli" +) + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) + Run(os.Args) +} + +// Run creates, configures and runs +// main cli.App +func Run(args []string) { + + app := cli.NewApp() + app.Name = "app" + app.Usage = "React server application" + + configFlag := cli.StringFlag{ + Name: "config, c", + Value: "local", + Usage: "configuration section name", + EnvVar: "CONFIG", + } + + app.Commands = []cli.Command{ + { + Name: "run", + Usage: "Runs server", + Action: RunServer, + Flags: []cli.Flag{configFlag}, + }, + } + app.Run(args) +} + +// RunServer creates, configures and runs +// main server.App +func RunServer(c *cli.Context) { + app := server.NewApp(server.AppOptions{ + Config: c.String("config"), + }) + app.Run() +} diff --git a/src/app/server/api.go b/src/app/server/api.go new file mode 100644 index 0000000..e53acd4 --- /dev/null +++ b/src/app/server/api.go @@ -0,0 +1,21 @@ +package server + +import ( + "github.com/gin-gonic/gin" +) + +// Api is a defined as struct bundle +// for api. Feel free to organize +// your app as you wish. +type Api struct{} + +// Bind attaches api routes +func (api *Api) Bind(group *gin.RouterGroup) { + group.GET("/v1/conf", api.ConfHandler) +} + +// Serve the app config, for example +func (_ *Api) ConfHandler(c *gin.Context) { + app := c.MustGet("app").(*App) + c.JSON(200, app.Conf.Root) +} diff --git a/src/app/server/api/api.go b/src/app/server/api/api.go deleted file mode 100644 index 80f6eb3..0000000 --- a/src/app/server/api/api.go +++ /dev/null @@ -1,13 +0,0 @@ -package api - -import ( - "app/server/utils" - - "github.com/gin-gonic/gin" -) - -// Serve the app config -func ConfHandler(c *gin.Context) { - kit := c.MustGet("kit").(*utils.Kit) - c.JSON(200, kit.Conf.Root) -} diff --git a/src/app/server/app.go b/src/app/server/app.go new file mode 100644 index 0000000..fe31195 --- /dev/null +++ b/src/app/server/app.go @@ -0,0 +1,113 @@ +package server + +import ( + "app/server/data" + + "github.com/elazarl/go-bindata-assetfs" + "github.com/gin-gonic/gin" + "github.com/nu7hatch/gouuid" + "github.com/olebedev/config" +) + +// There is no singleton anti-pattern, +// all variables defined locally inside +// this struct. +type App struct { + Engine *gin.Engine + Conf *config.Config + React *React + Api *Api +} + +// NewApp returns initialized struct +// of main server application. +func NewApp(opts ...AppOptions) *App { + var options AppOptions + for _, i := range opts { + options = i + i.init() + break + } + + // Parse config yaml string from ./conf.go + conf, err := config.ParseYaml(confString) + Must(err) + // Choise a config section by given string + conf, err = conf.Get(options.Config) + Must(err) + + // Parse environ variables for defined + // in config constants + conf.Env() + + // Set up gin + if !conf.UBool("debug") { + gin.SetMode(gin.ReleaseMode) + } + + // Make an engine + engine := gin.Default() + + // Initialize the application + app := &App{ + Conf: conf, + Engine: engine, + Api: &Api{}, + React: NewReact( + conf.UBool("debug"), + engine, + ), + } + + // Define routes and middlewares + app.Engine.StaticFS("/static", &assetfs.AssetFS{ + Asset: data.Asset, + AssetDir: data.AssetDir, + Prefix: "static", + }) + + // Map app struct to access from request handlers + // and middlewares + app.Engine.Use(func(c *gin.Context) { + c.Set("app", app) + }) + + // Avoid favicon react handling + app.Engine.GET("/favicon.ico", func(c *gin.Context) { + c.Redirect(301, "/static/images/favicon.ico") + }) + + // Bind api hadling for URL api.prefix + app.Api.Bind( + app.Engine.Group( + app.Conf.UString("api.prefix"), + ), + ) + + // Map uuid for every requests + app.Engine.Use(func(c *gin.Context) { + id, _ := uuid.NewV4() + c.Set("uuid", id) + }) + + // Handle all not found routes via react app + app.Engine.NoRoute(app.React.Handle) + + return app +} + +// Run runs the app +func (app *App) Run() { + Must(app.Engine.Run(":" + app.Conf.UString("port"))) +} + +// AppOptions is options struct +type AppOptions struct { + Config string +} + +func (ao *AppOptions) init() { + if ao.Config == "" { + ao.Config = "local" + } +} diff --git a/src/app/server/commands.go b/src/app/server/commands.go deleted file mode 100644 index f22a37c..0000000 --- a/src/app/server/commands.go +++ /dev/null @@ -1,66 +0,0 @@ -package server - -import ( - "app/server/api" - "app/server/data" - "app/server/react" - "app/server/utils" - - "github.com/codegangsta/cli" - "github.com/elazarl/go-bindata-assetfs" - "github.com/gin-gonic/gin" - "github.com/nu7hatch/gouuid" -) - -func Run(args []string) { - - app := cli.NewApp() - app.Name = "app" - app.Usage = "React server application" - - configFlag := cli.StringFlag{ - Name: "config, c", - Value: "local", - Usage: "configuration section name", - EnvVar: "CONFIG", - } - - app.Commands = []cli.Command{ - { - Name: "run", - Usage: "Runs server", - Action: runServer, - Flags: []cli.Flag{configFlag}, - }, - } - app.Run(args) -} - -func runServer(c *cli.Context) { - kit := utils.NewKit(c, conf) - kit.Engine = gin.Default() - - kit.Engine.StaticFS("/static", &assetfs.AssetFS{ - Asset: data.Asset, - AssetDir: data.AssetDir, - Prefix: "static", - }) - - kit.Engine.Use(func(c *gin.Context) { - c.Set("kit", kit) - }) - - // Avoid favicon react handling - kit.Engine.GET("/favicon.ico", func(c *gin.Context) { - c.Redirect(301, "/static/images/favicon.ico") - }) - - kit.Engine.GET("/api/v1/conf", api.ConfHandler) - - kit.Engine.Use(func(c *gin.Context) { - id, _ := uuid.NewV4() - c.Set("uuid", id) - }) - react.Bind(kit) - utils.Must(kit.Engine.Run(":" + kit.Conf.UString("port"))) -} diff --git a/src/app/server/conf.go b/src/app/server/conf.go index d2ec8ab..a6267d1 100644 --- a/src/app/server/conf.go +++ b/src/app/server/conf.go @@ -1,31 +1,25 @@ package server -import ( - . "app/server/utils" - - "github.com/olebedev/config" -) - -var conf *config.Config - -func init() { - c, err := config.ParseYaml(` +// Most easiest way to configure +// an application is define config as +// yaml string and then parse it into +// map. +// How it works see here: +// https://github.com/olebedev/config +var confString = ` local: debug: true port: 5000 - title: lmbd - db: ./db.sqlite + title: Go Starter Kit api: prefix: /api production: debug: false port: 5000 - title: lmbd + title: Go Starter Kit api: prefix: /api -`) - Must(err) - conf = c -} + +` diff --git a/src/app/server/react/react.go b/src/app/server/react.go similarity index 80% rename from src/app/server/react/react.go rename to src/app/server/react.go index bfbd05e..00a8c06 100644 --- a/src/app/server/react/react.go +++ b/src/app/server/react.go @@ -1,8 +1,7 @@ -package react +package server import ( "app/server/data" - "app/server/utils" "encoding/json" "fmt" "net/http" @@ -15,32 +14,32 @@ import ( "github.com/olebedev/go-duktape-fetch" ) -func Bind(kit *utils.Kit) { - r := react{kit: kit} - r.init() - kit.Engine.NoRoute(r.handle) -} - -type react struct { - pool pool - kit *utils.Kit -} - -func (r *react) init() { - if !r.kit.Conf.UBool("debug") { - r.pool = newDuktapePool(runtime.NumCPU(), r.kit.Engine) +// NewReact initialized React struct +func NewReact(debug bool, server http.Handler) *React { + r := &React{} + if !debug { + r.pool = newDuktapePool(runtime.NumCPU(), server) } else { // Use onDemandPool to load full react // app each time for any http requests. // Useful to debug the app. - r.pool = &onDemandPool{r.kit.Engine} + r.pool = &onDemandPool{server} } + return r +} + +// React struct is contains duktape +// pool to serve HTTP requests and +// separates some domain specific +// resources. +type React struct { + pool } // Handle handles all HTTP requests which // have no been caught via static file -// handler or other middlewares -func (r *react) handle(c *gin.Context) { +// handler or other middlewares. +func (r *React) Handle(c *gin.Context) { defer func() { if r := recover(); r != nil { UUID := c.MustGet("uuid").(*uuid.UUID) @@ -51,12 +50,11 @@ func (r *react) handle(c *gin.Context) { } }() - vm := r.pool.get() + vm := r.get() vm.Lock() vm.PushGlobalObject() vm.GetPropString(-1, "__router__") - // vm.Replace(-2) vm.PushString("renderToString") req := func() string { @@ -90,7 +88,7 @@ func (r *react) handle(c *gin.Context) { select { case re := <-ch: // Hold the context. This call is really important - // because async calls is possible. So, we cannot + // because async calls is possible. And we cannot // allow to break the context stack. vm.Lock() // Clean duktape vm stack @@ -101,7 +99,7 @@ func (r *react) handle(c *gin.Context) { // Release the context vm.Unlock() // Return vm back to the pool - r.pool.put(vm) + r.put(vm) // Handle the response if len(re.Redirect) == 0 && len(re.Error) == 0 { @@ -121,7 +119,8 @@ func (r *react) handle(c *gin.Context) { c.Abort() } case <-time.After(5 * time.Second): - r.pool.drop(vm) + // release duktape context + r.drop(vm) UUID := c.MustGet("uuid").(*uuid.UUID) c.Writer.WriteHeader(http.StatusInternalServerError) c.Writer.Header().Add("Content-Type", "text/plain") @@ -130,20 +129,23 @@ func (r *react) handle(c *gin.Context) { } } +// Resp is a struct for convinient +// react app response parsing. type resp struct { Error string `json:"error"` Redirect string `json:"redirect"` Body string `json:"body"` } -// Interface to serve React app on demand or from prepared pool +// Interface to serve React app on demand or from prepared pool. type pool interface { get() *duktape.Context put(*duktape.Context) drop(*duktape.Context) } -func newDuktapePool(size int, engine *gin.Engine) *duktapePool { +// NewDuktapePool return new duktape contexts pool. +func newDuktapePool(size int, engine http.Handler) *duktapePool { pool := &duktapePool{ ch: make(chan *duktape.Context, size), engine: engine, @@ -158,13 +160,13 @@ func newDuktapePool(size int, engine *gin.Engine) *duktapePool { return pool } -// Loads bundle.js to context -func newDuktapeContext(engine *gin.Engine) *duktape.Context { +// NewDuktapeContext loads bundle.js to context. +func newDuktapeContext(engine http.Handler) *duktape.Context { vm := duktape.New() vm.PevalString(`var console = {log:print,warn:print,error:print,info:print}`) fetch.Define(vm, engine) app, err := data.Asset("static/build/bundle.js") - utils.Must(err) + Must(err) fmt.Println("static/build/bundle.js loaded") if err := vm.PevalString(string(app)); err != nil { derr := err.(*duktape.Error) @@ -175,8 +177,10 @@ func newDuktapeContext(engine *gin.Engine) *duktape.Context { return vm } +// Pool's implementations + type onDemandPool struct { - engine *gin.Engine + engine http.Handler } func (f *onDemandPool) get() *duktape.Context { @@ -194,7 +198,7 @@ func (on *onDemandPool) drop(c *duktape.Context) { type duktapePool struct { ch chan *duktape.Context - engine *gin.Engine + engine http.Handler } func (o *duktapePool) get() *duktape.Context { diff --git a/src/app/server/utils.go b/src/app/server/utils.go new file mode 100644 index 0000000..7279224 --- /dev/null +++ b/src/app/server/utils.go @@ -0,0 +1,8 @@ +package server + +// Must raises an error if it not nil +func Must(e error) { + if e != nil { + panic(e) + } +} diff --git a/src/app/server/utils/utils.go b/src/app/server/utils/utils.go deleted file mode 100644 index 95e06fd..0000000 --- a/src/app/server/utils/utils.go +++ /dev/null @@ -1,35 +0,0 @@ -package utils - -import ( - "github.com/codegangsta/cli" - "github.com/gin-gonic/gin" - "github.com/olebedev/config" -) - -// Must raises an error if it not nil -func Must(e error) { - if e != nil { - panic(e) - } -} - -type Kit struct { - Conf *config.Config - Engine *gin.Engine -} - -func NewKit(c *cli.Context, conf *config.Config) *Kit { - co, err := conf.Get(c.String("config")) - // parse environ variables - co.Env() - Must(err) - - // set up gin - if !co.UBool("debug") { - gin.SetMode(gin.ReleaseMode) - } - - return &Kit{ - Conf: co, - } -}