nimona/go-nimona

View on GitHub
tilde/tilde.go

Summary

Maintainability
A
0 mins
Test Coverage
package tilde

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "strconv"
    "strings"

    "github.com/mitchellh/copystructure"
    "github.com/valyala/fastjson"
)

// Document is a map of key/value pairs where keys are strings and values
// are either primitive values, or other documents, as well as lists of
// either of these.
//
// A value can be one of the following kinds:
// - String (string)
// - Int64 (int64)
// - Uint64 (uint64)
// - Bytes ([]byte)
// - Bool (bool)
// - List (List of Values)
// - Map (map of string to Value)
//
// Values can be accessed using the Get and Set methods, and support nesting
// using the dot notation.
//
// Documents are strongly typed and their shape is defined by their schema.
// Trying to access a key that is not defined in the schema will result in an
// error, same goes for trying to set a value that does not match the type
// defined in the schema.
//
// Documents are the basic unit of data in Nimona. They are used to represent
// all other data types.
type (
    ValueKind int
    Hint      rune
    Schema    struct {
        Kind       ValueKind
        Properties map[string]*Schema
        Elements   *Schema
    }
)

const (
    HintInvalid Hint = '?'
    HintString  Hint = 's'
    HintInt64   Hint = 'i'
    HintUint64  Hint = 'u'
    HintBytes   Hint = 'd'
    HintRef     Hint = 'r'
    HintBool    Hint = 'b'
    HintList    Hint = 'a'
    HintMap     Hint = 'm'
    HintAny     Hint = '*'
)

const (
    KindInvalid ValueKind = iota
    KindString  ValueKind = iota
    KindInt64   ValueKind = iota
    KindUint64  ValueKind = iota
    KindBytes   ValueKind = iota
    KindRef     ValueKind = iota
    KindBool    ValueKind = iota
    KindList    ValueKind = iota
    KindMap     ValueKind = iota
    KindAny     ValueKind = iota
)

type (
    Value interface {
        hint() Hint
        cmp(Value) (int, error)
    }

    Int64  int64
    Uint64 uint64
    String string
    Bytes  []byte
    Ref    [32]byte
    Bool   bool
    Map    map[string]Value
    List   []Value
)

func (Int64) hint() Hint  { return HintInt64 }
func (Uint64) hint() Hint { return HintUint64 }
func (String) hint() Hint { return HintString }
func (Bytes) hint() Hint  { return HintBytes }
func (Bool) hint() Hint   { return HintBool }
func (Map) hint() Hint    { return HintMap }
func (List) hint() Hint   { return HintList }
func (Ref) hint() Hint    { return HintRef }

func (k ValueKind) String() string {
    switch k {
    case KindString:
        return "string"
    case KindInt64:
        return "int64"
    case KindUint64:
        return "uint64"
    case KindBytes:
        return "bytes"
    case KindBool:
        return "bool"
    case KindList:
        return "list"
    case KindMap:
        return "map"
    case KindAny:
        return "Value"
    }
    return "invalid"
}

func (k ValueKind) Name() string {
    switch k {
    case KindString:
        return "String"
    case KindInt64:
        return "Int64"
    case KindUint64:
        return "Uint64"
    case KindBytes:
        return "Bytes"
    case KindBool:
        return "Bool"
    case KindList:
        return "List"
    case KindMap:
        return "Map"
    case KindRef:
        return "Ref"
    case KindAny:
        return "Value"
    }
    return "InvalidValueKind" + strconv.Itoa(int(k))
}

func (k ValueKind) Hint() Hint {
    switch k {
    case KindString:
        return HintString
    case KindInt64:
        return HintInt64
    case KindUint64:
        return HintUint64
    case KindBytes:
        return HintBytes
    case KindBool:
        return HintBool
    case KindList:
        return HintList
    case KindMap:
        return HintMap
    case KindRef:
        return HintRef
    case KindAny:
        return HintAny
    }
    return HintInvalid
}

func KindFromString(s string) ValueKind {
    switch s {
    case "string":
        return KindString
    case "int64":
        return KindInt64
    case "uint64":
        return KindUint64
    case "bytes":
        return KindBytes
    case "bool":
        return KindBool
    case "array":
        return KindList
    case "map":
        return KindMap
    }
    return KindInvalid
}

