summaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:32:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:32:52 +0000
commit4d089f085623abbb6c29fe8ac2874b7bf88f1884 (patch)
tree0fce6046b135720c1f551a852d0672a71b40acb8 /internal
parentInitial commit. (diff)
downloadgolang-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.go102
-rw-r--r--internal/attributedstring/slice_test.go127
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)
+ }
+}