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 }