github.com/coveo/gotemplate@v2.7.7+incompatible/hcl/utilities.go (about) 1 package hcl 2 3 import ( 4 "fmt" 5 "reflect" 6 "regexp" 7 "runtime/debug" 8 "sort" 9 "strings" 10 11 "github.com/coveo/gotemplate/collections" 12 ) 13 14 // flatten converts array of map to single map if there is only one element in the array. 15 // By default, hc.Unmarshal returns array of map even if there is only a single map in the definition. 16 func flatten(source interface{}) interface{} { 17 switch value := source.(type) { 18 case []map[string]interface{}: 19 switch len(value) { 20 case 1: 21 source = flatten(value[0]) 22 default: 23 result := make([]map[string]interface{}, len(value)) 24 for i := range value { 25 result[i] = flatten(value[i]).(map[string]interface{}) 26 } 27 source = result 28 } 29 case map[string]interface{}: 30 for key := range value { 31 value[key] = flatten(value[key]) 32 } 33 source = value 34 case []interface{}: 35 for i, sub := range value { 36 value[i] = flatten(sub) 37 } 38 source = value 39 } 40 return source 41 } 42 43 func transform(out interface{}) { 44 result := transformElement(flatten(reflect.ValueOf(out).Elem().Interface())) 45 if _, isMap := out.(*map[string]interface{}); isMap { 46 // If the result is expected to be map[string]interface{}, we convert it back from internal dict type. 47 result = result.(hclIDict).Native() 48 } 49 reflect.ValueOf(out).Elem().Set(reflect.ValueOf(result)) 50 } 51 52 func transformElement(source interface{}) interface{} { 53 if value, err := hclHelper.TryAsDictionary(source); err == nil { 54 for _, key := range value.KeysAsString() { 55 value.Set(key, transformElement(value.Get(key))) 56 } 57 source = value 58 } else if value, err := hclHelper.TryAsList(source); err == nil { 59 for i, sub := range value.AsArray() { 60 value.Set(i, transformElement(sub)) 61 } 62 source = value 63 } 64 return source 65 } 66 67 func marshalHCL(value interface{}, fullHcl, head bool, prefix, indent string) (result string, err error) { 68 if value == nil { 69 result = "null" 70 return 71 } 72 73 ifIndent := func(vTrue, vFalse interface{}) interface{} { return collections.IIf(indent, vTrue, vFalse) } 74 const specialFormat = "#HCL_ARRAY_MAP#!" 75 76 switch value := value.(type) { 77 case int, uint, int64, uint64, float64, bool: 78 result = fmt.Sprint(value) 79 case string: 80 value = fmt.Sprintf("%q", value) 81 if indent != "" && strings.Contains(value, "\\n") { 82 // We unquote the value 83 unIndented := value[1 : len(value)-1] 84 // Then replace escaped characters, other escape chars are \a, \b, \f and \v are not managed 85 unIndented = strings.Replace(unIndented, `\n`, "\n", -1) 86 unIndented = strings.Replace(unIndented, `\\`, "\\", -1) 87 unIndented = strings.Replace(unIndented, `\"`, "\"", -1) 88 unIndented = strings.Replace(unIndented, `\r`, "\r", -1) 89 unIndented = strings.Replace(unIndented, `\t`, "\t", -1) 90 unIndented = collections.UnIndent(unIndented) 91 if strings.HasSuffix(unIndented, "\n") { 92 value = fmt.Sprintf("<<-EOF\n%sEOF", unIndented) 93 } 94 } 95 result = value 96 97 case []interface{}: 98 results := make([]string, len(value)) 99 if fullHcl && isArrayOfMap(value) { 100 for i, element := range value { 101 element := element.(map[string]interface{}) 102 for key := range element { 103 if results[i], err = marshalHCL(element[key], fullHcl, false, "", indent); err != nil { 104 return 105 } 106 if head { 107 results[i] = fmt.Sprintf(`%s%s%s`, id(key), ifIndent(" = ", ""), results[i]) 108 } else { 109 results[i] = fmt.Sprintf(`%s %s %s`, specialFormat, id(key), results[i]) 110 } 111 } 112 } 113 result = strings.Join(results, ifIndent("\n\n", " ").(string)) 114 break 115 } 116 var totalLength int 117 var newLine bool 118 for i := range value { 119 if results[i], err = marshalHCL(value[i], fullHcl, false, "", indent); err != nil { 120 return 121 } 122 totalLength += len(results[i]) 123 newLine = newLine || strings.Contains(results[i], "\n") 124 } 125 if totalLength > 60 && indent != "" || newLine { 126 result = fmt.Sprintf("[\n%s,\n]", collections.Indent(strings.Join(results, ",\n"), prefix+indent)) 127 } else { 128 result = fmt.Sprintf("[%s]", strings.Join(results, ifIndent(", ", ",").(string))) 129 } 130 131 case map[string]interface{}: 132 if key := singleMap(value); fullHcl && key != "" { 133 var element string 134 if element, err = marshalHCL(value[key], fullHcl, false, "", indent); err != nil { 135 return 136 } 137 result = fmt.Sprintf(`%s %s`, id(key), element) 138 break 139 } 140 141 keys := make([]string, 0, len(value)) 142 for key := range value { 143 keys = append(keys, key) 144 } 145 sort.Strings(keys) 146 rendered := make(map[string]string, len(keys)) 147 keyLen := 0 148 149 for key, val := range value { 150 if rendered[key], err = marshalHCL(val, fullHcl, false, "", indent); err != nil { 151 return 152 } 153 if strings.Contains(rendered[key], "\n") { 154 continue 155 } 156 if len(key) > keyLen && indent != "" { 157 keyLen = len(key) 158 } 159 } 160 161 items := make([]string, 0, len(value)+2) 162 for _, multiline := range []bool{false, true} { 163 for _, key := range keys { 164 rendered := rendered[key] 165 lines := strings.Split(rendered, "\n") 166 167 // We process the multilines elements after the single line one 168 if len(lines) > 1 && !multiline || len(lines) == 1 && multiline { 169 continue 170 } 171 172 if multiline && len(items) > 0 { 173 // Add a blank line between multilines elements 174 items = append(items, "") 175 keyLen = 0 176 } 177 178 equal := ifIndent(" = ", "=").(string) 179 if _, err := hclHelper.TryAsDictionary(value[key]); err == nil { 180 if multiline { 181 equal = " " 182 } else if indent == "" { 183 equal = "" 184 } 185 } 186 187 if strings.Contains(rendered, specialFormat) { 188 items = append(items, strings.Replace(rendered, specialFormat, id(key), -1)) 189 190 } else { 191 if indent == "" { 192 keyLen = 0 193 if equal == "" && !strings.HasPrefix(rendered, `{`) { 194 keyLen = len(id(key)) + 1 195 } 196 } 197 items = append(items, fmt.Sprintf("%*s%s%s", -keyLen, id(key), equal, rendered)) 198 } 199 } 200 } 201 202 if head { 203 result = strings.Join(items, ifIndent("\n", " ").(string)) 204 break 205 } 206 207 if indent == "" || len(items) == 0 { 208 result = fmt.Sprintf("{%s}", strings.Join(items, " ")) 209 break 210 } 211 212 result = fmt.Sprintf("{\n%s\n}", collections.Indent(strings.Join(items, "\n"), prefix+indent)) 213 214 default: 215 debug.PrintStack() 216 err = fmt.Errorf("marshalHCL Unknown type %[1]T %[1]v", value) 217 } 218 return 219 } 220 221 func isArrayOfMap(array []interface{}) bool { 222 if len(array) == 0 { 223 return false 224 } 225 for _, item := range array { 226 if item, isMap := item.([]map[string]interface{}); !isMap || len(item) != 1 { 227 return false 228 } 229 } 230 return true 231 } 232 233 func singleMap(m map[string]interface{}) string { 234 if len(m) != 1 { 235 return "" 236 } 237 for k := range m { 238 if _, isMap := m[k].(map[string]interface{}); isMap { 239 return k 240 } 241 } 242 return "" 243 } 244 245 var identifierRegex = regexp.MustCompile(`^[A-za-z][\w-]*$`) 246 247 func id(key string) string { 248 if identifierRegex.MatchString(key) { 249 return key 250 } 251 // The identifier contains characters that may be considered invalid, we have to quote it 252 return fmt.Sprintf("%q", key) 253 }