go-sprout/sprout

View on GitHub
registry/maps/functions.go

Summary

Maintainability
A
1 hr
Test Coverage
package maps

import (
    "errors"
    "fmt"

    "dario.cat/mergo"

    "github.com/go-sprout/sprout/deprecated"
    "github.com/go-sprout/sprout/internal/helpers"
)

// Dict creates a dictionary from a list of keys and values.
//
// Parameters:
//
//    values ...any - alternating keys and values.
//
// Returns:
//
//    map[string]any - the created dictionary.
//
// Example:
//
//    {{ dict "key1", "value1", "key2", "value2" }} // Output: {"key1": "value1", "key2": "value2"}
func (mr *MapsRegistry) Dict(values ...any) map[string]any {
    // Ensure even number of values for key-value pairs
    if len(values)%2 != 0 {
        values = append(values, "")
    }

    // Pre-allocate the map based on half the number of total elements,
    // since we expect every two elements to form a key-value pair.
    dict := make(map[string]any, len(values)/2)

    for i := 0; i < len(values); i += 2 {
        dict[helpers.ToString(values[i])] = values[i+1]
    }

    return dict
}

// Get retrieves the value associated with the specified key from the dictionary.
//
// Parameters:
//
//    key string - the key to look up.
//    dict map[string]any - the dictionary.
//
// Returns:
//
//    any - the value associated with the key, or an empty string if the key does not exist.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ {"key": "value"} | get "key" }} // Output: "value"
func (mr *MapsRegistry) Get(args ...any) (any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Get(map[string]any, string)
    // * New signature: Get(string, map[string]any)
    if len(args) != 2 {
        return "", deprecated.ErrArgsCount(2, len(args))
    }

    switch arg0 := args[0].(type) {
    case map[string]any:
        // Old signature
        deprecated.SignatureWarn(mr.handler.Logger(), "get", "{{ get dict key }}", "{{ dict | get key }}")
        return mr.Get(args[1].(string), arg0)
    case string:
        // New signature
        if value, ok := args[1].(map[string]any)[arg0]; ok {
            return value, nil
        }
        return "", nil
    default:
        return "", fmt.Errorf("expected map or string, got %T", arg0)
    }
}

// Set adds or updates a key with a specified value in the dictionary.
//
// Parameters:
//
//    key string - the key to set.
//    value any - the value to associate with the key.
//    dict map[string]any - the dictionary.
//
// Returns:
//
//    map[string]any - the updated dictionary.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ {"key": "oldValue"} | set "key", "newValue" }} // Output: {"key": "newValue"}
func (mr *MapsRegistry) Set(args ...any) (map[string]any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Set(map[string]any, string, string)
    // * New signature: Set(string, any, map[string]any)
    if len(args) != 3 {
        return nil, deprecated.ErrArgsCount(3, len(args))
    }

    switch arg0 := args[0].(type) {
    case map[string]any:
        // Old signature
        deprecated.SignatureWarn(mr.handler.Logger(), "set", "{{ set dict key value }}", "{{ dict | set key value }}")
        return mr.Set(args[1].(string), args[2], arg0)
    case string:
        // New signature
        if dict, ok := args[2].(map[string]any); ok {
            dict[arg0] = args[1]
            return dict, nil
        }
        return nil, errors.New("last argument must be a map[string]any")
    default:
        return nil, fmt.Errorf("expected map or string, got %T", arg0)
    }
}

// Unset removes a key from the dictionary.
//
// Parameters:
//
//    key string - the key to remove.
//    dict map[string]any - the dictionary.
//
// Returns:
//
//    map[string]any - the dictionary after removing the key.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ {"key": "value"} | unset "key" }} // Output: {}
func (mr *MapsRegistry) Unset(args ...any) (map[string]any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Unset(map[string]any, string)
    // * New signature: Unset(string, map[string]any)
    if len(args) != 2 {
        return nil, deprecated.ErrArgsCount(2, len(args))
    }

    switch arg0 := args[0].(type) {
    case map[string]any:
        // Old signature
        deprecated.SignatureWarn(mr.handler.Logger(), "unset", "{{ unset dict key }}", "{{ dict | unset key }}")
        return mr.Unset(args[1].(string), arg0)
    case string:
        // New signature
        if dict, ok := args[1].(map[string]any); ok {
            delete(dict, arg0)
            return dict, nil
        }
        return nil, errors.New("last argument must be a map[string]any")
    default:
        return nil, fmt.Errorf("expected map or string, got %T", arg0)
    }
}

// Keys retrieves all keys from one or more dictionaries.
//
// Parameters:
//
//    dicts ...map[string]any - one or more dictionaries.
//
// Returns:
//
//    []string - a list of all keys from the dictionaries.
//
// Example:
//
//    {{ keys {"key1": "value1", "key2": "value2"} }} // Output: ["key1", "key2"]
func (mr *MapsRegistry) Keys(dicts ...map[string]any) []string {
    var keyCount int
    for i := range dicts {
        keyCount += len(dicts[i])
    }

    keys := make([]string, 0, keyCount)

    for _, dict := range dicts {
        for key := range dict {
            keys = append(keys, key)
        }
    }

    return keys
}

