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 }