diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 16:32:52 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 16:32:52 +0000 |
commit | 4d089f085623abbb6c29fe8ac2874b7bf88f1884 (patch) | |
tree | 0fce6046b135720c1f551a852d0672a71b40acb8 /internal | |
parent | Initial commit. (diff) | |
download | golang-github-containers-common-4d089f085623abbb6c29fe8ac2874b7bf88f1884.tar.xz golang-github-containers-common-4d089f085623abbb6c29fe8ac2874b7bf88f1884.zip |
Adding upstream version 0.57.4+ds1.upstream/0.57.4+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'internal')
-rw-r--r-- | internal/attributedstring/slice.go | 102 | ||||
-rw-r--r-- | internal/attributedstring/slice_test.go | 127 |
2 files changed, 229 insertions, 0 deletions
diff --git a/internal/attributedstring/slice.go b/internal/attributedstring/slice.go new file mode 100644 index 0000000..ad4acc5 --- /dev/null +++ b/internal/attributedstring/slice.go @@ -0,0 +1,102 @@ +package attributedstring + +import ( + "bytes" + "fmt" + + "github.com/BurntSushi/toml" +) + +// Slice allows for extending a TOML string array with custom +// attributes that control how the array is marshaled into a Go string. +// +// Specifically, an Slice can be configured to avoid it being +// overridden by a subsequent unmarshal sequence. When the `append` attribute +// is specified, the array will be appended instead (e.g., `array=["9", +// {append=true}]`). +type Slice struct { // A "mixed-type array" in TOML. + // Note that the fields below _must_ be exported. Otherwise the TOML + // encoder would fail during type reflection. + Values []string + Attributes struct { // Using a struct allows for adding more attributes in the future. + Append *bool // Nil if not set by the user + } +} + +// NewSlice creates a new slice with the specified values. +func NewSlice(values []string) Slice { + return Slice{Values: values} +} + +// Get returns the Slice values or an empty string slice. +func (a *Slice) Get() []string { + if a.Values == nil { + return []string{} + } + return a.Values +} + +// Set overrides the values of the Slice. +func (a *Slice) Set(values []string) { + a.Values = values +} + +// UnmarshalTOML is the custom unmarshal method for Slice. +func (a *Slice) UnmarshalTOML(data interface{}) error { + iFaceSlice, ok := data.([]interface{}) + if !ok { + return fmt.Errorf("unable to cast to interface array: %v", data) + } + + var loadedStrings []string + for _, x := range iFaceSlice { // Iterate over each item in the slice. + switch val := x.(type) { + case string: // Strings are directly appended to the slice. + loadedStrings = append(loadedStrings, val) + case map[string]interface{}: // The attribute struct is represented as a map. + for k, v := range val { // Iterate over all _supported_ keys. + switch k { + case "append": + boolVal, ok := v.(bool) + if !ok { + return fmt.Errorf("unable to cast append to bool: %v", k) + } + a.Attributes.Append = &boolVal + default: // Unsupported map key. + return fmt.Errorf("unsupported key %q in map: %v", k, val) + } + } + default: // Unsupported item. + return fmt.Errorf("unsupported item in attributed string slice: %v", x) + } + } + + if a.Attributes.Append != nil && *a.Attributes.Append { // If _explicitly_ configured, append the loaded slice. + a.Values = append(a.Values, loadedStrings...) + } else { // Default: override the existing Slice. + a.Values = loadedStrings + } + return nil +} + +// MarshalTOML is the custom marshal method for Slice. +func (a *Slice) MarshalTOML() ([]byte, error) { + iFaceSlice := make([]interface{}, 0, len(a.Values)) + + for _, x := range a.Values { + iFaceSlice = append(iFaceSlice, x) + } + + if a.Attributes.Append != nil { + Attributes := make(map[string]any) + Attributes["append"] = *a.Attributes.Append + iFaceSlice = append(iFaceSlice, Attributes) + } + + buf := new(bytes.Buffer) + enc := toml.NewEncoder(buf) + if err := enc.Encode(iFaceSlice); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/internal/attributedstring/slice_test.go b/internal/attributedstring/slice_test.go new file mode 100644 index 0000000..dd2c636 --- /dev/null +++ b/internal/attributedstring/slice_test.go @@ -0,0 +1,127 @@ +package attributedstring + +import ( + "bytes" + "testing" + + "github.com/BurntSushi/toml" + "github.com/stretchr/testify/require" +) + +type testConfig struct { + Array Slice `toml:"array,omitempty"` +} + +const ( + confingDefault = `array=["1", "2", "3"]` + configAppendFront = `array=[{append=true},"4", "5", "6"]` + configAppendMid = `array=["7", {append=true}, "8"]` + configAppendBack = `array=["9", {append=true}]` + configAppendFalse = `array=["10", {append=false}]` +) + +var ( + bTrue = true + bFalse = false +) + +func loadConfigs(configs []string) (*testConfig, error) { + var config testConfig + for _, c := range configs { + if _, err := toml.Decode(c, &config); err != nil { + return nil, err + } + } + return &config, nil +} + +func TestSliceLoading(t *testing.T) { + for _, test := range []struct { + configs []string + expectedValues []string + expectedAppend *bool + expectedErrorSubstring string + }{ + // Load single configs + {[]string{confingDefault}, []string{"1", "2", "3"}, nil, ""}, + {[]string{configAppendFront}, []string{"4", "5", "6"}, &bTrue, ""}, + {[]string{configAppendMid}, []string{"7", "8"}, &bTrue, ""}, + {[]string{configAppendBack}, []string{"9"}, &bTrue, ""}, + {[]string{configAppendFalse}, []string{"10"}, &bFalse, ""}, + // Append=true + {[]string{confingDefault, configAppendFront}, []string{"1", "2", "3", "4", "5", "6"}, &bTrue, ""}, + {[]string{configAppendFront, confingDefault}, []string{"4", "5", "6", "1", "2", "3"}, &bTrue, ""}, // The attribute is sticky unless explicitly being turned off in a later config + {[]string{configAppendFront, confingDefault, configAppendBack}, []string{"4", "5", "6", "1", "2", "3", "9"}, &bTrue, ""}, + // Append=false + {[]string{confingDefault, configAppendFalse}, []string{"10"}, &bFalse, ""}, + {[]string{confingDefault, configAppendMid, configAppendFalse}, []string{"10"}, &bFalse, ""}, + {[]string{confingDefault, configAppendFalse, configAppendMid}, []string{"10", "7", "8"}, &bTrue, ""}, // Append can be re-enabled by a later config + + // Error checks + {[]string{`array=["1", false]`}, nil, nil, `unsupported item in attributed string slice: false`}, + {[]string{`array=["1", 42]`}, nil, nil, `unsupported item in attributed string slice: 42`}, // Stop a `int` such that it passes on 32bit as well + {[]string{`array=["1", {foo=true}]`}, nil, nil, `unsupported key "foo" in map: `}, + {[]string{`array=["1", {append="false"}]`}, nil, nil, `unable to cast append to bool: `}, + } { + result, err := loadConfigs(test.configs) + if test.expectedErrorSubstring != "" { + require.Error(t, err, "test is expected to fail: %v", test) + require.ErrorContains(t, err, test.expectedErrorSubstring, "error does not match: %v", test) + continue + } + require.NoError(t, err, "test is expected to succeed: %v", test) + require.NotNil(t, result, "loaded config must not be nil: %v", test) + require.Equal(t, result.Array.Values, test.expectedValues, "slices do not match: %v", test) + require.Equal(t, result.Array.Attributes.Append, test.expectedAppend, "append field does not match: %v", test) + } +} + +func TestSliceEncoding(t *testing.T) { + for _, test := range []struct { + configs []string + marshalledData string + expectedValues []string + expectedAppend *bool + }{ + { + []string{confingDefault}, + "array = [\"1\", \"2\", \"3\"]\n", + []string{"1", "2", "3"}, + nil, + }, + { + []string{configAppendFront}, + "array = [\"4\", \"5\", \"6\", {append = true}]\n", + []string{"4", "5", "6"}, + &bTrue, + }, + { + []string{configAppendFront, configAppendFalse}, + "array = [\"10\", {append = false}]\n", + []string{"10"}, + &bFalse, + }, + } { + // 1) Load the configs + result, err := loadConfigs(test.configs) + require.NoError(t, err, "loading config must succeed") + require.NotNil(t, result, "loaded config must not be nil") + require.Equal(t, result.Array.Values, test.expectedValues, "slices do not match: %v", test) + require.Equal(t, result.Array.Attributes.Append, test.expectedAppend, "append field does not match: %v", test) + + // 2) Marshal the config to emulate writing it to disk + buf := new(bytes.Buffer) + enc := toml.NewEncoder(buf) + encErr := enc.Encode(result) + require.NoError(t, encErr, "encoding config must work") + require.Equal(t, buf.String(), test.marshalledData) + + // 3) Reload the marshaled config to make sure that data is preserved + var reloadedConfig testConfig + _, decErr := toml.Decode(buf.String(), &reloadedConfig) + require.NoError(t, decErr, "loading config must succeed") + require.NotNil(t, reloadedConfig, "re-loaded config must not be nil") + require.Equal(t, reloadedConfig.Array.Values, test.expectedValues, "slices do not match: %v", test) + require.Equal(t, reloadedConfig.Array.Attributes.Append, test.expectedAppend, "append field does not match: %v", test) + } +} |