github.com/verrazzano/verrazzano@v1.7.0/pkg/yaml/expand.go (about)

     1  // Copyright (c) 2021, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package yaml
     5  
     6  import (
     7  	"errors"
     8  	"strings"
     9  )
    10  
    11  // Expand a dot notated name to a YAML string.  The value can be a string or string list.
    12  // The simplest YAML is:
    13  // a: b
    14  //
    15  // Nested values are expanded as follows:
    16  //
    17  //	a.b.c : v1
    18  //	  expands to
    19  //	a:
    20  //	  b:
    21  //	    c: v1
    22  //
    23  // If there is more than one value then
    24  //
    25  //	a.b : {v1,v2}
    26  //	  expands to
    27  //	a:
    28  //	  b:
    29  //	    - v1
    30  //	    - v2
    31  //
    32  // The last segment of the name might be a quoted string, for example:
    33  //
    34  //	controller.service.annotations."service\.beta\.kubernetes\.io/oci-load-balancer-shape" : flexible
    35  //
    36  // which translates to
    37  //
    38  //	controller:
    39  //	  service:
    40  //	    annotations:
    41  //	      service.beta.kubernetes.io/oci-load-balancer-shape: flexible
    42  //
    43  // If forcelist is true then always use the list format.
    44  func Expand(leftMargin int, forceList bool, name string, vals ...string) (string, error) {
    45  	const indent = 2
    46  	b := strings.Builder{}
    47  
    48  	// Remove trailing quote and split the string at a quote if is exists
    49  	name = strings.TrimSpace(name)
    50  	name = strings.TrimRight(name, "\"")
    51  	quoteSegs := strings.Split(name, "\"")
    52  	if len(quoteSegs) > 2 {
    53  		return "", errors.New("Name/Value pair has invalid name with more than 1 quoted string")
    54  	}
    55  	if len(quoteSegs) == 0 {
    56  		return "", errors.New("Name/Value pair has invalid name")
    57  	}
    58  	// Remove any trailing dot and split the first part of the string at the dots.
    59  	unquotedPart := strings.TrimRight(quoteSegs[0], ".")
    60  	// Replace backslashed dots because of no negative lookbehind
    61  	placeholder := "/*placeholder*/"
    62  	unquotedPart = strings.Replace(unquotedPart, "\\.", placeholder, -1)
    63  	nameSegs := strings.Split(unquotedPart, ".")
    64  	if len(quoteSegs) == 2 {
    65  		// Add back the original quoted string if it existed
    66  		// e.g. change service\.beta\.kubernetes\.io/oci-load-balancer-shape to
    67  		//             service.beta.kubernetes.io/oci-load-balancer-shape
    68  		s := strings.Replace(quoteSegs[1], "\\", "", -1)
    69  		nameSegs = append(nameSegs, s)
    70  	}
    71  	// Loop through all the name segments, for example, these 4:
    72  	//    controller, service, annotations, service.beta.kubernetes.io/oci-load-balancer-shape
    73  	listIndents := 0
    74  	nextValueList := false
    75  	indentVal := " "
    76  	for i, seg := range nameSegs {
    77  		// Get rid of placeholder
    78  		seg = strings.Replace(seg, placeholder, ".", -1)
    79  
    80  		// Create the padded indent
    81  		pad := strings.Repeat(indentVal, leftMargin+(indent*(i+listIndents)))
    82  
    83  		// last value for formatting
    84  		lastVal := i == len(nameSegs)-1
    85  
    86  		// Check if current value is a new list value
    87  		listValueString := ""
    88  		if nextValueList {
    89  			listValueString = "- "
    90  			listIndents++
    91  			nextValueList = false
    92  		}
    93  
    94  		// Check if internal list value next
    95  		splitList := strings.Split(seg, `[`)
    96  		if len(splitList) > 1 {
    97  			seg = splitList[0]
    98  			nextValueList = true
    99  		}
   100  
   101  		// Write the indent padding, then name followed by colon
   102  		if _, err := b.WriteString(pad + listValueString + seg + ":"); err != nil {
   103  			return "", err
   104  		}
   105  		// If this is the last segment then write the value, else LF
   106  		if lastVal {
   107  			// indent is different based on if the last value was a list
   108  			indentSize := 1
   109  			if nextValueList {
   110  				indentSize = 2
   111  			}
   112  			pad += strings.Repeat(indentVal, indent*indentSize)
   113  			if err := writeVals(&b, forceList || nextValueList, pad, vals...); err != nil {
   114  				return "", err
   115  			}
   116  		} else {
   117  			if _, err := b.WriteString("\n"); err != nil {
   118  				return "", err
   119  			}
   120  		}
   121  	}
   122  	return b.String(), nil
   123  }
   124  
   125  // writeVals writes a single value or a list of values to the string builder.
   126  // If forcelist is true then always use the list format.
   127  func writeVals(b *strings.Builder, forceList bool, pad string, vals ...string) error {
   128  	// check for multiline value
   129  	if len(vals) == 1 && strings.Contains(vals[0], "\n") {
   130  		b.WriteString(" |\n")
   131  		b.WriteString(pad + strings.Replace(vals[0], "\n", "\n"+pad, -1))
   132  		return nil
   133  	}
   134  	if len(vals) == 1 && !forceList {
   135  		// Write the single value, for example:
   136  		// key: val1
   137  		_, err := b.WriteString(" " + vals[0])
   138  		return err
   139  	}
   140  	// Write the list of values, for example
   141  	//  key:
   142  	//    - val1
   143  	//    - val2
   144  	for _, val := range vals {
   145  		if _, err := b.WriteString("\n"); err != nil {
   146  			return err
   147  		}
   148  		if _, err := b.WriteString(pad + "- " + val); err != nil {
   149  			return err
   150  		}
   151  	}
   152  	return nil
   153  }