github.com/amplia-iiot/yutil@v1.0.1-0.20231229120411-5d96a4c5a136/pkg/replace/replace.go (about)

     1  /*
     2  Copyright (c) 2023 Adrian Haasler GarcĂ­a <dev@ahaasler.com>
     3  
     4  Permission is hereby granted, free of charge, to any person obtaining a copy
     5  of this software and associated documentation files (the "Software"), to deal
     6  in the Software without restriction, including without limitation the rights
     7  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     8  copies of the Software, and to permit persons to whom the Software is
     9  furnished to do so, subject to the following conditions:
    10  
    11  The above copyright notice and this permission notice shall be included in all
    12  copies or substantial portions of the Software.
    13  
    14  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    15  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    16  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    17  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    18  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    19  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    20  SOFTWARE.
    21  */
    22  
    23  package replace
    24  
    25  import (
    26  	"fmt"
    27  	"os"
    28  	"sort"
    29  	"strings"
    30  
    31  	"github.com/amplia-iiot/yutil/internal/io"
    32  	"github.com/amplia-iiot/yutil/internal/replace"
    33  	"github.com/amplia-iiot/yutil/internal/yaml"
    34  	"github.com/amplia-iiot/yutil/pkg/merge"
    35  )
    36  
    37  type option func(o *options)
    38  
    39  type Engine int
    40  
    41  const (
    42  	Golang Engine = iota
    43  	Jinja2
    44  )
    45  
    46  type options struct {
    47  	replace.Options
    48  	rootNode                         string
    49  	replacementFiles                 []string
    50  	includeStdinInReplacements       bool
    51  	includeEnvironmentInReplacements bool
    52  	extensions                       []string
    53  }
    54  
    55  func (o *options) changeReplacementsRootNode() error {
    56  	if node, ok := o.Options.Replacements[o.rootNode]; ok {
    57  		if node, ok := node.(map[any]any); ok {
    58  			reps := map[string]interface{}{}
    59  			for k, v := range node {
    60  				if name, ok := k.(string); ok {
    61  					reps[name] = v
    62  				}
    63  			}
    64  			o.Options.Replacements = reps
    65  		} else {
    66  			return fmt.Errorf("node %s does not contain more elements", o.rootNode)
    67  		}
    68  	} else {
    69  		return fmt.Errorf("no %s node", o.rootNode)
    70  	}
    71  	return nil
    72  }
    73  
    74  // WithDirectory configures the root directory to search for files to be replaced (defaults to current directory).
    75  func WithDirectory(directory string) option {
    76  	return func(o *options) {
    77  		o.Directory = directory
    78  	}
    79  }
    80  
    81  // WithInclude configures the glob pattern for files to be included.
    82  func WithInclude(pattern ...string) option {
    83  	return func(o *options) {
    84  		o.Include = append(o.Include, pattern...)
    85  	}
    86  }
    87  
    88  // WithExclude configures the glob pattern for files to be excluded.
    89  func WithExclude(pattern ...string) option {
    90  	return func(o *options) {
    91  		o.Exclude = append(o.Exclude, pattern...)
    92  	}
    93  }
    94  
    95  // WithRootNode configures the root node to include only replacements from inside that node.
    96  func WithRootNode(node string) option {
    97  	return func(o *options) {
    98  		o.rootNode = node
    99  	}
   100  }
   101  
   102  // WithReplacementFile adds a file to be used as replacement file.
   103  func WithReplacementFile(file string) option {
   104  	return func(o *options) {
   105  		o.replacementFiles = append(o.replacementFiles, file)
   106  	}
   107  }
   108  
   109  // WithReplacementFiles adds multiple files to be used as replacement files (they will be merged).
   110  func WithReplacementFiles(files ...string) option {
   111  	return func(o *options) {
   112  		o.replacementFiles = append(o.replacementFiles, files...)
   113  	}
   114  }
   115  
   116  // IncludeStdinInReplacements includes stdin to be used as replacement file.
   117  func IncludeStdinInReplacements() option {
   118  	return WithIncludeStdinInReplacements(true)
   119  }
   120  
   121  // WithIncludeStdinInReplacements configures whether to use stdin as replacement file.
   122  func WithIncludeStdinInReplacements(include bool) option {
   123  	return func(o *options) {
   124  		o.includeStdinInReplacements = include
   125  	}
   126  }
   127  
   128  // IncludeEnvironmentInReplacements includes all environment variables to be used in the template engine inside the env node.
   129  func IncludeEnvironmentInReplacements(include bool) option {
   130  	return WithIncludeEnvironmentInReplacements(true)
   131  }
   132  
   133  // WithIncludeEnvironmentInReplacements configures whether to use include all environment variables to be used in the template engine inside the env node.
   134  func WithIncludeEnvironmentInReplacements(include bool) option {
   135  	return func(o *options) {
   136  		o.includeEnvironmentInReplacements = include
   137  	}
   138  }
   139  
   140  // WithExtension configures the extensions to be removed from a file name when it's saved after being passed through the template engine.
   141  func WithExtension(extension ...string) option {
   142  	return func(o *options) {
   143  		o.extensions = append(o.extensions, extension...)
   144  	}
   145  }
   146  
   147  // Replace uses the template engine to replace files following the optional configuration.
   148  func Replace(engine Engine, opts ...option) (err error) {
   149  	o := &options{
   150  		Options: replace.Options{
   151  			Directory: ".",
   152  		},
   153  	}
   154  	for _, opt := range opts {
   155  		opt(o)
   156  	}
   157  	var replacements string
   158  	switch engine {
   159  	case Golang:
   160  		o.Engine = replace.Golang
   161  	case Jinja2:
   162  		o.Engine = replace.Jinja2
   163  	}
   164  	if o.includeStdinInReplacements {
   165  		if len(o.replacementFiles) > 0 {
   166  			replacements, err = merge.MergeStdinWithFiles(o.replacementFiles)
   167  		} else {
   168  			replacements, err = io.ReadStdin()
   169  		}
   170  	} else {
   171  		switch len(o.replacementFiles) {
   172  		case 0:
   173  			err = fmt.Errorf("no replacement files defined")
   174  		case 1:
   175  			replacements, err = io.ReadAsString(o.replacementFiles[0])
   176  		default:
   177  			replacements, err = merge.MergeAllFiles(o.replacementFiles)
   178  		}
   179  	}
   180  	if err != nil {
   181  		return
   182  	}
   183  	o.Options.Replacements, err = yaml.Parse(replacements)
   184  	if err != nil {
   185  		return
   186  	}
   187  	if o.rootNode != "" {
   188  		err = o.changeReplacementsRootNode()
   189  		if err != nil {
   190  			return
   191  		}
   192  	}
   193  	if o.includeEnvironmentInReplacements {
   194  		env := map[string]interface{}{}
   195  		for _, envVar := range os.Environ() {
   196  			if name, value, ok := strings.Cut(envVar, "="); ok {
   197  				env[name] = value
   198  			}
   199  		}
   200  		o.Options.Replacements, err = yaml.Merge(o.Options.Replacements, map[string]interface{}{"env": env})
   201  		if err != nil {
   202  			return
   203  		}
   204  	}
   205  	if len(o.extensions) > 0 {
   206  		sort.SliceStable(o.extensions, func(i, j int) bool {
   207  			return len(o.extensions[i]) > len(o.extensions[j])
   208  		})
   209  		o.FileNameRenamer = func(s string) (res string) {
   210  			res = s
   211  			for _, extension := range o.extensions {
   212  				res = strings.ReplaceAll(res, extension, "")
   213  			}
   214  			return
   215  		}
   216  	}
   217  	return replace.Replace(o.Options)
   218  }