- GET /_menu/api/v1/menus/{code}?locale=xx, anonymous, menu-public-api bucket (120/min per client IP)
- classes.BuildTree: published entries nested by parent, sort_order then id, scheme-checked URLs
- registry entries resolved through signpost with url_pattern as the item override and the locale's alternate
- list types keep stored URL and title, generated children follow stored ones; unresolvable entries left out
- TestMenuAPISmoke; README with API, CORS paths, import workflow and Winter adaptations
- go mod tidy records the test dependencies
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
// Package api holds the menu plugin's public read API.
|
|
package api
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"regexp"
|
|
|
|
"git.golem15.com/golem15/sm-menu-plugin/classes"
|
|
"git.golem15.com/golem15/sm-menu-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/signpost"
|
|
"git.golem15.com/golem15/summercms/modules/wire"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// codePattern is the shape of a menu code the API serves: letters, digits,
|
|
// dashes and underscores. Anything else is the JSON 404, so every miss has
|
|
// the same body.
|
|
var codePattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
|
|
|
// Menu is the data object of GET /_menu/api/v1/menus/{code}.
|
|
type Menu struct {
|
|
Code string `json:"code"`
|
|
Name string `json:"name"`
|
|
CSSClass *string `json:"css_class"`
|
|
Items []classes.Item `json:"items"`
|
|
}
|
|
|
|
// Show is GET /_menu/api/v1/menus/{code}?locale=xx, anonymous. It serves the
|
|
// active menu with code and its published entries as a tree
|
|
// (classes.BuildTree), resolving registry entries through the framework's
|
|
// menu item type registry for the requested locale. An unknown or inactive
|
|
// menu is 404 {"error":"Menu not found"}.
|
|
func Show(app *backpack.App) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
code := r.PathValue("code")
|
|
if !codePattern.MatchString(code) {
|
|
writeError(w, http.StatusNotFound, "Menu not found")
|
|
return
|
|
}
|
|
db, err := gdb(app)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Internal server error")
|
|
return
|
|
}
|
|
db = db.WithContext(r.Context())
|
|
var menus []models.Menu
|
|
if err := db.Where("code = ? AND is_active = ?", code, true).Order("id ASC").Limit(1).Find(&menus).Error; err != nil {
|
|
slog.Default().Error("menu api: load menu", "code", code, "error", err)
|
|
writeError(w, http.StatusInternalServerError, "Internal server error")
|
|
return
|
|
}
|
|
if len(menus) == 0 {
|
|
writeError(w, http.StatusNotFound, "Menu not found")
|
|
return
|
|
}
|
|
menu := menus[0]
|
|
var entries []models.MenuEntry
|
|
if err := db.Where("menu_id = ? AND is_published = ?", menu.ID, true).
|
|
Order("sort_order ASC NULLS LAST, id ASC").Find(&entries).Error; err != nil {
|
|
slog.Default().Error("menu api: load entries", "code", code, "error", err)
|
|
writeError(w, http.StatusInternalServerError, "Internal server error")
|
|
return
|
|
}
|
|
var registry classes.Resolver
|
|
if reg, ok := app.Lookup[*signpost.Registry](); ok && reg != nil {
|
|
registry = reg
|
|
}
|
|
items := classes.BuildTree(r.Context(), entries, classes.TreeOptions{
|
|
Registry: registry,
|
|
Locale: r.URL.Query().Get("locale"),
|
|
})
|
|
var css *string
|
|
if menu.CSSClass != nil && *menu.CSSClass != "" {
|
|
css = menu.CSSClass
|
|
}
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"data": Menu{
|
|
Code: menu.Code,
|
|
Name: menu.Name,
|
|
CSSClass: css,
|
|
Items: items,
|
|
}})
|
|
}
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, status int, message string) {
|
|
wire.WriteJSON(w, status, map[string]any{"error": message})
|
|
}
|
|
|
|
func gdb(app *backpack.App) (*gorm.DB, error) {
|
|
if app == nil {
|
|
return nil, errors.New("menu api: application is not configured")
|
|
}
|
|
db, ok := app.Lookup[*gorm.DB]()
|
|
if !ok || db == nil {
|
|
return nil, errors.New("menu api: database is not configured")
|
|
}
|
|
return db, nil
|
|
}
|