// Values retrieves all values from one or more dictionaries.
//
// Parameters:
//
//    dict map[string]any - the dictionary.
//
// Returns:
//
//    []any - a list of all values from the dictionary.
//
// Example:
//
//    {{ values {"key1": "value1", "key2": "value2"} }} // Output: ["value1", "value2"]
func (mr *MapsRegistry) Values(dicts ...map[string]any) []any {
    var keyCount int
    for i := range dicts {
        keyCount += len(dicts[i])
    }

    values := make([]any, 0, keyCount)

    for _, dict := range dicts {
        for _, value := range dict {
            values = append(values, value)
        }
    }

    return values
}

// Pluck extracts values associated with a specified key from a list of dictionaries.
//
// Parameters:
//
//    key string - the key to pluck values for.
//    dicts ...map[string]any - one or more dictionaries.
//
// Returns:
//
//    []any - a list of values associated with the key from each dictionary.
//
// Example:
//
//    {{ $d1 := dict "key" "value1"}}
//    {{ $d2 := dict "key" "value2" }}
//    {{ pluck "key"    $d1 $d2 }} // Output: ["value1", "value2"]
func (mr *MapsRegistry) Pluck(key string, dicts ...map[string]any) []any {
    result := make([]any, 0, len(dicts))

    for _, dict := range dicts {
        if val, ok := dict[key]; ok {
            result = append(result, val)
        }
    }
    return result
}

// Pick creates a new dictionary containing only the specified keys from the
// original dictionary.
//
// Parameters:
//
//    keys ...string - the keys to include in the new dictionary.
//    dict map[string]any - the source dictionary.
//
// Returns:
//
//    map[string]any - a dictionary containing only the picked keys and their values.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ $d := dict "key1" "value1" "key2" "value2" "key3" "value3" }}
//    {{ $d | pick "key1" "key3" }} // Output: {"key1": "value1", "key3": "value3"}
func (mr *MapsRegistry) Pick(args ...any) (map[string]any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Pick(map[string]any, ...string)
    // * New signature: Pick(...string, map[string]any)
    if len(args) < 2 {
        return nil, deprecated.ErrArgsCount(2, len(args))
    }

    // Pre-allocate result map with the size of keys to avoid multiple allocations
    result := make(map[string]any, len(args)-1) // Remove the last argument which is the dictionary

    switch arg0 := args[0].(type) {
    case map[string]any:
        // Old signature
        deprecated.SignatureWarn(mr.handler.Logger(), "pick", "{{ pick dict key1 key2 }}", "{{ dict | pick key1 key2 }}")
        return mr.Pick(append(args[1:], args[0])...)
    case string:
        // New signature
        keys := args[:len(args)-1]
        dict, ok := args[len(args)-1].(map[string]any)
        if !ok {
            return nil, errors.New("last argument must be a map[string]any")
        }

        for _, key := range keys {
            keyStr, ok := key.(string)
            if !ok {
                return nil, errors.New("all keys must be strings")
            }
            if value, ok := dict[keyStr]; ok {
                result[keyStr] = value
            }
        }
        return result, nil
    default:
        return nil, fmt.Errorf("expected map or string, got %T", arg0)
    }
}

// Omit creates a new dictionary by excluding specified keys from the original dictionary.
//
// Parameters:
//
//    dict map[string]any - the source dictionary.
//    keys ...string - the keys to exclude from the new dictionary.
//
// Returns:
//
//    map[string]any - a dictionary without the omitted keys.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ $d := dict "key1" "value1" "key2" "value2" "key3" "value3" }}
//    {{ omit $d "key1" "key3" }} // Output: {"key2": "value2"}
func (mr *MapsRegistry) Omit(args ...any) (map[string]any, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: Omit(map[string]any, ...string)
    // * New signature: Omit(...string, map[string]any)
    if len(args) < 2 {
        return nil, deprecated.ErrArgsCount(2, len(args))
    }

    // Pre-allocate result map with the size of keys to avoid multiple allocations
    result := make(map[string]any, len(args)-1) // Remove the last argument which is the dictionary

    switch arg0 := args[0].(type) {
    case map[string]any:
        // Old signature
        deprecated.SignatureWarn(mr.handler.Logger(), "omit", "{{ omit dict key1 key2 }}", "{{ dict | omit key1 key2 }}")
        return mr.Omit(append(args[1:], args[0])...)
    case string:
        // New signature
        keys := args[:len(args)-1]
        dict, ok := args[len(args)-1].(map[string]any)
        if !ok {
            return nil, errors.New("last argument must be a map[string]any")
        }
        omit := make(map[string]struct{}, len(keys))
        for _, k := range keys {
            key, ok := k.(string)
            if !ok {
                return nil, errors.New("all keys must be strings")
            }
            omit[key] = struct{}{}
        }

        for key, value := range dict {
            if _, ok := omit[key]; !ok {
                result[key] = value
            }
        }
        return result, nil
    default:
        return nil, fmt.Errorf("expected map or string, got %T", arg0)
    }
}

