Files
fn-serverless/api/models/annotations.go
Tom Coupland d7139358ce List Cursor management moved into datastore layer. (#1102)
* Don't try to delete an app that wasn't successfully created in the case of failure

* Allow datastore implementations to inject additional annotations on objects

* Allow for datastores transparently adding annotations on apps, fns and triggers. Change NameIn filter to Name for apps.

* Move *List types including JSON annotations for App, Fn and Trigger into models

* Change return types for GetApps, GetFns and GetTriggers on datastore to
be models.*List and ove cursor generation into datastore

* Trigger cursor handling fixed into db layer

Also changes the name generation so that it is not in the same order
as the id (well is random), this means we are now testing our name ordering.

* GetFns now respects cursors

* Apps now feeds cursor back

* Mock fixes

* Fixing up api level cursor decoding

* Tidy up treatment of cursors in the db layer

* Adding conditions for non nil items lists

* fix mock test
2018-06-29 19:14:13 +01:00

243 lines
5.6 KiB
Go

package models
import (
"bytes"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"regexp"
)
// Annotations encapsulates key-value metadata associated with resource. The structure is immutable via its public API and nil-safe for its contract
// permissive nilability is here to simplify updates and reduce the need for nil handling in extensions - annotations should be updated by over-writing the original object:
// target.Annotations = target.Annotations.With("fooKey",1)
// old MD remains empty
// Annotations is lenable
type Annotations map[string]*annotationValue
// annotationValue encapsulates a value in the annotations map,
// This is stored in its compacted, un-parsed JSON format for later (re-) parsing into specific structs or values
// annotationValue objects are immutable after JSON load
type annotationValue []byte
const (
maxAnnotationValueBytes = 512
maxAnnotationKeyBytes = 128
maxAnnotationsKeys = 100
)
// Equals is defined based on un-ordered k/v comparison at of the annotation keys and (compacted) values of annotations, JSON object-value equality for values is property-order dependent
func (m Annotations) Equals(other Annotations) bool {
if len(m) != len(other) {
return false
}
return m.Subset(other)
}
func (m Annotations) Subset(other Annotations) bool {
for k1, v1 := range m {
v2, _ := other[k1]
if v2 == nil {
return false
}
if !bytes.Equal(*v1, *v2) {
return false
}
}
return true
}
func EmptyAnnotations() Annotations {
return nil
}
func (mv *annotationValue) String() string {
return string(*mv)
}
func (v *annotationValue) MarshalJSON() ([]byte, error) {
return *v, nil
}
func (mv *annotationValue) isEmptyValue() bool {
sval := string(*mv)
return sval == "\"\"" || sval == "null"
}
// UnmarshalJSON compacts annotation values but does not alter key-ordering for keys
func (mv *annotationValue) UnmarshalJSON(val []byte) error {
buf := bytes.Buffer{}
err := json.Compact(&buf, val)
if err != nil {
return err
}
if err != nil {
return err
}
*mv = buf.Bytes()
return nil
}
var validKeyRegex = regexp.MustCompile("^[!-~]+$")
func validateField(key string, value annotationValue) APIError {
if !validKeyRegex.Match([]byte(key)) {
return ErrInvalidAnnotationKey
}
keyLen := len([]byte(key))
if keyLen > maxAnnotationKeyBytes {
return ErrInvalidAnnotationKeyLength
}
if value.isEmptyValue() {
return ErrInvalidAnnotationValue
}
if len(value) > maxAnnotationValueBytes {
return ErrInvalidAnnotationValueLength
}
return nil
}
// With Creates a new annotations object containing the specified value - this does not perform size checks on the total number of keys
// this validates the correctness of the key and value. this returns a new the annotations object with the key set.
func (m Annotations) With(key string, data interface{}) (Annotations, error) {
if data == nil || data == "" {
return nil, errors.New("empty annotation value")
}
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
newVal := jsonBytes
err = validateField(key, newVal)
if err != nil {
return nil, err
}
var newMd Annotations
if m == nil {
newMd = make(Annotations, 1)
} else {
newMd = m.clone()
}
mv := annotationValue(newVal)
newMd[key] = &mv
return newMd, nil
}
// Validate validates a final annotations object prior to store,
// This will reject partial/patch changes with empty values (containing deletes)
func (m Annotations) Validate() APIError {
for k, v := range m {
err := validateField(k, *v)
if err != nil {
return err
}
}
if len(m) > maxAnnotationsKeys {
return ErrTooManyAnnotationKeys
}
return nil
}
// Get returns a raw JSON value of a annotation key
func (m Annotations) Get(key string) ([]byte, bool) {
if v, ok := m[key]; ok {
return *v, ok
}
return nil, false
}
// Without returns a new annotations object with a value excluded
func (m Annotations) Without(key string) Annotations {
nuVal := m.clone()
delete(nuVal, key)
return nuVal
}
// MergeChange merges a delta (possibly including deletes) with an existing annotations object and returns a new (copy) annotations object or an error.
// This assumes that both old and new annotations objects contain only valid keys and only newVs may contain deletes
func (m Annotations) MergeChange(newVs Annotations) Annotations {
newMd := m.clone()
for k, v := range newVs {
if v.isEmptyValue() {
delete(newMd, k)
} else {
if newMd == nil {
newMd = make(Annotations)
}
newMd[k] = v
}
}
if len(newMd) == 0 {
return EmptyAnnotations()
}
return newMd
}
// clone produces a key-wise copy of the underlying annotations
// publically MD can be copied by reference as it's (by contract) immutable
func (m Annotations) clone() Annotations {
if m == nil {
return nil
}
newMd := make(Annotations, len(m))
for ok, ov := range m {
newMd[ok] = ov
}
return newMd
}
// Value implements sql.Valuer, returning a string
func (m Annotations) Value() (driver.Value, error) {
if len(m) < 1 {
return driver.Value(string("")), nil
}
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(m)
return driver.Value(b.String()), err
}
// Scan implements sql.Scanner
func (m *Annotations) Scan(value interface{}) error {
if value == nil || value == "" {
*m = nil
return nil
}
bv, err := driver.String.ConvertValue(value)
if err == nil {
var b []byte
switch x := bv.(type) {
case []byte:
b = x
case string:
b = []byte(x)
}
if len(b) > 0 {
return json.Unmarshal(b, m)
}
*m = nil
return nil
}
// otherwise, return an error
return fmt.Errorf("annotations invalid db format: %T %T value, err: %v", value, bv, err)
}