From 50c564d8a2c8a644cb418a522122935faa95506f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 21 Jul 2025 11:24:58 +0200 Subject: [PATCH] AI docs --- util/sprig/crypto.go | 17 +++ util/sprig/date.go | 112 +++++++++++++-- util/sprig/date_test.go | 3 + util/sprig/defaults.go | 139 +++++++++++++++++-- util/sprig/dict.go | 118 ++++++++++++++++ util/sprig/flow_control.go | 1 + util/sprig/functions.go | 1 + util/sprig/functions_test.go | 2 +- util/sprig/list.go | 138 +++++++++++------- util/sprig/list_test.go | 3 + util/sprig/numeric.go | 238 +++++++++++++++++++++++++++++++- util/sprig/reflect.go | 42 ++++++ util/sprig/regex.go | 134 ++++++++++++++++++ util/sprig/strings.go | 261 ++++++++++++++++++++++++++++++++++- util/sprig/url.go | 1 - 15 files changed, 1132 insertions(+), 78 deletions(-) diff --git a/util/sprig/crypto.go b/util/sprig/crypto.go index db8a6814..da4bfc94 100644 --- a/util/sprig/crypto.go +++ b/util/sprig/crypto.go @@ -9,21 +9,38 @@ import ( "hash/adler32" ) +// sha512sum computes the SHA-512 hash of the input string and returns it as a hex-encoded string. +// This function can be used in templates to generate secure hashes of sensitive data. +// +// Example usage in templates: {{ "hello world" | sha512sum }} func sha512sum(input string) string { hash := sha512.Sum512([]byte(input)) return hex.EncodeToString(hash[:]) } +// sha256sum computes the SHA-256 hash of the input string and returns it as a hex-encoded string. +// This is a commonly used cryptographic hash function that produces a 256-bit (32-byte) hash value. +// +// Example usage in templates: {{ "hello world" | sha256sum }} func sha256sum(input string) string { hash := sha256.Sum256([]byte(input)) return hex.EncodeToString(hash[:]) } +// sha1sum computes the SHA-1 hash of the input string and returns it as a hex-encoded string. +// Note: SHA-1 is no longer considered secure against well-funded attackers for cryptographic purposes. +// Consider using sha256sum or sha512sum for security-critical applications. +// +// Example usage in templates: {{ "hello world" | sha1sum }} func sha1sum(input string) string { hash := sha1.Sum([]byte(input)) return hex.EncodeToString(hash[:]) } +// adler32sum computes the Adler-32 checksum of the input string and returns it as a decimal string. +// This is a non-cryptographic hash function primarily used for error detection. +// +// Example usage in templates: {{ "hello world" | adler32sum }} func adler32sum(input string) string { hash := adler32.Checksum([]byte(input)) return fmt.Sprintf("%d", hash) diff --git a/util/sprig/date.go b/util/sprig/date.go index f01dcf0b..3231e619 100644 --- a/util/sprig/date.go +++ b/util/sprig/date.go @@ -1,27 +1,61 @@ package sprig import ( + "math" "strconv" "time" ) -// Given a format and a date, format the date string. +// date formats a date according to the provided format string. // -// Date can be a `time.Time` or an `int, int32, int64`. -// In the later case, it is treated as seconds since UNIX -// epoch. +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02 15:04:05") +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// +// If date is not one of the recognized types, the current time is used. +// +// Example usage in templates: {{ now | date "2006-01-02" }} func date(fmt string, date any) string { return dateInZone(fmt, date, "Local") } +// htmlDate formats a date in HTML5 date format (YYYY-MM-DD). +// +// Parameters: +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// +// If date is not one of the recognized types, the current time is used. +// +// Example usage in templates: {{ now | htmlDate }} func htmlDate(date any) string { return dateInZone("2006-01-02", date, "Local") } +// htmlDateInZone formats a date in HTML5 date format (YYYY-MM-DD) in the specified timezone. +// +// Parameters: +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// - zone: Timezone name (e.g., "UTC", "America/New_York") +// +// If date is not one of the recognized types, the current time is used. +// If the timezone is invalid, UTC is used. +// +// Example usage in templates: {{ now | htmlDateInZone "UTC" }} func htmlDateInZone(date any, zone string) string { return dateInZone("2006-01-02", date, zone) } +// dateInZone formats a date according to the provided format string in the specified timezone. +// +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02 15:04:05") +// - date: Can be a time.Time, *time.Time, or int/int32/int64 (seconds since UNIX epoch) +// - zone: Timezone name (e.g., "UTC", "America/New_York") +// +// If date is not one of the recognized types, the current time is used. +// If the timezone is invalid, UTC is used. +// +// Example usage in templates: {{ now | dateInZone "2006-01-02 15:04:05" "UTC" }} func dateInZone(fmt string, date any, zone string) string { var t time.Time switch date := date.(type) { @@ -45,6 +79,15 @@ func dateInZone(fmt string, date any, zone string) string { return t.In(loc).Format(fmt) } +// dateModify modifies a date by adding a duration and returns the resulting time. +// +// Parameters: +// - fmt: A duration string (e.g., "24h", "-12h30m", "1h15m30s") +// - date: The time.Time to modify +// +// If the duration string is invalid, the original date is returned. +// +// Example usage in templates: {{ now | dateModify "-24h" }} func dateModify(fmt string, date time.Time) time.Time { d, err := time.ParseDuration(fmt) if err != nil { @@ -53,6 +96,15 @@ func dateModify(fmt string, date time.Time) time.Time { return date.Add(d) } +// mustDateModify modifies a date by adding a duration and returns the resulting time or an error. +// +// Parameters: +// - fmt: A duration string (e.g., "24h", "-12h30m", "1h15m30s") +// - date: The time.Time to modify +// +// Unlike dateModify, this function returns an error if the duration string is invalid. +// +// Example usage in templates: {{ now | mustDateModify "24h" }} func mustDateModify(fmt string, date time.Time) (time.Time, error) { d, err := time.ParseDuration(fmt) if err != nil { @@ -61,6 +113,14 @@ func mustDateModify(fmt string, date time.Time) (time.Time, error) { return date.Add(d), nil } +// dateAgo returns a string representing the time elapsed since the given date. +// +// Parameters: +// - date: Can be a time.Time, int, or int64 (seconds since UNIX epoch) +// +// If date is not one of the recognized types, the current time is used. +// +// Example usage in templates: {{ "2023-01-01" | toDate "2006-01-02" | dateAgo }} func dateAgo(date any) string { var t time.Time switch date := date.(type) { @@ -76,6 +136,12 @@ func dateAgo(date any) string { return time.Since(t).Round(time.Second).String() } +// duration converts seconds to a duration string. +// +// Parameters: +// - sec: Can be a string (parsed as int64), or int64 representing seconds +// +// Example usage in templates: {{ 3600 | duration }} -> "1h0m0s" func duration(sec any) string { var n int64 switch value := sec.(type) { @@ -89,6 +155,15 @@ func duration(sec any) string { return (time.Duration(n) * time.Second).String() } +// durationRound formats a duration in a human-readable rounded format. +// +// Parameters: +// - duration: Can be a string (parsed as duration), int64 (nanoseconds), +// or time.Time (time since that moment) +// +// Returns a string with the largest appropriate unit (y, mo, d, h, m, s). +// +// Example usage in templates: {{ 3600 | duration | durationRound }} -> "1h" func durationRound(duration any) string { var d time.Duration switch duration := duration.(type) { @@ -101,10 +176,7 @@ func durationRound(duration any) string { case time.Time: d = time.Since(duration) } - var u uint64 - if d < 0 { - u = -u - } + u := uint64(math.Abs(float64(d))) var ( year = uint64(time.Hour) * 24 * 365 month = uint64(time.Hour) * 24 * 30 @@ -130,15 +202,39 @@ func durationRound(duration any) string { return "0s" } +// toDate parses a string into a time.Time using the specified format. +// +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02") +// - str: The date string to parse +// +// If parsing fails, returns a zero time.Time. +// +// Example usage in templates: {{ "2023-01-01" | toDate "2006-01-02" }} func toDate(fmt, str string) time.Time { t, _ := time.ParseInLocation(fmt, str, time.Local) return t } +// mustToDate parses a string into a time.Time using the specified format or returns an error. +// +// Parameters: +// - fmt: A Go time format string (e.g., "2006-01-02") +// - str: The date string to parse +// +// Unlike toDate, this function returns an error if parsing fails. +// +// Example usage in templates: {{ mustToDate "2006-01-02" "2023-01-01" }} func mustToDate(fmt, str string) (time.Time, error) { return time.ParseInLocation(fmt, str, time.Local) } +// unixEpoch returns the Unix timestamp (seconds since January 1, 1970 UTC) for the given time. +// +// Parameters: +// - date: A time.Time value +// +// Example usage in templates: {{ now | unixEpoch }} func unixEpoch(date time.Time) string { return strconv.FormatInt(date.Unix(), 10) } diff --git a/util/sprig/date_test.go b/util/sprig/date_test.go index 496822cf..ee9a9cc6 100644 --- a/util/sprig/date_test.go +++ b/util/sprig/date_test.go @@ -117,4 +117,7 @@ func TestDurationRound(t *testing.T) { if err := runtv(tpl, "3mo", map[string]any{"Time": "2400h5s"}); err != nil { t.Error(err) } + if err := runtv(tpl, "1m", map[string]any{"Time": "-1m1s"}); err != nil { + t.Error(err) + } } diff --git a/util/sprig/defaults.go b/util/sprig/defaults.go index 948747b9..c5c14308 100644 --- a/util/sprig/defaults.go +++ b/util/sprig/defaults.go @@ -25,6 +25,21 @@ func defaultValue(d any, given ...any) any { } // empty returns true if the given value has the zero value for its type. +// This is a helper function used by defaultValue, coalesce, all, and anyNonEmpty. +// +// The following values are considered empty: +// - Invalid values +// - nil values +// - Zero-length arrays, slices, maps, and strings +// - Boolean false +// - Zero for all numeric types +// - Structs are never considered empty +// +// Parameters: +// - given: The value to check for emptiness +// +// Returns: +// - bool: True if the value is considered empty, false otherwise func empty(given any) bool { g := reflect.ValueOf(given) if !g.IsValid() { @@ -51,7 +66,16 @@ func empty(given any) bool { } } -// coalesce returns the first non-empty value. +// coalesce returns the first non-empty value from a list of values. +// If all values are empty, it returns nil. +// +// This is useful for providing a series of fallback values. +// +// Parameters: +// - v: A variadic list of values to check +// +// Returns: +// - any: The first non-empty value, or nil if all values are empty func coalesce(v ...any) any { for _, val := range v { if !empty(val) { @@ -61,8 +85,15 @@ func coalesce(v ...any) any { return nil } -// all returns true if empty(x) is false for all values x in the list. -// If the list is empty, return true. +// all checks if all values in a list are non-empty. +// Returns true if every value in the list is non-empty. +// If the list is empty, returns true (vacuously true). +// +// Parameters: +// - v: A variadic list of values to check +// +// Returns: +// - bool: True if all values are non-empty, false otherwise func all(v ...any) bool { for _, val := range v { if empty(val) { @@ -72,8 +103,15 @@ func all(v ...any) bool { return true } -// anyNonEmpty returns true if empty(x) is false for anyNonEmpty x in the list. -// If the list is empty, return false. +// anyNonEmpty checks if at least one value in a list is non-empty. +// Returns true if any value in the list is non-empty. +// If the list is empty, returns false. +// +// Parameters: +// - v: A variadic list of values to check +// +// Returns: +// - bool: True if at least one value is non-empty, false otherwise func anyNonEmpty(v ...any) bool { for _, val := range v { if !empty(val) { @@ -83,25 +121,58 @@ func anyNonEmpty(v ...any) bool { return false } -// fromJSON decodes JSON into a structured value, ignoring errors. +// fromJSON decodes a JSON string into a structured value. +// This function ignores any errors that occur during decoding. +// If the JSON is invalid, it returns nil. +// +// Parameters: +// - v: The JSON string to decode +// +// Returns: +// - any: The decoded value, or nil if decoding failed func fromJSON(v string) any { output, _ := mustFromJSON(v) return output } -// mustFromJSON decodes JSON into a structured value, returning errors. +// mustFromJSON decodes a JSON string into a structured value. +// Unlike fromJSON, this function returns any errors that occur during decoding. +// +// Parameters: +// - v: The JSON string to decode +// +// Returns: +// - any: The decoded value +// - error: Any error that occurred during decoding func mustFromJSON(v string) (any, error) { var output any err := json.Unmarshal([]byte(v), &output) return output, err } -// toJSON encodes an item into a JSON string +// toJSON encodes a value into a JSON string. +// This function ignores any errors that occur during encoding. +// If the value cannot be encoded, it returns an empty string. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value func toJSON(v any) string { output, _ := json.Marshal(v) return string(output) } +// mustToJSON encodes a value into a JSON string. +// Unlike toJSON, this function returns any errors that occur during encoding. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value +// - error: Any error that occurred during encoding func mustToJSON(v any) (string, error) { output, err := json.Marshal(v) if err != nil { @@ -110,12 +181,29 @@ func mustToJSON(v any) (string, error) { return string(output), nil } -// toPrettyJSON encodes an item into a pretty (indented) JSON string +// toPrettyJSON encodes a value into a pretty (indented) JSON string. +// This function ignores any errors that occur during encoding. +// If the value cannot be encoded, it returns an empty string. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The indented JSON string representation of the value func toPrettyJSON(v any) string { output, _ := json.MarshalIndent(v, "", " ") return string(output) } +// mustToPrettyJSON encodes a value into a pretty (indented) JSON string. +// Unlike toPrettyJSON, this function returns any errors that occur during encoding. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The indented JSON string representation of the value +// - error: Any error that occurred during encoding func mustToPrettyJSON(v any) (string, error) { output, err := json.MarshalIndent(v, "", " ") if err != nil { @@ -124,7 +212,15 @@ func mustToPrettyJSON(v any) (string, error) { return string(output), nil } -// toRawJSON encodes an item into a JSON string with no escaping of HTML characters. +// toRawJSON encodes a value into a JSON string with no escaping of HTML characters. +// This function panics if an error occurs during encoding. +// Unlike toJSON, HTML characters like <, >, and & are not escaped. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value without HTML escaping func toRawJSON(v any) string { output, err := mustToRawJSON(v) if err != nil { @@ -133,7 +229,16 @@ func toRawJSON(v any) string { return output } -// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters. +// mustToRawJSON encodes a value into a JSON string with no escaping of HTML characters. +// Unlike toRawJSON, this function returns any errors that occur during encoding. +// HTML characters like <, >, and & are not escaped in the output. +// +// Parameters: +// - v: The value to encode to JSON +// +// Returns: +// - string: The JSON string representation of the value without HTML escaping +// - error: Any error that occurred during encoding func mustToRawJSON(v any) (string, error) { buf := new(bytes.Buffer) enc := json.NewEncoder(buf) @@ -144,7 +249,17 @@ func mustToRawJSON(v any) (string, error) { return strings.TrimSuffix(buf.String(), "\n"), nil } -// ternary returns the first value if the last value is true, otherwise returns the second value. +// ternary implements a conditional (ternary) operator. +// It returns the first value if the condition is true, otherwise returns the second value. +// This is similar to the ?: operator in many programming languages. +// +// Parameters: +// - vt: The value to return if the condition is true +// - vf: The value to return if the condition is false +// - v: The boolean condition to evaluate +// +// Returns: +// - any: Either vt or vf depending on the value of v func ternary(vt any, vf any, v bool) any { if v { return vt diff --git a/util/sprig/dict.go b/util/sprig/dict.go index 6485763e..0a282add 100644 --- a/util/sprig/dict.go +++ b/util/sprig/dict.go @@ -1,5 +1,15 @@ package sprig +// get retrieves a value from a map by its key. +// If the key exists, returns the corresponding value. +// If the key doesn't exist, returns an empty string. +// +// Parameters: +// - d: The map to retrieve the value from +// - key: The key to look up +// +// Returns: +// - any: The value associated with the key, or an empty string if not found func get(d map[string]any, key string) any { if val, ok := d[key]; ok { return val @@ -7,21 +17,58 @@ func get(d map[string]any, key string) any { return "" } +// set adds or updates a key-value pair in a map. +// Modifies the map in place and returns the modified map. +// +// Parameters: +// - d: The map to modify +// - key: The key to set +// - value: The value to associate with the key +// +// Returns: +// - map[string]any: The modified map (same instance as the input map) func set(d map[string]any, key string, value any) map[string]any { d[key] = value return d } +// unset removes a key-value pair from a map. +// If the key doesn't exist, the map remains unchanged. +// Modifies the map in place and returns the modified map. +// +// Parameters: +// - d: The map to modify +// - key: The key to remove +// +// Returns: +// - map[string]any: The modified map (same instance as the input map) func unset(d map[string]any, key string) map[string]any { delete(d, key) return d } +// hasKey checks if a key exists in a map. +// +// Parameters: +// - d: The map to check +// - key: The key to look for +// +// Returns: +// - bool: True if the key exists in the map, false otherwise func hasKey(d map[string]any, key string) bool { _, ok := d[key] return ok } +// pluck extracts values for a specific key from multiple maps. +// Only includes values from maps where the key exists. +// +// Parameters: +// - key: The key to extract values for +// - d: A variadic list of maps to extract values from +// +// Returns: +// - []any: A slice containing all values associated with the key across all maps func pluck(key string, d ...map[string]any) []any { var res []any for _, dict := range d { @@ -32,6 +79,14 @@ func pluck(key string, d ...map[string]any) []any { return res } +// keys collects all keys from one or more maps. +// The returned slice may contain duplicate keys if multiple maps contain the same key. +// +// Parameters: +// - dicts: A variadic list of maps to collect keys from +// +// Returns: +// - []string: A slice containing all keys from all provided maps func keys(dicts ...map[string]any) []string { var k []string for _, dict := range dicts { @@ -42,6 +97,15 @@ func keys(dicts ...map[string]any) []string { return k } +// pick creates a new map containing only the specified keys from the original map. +// If a key doesn't exist in the original map, it won't be included in the result. +// +// Parameters: +// - dict: The source map +// - keys: A variadic list of keys to include in the result +// +// Returns: +// - map[string]any: A new map containing only the specified keys and their values func pick(dict map[string]any, keys ...string) map[string]any { res := map[string]any{} for _, k := range keys { @@ -52,6 +116,15 @@ func pick(dict map[string]any, keys ...string) map[string]any { return res } +// omit creates a new map excluding the specified keys from the original map. +// The original map remains unchanged. +// +// Parameters: +// - dict: The source map +// - keys: A variadic list of keys to exclude from the result +// +// Returns: +// - map[string]any: A new map containing all key-value pairs except those specified func omit(dict map[string]any, keys ...string) map[string]any { res := map[string]any{} omit := make(map[string]bool, len(keys)) @@ -66,6 +139,16 @@ func omit(dict map[string]any, keys ...string) map[string]any { return res } +// dict creates a new map from a list of key-value pairs. +// The arguments are treated as key-value pairs, where even-indexed arguments are keys +// and odd-indexed arguments are values. +// If there's an odd number of arguments, the last key will be assigned an empty string value. +// +// Parameters: +// - v: A variadic list of alternating keys and values +// +// Returns: +// - map[string]any: A new map containing the specified key-value pairs func dict(v ...any) map[string]any { dict := map[string]any{} lenv := len(v) @@ -80,6 +163,14 @@ func dict(v ...any) map[string]any { return dict } +// values collects all values from a map into a slice. +// The order of values in the resulting slice is not guaranteed. +// +// Parameters: +// - dict: The map to collect values from +// +// Returns: +// - []any: A slice containing all values from the map func values(dict map[string]any) []any { var values []any for _, value := range dict { @@ -88,6 +179,22 @@ func values(dict map[string]any) []any { return values } +// dig safely accesses nested values in maps using a sequence of keys. +// If any key in the path doesn't exist, it returns the default value. +// The function expects at least 3 arguments: one or more keys, a default value, and a map. +// +// Parameters: +// - ps: A variadic list where: +// - The first N-2 arguments are string keys forming the path +// - The second-to-last argument is the default value to return if the path doesn't exist +// - The last argument is the map to traverse +// +// Returns: +// - any: The value found at the specified path, or the default value if not found +// - error: Any error that occurred during traversal +// +// Panics: +// - If fewer than 3 arguments are provided func dig(ps ...any) (any, error) { if len(ps) < 3 { panic("dig needs at least three arguments") @@ -102,6 +209,17 @@ func dig(ps ...any) (any, error) { return digFromDict(dict, def, ks) } +// digFromDict is a helper function for dig that recursively traverses a map using a sequence of keys. +// If any key in the path doesn't exist, it returns the default value. +// +// Parameters: +// - dict: The map to traverse +// - d: The default value to return if the path doesn't exist +// - ks: A slice of string keys forming the path to traverse +// +// Returns: +// - any: The value found at the specified path, or the default value if not found +// - error: Any error that occurred during traversal func digFromDict(dict map[string]any, d any, ks []string) (any, error) { k, ns := ks[0], ks[1:] step, has := dict[k] diff --git a/util/sprig/flow_control.go b/util/sprig/flow_control.go index 2bdf382c..cfaa5081 100644 --- a/util/sprig/flow_control.go +++ b/util/sprig/flow_control.go @@ -2,6 +2,7 @@ package sprig import "errors" +// fail is a function that always returns an error with the given message. func fail(msg string) (string, error) { return "", errors.New(msg) } diff --git a/util/sprig/functions.go b/util/sprig/functions.go index f0232a5b..8dbb23f8 100644 --- a/util/sprig/functions.go +++ b/util/sprig/functions.go @@ -12,6 +12,7 @@ import ( const ( loopExecutionLimit = 10_000 // Limit the number of loop executions to prevent execution from taking too long stringLengthLimit = 100_000 // Limit the length of strings to prevent memory issues + sliceSizeLimit = 10_000 // Limit the size of slices to prevent memory issues ) // TxtFuncMap produces the function map. diff --git a/util/sprig/functions_test.go b/util/sprig/functions_test.go index e5989b98..4e83e993 100644 --- a/util/sprig/functions_test.go +++ b/util/sprig/functions_test.go @@ -52,7 +52,7 @@ func runtv(tpl, expect string, vars any) error { return err } if expect != b.String() { - return fmt.Errorf("Expected '%s', got '%s'", expect, b.String()) + return fmt.Errorf("expected '%s', got '%s'", expect, b.String()) } return nil } diff --git a/util/sprig/list.go b/util/sprig/list.go index d8882af0..fdcbf5e6 100644 --- a/util/sprig/list.go +++ b/util/sprig/list.go @@ -11,10 +11,15 @@ import ( // ints, and other types not implementing []any can be worked with. // For example, this is useful if you need to work on the output of regexs. +// list creates a new list (slice) containing the provided arguments. +// It accepts any number of arguments of any type and returns them as a slice. func list(v ...any) []any { return v } +// push appends an element to the end of a list (slice or array). +// It takes a list and a value, and returns a new list with the value appended. +// This function will panic if the first argument is not a slice or array. func push(list any, v any) []any { l, err := mustPush(list, v) if err != nil { @@ -23,99 +28,103 @@ func push(list any, v any) []any { return l } +// mustPush is the implementation of push that returns an error instead of panicking. +// It converts the input list to a slice of any type, then appends the value. func mustPush(list any, v any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() nl := make([]any, l) for i := 0; i < l; i++ { nl[i] = l2.Index(i).Interface() } - return append(nl, v), nil - default: return nil, fmt.Errorf("cannot push on type %s", tp) } } +// prepend adds an element to the beginning of a list (slice or array). +// It takes a list and a value, and returns a new list with the value at the start. +// This function will panic if the first argument is not a slice or array. func prepend(list any, v any) []any { l, err := mustPrepend(list, v) if err != nil { panic(err) } - return l } +// mustPrepend is the implementation of prepend that returns an error instead of panicking. +// It converts the input list to a slice of any type, then prepends the value. func mustPrepend(list any, v any) ([]any, error) { - //return append([]any{v}, list...) - tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() nl := make([]any, l) for i := 0; i < l; i++ { nl[i] = l2.Index(i).Interface() } - return append([]any{v}, nl...), nil - default: return nil, fmt.Errorf("cannot prepend on type %s", tp) } } +// chunk divides a list into sub-lists of the specified size. +// It takes a size and a list, and returns a list of lists, each containing +// up to 'size' elements from the original list. +// This function will panic if the second argument is not a slice or array. func chunk(size int, list any) [][]any { l, err := mustChunk(size, list) if err != nil { panic(err) } - return l } +// mustChunk is the implementation of chunk that returns an error instead of panicking. +// It divides the input list into chunks of the specified size. func mustChunk(size int, list any) ([][]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() - - cs := int(math.Floor(float64(l-1)/float64(size)) + 1) - nl := make([][]any, cs) - - for i := 0; i < cs; i++ { + numChunks := int(math.Floor(float64(l-1)/float64(size)) + 1) + if numChunks > sliceSizeLimit { + return nil, fmt.Errorf("number of chunks %d exceeds maximum limit of %d", numChunks, sliceSizeLimit) + } + result := make([][]any, numChunks) + for i := 0; i < numChunks; i++ { clen := size - if i == cs-1 { + // Handle the last chunk which might be smaller + if i == numChunks-1 { clen = int(math.Floor(math.Mod(float64(l), float64(size)))) if clen == 0 { clen = size } } - - nl[i] = make([]any, clen) - + result[i] = make([]any, clen) for j := 0; j < clen; j++ { ix := i*size + j - nl[i][j] = l2.Index(ix).Interface() + result[i][j] = l2.Index(ix).Interface() } } - - return nl, nil + return result, nil default: return nil, fmt.Errorf("cannot chunk type %s", tp) } } +// last returns the last element of a list (slice or array). +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func last(list any) any { l, err := mustLast(list) if err != nil { @@ -125,6 +134,8 @@ func last(list any) any { return l } +// mustLast is the implementation of last that returns an error instead of panicking. +// It returns the last element of the list or nil if the list is empty. func mustLast(list any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { @@ -142,6 +153,9 @@ func mustLast(list any) (any, error) { } } +// first returns the first element of a list (slice or array). +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func first(list any) any { l, err := mustFirst(list) if err != nil { @@ -151,6 +165,8 @@ func first(list any) any { return l } +// mustFirst is the implementation of first that returns an error instead of panicking. +// It returns the first element of the list or nil if the list is empty. func mustFirst(list any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { @@ -168,6 +184,9 @@ func mustFirst(list any) (any, error) { } } +// rest returns all elements of a list except the first one. +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func rest(list any) []any { l, err := mustRest(list) if err != nil { @@ -177,28 +196,30 @@ func rest(list any) []any { return l } +// mustRest is the implementation of rest that returns an error instead of panicking. +// It returns all elements of the list except the first one, or nil if the list is empty. func mustRest(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() if l == 0 { return nil, nil } - nl := make([]any, l-1) for i := 1; i < l; i++ { nl[i-1] = l2.Index(i).Interface() } - return nl, nil default: return nil, fmt.Errorf("cannot find rest on type %s", tp) } } +// initial returns all elements of a list except the last one. +// If the list is empty, it returns nil. +// This function will panic if the argument is not a slice or array. func initial(list any) []any { l, err := mustInitial(list) if err != nil { @@ -208,28 +229,30 @@ func initial(list any) []any { return l } +// mustInitial is the implementation of initial that returns an error instead of panicking. +// It returns all elements of the list except the last one, or nil if the list is empty. func mustInitial(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() if l == 0 { return nil, nil } - nl := make([]any, l-1) for i := 0; i < l-1; i++ { nl[i] = l2.Index(i).Interface() } - return nl, nil default: return nil, fmt.Errorf("cannot find initial on type %s", tp) } } +// sortAlpha sorts a list of strings alphabetically. +// If the input is not a slice or array, it returns a single-element slice +// containing the string representation of the input. func sortAlpha(list any) []string { k := reflect.Indirect(reflect.ValueOf(list)).Kind() switch k { @@ -242,6 +265,8 @@ func sortAlpha(list any) []string { return []string{strval(list)} } +// reverse returns a new list with the elements in reverse order. +// This function will panic if the argument is not a slice or array. func reverse(v any) []any { l, err := mustReverse(v) if err != nil { @@ -251,42 +276,45 @@ func reverse(v any) []any { return l } +// mustReverse is the implementation of reverse that returns an error instead of panicking. +// It returns a new list with the elements in reverse order. func mustReverse(v any) ([]any, error) { tp := reflect.TypeOf(v).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(v) - l := l2.Len() // We do not sort in place because the incoming array should not be altered. nl := make([]any, l) for i := 0; i < l; i++ { nl[l-i-1] = l2.Index(i).Interface() } - return nl, nil default: return nil, fmt.Errorf("cannot find reverse on type %s", tp) } } +// compact returns a new list with all "empty" elements removed. +// An element is considered empty if it's nil, zero, an empty string, or an empty collection. +// This function will panic if the argument is not a slice or array. func compact(list any) []any { l, err := mustCompact(list) if err != nil { panic(err) } - return l } +// mustCompact is the implementation of compact that returns an error instead of panicking. +// It returns a new list with all "empty" elements removed. func mustCompact(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() - nl := []any{} + var nl []any var item any for i := 0; i < l; i++ { item = l2.Index(i).Interface() @@ -294,30 +322,32 @@ func mustCompact(list any) ([]any, error) { nl = append(nl, item) } } - return nl, nil default: return nil, fmt.Errorf("cannot compact on type %s", tp) } } +// uniq returns a new list with duplicate elements removed. +// The first occurrence of each element is kept. +// This function will panic if the argument is not a slice or array. func uniq(list any) []any { l, err := mustUniq(list) if err != nil { panic(err) } - return l } +// mustUniq is the implementation of uniq that returns an error instead of panicking. +// It returns a new list with duplicate elements removed. func mustUniq(list any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() - dest := []any{} + var dest []any var item any for i := 0; i < l; i++ { item = l2.Index(i).Interface() @@ -325,13 +355,15 @@ func mustUniq(list any) ([]any, error) { dest = append(dest, item) } } - return dest, nil default: return nil, fmt.Errorf("cannot find uniq on type %s", tp) } } +// inList checks if a value is present in a list. +// It uses deep equality comparison to check for matches. +// Returns true if the value is found, false otherwise. func inList(haystack []any, needle any) bool { for _, h := range haystack { if reflect.DeepEqual(needle, h) { @@ -341,21 +373,23 @@ func inList(haystack []any, needle any) bool { return false } +// without returns a new list with all occurrences of the specified values removed. +// This function will panic if the first argument is not a slice or array. func without(list any, omit ...any) []any { l, err := mustWithout(list, omit...) if err != nil { panic(err) } - return l } +// mustWithout is the implementation of without that returns an error instead of panicking. +// It returns a new list with all occurrences of the specified values removed. func mustWithout(list any, omit ...any) ([]any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() res := []any{} var item any @@ -365,22 +399,25 @@ func mustWithout(list any, omit ...any) ([]any, error) { res = append(res, item) } } - return res, nil default: return nil, fmt.Errorf("cannot find without on type %s", tp) } } +// has checks if a value is present in a list. +// Returns true if the value is found, false otherwise. +// This function will panic if the second argument is not a slice or array. func has(needle any, haystack any) bool { l, err := mustHas(needle, haystack) if err != nil { panic(err) } - return l } +// mustHas is the implementation of has that returns an error instead of panicking. +// It checks if a value is present in a list. func mustHas(needle any, haystack any) (bool, error) { if haystack == nil { return false, nil @@ -397,38 +434,41 @@ func mustHas(needle any, haystack any) (bool, error) { return true, nil } } - return false, nil default: return false, fmt.Errorf("cannot find has on type %s", tp) } } +// slice extracts a portion of a list based on the provided indices. +// Usage examples: // $list := [1, 2, 3, 4, 5] // slice $list -> list[0:5] = list[:] // slice $list 0 3 -> list[0:3] = list[:3] // slice $list 3 5 -> list[3:5] // slice $list 3 -> list[3:5] = list[3:] +// +// This function will panic if the first argument is not a slice or array. func slice(list any, indices ...any) any { l, err := mustSlice(list, indices...) if err != nil { panic(err) } - return l } +// mustSlice is the implementation of slice that returns an error instead of panicking. +// It extracts a portion of a list based on the provided indices. func mustSlice(list any, indices ...any) (any, error) { tp := reflect.TypeOf(list).Kind() switch tp { case reflect.Slice, reflect.Array: l2 := reflect.ValueOf(list) - l := l2.Len() if l == 0 { return nil, nil } - + // Determine start and end indices var start, end int if len(indices) > 0 { start = toInt(indices[0]) @@ -438,13 +478,15 @@ func mustSlice(list any, indices ...any) (any, error) { } else { end = toInt(indices[1]) } - return l2.Slice(start, end).Interface(), nil default: return nil, fmt.Errorf("list should be type of slice or array but %s", tp) } } +// concat combines multiple lists into a single list. +// It takes any number of lists and returns a new list containing all elements. +// This function will panic if any argument is not a slice or array. func concat(lists ...any) any { var res []any for _, list := range lists { diff --git a/util/sprig/list_test.go b/util/sprig/list_test.go index ec4c4c14..e6693b2f 100644 --- a/util/sprig/list_test.go +++ b/util/sprig/list_test.go @@ -1,6 +1,7 @@ package sprig import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -68,6 +69,8 @@ func TestMustChunk(t *testing.T) { for tpl, expect := range tests { assert.NoError(t, runt(tpl, expect)) } + err := runt(`{{ tuple `+strings.Repeat(" 0", 10001)+` | mustChunk 1 }}`, "a") + assert.ErrorContains(t, err, "number of chunks 10001 exceeds maximum limit of 10000") } func TestPrepend(t *testing.T) { diff --git a/util/sprig/numeric.go b/util/sprig/numeric.go index 32466818..7ee3616d 100644 --- a/util/sprig/numeric.go +++ b/util/sprig/numeric.go @@ -9,7 +9,20 @@ import ( "strings" ) -// toFloat64 converts 64-bit floats +// toFloat64 converts a value to a 64-bit float. +// It handles various input types: +// - string: parsed as a float, returns 0 if parsing fails +// - integer types: converted to float64 +// - unsigned integer types: converted to float64 +// - float types: returned as is +// - bool: true becomes 1.0, false becomes 0.0 +// - other types: returns 0.0 +// +// Parameters: +// - v: The value to convert to float64 +// +// Returns: +// - float64: The converted value func toFloat64(v any) float64 { if str, ok := v.(string); ok { iv, err := strconv.ParseFloat(str, 64) @@ -39,12 +52,27 @@ func toFloat64(v any) float64 { } } +// toInt converts a value to a 32-bit integer. +// This is a wrapper around toInt64 that casts the result to int. +// +// Parameters: +// - v: The value to convert to int +// +// Returns: +// - int: The converted value func toInt(v any) int { // It's not optimal. But I don't want duplicate toInt64 code. return int(toInt64(v)) } -// toInt64 converts integer types to 64-bit integers +// toInt64 converts a value to a 64-bit integer. +// It handles various input types: +// - string: parsed as an integer, returns 0 if parsing fails +// - integer types: converted to int64 +// - unsigned integer types: converted to int64 (values > MaxInt64 become MaxInt64) +// - float types: truncated to int64 +// - bool: true becomes 1, false becomes 0 +// - other types: returns 0 func toInt64(v any) int64 { if str, ok := v.(string); ok { iv, err := strconv.ParseInt(str, 10, 64) @@ -53,7 +81,6 @@ func toInt64(v any) int64 { } return iv } - val := reflect.Indirect(reflect.ValueOf(v)) switch val.Kind() { case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: @@ -79,10 +106,26 @@ func toInt64(v any) int64 { } } +// add1 increments a value by 1. +// The input is first converted to int64 using toInt64. +// +// Parameters: +// - i: The value to increment +// +// Returns: +// - int64: The incremented value func add1(i any) int64 { return toInt64(i) + 1 } +// add sums all the provided values. +// All inputs are converted to int64 using toInt64 before addition. +// +// Parameters: +// - i: A variadic list of values to sum +// +// Returns: +// - int64: The sum of all values func add(i ...any) int64 { var a int64 for _, b := range i { @@ -91,18 +134,61 @@ func add(i ...any) int64 { return a } +// sub subtracts the second value from the first. +// Both inputs are converted to int64 using toInt64 before subtraction. +// +// Parameters: +// - a: The value to subtract from +// - b: The value to subtract +// +// Returns: +// - int64: The result of a - b func sub(a, b any) int64 { return toInt64(a) - toInt64(b) } +// div divides the first value by the second. +// Both inputs are converted to int64 using toInt64 before division. +// Note: This performs integer division, so the result is truncated. +// +// Parameters: +// - a: The dividend +// - b: The divisor +// +// Returns: +// - int64: The result of a / b +// +// Panics: +// - If b evaluates to 0 (division by zero) func div(a, b any) int64 { return toInt64(a) / toInt64(b) } +// mod returns the remainder of dividing the first value by the second. +// Both inputs are converted to int64 using toInt64 before the modulo operation. +// +// Parameters: +// - a: The dividend +// - b: The divisor +// +// Returns: +// - int64: The remainder of a / b +// +// Panics: +// - If b evaluates to 0 (modulo by zero) func mod(a, b any) int64 { return toInt64(a) % toInt64(b) } +// mul multiplies all the provided values. +// All inputs are converted to int64 using toInt64 before multiplication. +// +// Parameters: +// - a: The first value to multiply +// - v: Additional values to multiply with a +// +// Returns: +// - int64: The product of all values func mul(a any, v ...any) int64 { val := toInt64(a) for _, b := range v { @@ -111,10 +197,30 @@ func mul(a any, v ...any) int64 { return val } +// randInt generates a random integer between min (inclusive) and max (exclusive). +// +// Parameters: +// - min: The lower bound (inclusive) +// - max: The upper bound (exclusive) +// +// Returns: +// - int: A random integer in the range [min, max) +// +// Panics: +// - If max <= min (via rand.Intn) func randInt(min, max int) int { return rand.Intn(max-min) + min } +// maxAsInt64 returns the maximum value from a list of values as an int64. +// All inputs are converted to int64 using toInt64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - int64: The maximum value from all inputs func maxAsInt64(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { @@ -126,6 +232,15 @@ func maxAsInt64(a any, i ...any) int64 { return aa } +// maxAsFloat64 returns the maximum value from a list of values as a float64. +// All inputs are converted to float64 using toFloat64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - float64: The maximum value from all inputs func maxAsFloat64(a any, i ...any) float64 { m := toFloat64(a) for _, b := range i { @@ -134,6 +249,15 @@ func maxAsFloat64(a any, i ...any) float64 { return m } +// minAsInt64 returns the minimum value from a list of values as an int64. +// All inputs are converted to int64 using toInt64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - int64: The minimum value from all inputs func minAsInt64(a any, i ...any) int64 { aa := toInt64(a) for _, b := range i { @@ -145,6 +269,15 @@ func minAsInt64(a any, i ...any) int64 { return aa } +// minAsFloat64 returns the minimum value from a list of values as a float64. +// All inputs are converted to float64 using toFloat64 before comparison. +// +// Parameters: +// - a: The first value to compare +// - i: Additional values to compare +// +// Returns: +// - float64: The minimum value from all inputs func minAsFloat64(a any, i ...any) float64 { m := toFloat64(a) for _, b := range i { @@ -153,6 +286,14 @@ func minAsFloat64(a any, i ...any) float64 { return m } +// until generates a sequence of integers from 0 to count (exclusive). +// If count is negative, it generates a sequence from 0 to count (inclusive) with step -1. +// +// Parameters: +// - count: The end value (exclusive if positive, inclusive if negative) +// +// Returns: +// - []int: A slice containing the generated sequence func until(count int) []int { step := 1 if count < 0 { @@ -161,6 +302,23 @@ func until(count int) []int { return untilStep(0, count, step) } +// untilStep generates a sequence of integers from start to stop with the specified step. +// The sequence is generated as follows: +// - If step is 0, returns an empty slice +// - If stop < start and step < 0, generates a decreasing sequence from start to stop (exclusive) +// - If stop > start and step > 0, generates an increasing sequence from start to stop (exclusive) +// - Otherwise, returns an empty slice +// +// Parameters: +// - start: The starting value (inclusive) +// - stop: The ending value (exclusive) +// - step: The increment between values +// +// Returns: +// - []int: A slice containing the generated sequence +// +// Panics: +// - If the number of iterations would exceed loopExecutionLimit func untilStep(start, stop, step int) []int { var v []int if step == 0 { @@ -188,14 +346,44 @@ func untilStep(start, stop, step int) []int { return v } +// floor returns the greatest integer value less than or equal to the input. +// The input is first converted to float64 using toFloat64. +// +// Parameters: +// - a: The value to floor +// +// Returns: +// - float64: The greatest integer value less than or equal to a func floor(a any) float64 { return math.Floor(toFloat64(a)) } +// ceil returns the least integer value greater than or equal to the input. +// The input is first converted to float64 using toFloat64. +// +// Parameters: +// - a: The value to ceil +// +// Returns: +// - float64: The least integer value greater than or equal to a func ceil(a any) float64 { return math.Ceil(toFloat64(a)) } +// round rounds a number to a specified number of decimal places. +// The input is first converted to float64 using toFloat64. +// +// Parameters: +// - a: The value to round +// - p: The number of decimal places to round to +// - rOpt: Optional rounding threshold (default is 0.5) +// +// Returns: +// - float64: The rounded value +// +// Examples: +// - round(3.14159, 2) returns 3.14 +// - round(3.14159, 2, 0.6) returns 3.14 (only rounds up if fraction ≥ 0.6) func round(a any, p int, rOpt ...float64) float64 { roundOn := .5 if len(rOpt) > 0 { @@ -203,7 +391,6 @@ func round(a any, p int, rOpt ...float64) float64 { } val := toFloat64(a) places := toFloat64(p) - var round float64 pow := math.Pow(10, places) digit := pow * val @@ -216,7 +403,15 @@ func round(a any, p int, rOpt ...float64) float64 { return round / pow } -// converts unix octal to decimal +// toDecimal converts a value from octal to decimal. +// The input is first converted to a string using fmt.Sprint, then parsed as an octal number. +// If the parsing fails, it returns 0. +// +// Parameters: +// - v: The octal value to convert +// +// Returns: +// - int64: The decimal representation of the octal value func toDecimal(v any) int64 { result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) if err != nil { @@ -225,11 +420,34 @@ func toDecimal(v any) int64 { return result } +// atoi converts a string to an integer. +// If the conversion fails, it returns 0. +// +// Parameters: +// - a: The string to convert +// +// Returns: +// - int: The integer value of the string func atoi(a string) int { i, _ := strconv.Atoi(a) return i } +// seq generates a sequence of integers and returns them as a space-delimited string. +// The behavior depends on the number of parameters: +// - 0 params: Returns an empty string +// - 1 param: Generates sequence from 1 to param[0] +// - 2 params: Generates sequence from param[0] to param[1] +// - 3 params: Generates sequence from param[0] to param[2] with step param[1] +// +// If the end is less than the start, the sequence will be decreasing unless +// a positive step is explicitly provided (which would result in an empty string). +// +// Parameters: +// - params: Variable number of integers defining the sequence +// +// Returns: +// - string: A space-delimited string of the generated sequence func seq(params ...int) string { increment := 1 switch len(params) { @@ -266,6 +484,16 @@ func seq(params ...int) string { } } +// intArrayToString converts a slice of integers to a space-delimited string. +// The function removes the square brackets that would normally appear when +// converting a slice to a string. +// +// Parameters: +// - slice: The slice of integers to convert +// - delimiter: The delimiter to use between elements +// +// Returns: +// - string: A delimited string representation of the integer slice func intArrayToString(slice []int, delimiter string) string { return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimiter), "[]") } diff --git a/util/sprig/reflect.go b/util/sprig/reflect.go index 5e37f64f..6315a780 100644 --- a/util/sprig/reflect.go +++ b/util/sprig/reflect.go @@ -6,23 +6,65 @@ import ( ) // typeIs returns true if the src is the type named in target. +// It compares the type name of src with the target string. +// +// Parameters: +// - target: The type name to check against +// - src: The value whose type will be checked +// +// Returns: +// - bool: True if the type name of src matches target, false otherwise func typeIs(target string, src any) bool { return target == typeOf(src) } +// typeIsLike returns true if the src is the type named in target or a pointer to that type. +// This is useful when you need to check for both a type and a pointer to that type. +// +// Parameters: +// - target: The type name to check against +// - src: The value whose type will be checked +// +// Returns: +// - bool: True if the type of src matches target or "*"+target, false otherwise func typeIsLike(target string, src any) bool { t := typeOf(src) return target == t || "*"+target == t } +// typeOf returns the type of a value as a string. +// It uses fmt.Sprintf with the %T format verb to get the type name. +// +// Parameters: +// - src: The value whose type name will be returned +// +// Returns: +// - string: The type name of src func typeOf(src any) string { return fmt.Sprintf("%T", src) } +// kindIs returns true if the kind of src matches the target kind. +// This checks the underlying kind (e.g., "string", "int", "map") rather than the specific type. +// +// Parameters: +// - target: The kind name to check against +// - src: The value whose kind will be checked +// +// Returns: +// - bool: True if the kind of src matches target, false otherwise func kindIs(target string, src any) bool { return target == kindOf(src) } +// kindOf returns the kind of a value as a string. +// The kind represents the specific Go type category (e.g., "string", "int", "map", "slice"). +// +// Parameters: +// - src: The value whose kind will be returned +// +// Returns: +// - string: The kind of src as a string func kindOf(src any) string { return reflect.ValueOf(src).Kind().String() } diff --git a/util/sprig/regex.go b/util/sprig/regex.go index fab55101..9853d2e1 100644 --- a/util/sprig/regex.go +++ b/util/sprig/regex.go @@ -4,20 +4,60 @@ import ( "regexp" ) +// regexMatch checks if a string matches a regular expression pattern. +// It ignores any errors that might occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to match against +// - s: The string to check +// +// Returns: +// - bool: True if the string matches the pattern, false otherwise func regexMatch(regex string, s string) bool { match, _ := regexp.MatchString(regex, s) return match } +// mustRegexMatch checks if a string matches a regular expression pattern. +// Unlike regexMatch, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to match against +// - s: The string to check +// +// Returns: +// - bool: True if the string matches the pattern, false otherwise +// - error: Any error that occurred during regex compilation func mustRegexMatch(regex string, s string) (bool, error) { return regexp.MatchString(regex, s) } +// regexFindAll finds all matches of a regular expression in a string. +// It panics if the regex pattern cannot be compiled. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - n: The maximum number of matches to return (negative means all matches) +// +// Returns: +// - []string: A slice containing all matched substrings func regexFindAll(regex string, s string, n int) []string { r := regexp.MustCompile(regex) return r.FindAllString(s, n) } +// mustRegexFindAll finds all matches of a regular expression in a string. +// Unlike regexFindAll, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - n: The maximum number of matches to return (negative means all matches) +// +// Returns: +// - []string: A slice containing all matched substrings +// - error: Any error that occurred during regex compilation func mustRegexFindAll(regex string, s string, n int) ([]string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -26,11 +66,30 @@ func mustRegexFindAll(regex string, s string, n int) ([]string, error) { return r.FindAllString(s, n), nil } +// regexFind finds the first match of a regular expression in a string. +// It panics if the regex pattern cannot be compiled. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// +// Returns: +// - string: The first matched substring, or an empty string if no match func regexFind(regex string, s string) string { r := regexp.MustCompile(regex) return r.FindString(s) } +// mustRegexFind finds the first match of a regular expression in a string. +// Unlike regexFind, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// +// Returns: +// - string: The first matched substring, or an empty string if no match +// - error: Any error that occurred during regex compilation func mustRegexFind(regex string, s string) (string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -39,11 +98,34 @@ func mustRegexFind(regex string, s string) (string, error) { return r.FindString(s), nil } +// regexReplaceAll replaces all matches of a regular expression with a replacement string. +// It panics if the regex pattern cannot be compiled. +// The replacement string can contain $1, $2, etc. for submatches. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The replacement string (can contain $1, $2, etc. for submatches) +// +// Returns: +// - string: The resulting string after all replacements func regexReplaceAll(regex string, s string, repl string) string { r := regexp.MustCompile(regex) return r.ReplaceAllString(s, repl) } +// mustRegexReplaceAll replaces all matches of a regular expression with a replacement string. +// Unlike regexReplaceAll, this function returns any errors that occur during regex compilation. +// The replacement string can contain $1, $2, etc. for submatches. +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The replacement string (can contain $1, $2, etc. for submatches) +// +// Returns: +// - string: The resulting string after all replacements +// - error: Any error that occurred during regex compilation func mustRegexReplaceAll(regex string, s string, repl string) (string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -52,11 +134,34 @@ func mustRegexReplaceAll(regex string, s string, repl string) (string, error) { return r.ReplaceAllString(s, repl), nil } +// regexReplaceAllLiteral replaces all matches of a regular expression with a literal replacement string. +// It panics if the regex pattern cannot be compiled. +// Unlike regexReplaceAll, the replacement string is used literally (no $1, $2 processing). +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The literal replacement string +// +// Returns: +// - string: The resulting string after all replacements func regexReplaceAllLiteral(regex string, s string, repl string) string { r := regexp.MustCompile(regex) return r.ReplaceAllLiteralString(s, repl) } +// mustRegexReplaceAllLiteral replaces all matches of a regular expression with a literal replacement string. +// Unlike regexReplaceAllLiteral, this function returns any errors that occur during regex compilation. +// The replacement string is used literally (no $1, $2 processing). +// +// Parameters: +// - regex: The regular expression pattern to search for +// - s: The string to search within +// - repl: The literal replacement string +// +// Returns: +// - string: The resulting string after all replacements +// - error: Any error that occurred during regex compilation func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -65,11 +170,32 @@ func mustRegexReplaceAllLiteral(regex string, s string, repl string) (string, er return r.ReplaceAllLiteralString(s, repl), nil } +// regexSplit splits a string by a regular expression pattern. +// It panics if the regex pattern cannot be compiled. +// +// Parameters: +// - regex: The regular expression pattern to split on +// - s: The string to split +// - n: The maximum number of substrings to return (negative means all substrings) +// +// Returns: +// - []string: A slice containing the substrings between regex matches func regexSplit(regex string, s string, n int) []string { r := regexp.MustCompile(regex) return r.Split(s, n) } +// mustRegexSplit splits a string by a regular expression pattern. +// Unlike regexSplit, this function returns any errors that occur during regex compilation. +// +// Parameters: +// - regex: The regular expression pattern to split on +// - s: The string to split +// - n: The maximum number of substrings to return (negative means all substrings) +// +// Returns: +// - []string: A slice containing the substrings between regex matches +// - error: Any error that occurred during regex compilation func mustRegexSplit(regex string, s string, n int) ([]string, error) { r, err := regexp.Compile(regex) if err != nil { @@ -78,6 +204,14 @@ func mustRegexSplit(regex string, s string, n int) ([]string, error) { return r.Split(s, n), nil } +// regexQuoteMeta escapes all regular expression metacharacters in a string. +// This is useful when you want to use a string as a literal in a regular expression. +// +// Parameters: +// - s: The string to escape +// +// Returns: +// - string: The escaped string with all regex metacharacters quoted func regexQuoteMeta(s string) string { return regexp.QuoteMeta(s) } diff --git a/util/sprig/strings.go b/util/sprig/strings.go index 8a1bdc1b..e64f82d9 100644 --- a/util/sprig/strings.go +++ b/util/sprig/strings.go @@ -11,10 +11,25 @@ import ( "strings" ) +// base64encode encodes a string to base64 using standard encoding. +// +// Parameters: +// - v: The string to encode +// +// Returns: +// - string: The base64 encoded string func base64encode(v string) string { return base64.StdEncoding.EncodeToString([]byte(v)) } +// base64decode decodes a base64 encoded string. +// If the input is not valid base64, it returns the error message as a string. +// +// Parameters: +// - v: The base64 encoded string to decode +// +// Returns: +// - string: The decoded string, or an error message if decoding fails func base64decode(v string) string { data, err := base64.StdEncoding.DecodeString(v) if err != nil { @@ -23,10 +38,25 @@ func base64decode(v string) string { return string(data) } +// base32encode encodes a string to base32 using standard encoding. +// +// Parameters: +// - v: The string to encode +// +// Returns: +// - string: The base32 encoded string func base32encode(v string) string { return base32.StdEncoding.EncodeToString([]byte(v)) } +// base32decode decodes a base32 encoded string. +// If the input is not valid base32, it returns the error message as a string. +// +// Parameters: +// - v: The base32 encoded string to decode +// +// Returns: +// - string: The decoded string, or an error message if decoding fails func base32decode(v string) string { data, err := base32.StdEncoding.DecodeString(v) if err != nil { @@ -35,6 +65,14 @@ func base32decode(v string) string { return string(data) } +// quote adds double quotes around each non-nil string in the input and joins them with spaces. +// This uses Go's %q formatter which handles escaping special characters. +// +// Parameters: +// - str: A variadic list of values to quote +// +// Returns: +// - string: The quoted strings joined with spaces func quote(str ...any) string { out := make([]string, 0, len(str)) for _, s := range str { @@ -45,6 +83,14 @@ func quote(str ...any) string { return strings.Join(out, " ") } +// squote adds single quotes around each non-nil value in the input and joins them with spaces. +// Unlike quote, this doesn't escape special characters. +// +// Parameters: +// - str: A variadic list of values to quote +// +// Returns: +// - string: The single-quoted values joined with spaces func squote(str ...any) string { out := make([]string, 0, len(str)) for _, s := range str { @@ -55,25 +101,69 @@ func squote(str ...any) string { return strings.Join(out, " ") } +// cat concatenates all non-nil values into a single string. +// Nil values are removed before concatenation. +// +// Parameters: +// - v: A variadic list of values to concatenate +// +// Returns: +// - string: The concatenated string func cat(v ...any) string { v = removeNilElements(v) r := strings.TrimSpace(strings.Repeat("%v ", len(v))) return fmt.Sprintf(r, v...) } +// indent adds a specified number of spaces at the beginning of each line in a string. +// +// Parameters: +// - spaces: The number of spaces to add +// - v: The string to indent +// +// Returns: +// - string: The indented string func indent(spaces int, v string) string { pad := strings.Repeat(" ", spaces) return pad + strings.Replace(v, "\n", "\n"+pad, -1) } +// nindent adds a newline followed by an indented string. +// It's a shorthand for "\n" + indent(spaces, v). +// +// Parameters: +// - spaces: The number of spaces to add +// - v: The string to indent +// +// Returns: +// - string: A newline followed by the indented string func nindent(spaces int, v string) string { return "\n" + indent(spaces, v) } +// replace replaces all occurrences of a substring with another substring. +// +// Parameters: +// - old: The substring to replace +// - new: The replacement substring +// - src: The source string +// +// Returns: +// - string: The resulting string after all replacements func replace(old, new, src string) string { return strings.Replace(src, old, new, -1) } +// plural returns the singular or plural form of a word based on the count. +// If count is 1, it returns the singular form, otherwise it returns the plural form. +// +// Parameters: +// - one: The singular form of the word +// - many: The plural form of the word +// - count: The count to determine which form to use +// +// Returns: +// - string: Either the singular or plural form based on the count func plural(one, many string, count int) string { if count == 1 { return one @@ -81,6 +171,19 @@ func plural(one, many string, count int) string { return many } +// strslice converts a value to a slice of strings. +// It handles various input types: +// - []string: returned as is +// - []any: converted to []string, skipping nil values +// - arrays and slices: converted to []string, skipping nil values +// - nil: returns an empty slice +// - anything else: returns a single-element slice with the string representation +// +// Parameters: +// - v: The value to convert to a string slice +// +// Returns: +// - []string: A slice of strings func strslice(v any) []string { switch v := v.(type) { case []string: @@ -116,6 +219,14 @@ func strslice(v any) []string { } } +// removeNilElements creates a new slice with all nil elements removed. +// This is a helper function used by other functions like cat. +// +// Parameters: +// - v: The slice to process +// +// Returns: +// - []any: A new slice with all nil elements removed func removeNilElements(v []any) []any { newSlice := make([]any, 0, len(v)) for _, i := range v { @@ -126,6 +237,19 @@ func removeNilElements(v []any) []any { return newSlice } +// strval converts any value to a string. +// It handles various types: +// - string: returned as is +// - []byte: converted to string +// - error: returns the error message +// - fmt.Stringer: calls the String() method +// - anything else: uses fmt.Sprintf("%v", v) +// +// Parameters: +// - v: The value to convert to a string +// +// Returns: +// - string: The string representation of the value func strval(v any) string { switch v := v.(type) { case string: @@ -141,6 +265,17 @@ func strval(v any) string { } } +// trunc truncates a string to a specified length. +// If c is positive, it returns the first c characters. +// If c is negative, it returns the last |c| characters. +// If the string is shorter than the requested length, it returns the original string. +// +// Parameters: +// - c: The number of characters to keep (positive from start, negative from end) +// - s: The string to truncate +// +// Returns: +// - string: The truncated string func trunc(c int, s string) string { if c < 0 && len(s)+c > 0 { return s[len(s)+c:] @@ -151,14 +286,40 @@ func trunc(c int, s string) string { return s } +// title converts a string to title case. +// This uses the English language rules for capitalization. +// +// Parameters: +// - s: The string to convert +// +// Returns: +// - string: The string in title case func title(s string) string { return cases.Title(language.English).String(s) } +// join concatenates the elements of a slice with a separator. +// The input is first converted to a string slice using strslice. +// +// Parameters: +// - sep: The separator to use between elements +// - v: The value to join (will be converted to a string slice) +// +// Returns: +// - string: The joined string func join(sep string, v any) string { return strings.Join(strslice(v), sep) } +// split splits a string by a separator and returns a map. +// The keys in the map are "_0", "_1", etc., corresponding to the position of each part. +// +// Parameters: +// - sep: The separator to split on +// - orig: The string to split +// +// Returns: +// - map[string]string: A map with keys "_0", "_1", etc. and values being the split parts func split(sep, orig string) map[string]string { parts := strings.Split(orig, sep) res := make(map[string]string, len(parts)) @@ -168,10 +329,30 @@ func split(sep, orig string) map[string]string { return res } +// splitList splits a string by a separator and returns a slice. +// This is a simple wrapper around strings.Split. +// +// Parameters: +// - sep: The separator to split on +// - orig: The string to split +// +// Returns: +// - []string: A slice containing the split parts func splitList(sep, orig string) []string { return strings.Split(orig, sep) } +// splitn splits a string by a separator with a limit and returns a map. +// The keys in the map are "_0", "_1", etc., corresponding to the position of each part. +// It will split the string into at most n parts. +// +// Parameters: +// - sep: The separator to split on +// - n: The maximum number of parts to return +// - orig: The string to split +// +// Returns: +// - map[string]string: A map with keys "_0", "_1", etc. and values being the split parts func splitn(sep string, n int, orig string) map[string]string { parts := strings.SplitN(orig, sep, n) res := make(map[string]string, len(parts)) @@ -182,12 +363,20 @@ func splitn(sep string, n int, orig string) map[string]string { } // substring creates a substring of the given string. +// It extracts a portion of a string based on start and end indices. // -// If start is < 0, this calls string[:end]. +// Parameters: +// - start: The starting index (inclusive) +// - end: The ending index (exclusive) +// - s: The source string // -// If start is >= 0 and end < 0 or end bigger than s length, this calls string[start:] +// Behavior: +// - If start < 0, returns s[:end] +// - If start >= 0 and end < 0 or end > len(s), returns s[start:] +// - Otherwise, returns s[start:end] // -// Otherwise, this calls string[start, end]. +// Returns: +// - string: The extracted substring func substring(start, end int, s string) string { if start < 0 { return s[:end] @@ -198,6 +387,19 @@ func substring(start, end int, s string) string { return s[start:end] } +// repeat creates a new string by repeating the input string a specified number of times. +// It has safety limits to prevent excessive memory usage or infinite loops. +// +// Parameters: +// - count: The number of times to repeat the string +// - str: The string to repeat +// +// Returns: +// - string: The repeated string +// +// Panics: +// - If count exceeds loopExecutionLimit +// - If the resulting string length would exceed stringLengthLimit func repeat(count int, str string) string { if count > loopExecutionLimit { panic(fmt.Sprintf("repeat count %d exceeds limit of %d", count, loopExecutionLimit)) @@ -207,26 +409,79 @@ func repeat(count int, str string) string { return strings.Repeat(str, count) } +// trimAll removes all leading and trailing characters contained in the cutset. +// Note that the parameter order is reversed from the standard strings.Trim function. +// +// Parameters: +// - a: The cutset of characters to remove +// - b: The string to trim +// +// Returns: +// - string: The trimmed string func trimAll(a, b string) string { return strings.Trim(b, a) } +// trimPrefix removes the specified prefix from a string. +// If the string doesn't start with the prefix, it returns the original string. +// Note that the parameter order is reversed from the standard strings.TrimPrefix function. +// +// Parameters: +// - a: The prefix to remove +// - b: The string to trim +// +// Returns: +// - string: The string with the prefix removed, or the original string if it doesn't start with the prefix func trimPrefix(a, b string) string { return strings.TrimPrefix(b, a) } +// trimSuffix removes the specified suffix from a string. +// If the string doesn't end with the suffix, it returns the original string. +// Note that the parameter order is reversed from the standard strings.TrimSuffix function. +// +// Parameters: +// - a: The suffix to remove +// - b: The string to trim +// +// Returns: +// - string: The string with the suffix removed, or the original string if it doesn't end with the suffix func trimSuffix(a, b string) string { return strings.TrimSuffix(b, a) } +// contains checks if a string contains a substring. +// +// Parameters: +// - substr: The substring to search for +// - str: The string to search in +// +// Returns: +// - bool: True if str contains substr, false otherwise func contains(substr string, str string) bool { return strings.Contains(str, substr) } +// hasPrefix checks if a string starts with a specified prefix. +// +// Parameters: +// - substr: The prefix to check for +// - str: The string to check +// +// Returns: +// - bool: True if str starts with substr, false otherwise func hasPrefix(substr string, str string) bool { return strings.HasPrefix(str, substr) } +// hasSuffix checks if a string ends with a specified suffix. +// +// Parameters: +// - substr: The suffix to check for +// - str: The string to check +// +// Returns: +// - bool: True if str ends with substr, false otherwise func hasSuffix(substr string, str string) bool { return strings.HasSuffix(str, substr) } diff --git a/util/sprig/url.go b/util/sprig/url.go index 00826706..52dac3bb 100644 --- a/util/sprig/url.go +++ b/util/sprig/url.go @@ -60,7 +60,6 @@ func urlJoin(d map[string]any) string { } user = tempURL.User } - resURL.User = user return resURL.String() }