From 9fd7633feed65e186fc850cc1bfdc5f8cc8c2b4f Mon Sep 17 00:00:00 2001 From: Antons Kranga Date: Fri, 30 Dec 2022 11:56:13 +0200 Subject: [PATCH] Introducing new functions for gotemplate #21 --- cmd/hub/lifecycle/template.go | 186 ++++++++++++++++++++++++++++- cmd/hub/lifecycle/template_test.go | 141 ++++++++++++++++++++++ 2 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 cmd/hub/lifecycle/template_test.go diff --git a/cmd/hub/lifecycle/template.go b/cmd/hub/lifecycle/template.go index 02f4882..53ac3fa 100644 --- a/cmd/hub/lifecycle/template.go +++ b/cmd/hub/lifecycle/template.go @@ -17,7 +17,9 @@ import ( "os" "path" "path/filepath" + "reflect" "regexp" + "strconv" "strings" gotemplate "text/template" @@ -566,8 +568,190 @@ func bcryptStr(str string) (string, error) { return string(bytes), nil } +// Splits the string into a list of strings +// First argument is a string to split +// Second optional argument is a separator (default is space) +// Example: +// split "a b c" => ["a", "b", "c"] +// split "a-b-c", "-" => ["a", "b", "c"] +func split(args ...string) ([]string, error) { + if len(args) == 0 { + return nil, fmt.Errorf("split expects one or two arguments") + } + if len(args) == 1 { + return strings.Fields(args[0]), nil + } + return strings.Split(args[0], args[1]), nil +} + +// Removes empty string from the list of strings +// Accepts variable arguments arguments (easier tolerate template nature): +// +// Example: +// compact "string1" (compatibility with parametersx) +// compact "string1" "string2" "string3" +// compact ["string1", "string2", "string3"] +func compact(args ...interface{}) ([]string, error) { + var results []string + for _, arg := range args { + a := reflect.ValueOf(arg) + if a.Kind() == reflect.Slice { + if a.Len() == 0 { + continue + } + ret := make([]interface{}, a.Len()) + for i := 0; i < a.Len(); i++ { + ret[i] = a.Index(i).Interface() + } + res, _ := compact(ret...) + results = append(results, res...) + continue + } + if a.Kind() == reflect.String { + trimmed := strings.TrimSpace(a.String()) + if trimmed == "" { + continue + } + results = append(results, trimmed) + continue + } + return nil, fmt.Errorf("Argument type %T not yet supported", arg) + } + return results, nil +} + +// Joins the list of strings into a single string +// Last argument is a delimiter (default is space) +// Accepts variable arguments arguments (easier tolerate template nature) +// +// Example: +// join "string1" "string2" "delimiter" +// join ["string1", "string2"] "delimiter" +// join ["string1", "string2"] +// join "string1" +func join(args ...interface{}) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("join expects at least one argument") + } + var del string + if len(args) > 1 { + del = fmt.Sprintf("%v", args[len(args)-1]) + args = args[:len(args)-1] + } + if del == "" { + del = " " + } + + var result []string + for _, arg := range args { + a := reflect.ValueOf(arg) + if a.Kind() == reflect.Slice { + if a.Len() == 0 { + continue + } + for i := 0; i < a.Len(); i++ { + result = append(result, fmt.Sprintf("%v", a.Index(i).Interface())) + } + continue + } + if a.Kind() == reflect.String { + result = append(result, a.String()) + continue + } + return "", fmt.Errorf("Argument type %T not yet supported", arg) + } + + return strings.Join(result, del), nil +} + +// Returns the first argument from list +// +// Example: +// first ["string1" "string2" "string3"] => "string1" +func first(args []string) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("first expects at least one argument") + } + return args[0], nil +} + +// Converts the string into kubernetes acceptable name +// which consist of kebab lower case with alphanumeric characters. +// '.' is not allowed +// +// Arguments: +// First argument is a text to convert +// Second optional argument is a size of the name (default is 63) +// Third optional argument is a delimiter (default is '-') +func formatSubdomain(args ...interface{}) (string, error) { + if len(args) == 0 { + return "", fmt.Errorf("hostname expects at least one argument") + } + arg0 := reflect.ValueOf(args[0]) + if arg0.Kind() != reflect.String { + return "", fmt.Errorf("hostname expects string as first argument") + } + text := strings.TrimSpace(arg0.String()) + if text == "" { + return "", nil + } + + size := 63 + if len(args) > 1 { + arg1 := reflect.ValueOf(args[1]) + if arg1.Kind() == reflect.Int { + size = int(reflect.ValueOf(args[1]).Int()) + } else if arg1.Kind() == reflect.String { + size, _ = strconv.Atoi(arg1.String()) + } else { + return "", fmt.Errorf("Argument type %T not yet supported", args[1]) + } + } + + var del = "-" + if len(args) > 2 { + del = fmt.Sprintf("%v", args[2]) + } + + var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + var matchNonAlphanumericEnd = regexp.MustCompile("[^a-zA-Z0-9]+$") + var matchNonLetterStart = regexp.MustCompile("^[^a-zA-Z]+") + var matchNonAnumericOrDash = regexp.MustCompile("[^a-zA-Z0-9-]+") + var matchTwoOrMoreDashes = regexp.MustCompile("-{2,}") + + text = matchNonLetterStart.ReplaceAllString(text, "") + text = matchAllCap.ReplaceAllString(text, "${1}-${2}") + text = matchNonAnumericOrDash.ReplaceAllString(text, "-") + text = matchTwoOrMoreDashes.ReplaceAllString(text, "-") + text = strings.ToLower(text) + if len(text) > size { + text = text[:size] + } + text = matchNonAlphanumericEnd.ReplaceAllString(text, "") + if del != "-" { + text = strings.ReplaceAll(text, "-", del) + } + return text, nil +} + +// Removes single or double or back quotes from the string +func unquote(str string) (string, error) { + result, err := strconv.Unquote(str) + if err != nil && err.Error() == "invalid syntax" { + return str, err + } + return result, err +} + var hubGoTemplateFuncMap = map[string]interface{}{ - "bcrypt": bcryptStr, + "bcrypt": bcryptStr, + "split": split, + "compact": compact, + "join": join, + "first": first, + "formatSubdomain": formatSubdomain, + "unquote": unquote, + "uquote": unquote, } func processGo(content, filename, componentName string, kv map[string]interface{}) (string, error) { diff --git a/cmd/hub/lifecycle/template_test.go b/cmd/hub/lifecycle/template_test.go new file mode 100644 index 0000000..4720f92 --- /dev/null +++ b/cmd/hub/lifecycle/template_test.go @@ -0,0 +1,141 @@ +// Copyright (c) 2022 EPAM Systems, Inc. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package lifecycle + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSplit(t *testing.T) { + result, _ := split("") + assert.Equal(t, 0, len(result)) + + result, _ = split("a/b/c", "/") + expected := []string{"a", "b", "c"} + assert.EqualValues(t, expected, result) + + result, _ = split("a b c") + assert.EqualValues(t, expected, result) +} + +func TestCompact(t *testing.T) { + sample := []string{} + result, _ := compact(sample) + assert.Equal(t, 0, len(result)) + + sample = []string{"a", "b", "c"} + result, _ = compact(sample) + assert.EqualValues(t, []string{"a", "b", "c"}, result) + + sample = []string{"a", "", "c"} + result, _ = compact(sample) + assert.EqualValues(t, []string{"a", "c"}, result) + + result, _ = compact("abc") + assert.EqualValues(t, []string{"abc"}, result) + + result, _ = compact("a", "", "c") + assert.EqualValues(t, []string{"a", "c"}, result) +} + +func TestFirst(t *testing.T) { + sample := []string{} + result, _ := first(sample) + assert.Equal(t, "", result) + + sample = []string{"a", "b", "c"} + result, _ = first(sample) + assert.Equal(t, "a", result) +} + +func TestJoin(t *testing.T) { + sample := []string{} + result, _ := join(sample) + assert.Equal(t, "", result) + + sample = []string{"a", "b", "c"} + result, _ = join(sample) + assert.Equal(t, "a b c", result) + + sample = []string{"a", "b", "c"} + result, _ = join(sample, "/") + assert.Equal(t, "a/b/c", result) + + result, _ = join("a", "b", "c", "/") + assert.Equal(t, "a/b/c", result) +} + +func TestFormatSubdomain(t *testing.T) { + result, _ := formatSubdomain("") + assert.Equal(t, "", result) + + result, _ = formatSubdomain("a") + assert.Equal(t, "a", result) + + result, _ = formatSubdomain("a b") + assert.Equal(t, "a-b", result) + + // dashes cannot repeat + result, _ = formatSubdomain("A B c") + assert.Equal(t, "a-b-c", result) + + // cannot start and finish with dash + result, _ = formatSubdomain("--a b c--") + assert.Equal(t, "a-b-c", result) + + // cannot start but can finish with digit + result, _ = formatSubdomain("12a3 b c4") + assert.Equal(t, "a3-b-c4", result) + + // max length + result, _ = formatSubdomain("a b c", 3) + assert.Equal(t, "a-b", result) + + // second param may be string + result, _ = formatSubdomain("a b c", "3") + assert.Equal(t, "a-b", result) + + // max length and delimiter + result, _ = formatSubdomain("a b c", 3, "_") + assert.Equal(t, "a_b", result) +} + +func TestUnquote(t *testing.T) { + result, _ := unquote("") + assert.Equal(t, "", result) + + result, _ = unquote("a") + assert.Equal(t, "a", result) + + result, _ = unquote("'a'") + assert.Equal(t, "a", result) + + result, _ = unquote("\"a\"") + assert.Equal(t, "a", result) + + result, _ = unquote("\"a") + assert.Equal(t, "\"a", result) + + result, _ = unquote("a\"") + assert.Equal(t, "a\"", result) + + result, err := unquote("'a") + assert.Equal(t, "'a", result) + assert.EqualError(t, err, "invalid syntax") + + result, err = unquote("a'") + assert.Equal(t, "a'", result) + assert.EqualError(t, err, "invalid syntax") + + result, _ = unquote("\"a'b\"") + assert.Equal(t, "a'b", result) + + _, err = unquote("'a\"b'") + assert.EqualError(t, err, "invalid syntax") +}