// Dig navigates through a nested dictionary structure using a sequence of keys
// and returns the value found at the specified path.
//
// Parameters:
//
//    args ...any - a sequence of keys followed by a dictionary as the last argument.
//
// Returns:
//
//    any - the value found at the nested key path or nil if any key in the path is not found.
//    error - an error if there are fewer than three arguments, if the last argument is not a dictionary, or if any key is not a string.
//
// Example:
//
//    {{ dig "user", "profile", "name", {"user": {"profile": {"name": "John Doe"}}} }} // Output: "John Doe", nil
//    {{ dig "user.profile.age", {"user": {"profile": {"name": "John Doe"}}} }} // Output: nil, nil
func (mr *MapsRegistry) Dig(args ...any) (any, error) {
    if len(args) < 2 {
        return nil, errors.New("dig requires at least two arguments: a sequence of keys and a dictionary")
    }

    dict, ok := args[len(args)-1].(map[string]any)
    if !ok {
        return nil, errors.New("last argument must be a map[string]any")
    }

    keys, err := mr.parseKeys(args[:len(args)-1])
    if err != nil {
        return nil, fmt.Errorf("cannot parse keys: %w", err)
    }

    return mr.digIntoDict(dict, keys)
}

// HasKey checks if the specified key exists in the dictionary.
//
// Parameters:
//
//    key string - the key to look for.
//    dict map[string]any - the dictionary to check.
//
// Returns:
//
//    bool - true if the key exists, otherwise false.
//    error - protect against undesired behavior due to migration to new signature.
//
// Example:
//
//    {{ {"key": "value"} | hasKey "key" }} // Output: true
func (mr *MapsRegistry) HasKey(args ...any) (bool, error) {
    // ! BACKWARDS COMPATIBILITY: deprecated in v1.0 and removed in v1.1
    // ! Due to change in signature, this function still supports the old signature
    // ! to let users transition to the new signature.
    // * Old signature: HasKey(dict map[string]any, key string)
    // * New signature: HasKey(key string, map[string]any)
    if len(args) != 2 {
        return false, deprecated.ErrArgsCount(2, len(args))
    }

    switch arg0 := args[0].(type) {
    case map[string]any:
        // Old signature
        deprecated.SignatureWarn(mr.handler.Logger(), "hasKey", "{{ hasKey dict key }}", "{{ dict | hasKey key }}")
        return mr.HasKey(args[1].(string), arg0)
    case string:
        // New signature
        _, ok := args[1].(map[string]any)[arg0]
        return ok, nil
    default:
        return false, fmt.Errorf("expected map or string, got %T", arg0)
    }
}

// Merge merges multiple source maps into a destination map without
// overwriting existing keys in the destination.
// If an error occurs during merging, it returns nil and the error.
//
// Parameters:
//
//    dest map[string]any - the destination map to which all source map key-values are added.
//    srcs ...map[string]any - one or more source maps whose key-values are added to the destination.
//
// Returns:
//
//    any - the merged destination map.
//    error - error if the merge fails.
//
// Example:
//
//    {{ merge {}, {"a": 1, "b": 2}, {"b": 3, "c": 4}  }} // Output: {"a": 1, "b": 2, "c": 4}, nil
func (mr *MapsRegistry) Merge(dest map[string]any, srcs ...map[string]any) (any, error) {
    for _, src := range srcs {
        if err := mergo.Merge(&dest, src, mergo.WithoutDereference); err != nil {
            // This error is not expected to occur, as we ensure types are correct in
            // the function signature. If it does, it is a bug in the function implementation.
            return nil, err
        }
    }
    return dest, nil
}

// MergeOverwrite merges multiple source maps into a destination map,
// overwriting existing keys in the destination.
// If an error occurs during merging, it returns nil and the error.
//
// Parameters:
//
//    dest map[string]any - the destination map to which all source map key-values are added.
//    srcs ...map[string]any - one or more source maps whose key-values are added to the destination, potentially overwriting existing keys.
//
// Returns:
//
//    any - the merged destination map with overwritten values where applicable.
//    error - error if the merge fails.
//
// Example:
//
//    {{ mergeOverwrite {}, {"a": 1, "b": 2}, {"b": 3, "c": 4} }} // Output: {"a": 1, "b": 3, "c": 4}, nil
func (mr *MapsRegistry) MergeOverwrite(dest map[string]any, srcs ...map[string]any) (any, error) {
    for _, src := range srcs {
        if err := mergo.Merge(&dest, src, mergo.WithOverride, mergo.WithoutDereference); err != nil {
            // This error is not expected to occur, as we ensure types are correct in
            // the function signature. If it does, it is a bug in the function implementation.
            return nil, err
        }
    }
    return dest, nil
}