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 }