github.com/openshift-online/ocm-sdk-go@v0.1.473/configuration/object.go (about) 1 /* 2 Copyright (c) 2020 Red Hat, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // This file contains the the implementation of the configuration object. 18 19 package configuration 20 21 import ( 22 "fmt" 23 "io" 24 "os" 25 "path/filepath" 26 "regexp" 27 "sort" 28 "strconv" 29 30 "gopkg.in/yaml.v3" 31 ) 32 33 // Builder contains the data and logic needed to populate a configuration object. Don't create 34 // instances of this type directly, use the New function instead. 35 type Builder struct { 36 // sources contains the list of sources where the configuration should be loaded from. 37 sources []interface{} 38 39 // tags contains for each tag information describing it, like its name and the kind of nodes 40 // that it can process. 41 tags []tagRegistryEntry 42 43 // titles contains the title for each node. This will usually be the name of the file that 44 // the node was loaded from. This is used to generate error messages that include the name 45 // of the file. 46 titles map[*yaml.Node]string 47 } 48 49 // Object contains configuration data. 50 type Object struct { 51 tree *yaml.Node 52 } 53 54 // New creates a new builder that can be use to populate a configuration object. 55 func New() *Builder { 56 return &Builder{} 57 } 58 59 // Load adds the given objects as sources where the configuration will be loaded from. 60 // 61 // If a source is a string ending in `.yaml` or `.yml` it will be interpreted as the name of a 62 // file containing the YAML text. 63 // 64 // If a source is a string ending in `.d` it will be interpreted as a directory containing YAML 65 // files. The `.yaml` or `.yml` files inside that directory will be loaded in alphabetical order. 66 // 67 // Any string not ending in `.yaml`, `.yml` or `.d` will be interprested as actual YAML text. In 68 // order to simplify embedding these strings in Go programs leading tabs will be removed from all 69 // the lines of that YAML text. 70 // 71 // If a source is an array of bytes it will be interpreted as actual YAML text. 72 // 73 // If a source implements the io.Reader interface, then it will be used to read in memory the YAML 74 // text. 75 // 76 // If the source can also be a yaml.Node or another configuration Object. In those cases the 77 // content of the source will be copied. 78 // 79 // If the source is any other kind of object then it will be serialized as YAML and then loaded. 80 func (b *Builder) Load(sources ...interface{}) *Builder { 81 b.sources = append(b.sources, sources...) 82 return b 83 } 84 85 // Build uses the information stored in the builder to create and populate a configuration 86 // object. 87 func (b *Builder) Build() (object *Object, err error) { 88 // Add the builtin tags to the tag registry: 89 b.registerTag("boolean", yaml.ScalarNode, b.processBooleanTag) 90 b.registerTag("file", yaml.ScalarNode, b.processFileTag) 91 b.registerTag("float", yaml.ScalarNode, b.processFloatTag) 92 b.registerTag("integer", yaml.ScalarNode, b.processIntegerTag) 93 b.registerTag("script", yaml.ScalarNode, b.processScriptTag) 94 b.registerTag("string", yaml.ScalarNode, b.processStringTag) 95 b.registerTag("trim", yaml.ScalarNode, b.processTrimTag) 96 b.registerTag("variable", yaml.ScalarNode, b.processVariableTag) 97 b.registerTag("yaml", yaml.ScalarNode, b.processYamlTag) 98 99 // Initialize the titles index: 100 b.titles = map[*yaml.Node]string{} 101 102 // Merge the sources: 103 tree := &yaml.Node{} 104 for _, current := range b.sources { 105 switch source := current.(type) { 106 case string: 107 err = b.mergeString(source, tree) 108 case []byte: 109 err = b.mergeBytes("", source, tree) 110 case io.Reader: 111 err = b.mergeReader(source, tree) 112 case yaml.Node: 113 err = b.mergeNode(&source, tree) 114 case *yaml.Node: 115 err = b.mergeNode(source, tree) 116 case *Object: 117 err = b.mergeNode(source.tree, tree) 118 case Object: 119 err = b.mergeNode(source.tree, tree) 120 default: 121 err = b.mergeAny(source, tree) 122 } 123 if err != nil { 124 return 125 } 126 } 127 128 // Create and populate the object: 129 object = &Object{ 130 tree: tree, 131 } 132 133 return 134 } 135 136 func (b *Builder) mergeString(src string, dst *yaml.Node) error { 137 ext := filepath.Ext(src) 138 if ext == ".yaml" || ext == ".yml" || ext == ".d" { 139 return b.mergeFile(src, dst) 140 } 141 src = b.removeLeadingTabs(src) 142 return b.mergeBytes("", []byte(src), dst) 143 } 144 145 func (b *Builder) mergeReader(src io.Reader, dst *yaml.Node) error { 146 buffer, err := io.ReadAll(src) 147 if err != nil { 148 return err 149 } 150 return b.mergeBytes("", buffer, dst) 151 } 152 153 func (b *Builder) mergeFile(src string, dst *yaml.Node) error { 154 info, err := os.Stat(src) 155 if err != nil { 156 return err 157 } 158 if info.IsDir() { 159 return b.mergeDir(src, dst) 160 } 161 buffer, err := os.ReadFile(src) // #nosec G304 162 if err != nil { 163 return err 164 } 165 return b.mergeBytes(src, buffer, dst) 166 } 167 168 func (b *Builder) mergeDir(src string, dst *yaml.Node) error { 169 infos, err := os.ReadDir(src) 170 if err != nil { 171 return err 172 } 173 files := make([]string, 0, len(infos)) 174 for _, info := range infos { 175 name := info.Name() 176 ext := filepath.Ext(name) 177 if ext == ".yaml" || ext == ".yml" { 178 files = append(files, filepath.Join(src, name)) 179 } 180 } 181 sort.Strings(files) 182 for _, file := range files { 183 err = b.mergeFile(file, dst) 184 if err != nil { 185 return err 186 } 187 } 188 return nil 189 } 190 191 func (b *Builder) mergeAny(src interface{}, dst *yaml.Node) error { 192 buffer, err := yaml.Marshal(src) 193 if err != nil { 194 return err 195 } 196 return b.mergeBytes("", buffer, dst) 197 } 198 199 func (b *Builder) mergeBytes(title string, src []byte, dst *yaml.Node) error { 200 var tree yaml.Node 201 err := yaml.Unmarshal(src, &tree) 202 if err != nil { 203 return err 204 } 205 b.titleTree(title, &tree) 206 err = b.processTreeTags(&tree) 207 if err != nil { 208 return err 209 } 210 return b.mergeNode(&tree, dst) 211 } 212 213 func (b *Builder) mergeNode(src, dst *yaml.Node) error { 214 if src.Kind != dst.Kind { 215 b.deepCopy(src, dst) 216 return nil 217 } 218 switch src.Kind { 219 case 0: 220 return b.mergeEmpty(src, dst) 221 case yaml.DocumentNode: 222 return b.mergeDocument(src, dst) 223 case yaml.SequenceNode: 224 return b.mergeSequence(src, dst) 225 case yaml.MappingNode: 226 return b.mergeMapping(src, dst) 227 case yaml.ScalarNode: 228 return b.mergeScalar(src, dst) 229 case yaml.AliasNode: 230 return b.mergeAlias(src, dst) 231 default: 232 return fmt.Errorf("don't know how to handle YAML node of type %d", src.Kind) 233 } 234 } 235 236 func (b *Builder) mergeDocument(src, dst *yaml.Node) error { 237 return b.mergeNode(src.Content[0], dst.Content[0]) 238 } 239 240 func (b *Builder) mergeEmpty(src, dst *yaml.Node) error { 241 return nil 242 } 243 244 func (b *Builder) mergeSequence(src, dst *yaml.Node) error { 245 size := len(src.Content) 246 nodes := make([]*yaml.Node, size) 247 for i := 0; i < size; i++ { 248 nodes[i] = &yaml.Node{} 249 b.deepCopy(src.Content[i], nodes[i]) 250 } 251 dst.Content = append(dst.Content, nodes...) 252 return nil 253 } 254 255 func (b *Builder) mergeMapping(src, dst *yaml.Node) error { 256 srcSize := len(src.Content) / 2 257 i := 0 258 for i < srcSize { 259 srcKey := src.Content[2*i] 260 srcValue := src.Content[2*i+1] 261 dstSize := len(dst.Content) / 2 262 j := 0 263 for j < dstSize { 264 dstKey := dst.Content[2*j] 265 dstValue := dst.Content[2*j+1] 266 if srcKey.Value == dstKey.Value { 267 err := b.mergeNode(srcValue, dstValue) 268 if err != nil { 269 return err 270 } 271 break 272 } 273 j++ 274 } 275 if j == dstSize { 276 dstKey := &yaml.Node{} 277 b.deepCopy(srcKey, dstKey) 278 dstValue := &yaml.Node{} 279 b.deepCopy(srcValue, dstValue) 280 dst.Content = append(dst.Content, dstKey, dstValue) 281 } 282 i++ 283 } 284 return nil 285 } 286 287 func (b *Builder) mergeScalar(src, dst *yaml.Node) error { 288 b.deepCopy(src, dst) 289 return nil 290 } 291 292 func (b *Builder) mergeAlias(src, dst *yaml.Node) error { 293 b.deepCopy(src, dst) 294 return nil 295 } 296 297 func (b *Builder) deepCopy(src, dst *yaml.Node) { 298 // Copy the title: 299 b.titles[dst] = b.titles[src] 300 301 // Copy the content: 302 *dst = *src 303 if src.Content != nil { 304 size := len(src.Content) 305 dst.Content = make([]*yaml.Node, size) 306 for i := 0; i < size; i++ { 307 dst.Content[i] = &yaml.Node{} 308 b.deepCopy(src.Content[i], dst.Content[i]) 309 } 310 } 311 } 312 313 func (b *Builder) nodeError(node *yaml.Node, format string, a ...interface{}) error { 314 format = "%s:%d:%d: " + format 315 title := b.titles[node] 316 if title == "" { 317 title = "unknown" 318 } 319 v := make([]interface{}, len(a)+3) 320 v[0], v[1], v[2] = title, node.Line, node.Column 321 copy(v[3:], a) 322 return fmt.Errorf(format, v...) 323 } 324 325 func (b *Builder) titleTree(title string, tree *yaml.Node) { 326 b.titles[tree] = title 327 for _, node := range tree.Content { 328 b.titleTree(title, node) 329 } 330 } 331 332 func (b *Builder) quoteForError(s string) string { 333 r := strconv.Quote(s) 334 return r[1 : len(r)-1] 335 } 336 337 func (b *Builder) kindString(kind yaml.Kind) string { 338 switch kind { 339 case yaml.DocumentNode: 340 return "document" 341 case yaml.SequenceNode: 342 return "sequence" 343 case yaml.MappingNode: 344 return "mapping" 345 case yaml.ScalarNode: 346 return "scalar" 347 case yaml.AliasNode: 348 return "alias" 349 } 350 return "" 351 } 352 353 // removeLeadingTabs removes the leading tabs from the lines of the given string. 354 func (b *Builder) removeLeadingTabs(s string) string { 355 return leadingTabsRE.ReplaceAllString(s, "") 356 } 357 358 // leadingTabsRE is the regular expression used to remove leading tabs from strings generated with 359 // the EvaluateTemplate function. 360 var leadingTabsRE = regexp.MustCompile(`(?m)^\t*`) 361 362 // Populate populates the given destination object with the information stored in this 363 // configuration object. The destination object should be a pointer to a variable containing 364 // the same tags used by the yaml.Unmarshal method of the YAML library. 365 func (o *Object) Populate(v interface{}) error { 366 return o.tree.Decode(v) 367 } 368 369 // Effective returns an array of bytes containing the YAML representation of the configuration 370 // after processing all the tags. 371 func (o *Object) Effective() (out []byte, err error) { 372 return yaml.Marshal(o.tree) 373 } 374 375 // MarshalYAML is the implementation of the yaml.Marshaller interface. This is intended to be able 376 // use the type for fields inside other structs. Refrain from calling this method for any other 377 // use. 378 func (o *Object) MarshalYAML() (result interface{}, err error) { 379 result = o.tree 380 return 381 } 382 383 // UnmarshalYAML is the implementation of the yaml.Unmarshaller interface. This is intended to be 384 // able to use the type for fields inside structs. Refraim from calling this method for any other 385 // use. 386 func (o *Object) UnmarshalYAML(value *yaml.Node) error { 387 o.tree = value 388 return nil 389 }