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  }