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