Add revision-aware CMS taxonomies and relationships
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Package cms supplies bounded classification and editorial relationship values.
|
||||
// It does not define content types, layouts, authorization, or commerce policy.
|
||||
// Applications own those decisions and publish exact immutable revisions.
|
||||
package cms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalid = errors.New("cms: invalid value")
|
||||
ErrConflict = errors.New("cms: revision or slug conflict")
|
||||
ErrNotFound = errors.New("cms: not found")
|
||||
identifier = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$`)
|
||||
slug = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
|
||||
kind = regexp.MustCompile(`^[a-z][a-z0-9-]{0,39}$`)
|
||||
)
|
||||
|
||||
const MaxTerms = 24
|
||||
const MaxLinks = 16
|
||||
|
||||
// Reference names an application-owned resource, never a mutable URL or title.
|
||||
type Reference struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func ValidID(value string) bool { return identifier.MatchString(value) }
|
||||
func ValidSlug(value string) bool { return len(value) <= 80 && slug.MatchString(value) }
|
||||
func ValidText(value string, max int) bool {
|
||||
return utf8.ValidString(value) && len(value) <= max && strings.TrimSpace(value) == value && !strings.ContainsFunc(value, unicode.IsControl)
|
||||
}
|
||||
func (r Reference) Validate() error {
|
||||
if !kind.MatchString(r.Kind) || !ValidID(r.ID) {
|
||||
return ErrInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Associations are an immutable revision's term memberships and explicit links.
|
||||
// Reverse discovery reads the same links; callers must not store a second edge.
|
||||
type Associations struct {
|
||||
Terms []string `json:"terms,omitempty"`
|
||||
Links []Reference `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
func (a Associations) Validate(source Reference) error {
|
||||
if source.Validate() != nil || len(a.Terms) > MaxTerms || len(a.Links) > MaxLinks {
|
||||
return ErrInvalid
|
||||
}
|
||||
terms := map[string]bool{}
|
||||
for _, id := range a.Terms {
|
||||
if !ValidID(id) || terms[id] {
|
||||
return ErrInvalid
|
||||
}
|
||||
terms[id] = true
|
||||
}
|
||||
links := map[Reference]bool{}
|
||||
for _, ref := range a.Links {
|
||||
if ref.Validate() != nil || ref == source || links[ref] {
|
||||
return ErrInvalid
|
||||
}
|
||||
links[ref] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Taxonomy is an editor-named vocabulary, not a database-defined content type.
|
||||
// Slug is fixed after creation; Name/Description and availability may change.
|
||||
type Taxonomy struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Revision int64 `json:"revision"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (v Taxonomy) Validate() error {
|
||||
if !ValidID(v.ID) || !ValidSlug(v.Slug) || v.Name == "" || !ValidText(v.Name, 120) || !ValidText(v.Description, 500) || v.Revision < 1 {
|
||||
return ErrInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Term identity survives renaming and retirement. Retirement hides discovery;
|
||||
// it does not rewrite old associations or imply removal of related resources.
|
||||
type Term struct {
|
||||
ID string `json:"id"`
|
||||
TaxonomyID string `json:"taxonomy_id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Revision int64 `json:"revision"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (v Term) Validate() error {
|
||||
if !ValidID(v.ID) || !ValidID(v.TaxonomyID) || !ValidSlug(v.Slug) || v.Name == "" || !ValidText(v.Name, 120) || !ValidText(v.Description, 500) || v.Revision < 1 {
|
||||
return ErrInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResourceVersion struct {
|
||||
Reference
|
||||
Revision int64 `json:"revision"`
|
||||
}
|
||||
type Page struct {
|
||||
Items []ResourceVersion `json:"items"`
|
||||
Next *Reference `json:"next,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user