github.com/bosssauce/ponzu@v0.11.1-0.20200102001432-9bc41b703131/system/item/item.go (about)

     1  // Package item provides the default functionality to Ponzu's content/data types,
     2  // how they interact with the API, and how to override or enhance their abilities
     3  // using various interfaces.
     4  package item
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"regexp"
    10  	"strings"
    11  	"unicode"
    12  
    13  	"github.com/blevesearch/bleve"
    14  	"github.com/blevesearch/bleve/mapping"
    15  	"github.com/gofrs/uuid"
    16  	"golang.org/x/text/transform"
    17  	"golang.org/x/text/unicode/norm"
    18  )
    19  
    20  var rxList map[*regexp.Regexp][]byte
    21  
    22  func init() {
    23  	// Compile regex once to use in stringToSlug().
    24  	// We store the compiled regex as the key
    25  	// and assign the replacement as the map's value.
    26  	rxList = map[*regexp.Regexp][]byte{
    27  		regexp.MustCompile("`[-]+`"):                                                                         []byte("-"),
    28  		regexp.MustCompile("[[:space:]]"):                                                                    []byte("-"),
    29  		regexp.MustCompile("[[:blank:]]"):                                                                    []byte(""),
    30  		regexp.MustCompile("`[^a-z0-9]`i"):                                                                   []byte("-"),
    31  		regexp.MustCompile("[!/:-@[-`{-~]"):                                                                  []byte(""),
    32  		regexp.MustCompile("/[^\x20-\x7F]/"):                                                                 []byte(""),
    33  		regexp.MustCompile("`&(amp;)?#?[a-z0-9]+;`i"):                                                        []byte("-"),
    34  		regexp.MustCompile("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i"): []byte("\\1"),
    35  	}
    36  }
    37  
    38  // Sluggable makes a struct locatable by URL with it's own path.
    39  // As an Item implementing Sluggable, slugs may overlap. If this is an issue,
    40  // make your content struct (or one which embeds Item) implement Sluggable
    41  // and it will override the slug created by Item's SetSlug with your own
    42  type Sluggable interface {
    43  	SetSlug(string)
    44  	ItemSlug() string
    45  }
    46  
    47  // Identifiable enables a struct to have its ID set/get. Typically this is done
    48  // to set an ID to -1 indicating it is new for DB inserts, since by default
    49  // a newly initialized struct would have an ID of 0, the int zero-value, and
    50  // BoltDB's starting key per bucket is 0, thus overwriting the first record.
    51  type Identifiable interface {
    52  	ItemID() int
    53  	SetItemID(int)
    54  	UniqueID() uuid.UUID
    55  	String() string
    56  }
    57  
    58  // Sortable ensures data is sortable by time
    59  type Sortable interface {
    60  	Time() int64
    61  	Touch() int64
    62  }
    63  
    64  // Hookable provides our user with an easy way to intercept or add functionality
    65  // to the different lifecycles/events a struct may encounter. Item implements
    66  // Hookable with no-ops so our user can override only whichever ones necessary.
    67  type Hookable interface {
    68  	BeforeAPIResponse(http.ResponseWriter, *http.Request, []byte) ([]byte, error)
    69  	AfterAPIResponse(http.ResponseWriter, *http.Request, []byte) error
    70  
    71  	BeforeAPICreate(http.ResponseWriter, *http.Request) error
    72  	AfterAPICreate(http.ResponseWriter, *http.Request) error
    73  
    74  	BeforeAPIUpdate(http.ResponseWriter, *http.Request) error
    75  	AfterAPIUpdate(http.ResponseWriter, *http.Request) error
    76  
    77  	BeforeAPIDelete(http.ResponseWriter, *http.Request) error
    78  	AfterAPIDelete(http.ResponseWriter, *http.Request) error
    79  
    80  	BeforeAdminCreate(http.ResponseWriter, *http.Request) error
    81  	AfterAdminCreate(http.ResponseWriter, *http.Request) error
    82  
    83  	BeforeAdminUpdate(http.ResponseWriter, *http.Request) error
    84  	AfterAdminUpdate(http.ResponseWriter, *http.Request) error
    85  
    86  	BeforeAdminDelete(http.ResponseWriter, *http.Request) error
    87  	AfterAdminDelete(http.ResponseWriter, *http.Request) error
    88  
    89  	BeforeSave(http.ResponseWriter, *http.Request) error
    90  	AfterSave(http.ResponseWriter, *http.Request) error
    91  
    92  	BeforeDelete(http.ResponseWriter, *http.Request) error
    93  	AfterDelete(http.ResponseWriter, *http.Request) error
    94  
    95  	BeforeApprove(http.ResponseWriter, *http.Request) error
    96  	AfterApprove(http.ResponseWriter, *http.Request) error
    97  
    98  	BeforeReject(http.ResponseWriter, *http.Request) error
    99  	AfterReject(http.ResponseWriter, *http.Request) error
   100  
   101  	// Enable/Disable used for addons
   102  	BeforeEnable(http.ResponseWriter, *http.Request) error
   103  	AfterEnable(http.ResponseWriter, *http.Request) error
   104  
   105  	BeforeDisable(http.ResponseWriter, *http.Request) error
   106  	AfterDisable(http.ResponseWriter, *http.Request) error
   107  }
   108  
   109  // Hideable lets a user keep items hidden
   110  type Hideable interface {
   111  	Hide(http.ResponseWriter, *http.Request) error
   112  }
   113  
   114  // Pushable lets a user define which values of certain struct fields are
   115  // 'pushed' down to  a client via HTTP/2 Server Push. All items in the slice
   116  // should be the json tag names of the struct fields to which they correspond.
   117  type Pushable interface {
   118  	// the values contained by fields returned by Push must strictly be URL paths
   119  	Push(http.ResponseWriter, *http.Request) ([]string, error)
   120  }
   121  
   122  // Omittable lets a user define certin fields within a content struct to remove
   123  // from an API response. Helpful when you want data in the CMS, but not entirely
   124  // shown or available from the content API. All items in the slice should be the
   125  // json tag names of the struct fields to which they correspond.
   126  type Omittable interface {
   127  	Omit(http.ResponseWriter, *http.Request) ([]string, error)
   128  }
   129  
   130  // Item should only be embedded into content type structs.
   131  type Item struct {
   132  	UUID      uuid.UUID `json:"uuid"`
   133  	ID        int       `json:"id"`
   134  	Slug      string    `json:"slug"`
   135  	Timestamp int64     `json:"timestamp"`
   136  	Updated   int64     `json:"updated"`
   137  }
   138  
   139  // Time partially implements the Sortable interface
   140  func (i Item) Time() int64 {
   141  	return i.Timestamp
   142  }
   143  
   144  // Touch partially implements the Sortable interface
   145  func (i Item) Touch() int64 {
   146  	return i.Updated
   147  }
   148  
   149  // SetSlug sets the item's slug for its URL
   150  func (i *Item) SetSlug(slug string) {
   151  	i.Slug = slug
   152  }
   153  
   154  // ItemSlug sets the item's slug for its URL
   155  func (i *Item) ItemSlug() string {
   156  	return i.Slug
   157  }
   158  
   159  // ItemID gets the Item's ID field
   160  // partially implements the Identifiable interface
   161  func (i Item) ItemID() int {
   162  	return i.ID
   163  }
   164  
   165  // SetItemID sets the Item's ID field
   166  // partially implements the Identifiable interface
   167  func (i *Item) SetItemID(id int) {
   168  	i.ID = id
   169  }
   170  
   171  // UniqueID gets the Item's UUID field
   172  // partially implements the Identifiable interface
   173  func (i Item) UniqueID() uuid.UUID {
   174  	return i.UUID
   175  }
   176  
   177  // String formats an Item into a printable value
   178  // partially implements the Identifiable interface
   179  func (i Item) String() string {
   180  	return fmt.Sprintf("Item ID: %s", i.UniqueID())
   181  }
   182  
   183  // BeforeAPIResponse is a no-op to ensure structs which embed Item implement Hookable
   184  func (i Item) BeforeAPIResponse(res http.ResponseWriter, req *http.Request, data []byte) ([]byte, error) {
   185  	return data, nil
   186  }
   187  
   188  // AfterAPIResponse is a no-op to ensure structs which embed Item implement Hookable
   189  func (i Item) AfterAPIResponse(res http.ResponseWriter, req *http.Request, data []byte) error {
   190  	return nil
   191  }
   192  
   193  // BeforeAPICreate is a no-op to ensure structs which embed Item implement Hookable
   194  func (i Item) BeforeAPICreate(res http.ResponseWriter, req *http.Request) error {
   195  	return nil
   196  }
   197  
   198  // AfterAPICreate is a no-op to ensure structs which embed Item implement Hookable
   199  func (i Item) AfterAPICreate(res http.ResponseWriter, req *http.Request) error {
   200  	return nil
   201  }
   202  
   203  // BeforeAPIUpdate is a no-op to ensure structs which embed Item implement Hookable
   204  func (i Item) BeforeAPIUpdate(res http.ResponseWriter, req *http.Request) error {
   205  	return nil
   206  }
   207  
   208  // AfterAPIUpdate is a no-op to ensure structs which embed Item implement Hookable
   209  func (i Item) AfterAPIUpdate(res http.ResponseWriter, req *http.Request) error {
   210  	return nil
   211  }
   212  
   213  // BeforeAPIDelete is a no-op to ensure structs which embed Item implement Hookable
   214  func (i Item) BeforeAPIDelete(res http.ResponseWriter, req *http.Request) error {
   215  	return nil
   216  }
   217  
   218  // AfterAPIDelete is a no-op to ensure structs which embed Item implement Hookable
   219  func (i Item) AfterAPIDelete(res http.ResponseWriter, req *http.Request) error {
   220  	return nil
   221  }
   222  
   223  // BeforeAdminCreate is a no-op to ensure structs which embed Item implement Hookable
   224  func (i Item) BeforeAdminCreate(res http.ResponseWriter, req *http.Request) error {
   225  	return nil
   226  }
   227  
   228  // AfterAdminCreate is a no-op to ensure structs which embed Item implement Hookable
   229  func (i Item) AfterAdminCreate(res http.ResponseWriter, req *http.Request) error {
   230  	return nil
   231  }
   232  
   233  // BeforeAdminUpdate is a no-op to ensure structs which embed Item implement Hookable
   234  func (i Item) BeforeAdminUpdate(res http.ResponseWriter, req *http.Request) error {
   235  	return nil
   236  }
   237  
   238  // AfterAdminUpdate is a no-op to ensure structs which embed Item implement Hookable
   239  func (i Item) AfterAdminUpdate(res http.ResponseWriter, req *http.Request) error {
   240  	return nil
   241  }
   242  
   243  // BeforeAdminDelete is a no-op to ensure structs which embed Item implement Hookable
   244  func (i Item) BeforeAdminDelete(res http.ResponseWriter, req *http.Request) error {
   245  	return nil
   246  }
   247  
   248  // AfterAdminDelete is a no-op to ensure structs which embed Item implement Hookable
   249  func (i Item) AfterAdminDelete(res http.ResponseWriter, req *http.Request) error {
   250  	return nil
   251  }
   252  
   253  // BeforeSave is a no-op to ensure structs which embed Item implement Hookable
   254  func (i Item) BeforeSave(res http.ResponseWriter, req *http.Request) error {
   255  	return nil
   256  }
   257  
   258  // AfterSave is a no-op to ensure structs which embed Item implement Hookable
   259  func (i Item) AfterSave(res http.ResponseWriter, req *http.Request) error {
   260  	return nil
   261  }
   262  
   263  // BeforeDelete is a no-op to ensure structs which embed Item implement Hookable
   264  func (i Item) BeforeDelete(res http.ResponseWriter, req *http.Request) error {
   265  	return nil
   266  }
   267  
   268  // AfterDelete is a no-op to ensure structs which embed Item implement Hookable
   269  func (i Item) AfterDelete(res http.ResponseWriter, req *http.Request) error {
   270  	return nil
   271  }
   272  
   273  // BeforeApprove is a no-op to ensure structs which embed Item implement Hookable
   274  func (i Item) BeforeApprove(res http.ResponseWriter, req *http.Request) error {
   275  	return nil
   276  }
   277  
   278  // AfterApprove is a no-op to ensure structs which embed Item implement Hookable
   279  func (i Item) AfterApprove(res http.ResponseWriter, req *http.Request) error {
   280  	return nil
   281  }
   282  
   283  // BeforeReject is a no-op to ensure structs which embed Item implement Hookable
   284  func (i Item) BeforeReject(res http.ResponseWriter, req *http.Request) error {
   285  	return nil
   286  }
   287  
   288  // AfterReject is a no-op to ensure structs which embed Item implement Hookable
   289  func (i Item) AfterReject(res http.ResponseWriter, req *http.Request) error {
   290  	return nil
   291  }
   292  
   293  // BeforeEnable is a no-op to ensure structs which embed Item implement Hookable
   294  func (i Item) BeforeEnable(res http.ResponseWriter, req *http.Request) error {
   295  	return nil
   296  }
   297  
   298  // AfterEnable is a no-op to ensure structs which embed Item implement Hookable
   299  func (i Item) AfterEnable(res http.ResponseWriter, req *http.Request) error {
   300  	return nil
   301  }
   302  
   303  // BeforeDisable is a no-op to ensure structs which embed Item implement Hookable
   304  func (i Item) BeforeDisable(res http.ResponseWriter, req *http.Request) error {
   305  	return nil
   306  }
   307  
   308  // AfterDisable is a no-op to ensure structs which embed Item implement Hookable
   309  func (i Item) AfterDisable(res http.ResponseWriter, req *http.Request) error {
   310  	return nil
   311  }
   312  
   313  // SearchMapping returns a default implementation of a Bleve IndexMappingImpl
   314  // partially implements search.Searchable
   315  func (i Item) SearchMapping() (*mapping.IndexMappingImpl, error) {
   316  	mapping := bleve.NewIndexMapping()
   317  	mapping.StoreDynamic = false
   318  
   319  	return mapping, nil
   320  }
   321  
   322  // IndexContent determines if a type should be indexed for searching
   323  // partially implements search.Searchable
   324  func (i Item) IndexContent() bool {
   325  	return false
   326  }
   327  
   328  // Slug returns a URL friendly string from the title of a post item
   329  func Slug(i Identifiable) (string, error) {
   330  	// get the name of the post item
   331  	name := strings.TrimSpace(i.String())
   332  
   333  	// filter out non-alphanumeric character or non-whitespace
   334  	slug, err := stringToSlug(name)
   335  	if err != nil {
   336  		return "", err
   337  	}
   338  
   339  	return slug, nil
   340  }
   341  
   342  func isMn(r rune) bool {
   343  	return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks
   344  }
   345  
   346  // modified version of: https://www.socketloop.com/tutorials/golang-format-strings-to-seo-friendly-url-example
   347  func stringToSlug(s string) (string, error) {
   348  	src := []byte(strings.ToLower(s))
   349  
   350  	// Range over compiled regex and replacements from init().
   351  	for rx := range rxList {
   352  		src = rx.ReplaceAll(src, rxList[rx])
   353  	}
   354  
   355  	str := strings.Replace(string(src), "'", "", -1)
   356  	str = strings.Replace(str, `"`, "", -1)
   357  	str = strings.Replace(str, "&", "-", -1)
   358  
   359  	t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
   360  	slug, _, err := transform.String(t, str)
   361  	if err != nil {
   362  		return "", err
   363  	}
   364  
   365  	return strings.TrimSpace(slug), nil
   366  }
   367  
   368  // NormalizeString removes and replaces illegal characters for URLs and other
   369  // path entities. Useful for taking user input and converting it for keys or URLs.
   370  func NormalizeString(s string) (string, error) {
   371  	return stringToSlug(s)
   372  }