diff --git a/useragent/from.go b/useragent/from.go new file mode 100644 index 00000000..e0a45347 --- /dev/null +++ b/useragent/from.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package useragent + +import ( + "strings" + + "github.com/hashicorp/aws-sdk-go-base/v2/internal/config" + "github.com/hashicorp/aws-sdk-go-base/v2/internal/slices" +) + +// FromSlice applies the conversion defined in [fromAny] to all elements +// of a slice +// +// Slices of types which cannot assert to a string, empty string values, and string +// values which do not match the expected `{product}/{version} ({comment})` +// pattern (where version and comment are optional) return a zero value struct. +func FromSlice[T any](sl []T) config.UserAgentProducts { + return slices.ApplyToAll(sl, func(v T) config.UserAgentProduct { return from(v) }) +} + +func from[T any](v T) config.UserAgentProduct { + if s, ok := any(v).(string); ok { + parts := strings.Split(s, "/") + switch len(parts) { + case 1: + return config.UserAgentProduct{Name: parts[0]} + case 2: //nolint: mnd + subparts := strings.Split(parts[1], "(") + if len(subparts) == 2 { //nolint: mnd + version := strings.TrimSpace(subparts[0]) + comment := strings.TrimSuffix(subparts[1], ")") + return config.UserAgentProduct{Name: parts[0], Version: version, Comment: comment} + } + return config.UserAgentProduct{Name: parts[0], Version: parts[1]} + } + } + + return config.UserAgentProduct{} +} diff --git a/useragent/from_test.go b/useragent/from_test.go new file mode 100644 index 00000000..e2215936 --- /dev/null +++ b/useragent/from_test.go @@ -0,0 +1,145 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package useragent + +import ( + "reflect" + "testing" + + "github.com/hashicorp/aws-sdk-go-base/v2/internal/config" +) + +func TestFromSlice(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + s []any + want config.UserAgentProducts + }{ + { + "nil", + nil, + config.UserAgentProducts{}, + }, + { + "non-string element", + []any{false}, + config.UserAgentProducts{config.UserAgentProduct{}}, + }, + { + "valid string", + []any{"my-product/v1.2.3"}, + config.UserAgentProducts{ + config.UserAgentProduct{ + Name: "my-product", + Version: "v1.2.3", + }, + }, + }, + { + "valid and invalid string", + []any{"my-product/v1.2.3", "foo/bar/baz/qux"}, + config.UserAgentProducts{ + config.UserAgentProduct{ + Name: "my-product", + Version: "v1.2.3", + }, + config.UserAgentProduct{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := FromSlice(tt.s) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FromSlice() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func Test_from(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + v any + want config.UserAgentProduct + }{ + { + "nil", + nil, + config.UserAgentProduct{}, + }, + { + "non-string", + 1, + config.UserAgentProduct{}, + }, + { + "empty", + "", + config.UserAgentProduct{}, + }, + { + "name only", + "my-product", + config.UserAgentProduct{ + Name: "my-product", + }, + }, + { + "name and version", + "my-product/v1.2.3", + config.UserAgentProduct{ + Name: "my-product", + Version: "v1.2.3", + }, + }, + { + "name, version, and comment", + "my-product/v1.2.3 (a comment)", + config.UserAgentProduct{ + Name: "my-product", + Version: "v1.2.3", + Comment: "a comment", + }, + }, + { + "comment malformed closing", + "my-product/v1.2.3 (a comment", + config.UserAgentProduct{ + Name: "my-product", + Version: "v1.2.3", + Comment: "a comment", + }, + }, + { + "comment missing parenthesis", + "my-product/v1.2.3 a comment", + // This is a known edge case, but the processed output will render identical + // to the original input despite the version and comment merging. + config.UserAgentProduct{ + Name: "my-product", + Version: "v1.2.3 a comment", + }, + }, + { + "all the slash", + "foo/bar/baz/qux", + config.UserAgentProduct{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := from(tt.v) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("from() = %+v, want %+v", got, tt.want) + } + }) + } +}