- ContentType, ContentTypeForController, Records and MatchPath on the registry; the pure MatchPattern for whole-segment placeholder matching - two types declaring the same Content.Model or Controller stop the boot - TestContent* cover lookups, optional Records, boot errors, MatchPattern and MatchPath precedence (first exact, else first inexact) - docs: Content descriptors in menu-item-types.md, signpost README
230 lines
7.2 KiB
Go
230 lines
7.2 KiB
Go
package signpost
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
// addContent validates a type's content descriptor before the type is
|
|
// added: Model and Describe are required when Content is set, and no two
|
|
// types may describe the same Model or name the same non-empty Controller.
|
|
func (r *Registry) addContent(owner string, t pact.MenuItemType) error {
|
|
c := t.Content
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(c.Model) == "" {
|
|
return fmt.Errorf("signpost: menu item type %q of plugin %s has a content descriptor without Model", t.Key, owner)
|
|
}
|
|
if c.Describe == nil {
|
|
return fmt.Errorf("signpost: menu item type %q of plugin %s has a content descriptor without Describe", t.Key, owner)
|
|
}
|
|
if first, dup := r.byModel[c.Model]; dup {
|
|
return fmt.Errorf("signpost: menu item type %q of plugin %s describes model %q, which %q of plugin %s already describes", t.Key, owner, c.Model, first, r.owners[first])
|
|
}
|
|
if c.Controller != "" {
|
|
if first, dup := r.byController[c.Controller]; dup {
|
|
return fmt.Errorf("signpost: menu item type %q of plugin %s names controller %q, which %q of plugin %s already names", t.Key, owner, c.Controller, first, r.owners[first])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// indexContent records a type's descriptor once the type is added.
|
|
func (r *Registry) indexContent(t pact.MenuItemType) {
|
|
if t.Content == nil {
|
|
return
|
|
}
|
|
r.byModel[t.Content.Model] = t.Key
|
|
if t.Content.Controller != "" {
|
|
r.byController[t.Content.Controller] = t.Key
|
|
}
|
|
}
|
|
|
|
// ContentTypes returns the types that carry a content descriptor
|
|
// (pact.MenuItemType.Content), in registration order. The slice is a copy.
|
|
func (r *Registry) ContentTypes() []pact.MenuItemType {
|
|
out := []pact.MenuItemType{}
|
|
if r == nil {
|
|
return out
|
|
}
|
|
for _, t := range r.types {
|
|
if t.Content != nil {
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ContentType returns the type registered under key when it carries a
|
|
// content descriptor.
|
|
func (r *Registry) ContentType(key string) (pact.MenuItemType, bool) {
|
|
t, ok := r.Type(key)
|
|
if !ok || t.Content == nil {
|
|
return pact.MenuItemType{}, false
|
|
}
|
|
return t, true
|
|
}
|
|
|
|
// ContentTypeForController returns the content type whose descriptor names
|
|
// the admin controller id, the controller whose form edits its records.
|
|
func (r *Registry) ContentTypeForController(id string) (pact.MenuItemType, bool) {
|
|
if r == nil || id == "" {
|
|
return pact.MenuItemType{}, false
|
|
}
|
|
key, ok := r.byController[id]
|
|
if !ok {
|
|
return pact.MenuItemType{}, false
|
|
}
|
|
return r.Type(key)
|
|
}
|
|
|
|
// ContentTypeForModel returns the content type whose descriptor declares
|
|
// model, the morph type record events carry (pact.RecordSaved.Model).
|
|
func (r *Registry) ContentTypeForModel(model string) (pact.MenuItemType, bool) {
|
|
if r == nil || model == "" {
|
|
return pact.MenuItemType{}, false
|
|
}
|
|
key, ok := r.byModel[model]
|
|
if !ok {
|
|
return pact.MenuItemType{}, false
|
|
}
|
|
return r.Type(key)
|
|
}
|
|
|
|
// Describe describes one record of the content type key through its
|
|
// descriptor, handing it the layered pattern (Pattern(key, "")). An unknown
|
|
// key or a type without a content descriptor is an error; a record the type
|
|
// does not know answers nil.
|
|
func (r *Registry) Describe(ctx context.Context, key, id, unprefixedLocale string) (*pact.ContentRecord, error) {
|
|
t, err := r.contentType(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return t.Content.Describe(ctx, pact.ContentDescribe{
|
|
ID: id,
|
|
Pattern: r.Pattern(key, ""),
|
|
UnprefixedLocale: unprefixedLocale,
|
|
})
|
|
}
|
|
|
|
// Records pages through the records of the content type key: after is the
|
|
// ID of the last record of the previous page ("" for the first), limit the
|
|
// page size the caller wants. The descriptor gets the layered pattern
|
|
// (Pattern(key, "")). A content type without Records answers an empty
|
|
// page; an unknown key or a type without a content descriptor is an error.
|
|
func (r *Registry) Records(ctx context.Context, key, after string, limit int, unprefixedLocale string) (pact.ContentPage, error) {
|
|
t, err := r.contentType(key)
|
|
if err != nil {
|
|
return pact.ContentPage{}, err
|
|
}
|
|
if t.Content.Records == nil {
|
|
return pact.ContentPage{Records: []pact.ContentRecord{}}, nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return t.Content.Records(ctx, pact.ContentList{
|
|
Pattern: r.Pattern(key, ""),
|
|
After: after,
|
|
Limit: limit,
|
|
UnprefixedLocale: unprefixedLocale,
|
|
})
|
|
}
|
|
|
|
// MatchPath finds the record a public path belongs to: it asks every
|
|
// content type with Match, in registration order, with the type's layered
|
|
// pattern. The first exact match wins; without one, the first non-exact
|
|
// match (a record the path names with a segment that does not fit it) is
|
|
// returned; without any, key is "" and res nil. A Match error stops the
|
|
// search and is returned.
|
|
func (r *Registry) MatchPath(ctx context.Context, path, unprefixedLocale string) (key string, res *pact.ContentMatchResult, err error) {
|
|
if r == nil {
|
|
return "", nil, nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
var (
|
|
fallbackKey string
|
|
fallback *pact.ContentMatchResult
|
|
)
|
|
for _, t := range r.types {
|
|
if t.Content == nil || t.Content.Match == nil {
|
|
continue
|
|
}
|
|
got, err := t.Content.Match(ctx, pact.ContentMatch{
|
|
Path: path,
|
|
Pattern: r.Pattern(t.Key, ""),
|
|
UnprefixedLocale: unprefixedLocale,
|
|
})
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("signpost: match %q in menu item type %q: %w", path, t.Key, err)
|
|
}
|
|
if got == nil {
|
|
continue
|
|
}
|
|
if got.Exact {
|
|
return t.Key, got, nil
|
|
}
|
|
if fallback == nil {
|
|
fallbackKey, fallback = t.Key, got
|
|
}
|
|
}
|
|
return fallbackKey, fallback, nil
|
|
}
|
|
|
|
// MatchPattern matches a path against a URL pattern such as
|
|
// "/blog/:category/:slug" and returns the placeholder values. A placeholder
|
|
// is a whole segment starting with ":"; every other segment must equal the
|
|
// path's, the path must have exactly as many segments, a placeholder never
|
|
// matches an empty segment, and a placeholder used twice must match the
|
|
// same value. Segments are compared as given: the caller decodes the path.
|
|
// Content types use it in Match to read a path's values.
|
|
func MatchPattern(pattern, path string) (map[string]string, bool) {
|
|
if !strings.HasPrefix(pattern, "/") || !strings.HasPrefix(path, "/") {
|
|
return nil, false
|
|
}
|
|
want := strings.Split(pattern[1:], "/")
|
|
got := strings.Split(path[1:], "/")
|
|
if len(want) != len(got) {
|
|
return nil, false
|
|
}
|
|
values := map[string]string{}
|
|
for i, seg := range want {
|
|
if len(seg) > 1 && seg[0] == ':' {
|
|
name := seg[1:]
|
|
if got[i] == "" {
|
|
return nil, false
|
|
}
|
|
if prev, seen := values[name]; seen && prev != got[i] {
|
|
return nil, false
|
|
}
|
|
values[name] = got[i]
|
|
continue
|
|
}
|
|
if seg != got[i] {
|
|
return nil, false
|
|
}
|
|
}
|
|
return values, true
|
|
}
|
|
|
|
// contentType is the type key with its content descriptor.
|
|
func (r *Registry) contentType(key string) (pact.MenuItemType, error) {
|
|
t, ok := r.Type(key)
|
|
if !ok {
|
|
return pact.MenuItemType{}, fmt.Errorf("signpost: unknown menu item type %q", key)
|
|
}
|
|
if t.Content == nil {
|
|
return pact.MenuItemType{}, fmt.Errorf("signpost: menu item type %q has no content descriptor", key)
|
|
}
|
|
return t, nil
|
|
}
|