// Get returns the value at the given path.
// Supports dot notation for nested values.
func (m Map) Get(path string) (Value, error) {
    keyFirst, keyRest, _ := strings.Cut(path, ".")
    v, ok := m[keyFirst]
    if !ok {
        return nil, fmt.Errorf("key %s not found", keyFirst)
    }

    if keyRest == "" {
        return v, nil
    }

    switch v := v.(type) {
    case Map:
        return v.Get(keyRest)
    case List:
        return v.Get(keyRest)
    default:
        return v, nil
    }
}

// Get returns the value at the given index.
func (l List) Get(index string) (Value, error) {
    i, keyRest, _ := strings.Cut(index, ".")
    v, err := strconv.Atoi(i)
    if err != nil {
        return nil, err
    }

    if v >= len(l) {
        return nil, fmt.Errorf("index %d out of range", v)
    }

    if keyRest == "" {
        return l[v], nil
    }

    switch v := l[v].(type) {
    case Map:
        return v.Get(keyRest)
    case List:
        return v.Get(keyRest)
    default:
        return nil, fmt.Errorf("index %d is not a map or list", v)
    }
}

// Set sets the value at the given path.
// Supports dot notation for nested values.
func (m Map) Set(path string, value Value) error {
    // attempt to figure out the kind of the value
    // TODO: at some point the map should accept a schema
    keyFirst, keyRest, _ := strings.Cut(path, ".")
    v, ok := m[keyFirst]
    if !ok && strings.Contains(keyRest, ".") {
        keySecond, _, _ := strings.Cut(keyRest, ".")
        if _, err := strconv.Atoi(keySecond); err == nil {
            m[keyFirst] = List{}
            v = m[keyFirst]
        } else {
            m[keyFirst] = Map{}
            v = m[keyFirst]
        }
    }

    if keyRest == "" {
        m[keyFirst] = value
        return nil
    }

    switch v := v.(type) {
    case Map:
        return v.Set(keyRest, value)
    case List:
        return v.Set(keyRest, value)
    default:
        return fmt.Errorf("key %s is not a map or list", keyFirst)
    }
}

// Set sets the value at the given path.
func (l List) Set(path string, value Value) error {
    i, keyRest, _ := strings.Cut(path, ".")
    v, err := strconv.Atoi(i)
    if err != nil {
        return err
    }

    if v >= len(l) {
        return fmt.Errorf("index %d out of range", v)
    }

    if keyRest == "" {
        l[v] = value
        return nil
    }

    switch v := l[v].(type) {
    case Map:
        return v.Set(keyRest, value)
    case List:
        return v.Set(keyRest, value)
    default:
        return fmt.Errorf("index %d is not a map or list", v)
    }
}

// Append appends the given value to the list.
// If the list is nil, a new list is created.
// Supports dot notation for nested values.
func (m Map) Append(path string, value Value) error {
    keyFirst, keyRest, _ := strings.Cut(path, ".")
    v, ok := m[keyFirst]
    if !ok {
        if strings.Contains(keyRest, ".") {
            // if the key rest contains a dot, we need to figure out
            // if the next level is a map or a list
            // TODO: at some point we should use a schema for this
            keySecond, _, _ := strings.Cut(keyRest, ".")
            if _, err := strconv.Atoi(keySecond); err == nil {
                m[keyFirst] = List{}
                v = m[keyFirst]
            } else {
                m[keyFirst] = Map{}
                v = m[keyFirst]
            }
        } else {
            // else we assume it's a list
            m[keyFirst] = List{value}
            return nil
        }
    }

    switch v := v.(type) {
    case Map:
        return v.Append(keyRest, value)
    case List:
        lv, err := v.appendPath(keyRest, value)
        if err != nil {
            return fmt.Errorf("cannot append to list: %v", err)
        }
        m[keyFirst] = lv
        return err
    default:
        return fmt.Errorf("key %s is not a map or list, got %T", keyFirst, v)
    }
}

