diff --git a/.golangci.yaml b/.golangci.yaml index 346058c..3f7a4c3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -89,6 +89,8 @@ linters: varnamelen: max-distance: 10 + ignore-decls: + - c *Config[T] whitespace: multi-if: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8276f..fbb4d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ SPDX-License-Identifier: MPL-2.0 --> +Release 0.10.0 +============== + +- added functional options for construction +- added `WithValue` option that generates values accessible + via `.Values.valName` inside of configuration templates +- update dependencies + Release 0.9.4 ============= diff --git a/README.md b/README.md index 623c227..f5c030b 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,62 @@ func main() { } ``` +#### Using Custom Values + +So far, the cases involved the convenience wrappers provided by *templig*. Beneath that layer there is a standard +functional options pattern to generate a *templig* configuration. All options start with the `With` preix. + +So far, it was not possible for a programmer to provide own values to the configuration other than defining custom +environment variables. To address this shortcoming, custom values are introduced, that allow the configuration to +get that information via the `.Values` variable. + +Having a templated configuration file like this one: + +```yaml +id: 23 +name: Interesting Name +pass: {{ .Values.pass | required "password value required" | quote }} +``` + +One can see the templating code between the double curly braces `{{` and `}}`. +The following program is essentially the same as in the [Simple Case](#simple-case). +It just adds the `pass` field to the configuration: + +```go +package main + +import ( + "fmt" + "strings" + + "github.com/AlphaOne1/templig" +) + +// Config is the configuration structure +type Config struct { + ID int `yaml:"id"` + Name string `yaml:"name"` + Pass string `yaml:"pass"` +} + +// main will read and display the configuration +func main() { + c, confErr := templig.New[Config]( + templig.WithFile[Config]("my_config.yaml"), + templig.WithValue[Config]("pass", "secret")) + + fmt.Printf("read errors: %v", confErr) + + if confErr == nil { + fmt.Printf("ID: %v\n", c.Get().ID) + fmt.Printf("Name: %v\n", c.Get().Name) + fmt.Printf("Pass: %v\n", strings.Repeat("*", len(c.Get().Pass))) + } +} +``` + +It should be noted, that using the `New` method with the functional options provides also the means to intermix file +and io.Reader inputs freely. ### Validation @@ -461,9 +517,9 @@ An example usage can be found [here](examples/templating/env). The regular expression used to identify secrets to hide can be changed globally setting `templig.SecretRE` to a different value. It also can be set for each `Config` instance using the `SetSecretRE` method. To hide, e.g., also -tokens, one could use the following (with `SecretDefaultRE` containing the original regular expression text): +identifications, one could use the following (with `SecretDefaultRE` containing the original regular expression text): ```go c, _ := templig.FromFile[Config]("my_config.yaml") -c.SetSecretRE(regexp.MustCompile(templig.SecretDefaultRE + "|token")) +c.SetSecretRE(regexp.MustCompile(templig.SecretDefaultRE + "|identification")) ``` diff --git a/config.go b/config.go index 827ae28..6e3f946 100644 --- a/config.go +++ b/config.go @@ -34,21 +34,126 @@ type Validator interface { Validate() error } +type source struct { + fileName string + reader io.Reader +} + +func (s source) Reader() (io.ReadCloser, error) { + if s.reader != nil { + return io.NopCloser(s.reader), nil + } + + if s.fileName != "" { + r, err := os.Open(s.fileName) + + if err != nil { + return nil, fmt.Errorf("could not open config file: %w", err) + } + + return r, nil + } + + return nil, errors.Join(ErrNoConfigPaths, ErrNoConfigReaders) +} + // Config is the generic structure holding the configuration information for the specified type. type Config[T any] struct { node *yaml.Node content T secretRE *regexp.Regexp + sources []source + values map[string]any +} + +// Option defines a functional option for configuring a Config instance. +// It applies modifications or settings to the Config. +type Option[T any] func(*Config[T]) error + +// WithSecretRE returns an Option to set the regular expression used for hiding secrets in the configuration. +func WithSecretRE[T any](re *regexp.Regexp) Option[T] { + return func(c *Config[T]) error { + return c.SetSecretRE(re) + } +} + +// WithFile creates an Option to specify file paths as configuration sources for the Config instance. +func WithFile[T any](fileNames ...string) Option[T] { + return func(c *Config[T]) error { + if len(fileNames) == 0 { + return ErrNoConfigPaths + } + + sources := make([]source, len(c.sources)+len(fileNames)) + + copy(sources, c.sources) + + for i, j := len(c.sources), 0; j < len(fileNames); i, j = i+1, j+1 { + sources[i] = source{fileName: fileNames[j]} + } + + c.sources = sources + + return nil + } } -// newConfig initializes a new Config object. -func newConfig[T any]() (*Config[T], error) { +// WithReader creates an Option that adds the provided io.Reader instances as configuration sources. +func WithReader[T any](readers ...io.Reader) Option[T] { + return func(c *Config[T]) error { + if len(readers) == 0 { + return ErrNoConfigReaders + } + + sources := make([]source, len(c.sources)+len(readers)) + + copy(sources, c.sources) + + for i, j := len(c.sources), 0; j < len(readers); i, j = i+1, j+1 { + sources[i] = source{reader: readers[j]} + } + + c.sources = sources + + return nil + } +} + +// WithValue creates an Option that sets a key-value pair in the configuration's values map. +func WithValue[T any](k string, v any) Option[T] { + return func(c *Config[T]) error { + c.values[k] = v + + return nil + } +} + +// New creates a new Config instance of type T using the provided options and +// returns it along with any errors encountered. +func New[T any](opts ...Option[T]) (*Config[T], error) { c := new(Config[T]) + c.values = make(map[string]any) if err := c.SetSecretRE(SecretRE); err != nil { return nil, err } + var errs []error + + for _, opt := range opts { + if err := opt(c); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return nil, errors.Join(errs...) + } + + if err := c.readSources(); err != nil { + return nil, err + } + return c, nil } @@ -58,57 +163,72 @@ func (c *Config[T]) Get() *T { return &c.content } -// overlay is called repeatedly and overlays the current intermediate configuration -// with the content of the given io.Reader. -func (c *Config[T]) overlay(r io.Reader) error { - additionalConfig, aErr := fromSingle[yaml.Node](r) - - if aErr != nil { - return aErr +// readSources processes all configuration sources, deserializes their content, and +// validates the resulting configuration. +func (c *Config[T]) readSources() error { + if len(c.sources) == 0 { + return errors.Join(ErrNoConfigPaths, ErrNoConfigReaders) } - if c.node == nil { - c.node = additionalConfig.Get() + var decodeErr error + var validateErr error + + if len(c.sources) == 1 { + // to optimize the most common case of a single reader, we do not need to + // go over the yaml.Node structure first. + decodeErr = func() error { + r, err := c.sources[0].Reader() + + if err != nil { + return err + } + + defer func() { _ = r.Close() }() + + return c.fromSingle(r) + }() } else { - merged, mergeErr := MergeYAMLNodes(c.node, additionalConfig.Get()) + for _, v := range c.sources { + if err := func() error { + r, err := v.Reader() - if mergeErr != nil { - return mergeErr - } + if err != nil { + return err + } - c.node = merged - } + defer func() { _ = r.Close() }() - return nil -} + return c.overlay(r) + }(); err != nil { + return err + } + } -// overlayFile opens a given configuration file and loads it as an intermediate using the overlay function. -func (c *Config[T]) overlayFile(path string) error { - f, err := os.Open(filepath.Clean(path)) + decodeErr = c.node.Decode(&c.content) - if err != nil { - return fmt.Errorf("could not open overlay file %v: %w", path, err) + // cleanup + c.node = nil } - defer func() { _ = f.Close() }() + if decodeErr == nil { + validateErr = c.Validate() + } + + if resultErr := errors.Join(decodeErr, validateErr); resultErr != nil { + return resultErr + } - return c.overlay(f) + return nil } // fromSingle reads a configuration from the single given io.Reader and // runs - if necessary - the contained template functions. -// It does not retain a node structure that is needed as base for merges with other configurations. -func fromSingle[T any](r io.Reader) (*Config[T], error) { - config, createErr := newConfig[T]() - - if createErr != nil { - return nil, createErr - } - +// It does not retain a node structure needed as a base for merges with other configurations. +func (c *Config[T]) fromSingle(r io.Reader) error { fileContent, err := io.ReadAll(r) if err != nil { - return nil, fmt.Errorf("could not read from reader: %w", err) + return fmt.Errorf("could not read from reader: %w", err) } var tmpl *template.Template @@ -117,76 +237,62 @@ func fromSingle[T any](r io.Reader) (*Config[T], error) { New("config"). Funcs(templigFunctions()). Parse(string(fileContent)); err != nil { - return nil, fmt.Errorf("could not parse template: %w", err) + return fmt.Errorf("could not parse template: %w", err) } var b bytes.Buffer - if err = tmpl.Execute(&b, nil); err != nil { - return nil, fmt.Errorf("could not execute template: %w", err) + if err = tmpl.Execute(&b, map[string]any{"Values": c.values}); err != nil { + return fmt.Errorf("could not execute template: %w", err) } - if decodeErr := yaml.NewDecoder(&b).Decode(&config.content); decodeErr != nil { - return nil, fmt.Errorf("could not parse configuration: %w", decodeErr) + if decodeErr := yaml.NewDecoder(&b).Decode(&c.content); decodeErr != nil { + return fmt.Errorf("could not parse configuration: %w", decodeErr) } - return config, nil + return nil } -// Validate checks if the configuration is valid if the content fulfills the Validator interface. -func (c *Config[T]) Validate() error { - if v, ok := any(&c.content).(Validator); ok { - if err := v.Validate(); err != nil { - return fmt.Errorf("validation failed: %w", err) - } +// overlay is called repeatedly and overlays the current intermediate configuration +// with the content of the given io.Reader. +func (c *Config[T]) overlay(r io.Reader) error { + opts := make([]Option[yaml.Node], 0, len(c.values)+1) + opts = append(opts, WithReader[yaml.Node](r)) + + for k, v := range c.values { + opts = append(opts, WithValue[yaml.Node](k, v)) } - return nil -} + additionalConfig, aErr := New[yaml.Node](opts...) -// From reads a configuration from the given set of io.Reader. -func From[T any](readers ...io.Reader) (*Config[T], error) { - if len(readers) == 0 { - return nil, ErrNoConfigReaders + if aErr != nil { + return aErr } - var config *Config[T] - var createErr error - var decodeErr error - var validateErr error - - if len(readers) == 1 { - // to optimize the most common case of a single reader, we do not need to - // go over the yaml.Node structure first. - config, decodeErr = fromSingle[T](readers[0]) + if c.node == nil { + c.node = additionalConfig.Get() } else { - config, createErr = newConfig[T]() - - if createErr != nil { - return nil, createErr - } + merged, mergeErr := MergeYAMLNodes(c.node, additionalConfig.Get()) - for _, v := range readers { - if err := config.overlay(v); err != nil { - return nil, err - } + if mergeErr != nil { + return mergeErr } - decodeErr = config.node.Decode(&config.content) - - // cleanup - config.node = nil + c.node = merged } - if decodeErr == nil { - validateErr = config.Validate() - } + return nil +} - if resultErr := errors.Join(decodeErr, validateErr); resultErr != nil { - return nil, resultErr +// Validate checks if the configuration is valid if the content fulfills the Validator interface. +func (c *Config[T]) Validate() error { + if v, ok := any(&c.content).(Validator); ok { + if err := v.Validate(); err != nil { + return fmt.Errorf("validation failed: %w", err) + } } - return config, nil + return nil } // To writes a configuration to the given io.Writer. @@ -198,6 +304,19 @@ func (c *Config[T]) To(w io.Writer) error { return errors.Join(err, encCloseErr) } +// ToFile saves a configuration to a file with the given name, replacing it in case. +func (c *Config[T]) ToFile(path string) error { + f, err := os.Create(filepath.Clean(path)) + + if err != nil { + return fmt.Errorf("could not create file %s: %w", path, err) + } + + defer func() { _ = f.Close() }() + + return c.To(f) +} + // ToSecretsHidden writes the configuration to the given io.Writer and hides secret values using [SecretRE] of the // initialization time of the instance if not set to another value using `SetSecretRE`. // Strings are replaced with the number of * corresponding to their length. @@ -266,86 +385,6 @@ func (c *Config[T]) ToSecretsHiddenStructured(w io.Writer) error { return errors.Join(encodeErr, writeErr, encCloseErr) } -// FromFile loads a series of configuration files. The first file is considered the base, all others are -// loaded on top of that one using the [MergeYAMLNodes] functionality. -func FromFile[T any](paths ...string) (*Config[T], error) { - if len(paths) == 0 { - return nil, ErrNoConfigPaths - } - - var config *Config[T] - var createErr error - var decodeErr error - var validateErr error - - if len(paths) == 1 { - // to optimize the most common case of a single file, we do not need to - // go over the yaml.Node structure first. - f, err := os.Open(paths[0]) - - if err != nil { - return nil, fmt.Errorf("could not open %s: %w", paths[0], err) - } - - defer func() { _ = f.Close() }() - - config, decodeErr = fromSingle[T](f) - } else { - config, createErr = newConfig[T]() - - if createErr != nil { - return nil, createErr - } - - for _, addOn := range paths { - aErr := config.overlayFile(addOn) - - if aErr != nil { - return nil, aErr - } - } - - decodeErr = config.node.Decode(&config.content) - - // cleanup - config.node = nil - } - - if decodeErr == nil { - validateErr = config.Validate() - } - - if resultErr := errors.Join(decodeErr, validateErr); resultErr != nil { - return nil, resultErr - } - - return config, nil -} - -// FromFiles loads a series of configuration files. The first file is considered the base, all others are -// loaded on top of that one using the [MergeYAMLNodes] functionality. -// -// Deprecated: As of version 'v0.6.0' this function is deprecated and will be removed in the next major release. -// -//nolint:gocheckcompilerdirectives -//go:fix inline -func FromFiles[T any](paths []string) (*Config[T], error) { - return FromFile[T](paths...) -} - -// ToFile saves a configuration to a file with the given name, replacing it in case. -func (c *Config[T]) ToFile(path string) error { - f, err := os.Create(filepath.Clean(path)) - - if err != nil { - return fmt.Errorf("could not create file %s: %w", path, err) - } - - defer func() { _ = f.Close() }() - - return c.To(f) -} - // SecretRE returns a copy of the regular expression used for hiding secrets of that specific instance. func (c *Config[T]) SecretRE() *regexp.Regexp { if c.secretRE == nil { @@ -367,3 +406,29 @@ func (c *Config[T]) SetSecretRE(re *regexp.Regexp) error { return nil } + +// +// Convenience Wrappers +// + +// From is a convenience wrapper that reads a configuration from the given set of io.Reader. +func From[T any](readers ...io.Reader) (*Config[T], error) { + return New[T](WithReader[T](readers...)) +} + +// FromFile is a convenience wrapper that loads a series of configuration files. The first file is considered the base, +// all others are loaded on top of that one using the [MergeYAMLNodes] functionality. +func FromFile[T any](paths ...string) (*Config[T], error) { + return New[T](WithFile[T](paths...)) +} + +// FromFiles is a convenience wrapper that loads a series of configuration files. The first file is considered the base, +// all others are loaded on top of that one using the [MergeYAMLNodes] functionality. +// +// Deprecated: As of version 'v0.6.0' this function is deprecated and will be removed in the next major release. +// +//nolint:gocheckcompilerdirectives +//go:fix inline +func FromFiles[T any](paths []string) (*Config[T], error) { + return FromFile[T](paths...) +} diff --git a/config_internal_test.go b/config_internal_test.go new file mode 100644 index 0000000..fdd5b10 --- /dev/null +++ b/config_internal_test.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 The templig contributors. +// SPDX-License-Identifier: MPL-2.0 + +package templig + +import ( + "errors" + "testing" +) + +func TestEmptySource0(t *testing.T) { + t.Parallel() + + s := source{} + + _, err := s.Reader() + + if !errors.Is(err, ErrNoConfigPaths) || !errors.Is(err, ErrNoConfigReaders) { + t.Errorf("reading from empty source should have returned an error") + } +} diff --git a/config_test.go b/config_test.go index a647b26..7575db8 100644 --- a/config_test.go +++ b/config_test.go @@ -34,6 +34,7 @@ func TestReadConfig(t *testing.T) { inFile string env map[string]string args []string + values map[string]any want TestConfig wantErr bool }{ @@ -277,6 +278,23 @@ func TestReadConfig(t *testing.T) { args: []string{"--param00"}, wantErr: false, }, + { // 20 + inFile: "testData/test_config_3.yaml", + values: map[string]any{ + "secret": "pass0", + }, + want: TestConfig{ + ID: 9, + Name: "Name1", + Conn: &TestConn{ + URL: "https://www.tests.to", + Passes: []string{ + "pass0", + }, + }, + }, + wantErr: false, + }, } for testIndex, test := range tests { @@ -299,18 +317,26 @@ func TestReadConfig(t *testing.T) { os.Args = append(os.Args, test.args...) } + options := []templig.Option[TestConfig]{} + + for k, v := range test.values { + options = append(options, templig.WithValue[TestConfig](k, v)) + } + var config *templig.Config[TestConfig] var fromErr error switch { case len(test.in) > 0: - config, fromErr = templig.From[TestConfig](&testBuf) + options = append(options, templig.WithReader[TestConfig](&testBuf)) case len(test.inFile) > 0: - config, fromErr = templig.FromFile[TestConfig](test.inFile) + options = append(options, templig.WithFile[TestConfig](test.inFile)) default: t.Errorf("%v: neither input data nor input file given", testIndex) } + config, fromErr = templig.New[TestConfig](options...) + if !test.wantErr && config.SecretRE() == nil { t.Errorf("%v: wanted SecretRE to be initialized", testIndex) } @@ -391,10 +417,10 @@ func TestNoReaders(t *testing.T) { func TestReadOverlayConfig(t *testing.T) { t.Parallel() - config, configErr := templig.FromFile[TestConfig]( - "testData/test_config_0.yaml", - "testData/test_config_0_overlay.yaml", - ) + config, configErr := templig.New[TestConfig]( + templig.WithValue[TestConfig]("pass2", "pass2"), + templig.WithFile[TestConfig]("testData/test_config_0.yaml"), + templig.WithFile[TestConfig]("testData/test_config_0_overlay.yaml")) if configErr != nil { t.Errorf("no error expected reading multiple files: %v", configErr) @@ -419,7 +445,9 @@ func TestReadOverlayConfigReader(t *testing.T) { f0, _ := os.Open("testData/test_config_0.yaml") f1, _ := os.Open("testData/test_config_0_overlay.yaml") - config, configErr := templig.From[TestConfig](f0, f1) + config, configErr := templig.New[TestConfig]( + templig.WithValue[TestConfig]("pass2", "pass2"), + templig.WithReader[TestConfig](f0, f1)) if configErr != nil { t.Errorf("no error expected reading multiple files: %v", configErr) @@ -564,6 +592,18 @@ func TestNoFilesDeprecated(t *testing.T) { } } +func TestNoSources(t *testing.T) { + t.Parallel() + + _, err := templig.New[TestConfig]() + + if !errors.Is(err, templig.ErrNoConfigPaths) || + !errors.Is(err, templig.ErrNoConfigReaders) { + + t.Errorf("reading from broken reader should have returned an error") + } +} + func TestBrokenWriter(t *testing.T) { t.Parallel() @@ -818,9 +858,11 @@ func TestSetSecretRE(t *testing.T) { func TestSetSecretRENil(t *testing.T) { t.Parallel() - c, _ := templig.FromFile[TestConfig]("testData/test_config_0.yaml") + _, err := templig.New[TestConfig]( + templig.WithFile[TestConfig]("testData/test_config_0.yaml"), + templig.WithSecretRE[TestConfig](nil)) - if err := c.SetSecretRE(nil); err == nil { + if !errors.Is(err, templig.ErrNoSecretRegexp) { t.Errorf("setting secret regex to nil should return an error") } } @@ -841,7 +883,8 @@ func TestDefaultSecretRENil(t *testing.T) { templig.SecretRE = nil - if _, err := templig.FromFile[TestConfig]("testData/test_config_0.yaml"); err == nil { + if _, err := templig.FromFile[TestConfig]( + "testData/test_config_0.yaml"); !errors.Is(err, templig.ErrNoSecretRegexp) { t.Errorf("reading config with default secret regex nil should return an error") } diff --git a/examples/templating/env/main_test.go b/examples/templating/env/main_test.go index 203e2d0..b367a07 100644 --- a/examples/templating/env/main_test.go +++ b/examples/templating/env/main_test.go @@ -18,7 +18,5 @@ func TestMainGood(t *testing.T) { func TestMainBad(t *testing.T) { t.Parallel() - os.Args = []string{"main"} - main() } diff --git a/examples/templating/value/main.go b/examples/templating/value/main.go new file mode 100644 index 0000000..bec3c3a --- /dev/null +++ b/examples/templating/value/main.go @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 The templig contributors. +// SPDX-License-Identifier: MPL-2.0 + +// Package main of the templating variable `.Value` example. +// This example demonstrates the use of the `.Value` variable in a templated configuration. +// The use of the `required` function is demonstrated in conjunction with `.Value`. +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/AlphaOne1/templig" +) + +// Config is the configuration structure that is to be filled by templig. +type Config struct { + ID int `yaml:"id"` + Name string `yaml:"name"` + Pass string `yaml:"pass"` +} + +// main reads a configuration file. The configuration file then uses the .Value variable to read the password. +// To insert a custom variable into the configuration, the WithValue option is used. +func main() { + cfg, confErr := templig.New[Config]( + templig.WithFile[Config]("my_config.yaml"), + templig.WithValue[Config]("pass", "secret")) + + fmt.Printf("read errors: %v\n", confErr) + + if confErr == nil { + fmt.Printf("ID: %v\n", cfg.Get().ID) + fmt.Printf("Name: %v\n", cfg.Get().Name) + fmt.Printf("Pass: %v\n", strings.Repeat("*", len(cfg.Get().Pass))) + fmt.Println("Config printed by templig with hidden secrets:") + + _ = cfg.ToSecretsHiddenStructured(os.Stdout) + } +} diff --git a/examples/templating/value/main_test.go b/examples/templating/value/main_test.go new file mode 100644 index 0000000..0c71162 --- /dev/null +++ b/examples/templating/value/main_test.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 The templig contributors. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "testing" +) + +func TestMainGood(t *testing.T) { + t.Parallel() + + main() +} diff --git a/examples/templating/value/my_config.yaml b/examples/templating/value/my_config.yaml new file mode 100644 index 0000000..b4359a6 --- /dev/null +++ b/examples/templating/value/my_config.yaml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2026 The templig contributors. +# SPDX-License-Identifier: MPL-2.0 + +id: 23 +name: Interesting Name +pass: {{ .Values.pass | required "pass value required" | quote }} \ No newline at end of file diff --git a/go.mod b/go.mod index 5b4eb9c..9fafc84 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.38.0 // indirect - golang.org/x/tools v0.46.0 // indirect + golang.org/x/tools v0.47.0 // indirect gotest.tools/gotestsum v1.13.0 // indirect ) diff --git a/go.sum b/go.sum index 8273b0a..c90b0de 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools/gotestsum v1.13.0 h1:+Lh454O9mu9AMG1APV4o0y7oDYKyik/3kBOiCqiEpRo= diff --git a/testData/test_config_0_overlay.yaml b/testData/test_config_0_overlay.yaml index d422f4a..963d59c 100644 --- a/testData/test_config_0_overlay.yaml +++ b/testData/test_config_0_overlay.yaml @@ -3,4 +3,4 @@ conn: passes: - - pass2 \ No newline at end of file + - {{ .Values.pass2 | default "unknown" | quote }} \ No newline at end of file diff --git a/testData/test_config_3.yaml b/testData/test_config_3.yaml new file mode 100644 index 0000000..f7f0f83 --- /dev/null +++ b/testData/test_config_3.yaml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 The templig contributors. +# SPDX-License-Identifier: MPL-2.0 + +id: 9 +name: Name1 +conn: + url: https://www.tests.to + passes: + - {{ .Values.secret | required "secret.txt must be readable" | quote }} \ No newline at end of file