|
|
@@ -0,0 +1,1196 @@
|
|
|
+# Flamego Migration: Code Examples
|
|
|
+
|
|
|
+This document provides practical, side-by-side code examples showing how to migrate from Macaron to Flamego in the Gogs codebase.
|
|
|
+
|
|
|
+## Table of Contents
|
|
|
+
|
|
|
+1. [Basic Application Setup](#basic-application-setup)
|
|
|
+2. [Middleware Configuration](#middleware-configuration)
|
|
|
+3. [Route Definitions](#route-definitions)
|
|
|
+4. [Handler Functions](#handler-functions)
|
|
|
+5. [Context Usage](#context-usage)
|
|
|
+6. [Form Binding](#form-binding)
|
|
|
+7. [Template Rendering](#template-rendering)
|
|
|
+8. [Custom Middleware](#custom-middleware)
|
|
|
+9. [Complete Example](#complete-example)
|
|
|
+
|
|
|
+## Basic Application Setup
|
|
|
+
|
|
|
+### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+// internal/cmd/web.go
|
|
|
+package cmd
|
|
|
+
|
|
|
+import (
|
|
|
+ "gopkg.in/macaron.v1"
|
|
|
+ "github.com/go-macaron/session"
|
|
|
+ "github.com/go-macaron/csrf"
|
|
|
+)
|
|
|
+
|
|
|
+func newMacaron() *macaron.Macaron {
|
|
|
+ m := macaron.New()
|
|
|
+
|
|
|
+ // Basic middleware
|
|
|
+ if !conf.Server.DisableRouterLog {
|
|
|
+ m.Use(macaron.Logger())
|
|
|
+ }
|
|
|
+ m.Use(macaron.Recovery())
|
|
|
+
|
|
|
+ // Optional gzip
|
|
|
+ if conf.Server.EnableGzip {
|
|
|
+ m.Use(gzip.Gziper())
|
|
|
+ }
|
|
|
+
|
|
|
+ return m
|
|
|
+}
|
|
|
+
|
|
|
+func runWeb(c *cli.Context) error {
|
|
|
+ m := newMacaron()
|
|
|
+
|
|
|
+ // Configure routes
|
|
|
+ m.Get("/", route.Home)
|
|
|
+
|
|
|
+ // Start server
|
|
|
+ return http.ListenAndServe(":3000", m)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+// internal/cmd/web.go
|
|
|
+package cmd
|
|
|
+
|
|
|
+import (
|
|
|
+ "github.com/flamego/flamego"
|
|
|
+ "github.com/flamego/session"
|
|
|
+ "github.com/flamego/csrf"
|
|
|
+)
|
|
|
+
|
|
|
+func newFlamego() *flamego.Flame {
|
|
|
+ f := flamego.New()
|
|
|
+
|
|
|
+ // Basic middleware
|
|
|
+ if !conf.Server.DisableRouterLog {
|
|
|
+ f.Use(flamego.Logger())
|
|
|
+ }
|
|
|
+ f.Use(flamego.Recovery())
|
|
|
+
|
|
|
+ // Optional gzip
|
|
|
+ if conf.Server.EnableGzip {
|
|
|
+ f.Use(gzip.Gziper())
|
|
|
+ }
|
|
|
+
|
|
|
+ return f
|
|
|
+}
|
|
|
+
|
|
|
+func runWeb(c *cli.Context) error {
|
|
|
+ f := newFlamego()
|
|
|
+
|
|
|
+ // Configure routes
|
|
|
+ f.Get("/", route.Home)
|
|
|
+
|
|
|
+ // Start server
|
|
|
+ return f.Run(":3000")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Middleware Configuration
|
|
|
+
|
|
|
+### Session Middleware
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/go-macaron/session"
|
|
|
+
|
|
|
+m.Use(session.Sessioner(session.Options{
|
|
|
+ Provider: conf.Session.Provider,
|
|
|
+ ProviderConfig: conf.Session.ProviderConfig,
|
|
|
+ CookieName: conf.Session.CookieName,
|
|
|
+ CookiePath: conf.Server.Subpath,
|
|
|
+ Gclifetime: conf.Session.GCInterval,
|
|
|
+ Maxlifetime: conf.Session.MaxLifeTime,
|
|
|
+ Secure: conf.Session.CookieSecure,
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage
|
|
|
+func handler(sess session.Store) {
|
|
|
+ sess.Set("user_id", 123)
|
|
|
+ userID := sess.Get("user_id")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/flamego/session"
|
|
|
+
|
|
|
+f.Use(session.Sessioner(session.Options{
|
|
|
+ // Config depends on provider type
|
|
|
+ Config: session.RedisConfig{
|
|
|
+ Options: &redis.Options{
|
|
|
+ Addr: conf.Session.ProviderConfig,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Cookie: session.CookieOptions{
|
|
|
+ Name: conf.Session.CookieName,
|
|
|
+ Path: conf.Server.Subpath,
|
|
|
+ MaxAge: conf.Session.MaxLifeTime,
|
|
|
+ Secure: conf.Session.CookieSecure,
|
|
|
+ },
|
|
|
+ // For memory provider:
|
|
|
+ // Config: session.MemoryConfig{
|
|
|
+ // GCInterval: conf.Session.GCInterval,
|
|
|
+ // },
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage - interface name changed
|
|
|
+func handler(sess session.Session) {
|
|
|
+ sess.Set("user_id", 123)
|
|
|
+ userID := sess.Get("user_id")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### CSRF Middleware
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/go-macaron/csrf"
|
|
|
+
|
|
|
+m.Use(csrf.Csrfer(csrf.Options{
|
|
|
+ Secret: conf.Security.SecretKey,
|
|
|
+ Header: "X-CSRF-Token",
|
|
|
+ Cookie: conf.Session.CSRFCookieName,
|
|
|
+ CookieDomain: conf.Server.URL.Hostname(),
|
|
|
+ CookiePath: conf.Server.Subpath,
|
|
|
+ CookieHttpOnly: true,
|
|
|
+ SetCookie: true,
|
|
|
+ Secure: conf.Server.URL.Scheme == "https",
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage
|
|
|
+func handler(x csrf.CSRF) {
|
|
|
+ token := x.GetToken()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/flamego/csrf"
|
|
|
+
|
|
|
+f.Use(csrf.Csrfer(csrf.Options{
|
|
|
+ Secret: conf.Security.SecretKey,
|
|
|
+ Header: "X-CSRF-Token",
|
|
|
+ Cookie: conf.Session.CSRFCookieName,
|
|
|
+ CookiePath: conf.Server.Subpath,
|
|
|
+ Secure: conf.Server.URL.Scheme == "https",
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage - method name changed
|
|
|
+func handler(x csrf.CSRF) {
|
|
|
+ token := x.Token() // Changed from GetToken()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Template Middleware
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+m.Use(macaron.Renderer(macaron.RenderOptions{
|
|
|
+ Directory: filepath.Join(conf.WorkDir(), "templates"),
|
|
|
+ AppendDirectories: []string{customDir},
|
|
|
+ Funcs: template.FuncMap(),
|
|
|
+ IndentJSON: macaron.Env != macaron.PROD,
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage
|
|
|
+func handler(c *macaron.Context) {
|
|
|
+ c.Data["Title"] = "Home"
|
|
|
+ c.HTML(200, "home")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/flamego/template"
|
|
|
+
|
|
|
+f.Use(template.Templater(template.Options{
|
|
|
+ Directory: filepath.Join(conf.WorkDir(), "templates"),
|
|
|
+ AppendDirectories: []string{customDir},
|
|
|
+ FuncMaps: []template.FuncMap{template.FuncMap()},
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage - separate template and data injection
|
|
|
+func handler(t template.Template, data template.Data) {
|
|
|
+ data["Title"] = "Home"
|
|
|
+ t.HTML(200, "home")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Cache Middleware
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/go-macaron/cache"
|
|
|
+
|
|
|
+m.Use(cache.Cacher(cache.Options{
|
|
|
+ Adapter: conf.Cache.Adapter,
|
|
|
+ AdapterConfig: conf.Cache.Host,
|
|
|
+ Interval: conf.Cache.Interval,
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage
|
|
|
+func handler(cache cache.Cache) {
|
|
|
+ cache.Put("key", "value", 60)
|
|
|
+ value := cache.Get("key")
|
|
|
+ cache.Delete("key")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/flamego/cache"
|
|
|
+
|
|
|
+var cacheConfig cache.Config
|
|
|
+switch conf.Cache.Adapter {
|
|
|
+case "memory":
|
|
|
+ cacheConfig = cache.MemoryConfig{
|
|
|
+ GCInterval: conf.Cache.Interval,
|
|
|
+ }
|
|
|
+case "redis":
|
|
|
+ cacheConfig = cache.RedisConfig{
|
|
|
+ Options: &redis.Options{
|
|
|
+ Addr: conf.Cache.Host,
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+f.Use(cache.Cacher(cache.Options{
|
|
|
+ Config: cacheConfig,
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage - method names changed
|
|
|
+func handler(c cache.Cache) {
|
|
|
+ c.Set("key", "value", 60) // Changed from Put
|
|
|
+ value := c.Get("key")
|
|
|
+ c.Delete("key")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### i18n Middleware
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/go-macaron/i18n"
|
|
|
+
|
|
|
+m.Use(i18n.I18n(i18n.Options{
|
|
|
+ SubURL: conf.Server.Subpath,
|
|
|
+ Files: localeFiles,
|
|
|
+ CustomDirectory: filepath.Join(conf.CustomDir(), "conf", "locale"),
|
|
|
+ Langs: conf.I18n.Langs,
|
|
|
+ Names: conf.I18n.Names,
|
|
|
+ DefaultLang: "en-US",
|
|
|
+ Redirect: true,
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage
|
|
|
+func handler(l i18n.Locale) {
|
|
|
+ text := l.Tr("user.login")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/flamego/i18n"
|
|
|
+
|
|
|
+f.Use(i18n.I18n(i18n.Options{
|
|
|
+ URLPrefix: conf.Server.Subpath,
|
|
|
+ Files: localeFiles,
|
|
|
+ CustomDirectory: filepath.Join(conf.CustomDir(), "conf", "locale"),
|
|
|
+ Languages: conf.I18n.Langs, // Changed from Langs
|
|
|
+ Names: conf.I18n.Names,
|
|
|
+ DefaultLanguage: "en-US", // Changed from DefaultLang
|
|
|
+ Redirect: true,
|
|
|
+}))
|
|
|
+
|
|
|
+// Handler usage - same interface
|
|
|
+func handler(l i18n.Locale) {
|
|
|
+ text := l.Tr("user.login")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Route Definitions
|
|
|
+
|
|
|
+### Basic Routes
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+m.Get("/", ignSignIn, route.Home)
|
|
|
+m.Post("/login", bindIgnErr(form.SignIn{}), user.LoginPost)
|
|
|
+m.Get("/:username", user.Profile)
|
|
|
+m.Get("/:username/:reponame", context.RepoAssignment(), repo.Home)
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+f.Get("/", ignSignIn, route.Home)
|
|
|
+f.Post("/login", binding.Form(form.SignIn{}), user.LoginPost)
|
|
|
+f.Get("/<username>", user.Profile)
|
|
|
+f.Get("/<username>/<reponame>", context.RepoAssignment(), repo.Home)
|
|
|
+```
|
|
|
+
|
|
|
+**Key Changes:**
|
|
|
+- `:param` becomes `<param>`
|
|
|
+- `bindIgnErr(form)` becomes `binding.Form(form)`
|
|
|
+
|
|
|
+### Route Groups
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+m.Group("/user", func() {
|
|
|
+ m.Group("/login", func() {
|
|
|
+ m.Combo("").Get(user.Login).
|
|
|
+ Post(bindIgnErr(form.SignIn{}), user.LoginPost)
|
|
|
+ m.Combo("/two_factor").Get(user.LoginTwoFactor).
|
|
|
+ Post(user.LoginTwoFactorPost)
|
|
|
+ })
|
|
|
+
|
|
|
+ m.Get("/sign_up", user.SignUp)
|
|
|
+ m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost)
|
|
|
+}, reqSignOut)
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+f.Group("/user", func() {
|
|
|
+ f.Group("/login", func() {
|
|
|
+ f.Combo("").Get(user.Login).
|
|
|
+ Post(binding.Form(form.SignIn{}), user.LoginPost)
|
|
|
+ f.Combo("/two_factor").Get(user.LoginTwoFactor).
|
|
|
+ Post(user.LoginTwoFactorPost)
|
|
|
+ })
|
|
|
+
|
|
|
+ f.Get("/sign_up", user.SignUp)
|
|
|
+ f.Post("/sign_up", binding.Form(form.Register{}), user.SignUpPost)
|
|
|
+}, reqSignOut)
|
|
|
+```
|
|
|
+
|
|
|
+**Key Changes:**
|
|
|
+- `m.Group` becomes `f.Group`
|
|
|
+- `bindIgnErr` becomes `binding.Form`
|
|
|
+
|
|
|
+### Regex Routes
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+f.Get("/<type:issues|pulls>", reqSignIn, user.Issues)
|
|
|
+```
|
|
|
+
|
|
|
+### Route with Optional Segments
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+// Not well supported - need multiple routes
|
|
|
+m.Get("/wiki", repo.Wiki)
|
|
|
+m.Get("/wiki/:page", repo.Wiki)
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+// Better support for optional segments
|
|
|
+f.Get("/wiki/?<page>", repo.Wiki)
|
|
|
+```
|
|
|
+
|
|
|
+## Handler Functions
|
|
|
+
|
|
|
+### Basic Handler
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func Home(c *context.Context) {
|
|
|
+ c.Data["Title"] = "Home"
|
|
|
+ c.HTML(http.StatusOK, "home")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+func Home(c *context.Context, t template.Template, data template.Data) {
|
|
|
+ data["Title"] = "Home"
|
|
|
+ t.HTML(http.StatusOK, "home")
|
|
|
+}
|
|
|
+
|
|
|
+// Note: context.Context needs to be updated to wrap flamego.Context
|
|
|
+```
|
|
|
+
|
|
|
+### Handler with Parameters
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func UserProfile(c *context.Context) {
|
|
|
+ username := c.Params(":username")
|
|
|
+
|
|
|
+ user, err := database.GetUserByName(username)
|
|
|
+ if err != nil {
|
|
|
+ c.NotFoundOrError(err, "get user")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.Data["User"] = user
|
|
|
+ c.HTML(http.StatusOK, "user/profile")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+func UserProfile(c *context.Context, t template.Template, data template.Data) {
|
|
|
+ username := c.Param("username") // No colon prefix
|
|
|
+
|
|
|
+ user, err := database.GetUserByName(username)
|
|
|
+ if err != nil {
|
|
|
+ c.NotFoundOrError(err, "get user")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ data["User"] = user
|
|
|
+ t.HTML(http.StatusOK, "user/profile")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Handler with Form Binding
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+type LoginForm struct {
|
|
|
+ Username string `form:"username" binding:"Required"`
|
|
|
+ Password string `form:"password" binding:"Required"`
|
|
|
+}
|
|
|
+
|
|
|
+func LoginPost(c *context.Context, form LoginForm) {
|
|
|
+ if !database.ValidateUser(form.Username, form.Password) {
|
|
|
+ c.RenderWithErr("Invalid credentials", "user/login", &form)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.Session.Set("user_id", user.ID)
|
|
|
+ c.Redirect("/")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+type LoginForm struct {
|
|
|
+ Username string `form:"username" validate:"required"`
|
|
|
+ Password string `form:"password" validate:"required"`
|
|
|
+}
|
|
|
+
|
|
|
+func LoginPost(c *context.Context, form LoginForm, t template.Template, data template.Data) {
|
|
|
+ if !database.ValidateUser(form.Username, form.Password) {
|
|
|
+ c.RenderWithErr("Invalid credentials", "user/login", &form, t, data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.Session().Set("user_id", user.ID)
|
|
|
+ c.Redirect("/")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Handler with Session
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func RequireLogin(c *context.Context, sess session.Store) {
|
|
|
+ userID := sess.Get("user_id")
|
|
|
+ if userID == nil {
|
|
|
+ c.Redirect("/login")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ user, err := database.GetUserByID(userID.(int64))
|
|
|
+ if err != nil {
|
|
|
+ c.Error(err, "get user")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.User = user
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+func RequireLogin(c *context.Context, sess session.Session) {
|
|
|
+ userID := sess.Get("user_id")
|
|
|
+ if userID == nil {
|
|
|
+ c.Redirect("/login")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ user, err := database.GetUserByID(userID.(int64))
|
|
|
+ if err != nil {
|
|
|
+ c.Error(err, "get user")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.User = user
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### JSON API Handler
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func APIUserInfo(c *context.APIContext) {
|
|
|
+ user := c.User
|
|
|
+
|
|
|
+ c.JSON(http.StatusOK, &api.User{
|
|
|
+ ID: user.ID,
|
|
|
+ Username: user.Name,
|
|
|
+ Email: user.Email,
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import "encoding/json"
|
|
|
+
|
|
|
+func APIUserInfo(c *context.APIContext) {
|
|
|
+ user := c.User
|
|
|
+
|
|
|
+ resp := &api.User{
|
|
|
+ ID: user.ID,
|
|
|
+ Username: user.Name,
|
|
|
+ Email: user.Email,
|
|
|
+ }
|
|
|
+
|
|
|
+ c.ResponseWriter().Header().Set("Content-Type", "application/json")
|
|
|
+ c.ResponseWriter().WriteHeader(http.StatusOK)
|
|
|
+ json.NewEncoder(c.ResponseWriter()).Encode(resp)
|
|
|
+}
|
|
|
+
|
|
|
+// Or create a helper method on context.APIContext
|
|
|
+func (c *APIContext) JSON(status int, v any) {
|
|
|
+ c.ResponseWriter().Header().Set("Content-Type", "application/json")
|
|
|
+ c.ResponseWriter().WriteHeader(status)
|
|
|
+ json.NewEncoder(c.ResponseWriter()).Encode(v)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Context Usage
|
|
|
+
|
|
|
+### Context Wrapper Update
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+// internal/context/context.go
|
|
|
+package context
|
|
|
+
|
|
|
+import (
|
|
|
+ "github.com/go-macaron/cache"
|
|
|
+ "github.com/go-macaron/csrf"
|
|
|
+ "github.com/go-macaron/session"
|
|
|
+ "gopkg.in/macaron.v1"
|
|
|
+)
|
|
|
+
|
|
|
+type Context struct {
|
|
|
+ *macaron.Context
|
|
|
+ Cache cache.Cache
|
|
|
+ csrf csrf.CSRF
|
|
|
+ Flash *session.Flash
|
|
|
+ Session session.Store
|
|
|
+
|
|
|
+ Link string
|
|
|
+ User *database.User
|
|
|
+ IsLogged bool
|
|
|
+ Repo *Repository
|
|
|
+ Org *Organization
|
|
|
+}
|
|
|
+
|
|
|
+// Contexter middleware
|
|
|
+func Contexter(store Store) macaron.Handler {
|
|
|
+ return func(
|
|
|
+ ctx *macaron.Context,
|
|
|
+ l i18n.Locale,
|
|
|
+ cache cache.Cache,
|
|
|
+ sess session.Store,
|
|
|
+ f *session.Flash,
|
|
|
+ x csrf.CSRF,
|
|
|
+ ) {
|
|
|
+ c := &Context{
|
|
|
+ Context: ctx,
|
|
|
+ Cache: cache,
|
|
|
+ csrf: x,
|
|
|
+ Flash: f,
|
|
|
+ Session: sess,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Authentication logic...
|
|
|
+ c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(store, c.Context, c.Session)
|
|
|
+
|
|
|
+ ctx.Map(c)
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+// internal/context/context.go
|
|
|
+package context
|
|
|
+
|
|
|
+import (
|
|
|
+ "github.com/flamego/flamego"
|
|
|
+ "github.com/flamego/cache"
|
|
|
+ "github.com/flamego/csrf"
|
|
|
+ "github.com/flamego/session"
|
|
|
+)
|
|
|
+
|
|
|
+type Context struct {
|
|
|
+ flamego.Context // Embedded instead of pointer
|
|
|
+ cache cache.Cache
|
|
|
+ csrf csrf.CSRF
|
|
|
+ flash *session.Flash
|
|
|
+ session session.Session
|
|
|
+
|
|
|
+ Link string
|
|
|
+ User *database.User
|
|
|
+ IsLogged bool
|
|
|
+ Repo *Repository
|
|
|
+ Org *Organization
|
|
|
+}
|
|
|
+
|
|
|
+// Accessor methods
|
|
|
+func (c *Context) Cache() cache.Cache { return c.cache }
|
|
|
+func (c *Context) CSRF() csrf.CSRF { return c.csrf }
|
|
|
+func (c *Context) Flash() *session.Flash { return c.flash }
|
|
|
+func (c *Context) Session() session.Session { return c.session }
|
|
|
+
|
|
|
+// Contexter middleware
|
|
|
+func Contexter(store Store) flamego.Handler {
|
|
|
+ return func(
|
|
|
+ ctx flamego.Context,
|
|
|
+ l i18n.Locale,
|
|
|
+ cache cache.Cache,
|
|
|
+ sess session.Session,
|
|
|
+ f *session.Flash,
|
|
|
+ x csrf.CSRF,
|
|
|
+ ) {
|
|
|
+ c := &Context{
|
|
|
+ Context: ctx,
|
|
|
+ cache: cache,
|
|
|
+ csrf: x,
|
|
|
+ flash: f,
|
|
|
+ session: sess,
|
|
|
+ }
|
|
|
+
|
|
|
+ // Authentication logic - note Session is now a method
|
|
|
+ c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(store, c, c.session)
|
|
|
+
|
|
|
+ ctx.MapTo(c, (*Context)(nil))
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Response Methods
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func (c *Context) HTML(status int, name string) {
|
|
|
+ log.Trace("Template: %s", name)
|
|
|
+ c.Context.HTML(status, name)
|
|
|
+}
|
|
|
+
|
|
|
+func (c *Context) JSON(status int, data any) {
|
|
|
+ c.Context.JSON(status, data)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+// These methods need to be updated to work with injected services
|
|
|
+
|
|
|
+// Option 1: Require template.Template injection
|
|
|
+func (c *Context) HTML(status int, name string, t template.Template, data template.Data) {
|
|
|
+ log.Trace("Template: %s", name)
|
|
|
+
|
|
|
+ // Copy c.Data to template.Data if needed
|
|
|
+ for k, v := range c.Data {
|
|
|
+ data[k] = v
|
|
|
+ }
|
|
|
+
|
|
|
+ t.HTML(status, name)
|
|
|
+}
|
|
|
+
|
|
|
+// Option 2: Store template reference in context during initialization
|
|
|
+func (c *Context) HTML(status int, name string) {
|
|
|
+ if c.template == nil {
|
|
|
+ panic("template not initialized")
|
|
|
+ }
|
|
|
+
|
|
|
+ log.Trace("Template: %s", name)
|
|
|
+ c.template.HTML(status, name)
|
|
|
+}
|
|
|
+
|
|
|
+func (c *Context) JSON(status int, data any) {
|
|
|
+ c.ResponseWriter().Header().Set("Content-Type", "application/json")
|
|
|
+ c.ResponseWriter().WriteHeader(status)
|
|
|
+ json.NewEncoder(c.ResponseWriter()).Encode(data)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Form Binding
|
|
|
+
|
|
|
+### Form Struct Tags
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+type CreateRepoForm struct {
|
|
|
+ UserID int64 `form:"user_id" binding:"Required"`
|
|
|
+ RepoName string `form:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
|
|
|
+ Private bool `form:"private"`
|
|
|
+ Description string `form:"description" binding:"MaxSize(255)"`
|
|
|
+ AutoInit bool `form:"auto_init"`
|
|
|
+ Gitignores string `form:"gitignores"`
|
|
|
+ License string `form:"license"`
|
|
|
+ Readme string `form:"readme"`
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+type CreateRepoForm struct {
|
|
|
+ UserID int64 `form:"user_id" validate:"required"`
|
|
|
+ RepoName string `form:"repo_name" validate:"required,alphaDashDot,max=100"`
|
|
|
+ Private bool `form:"private"`
|
|
|
+ Description string `form:"description" validate:"max=255"`
|
|
|
+ AutoInit bool `form:"auto_init"`
|
|
|
+ Gitignores string `form:"gitignores"`
|
|
|
+ License string `form:"license"`
|
|
|
+ Readme string `form:"readme"`
|
|
|
+}
|
|
|
+
|
|
|
+// Note: Custom validators like AlphaDashDot need to be registered with Flamego's validator
|
|
|
+```
|
|
|
+
|
|
|
+### Custom Validators
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/go-macaron/binding"
|
|
|
+
|
|
|
+const (
|
|
|
+ AlphaDashDotSlash binding.Rule = "AlphaDashDotSlash"
|
|
|
+)
|
|
|
+
|
|
|
+func init() {
|
|
|
+ binding.SetNameMapper(com.ToSnakeCase)
|
|
|
+ binding.AddRule(&binding.Rule{
|
|
|
+ IsMatch: func(rule string) bool {
|
|
|
+ return rule == "AlphaDashDotSlash"
|
|
|
+ },
|
|
|
+ IsValid: func(errs binding.Errors, name string, v interface{}) (bool, binding.Errors) {
|
|
|
+ str := v.(string)
|
|
|
+ if !alphaDashDotSlashPattern.MatchString(str) {
|
|
|
+ errs = append(errs, binding.Error{
|
|
|
+ FieldNames: []string{name},
|
|
|
+ Message: name + " must be valid alpha, dash, dot or slash",
|
|
|
+ })
|
|
|
+ return false, errs
|
|
|
+ }
|
|
|
+ return true, errs
|
|
|
+ },
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import (
|
|
|
+ "github.com/flamego/binding"
|
|
|
+ "github.com/go-playground/validator/v10"
|
|
|
+)
|
|
|
+
|
|
|
+func init() {
|
|
|
+ // Register custom validator
|
|
|
+ binding.RegisterValidation("alphaDashDotSlash", func(fl validator.FieldLevel) bool {
|
|
|
+ str := fl.Field().String()
|
|
|
+ return alphaDashDotSlashPattern.MatchString(str)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// Usage in struct
|
|
|
+type Form struct {
|
|
|
+ Path string `form:"path" validate:"required,alphaDashDotSlash"`
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Multipart Form
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/go-macaron/binding"
|
|
|
+
|
|
|
+type AvatarForm struct {
|
|
|
+ Avatar *multipart.FileHeader `form:"avatar"`
|
|
|
+}
|
|
|
+
|
|
|
+m.Post("/avatar", binding.MultipartForm(AvatarForm{}), handler)
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+import "github.com/flamego/binding"
|
|
|
+
|
|
|
+type AvatarForm struct {
|
|
|
+ Avatar *multipart.FileHeader `form:"avatar"`
|
|
|
+}
|
|
|
+
|
|
|
+f.Post("/avatar", binding.MultipartForm(AvatarForm{}), handler)
|
|
|
+```
|
|
|
+
|
|
|
+## Template Rendering
|
|
|
+
|
|
|
+### Render with Data
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func ShowRepo(c *context.Context) {
|
|
|
+ c.Data["Title"] = c.Repo.Repository.Name
|
|
|
+ c.Data["Owner"] = c.Repo.Owner
|
|
|
+ c.Data["Repository"] = c.Repo.Repository
|
|
|
+ c.Data["IsRepositoryAdmin"] = c.Repo.IsAdmin()
|
|
|
+
|
|
|
+ c.HTML(http.StatusOK, "repo/home")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+func ShowRepo(c *context.Context, t template.Template, data template.Data) {
|
|
|
+ data["Title"] = c.Repo.Repository.Name
|
|
|
+ data["Owner"] = c.Repo.Owner
|
|
|
+ data["Repository"] = c.Repo.Repository
|
|
|
+ data["IsRepositoryAdmin"] = c.Repo.IsAdmin()
|
|
|
+
|
|
|
+ t.HTML(http.StatusOK, "repo/home")
|
|
|
+}
|
|
|
+
|
|
|
+// Or if context has template reference:
|
|
|
+func ShowRepo(c *context.Context) {
|
|
|
+ c.Data["Title"] = c.Repo.Repository.Name
|
|
|
+ c.Data["Owner"] = c.Repo.Owner
|
|
|
+ c.Data["Repository"] = c.Repo.Repository
|
|
|
+ c.Data["IsRepositoryAdmin"] = c.Repo.IsAdmin()
|
|
|
+
|
|
|
+ c.HTML(http.StatusOK, "repo/home")
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Render with Error
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func (c *Context) RenderWithErr(msg, tpl string, f any) {
|
|
|
+ if f != nil {
|
|
|
+ form.Assign(f, c.Data)
|
|
|
+ }
|
|
|
+ c.Flash.ErrorMsg = msg
|
|
|
+ c.Data["Flash"] = c.Flash
|
|
|
+ c.HTML(http.StatusOK, tpl)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+func (c *Context) RenderWithErr(msg, tpl string, f any, t template.Template, data template.Data) {
|
|
|
+ if f != nil {
|
|
|
+ form.Assign(f, c.Data)
|
|
|
+ // Also need to assign to data
|
|
|
+ for k, v := range c.Data {
|
|
|
+ data[k] = v
|
|
|
+ }
|
|
|
+ }
|
|
|
+ c.Flash().ErrorMsg = msg
|
|
|
+ data["Flash"] = c.Flash()
|
|
|
+ t.HTML(http.StatusOK, tpl)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Custom Middleware
|
|
|
+
|
|
|
+### Authentication Middleware
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func Toggle(options *ToggleOptions) macaron.Handler {
|
|
|
+ return func(c *Context) {
|
|
|
+ // Check authentication
|
|
|
+ if options.SignInRequired {
|
|
|
+ if !c.IsLogged {
|
|
|
+ c.SetCookie("redirect_to", c.Req.RequestURI, 0, conf.Server.Subpath)
|
|
|
+ c.Redirect(conf.Server.Subpath + "/user/login")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check admin
|
|
|
+ if options.AdminRequired {
|
|
|
+ if !c.User.IsAdmin {
|
|
|
+ c.Error(nil, http.StatusForbidden)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+func Toggle(options *ToggleOptions) flamego.Handler {
|
|
|
+ return func(c *Context) {
|
|
|
+ // Check authentication
|
|
|
+ if options.SignInRequired {
|
|
|
+ if !c.IsLogged {
|
|
|
+ c.SetCookie(http.Cookie{
|
|
|
+ Name: "redirect_to",
|
|
|
+ Value: c.Request().RequestURI,
|
|
|
+ Path: conf.Server.Subpath,
|
|
|
+ })
|
|
|
+ c.Redirect(conf.Server.Subpath + "/user/login")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check admin
|
|
|
+ if options.AdminRequired {
|
|
|
+ if !c.User.IsAdmin {
|
|
|
+ c.Error(nil, http.StatusForbidden)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Repository Context Middleware
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+func RepoAssignment() macaron.Handler {
|
|
|
+ return func(c *Context) {
|
|
|
+ userName := c.Params(":username")
|
|
|
+ repoName := c.Params(":reponame")
|
|
|
+
|
|
|
+ owner, err := database.GetUserByName(userName)
|
|
|
+ if err != nil {
|
|
|
+ c.NotFoundOrError(err, "get user")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ c.Repo.Owner = owner
|
|
|
+
|
|
|
+ repo, err := database.GetRepositoryByName(owner.ID, repoName)
|
|
|
+ if err != nil {
|
|
|
+ c.NotFoundOrError(err, "get repository")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ c.Repo.Repository = repo
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+func RepoAssignment() flamego.Handler {
|
|
|
+ return func(c *Context) {
|
|
|
+ userName := c.Param("username") // No colon prefix
|
|
|
+ repoName := c.Param("reponame")
|
|
|
+
|
|
|
+ owner, err := database.GetUserByName(userName)
|
|
|
+ if err != nil {
|
|
|
+ c.NotFoundOrError(err, "get user")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ c.Repo.Owner = owner
|
|
|
+
|
|
|
+ repo, err := database.GetRepositoryByName(owner.ID, repoName)
|
|
|
+ if err != nil {
|
|
|
+ c.NotFoundOrError(err, "get repository")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ c.Repo.Repository = repo
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Complete Example
|
|
|
+
|
|
|
+### Full Route Handler Chain
|
|
|
+
|
|
|
+#### Macaron (Current)
|
|
|
+
|
|
|
+```go
|
|
|
+// Setup
|
|
|
+m := macaron.New()
|
|
|
+m.Use(macaron.Logger())
|
|
|
+m.Use(macaron.Recovery())
|
|
|
+m.Use(session.Sessioner())
|
|
|
+m.Use(csrf.Csrfer())
|
|
|
+m.Use(context.Contexter(store))
|
|
|
+
|
|
|
+// Middleware
|
|
|
+reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
|
|
|
+
|
|
|
+// Routes
|
|
|
+m.Group("/:username/:reponame", func() {
|
|
|
+ m.Get("/issues", repo.Issues)
|
|
|
+ m.Combo("/issues/new").
|
|
|
+ Get(repo.NewIssue).
|
|
|
+ Post(bindIgnErr(form.NewIssue{}), repo.NewIssuePost)
|
|
|
+}, reqSignIn, context.RepoAssignment())
|
|
|
+
|
|
|
+// Handler
|
|
|
+func NewIssuePost(c *context.Context, form form.NewIssue) {
|
|
|
+ if c.HasError() {
|
|
|
+ c.RenderWithErr(c.GetErrMsg(), "repo/issue/new", &form)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ issue, err := database.NewIssue(&database.Issue{
|
|
|
+ RepoID: c.Repo.Repository.ID,
|
|
|
+ Index: c.Repo.Repository.NextIssueIndex(),
|
|
|
+ Title: form.Title,
|
|
|
+ Content: form.Content,
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ c.Error(err, "create issue")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.Redirect(fmt.Sprintf("/%s/%s/issues/%d",
|
|
|
+ c.Repo.Owner.Name, c.Repo.Repository.Name, issue.Index))
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Flamego (Target)
|
|
|
+
|
|
|
+```go
|
|
|
+// Setup
|
|
|
+f := flamego.New()
|
|
|
+f.Use(flamego.Logger())
|
|
|
+f.Use(flamego.Recovery())
|
|
|
+f.Use(session.Sessioner())
|
|
|
+f.Use(csrf.Csrfer())
|
|
|
+f.Use(template.Templater())
|
|
|
+f.Use(context.Contexter(store))
|
|
|
+
|
|
|
+// Middleware
|
|
|
+reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
|
|
|
+
|
|
|
+// Routes - note parameter syntax change
|
|
|
+f.Group("/<username>/<reponame>", func() {
|
|
|
+ f.Get("/issues", repo.Issues)
|
|
|
+ f.Combo("/issues/new").
|
|
|
+ Get(repo.NewIssue).
|
|
|
+ Post(binding.Form(form.NewIssue{}), repo.NewIssuePost)
|
|
|
+}, reqSignIn, context.RepoAssignment())
|
|
|
+
|
|
|
+// Handler - note template injection
|
|
|
+func NewIssuePost(
|
|
|
+ c *context.Context,
|
|
|
+ form form.NewIssue,
|
|
|
+ t template.Template,
|
|
|
+ data template.Data,
|
|
|
+) {
|
|
|
+ if c.HasError() {
|
|
|
+ c.RenderWithErr(c.GetErrMsg(), "repo/issue/new", &form, t, data)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ issue, err := database.NewIssue(&database.Issue{
|
|
|
+ RepoID: c.Repo.Repository.ID,
|
|
|
+ Index: c.Repo.Repository.NextIssueIndex(),
|
|
|
+ Title: form.Title,
|
|
|
+ Content: form.Content,
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ c.Error(err, "create issue")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ c.Redirect(fmt.Sprintf("/%s/%s/issues/%d",
|
|
|
+ c.Repo.Owner.Name, c.Repo.Repository.Name, issue.Index))
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Key Takeaways
|
|
|
+
|
|
|
+1. **Parameter Names**: Remove `:` prefix when getting params (`c.Param("name")` vs `c.Params(":name")`)
|
|
|
+2. **Route Syntax**: Use `<param>` instead of `:param`
|
|
|
+3. **Interface Names**: `session.Store` → `session.Session`
|
|
|
+4. **Method Names**: `GetToken()` → `Token()`, `Put()` → `Set()`
|
|
|
+5. **Template Injection**: Need to inject `template.Template` and `template.Data` parameters
|
|
|
+6. **Response Access**: `c.Resp` → `c.ResponseWriter()`
|
|
|
+7. **Request Access**: `c.Req` → `c.Request()`
|
|
|
+8. **Context Embedding**: Use `flamego.Context` interface instead of `*macaron.Context` pointer
|
|
|
+
|
|
|
+## Summary
|
|
|
+
|
|
|
+The migration from Macaron to Flamego is largely mechanical with clear patterns:
|
|
|
+
|
|
|
+- Most middleware has direct equivalents
|
|
|
+- Handler signatures gain template parameters
|
|
|
+- Route parameter syntax changes
|
|
|
+- Context access changes from fields to methods
|
|
|
+- Overall structure and patterns remain similar
|
|
|
+
|
|
|
+The main work is updating ~150+ files to follow these new patterns consistently.
|