// appendPath appends the given value to the list.
// If the list is nil, a new list is created.
// Supports dot notation for nested values.
func (l List) appendPath(path string, value Value) (List, error) {
    if path == "" {
        return append(l, value), nil
    }

    keyFirst, keyRest, _ := strings.Cut(path, ".")
    index, err := strconv.Atoi(keyFirst)
    if err != nil {
        return l, fmt.Errorf("invalid index %d", index)
    }

    if keyRest == "" {
        return append(l, value), nil
    }

    if index >= len(l) {
        return l, fmt.Errorf("index %d out of range", index)
    }

    switch v := l[index].(type) {
    case Map:
        err := v.Set(keyRest, value)
        if err != nil {
            return l, fmt.Errorf("error setting %s: %s", keyRest, err)
        }
        l[index] = v
        return l, nil
    case List:
        lv, err := v.appendPath(keyRest, value)
        if err != nil {
            return l, fmt.Errorf("error appending %s: %s", keyRest, err)
        }
        l[index] = lv
        return l, nil
    default:
        return l, fmt.Errorf("index %d is not a map or list", v)
    }
}

func (r Ref) MarshalJSON() ([]byte, error) {
    return json.Marshal(r[:])
}

func (m *Map) UnmarshalJSON(data []byte) error {
    var sc fastjson.Scanner

    sc.Init(string(data))

    if !sc.Next() {
        return fmt.Errorf("expected object, got nothing")
    }

    o := sc.Value()
    if o.Type() != fastjson.TypeObject {
        return fmt.Errorf("expected object, got %s", o.Type())
    }

    nm, err := unmarshalValue(o)
    if err != nil {
        return fmt.Errorf("error unmarshaling: %w", err)
    }

    *m = nm.(Map)

    return nil
}

func (m Map) MarshalJSON() ([]byte, error) {
    mm := make(map[string]interface{}, len(m))
    for k, v := range m {
        hk := keyWithHint(k, v.hint())
        switch v := v.(type) {
        case Map:
            b, err := v.MarshalJSON()
            if err != nil {
                return nil, fmt.Errorf("error marshaling map: %w", err)
            }
            mm[hk] = json.RawMessage(b)
        case List:
            cv := v
            hints := []Hint{
                cv.hint(),
            }
            for len(cv) != 0 && cv[0] != nil {
                hints = append(hints, cv[0].hint())
                if cv[0].hint() != HintList {
                    break
                }
                cv = cv[0].(List)
            }
            hk = keyWithHint(k, hints...)
            mm[hk] = v
        default:
            mm[hk] = v
        }
    }
    return json.Marshal(mm)
}

func keyWithHint(key string, hints ...Hint) string {
    if len(hints) == 0 {
        return key
    }
    suffix := ""
    for _, hint := range hints {
        suffix += string(hint)
    }
    return fmt.Sprintf("%s:%s", key, suffix)
}

func Copy[T Value](v T) T {
    nv, err := copystructure.Copy(v)
    if err != nil {
        panic(fmt.Errorf("error copying value of type %T: %w", v, err))
    }
    return nv.(T)
}

// Compare compares two values of the same type.
// If the first value is less than the second, -1 is returned.
// If the first value is greater than the second, 1 is returned.
// If the values are equal, 0 is returned.
func Compare[V Value](a, b V) (int, error) {
    return a.cmp(b)
}

func sameType(a, b Value) bool {
    return a.hint() == b.hint()
}

func (a Int64) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(Int64)
    if a < b {
        return -1, nil
    }
    if a > b {
        return 1, nil
    }
    return 0, nil
}

func (a Uint64) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(Uint64)
    if a < b {
        return -1, nil
    }
    if a > b {
        return 1, nil
    }
    return 0, nil
}

func (a String) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(String)
    return strings.Compare(string(a), string(b)), nil
}

func (a Bytes) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(Bytes)
    return bytes.Compare(a, b), nil
}

func (a Ref) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(Ref)
    return bytes.Compare(a[:], b[:]), nil
}

func (a Bool) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(Bool)
    if a == b {
        return 0, nil
    }
    if a {
        return 1, nil
    }
    return -1, nil
}

func (a Map) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(Map)
    if len(a) < len(b) {
        return -1, nil
    }
    if len(a) > len(b) {
        return 1, nil
    }
    return 0, errors.New("not implemented")
}

func (a List) cmp(v Value) (int, error) {
    if !sameType(a, v) {
        return 0, fmt.Errorf("cannot compare %T and %T", a, v)
    }
    b := v.(List)
    if len(a) < len(b) {
        return -1, nil
    }
    if len(a) > len(b) {
        return 1, nil
    }
    return 0, errors.New("not implemented")
}