Files
Gogs/docs/dev/flamego_migration_examples.md
2026-01-25 04:26:38 +00:00

27 KiB

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
  2. Middleware Configuration
  3. Route Definitions
  4. Handler Functions
  5. Context Usage
  6. Form Binding
  7. Template Rendering
  8. Custom Middleware
  9. Complete Example

Basic Application Setup

Macaron (Current)

// 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)

// 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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)

Flamego (Target)

f.Get("/<type:issues|pulls>", reqSignIn, user.Issues)

Route with Optional Segments

Macaron (Current)

// Not well supported - need multiple routes
m.Get("/wiki", repo.Wiki)
m.Get("/wiki/:page", repo.Wiki)

Flamego (Target)

// Better support for optional segments
f.Get("/wiki/?<page>", repo.Wiki)

Handler Functions

Basic Handler

Macaron (Current)

func Home(c *context.Context) {
    c.Data["Title"] = "Home"
    c.HTML(http.StatusOK, "home")
}

Flamego (Target)

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)

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)

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)

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)

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)

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)

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)

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)

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)

// 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)

// 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)

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)

// 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)

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)

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)

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)

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)

import "github.com/go-macaron/binding"

type AvatarForm struct {
    Avatar *multipart.FileHeader `form:"avatar"`
}

m.Post("/avatar", binding.MultipartForm(AvatarForm{}), handler)

Flamego (Target)

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)

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)

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)

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)

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)

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)

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)

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)

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)

// 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)

// 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.Storesession.Session
  4. Method Names: GetToken()Token(), Put()Set()
  5. Template Injection: Need to inject template.Template and template.Data parameters
  6. Response Access: c.Respc.ResponseWriter()
  7. Request Access: c.Reqc.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.