diff --git a/azcfg.go b/azcfg.go index 330a87c..277cd40 100644 --- a/azcfg.go +++ b/azcfg.go @@ -12,9 +12,10 @@ import ( ) const ( - secretTag = "secret" - settingTag = "setting" - requiredTag = "required" + // secretTag is the name of the tag for secrets. + secretTag = "secret" + // settingTag is the name of the tag for settings. + settingTag = "setting" ) // Parse secrets from an Azure Key Vault and settings from an @@ -27,14 +28,8 @@ func Parse(ctx context.Context, v any, options ...Option) error { return parser.Parse(ctx, v) } -// parseOptions contains options for the parser. -type parseOptions struct { - secretClient secretClient - settingClient settingClient -} - // Parse secrets into the configuration. -func parse(ctx context.Context, d any, opts parseOptions) error { +func parse(ctx context.Context, d any, secretClient secretClient, settingClient settingClient) error { v := reflect.ValueOf(d) if v.Kind() != reflect.Pointer { return errors.New("must provide a pointer to a struct") @@ -44,13 +39,10 @@ func parse(ctx context.Context, d any, opts parseOptions) error { return errors.New("provided value is not a struct") } + values := &values{v: make(map[string]string)} var wg sync.WaitGroup - secretsCh := make(chan map[string]Secret) - settingsCh := make(chan map[string]Setting) errCh := make(chan error) - done := make(chan struct{}) - secretClient := opts.secretClient secretFields, requiredSecrets := getFields(v, secretTag) if len(secretFields) > 0 { if secretClient == nil { @@ -66,12 +58,10 @@ func parse(ctx context.Context, d any, opts parseOptions) error { errCh <- fmt.Errorf("%w: %s", ErrSecretRetrieval, err.Error()) return } - secretsCh <- secrets - close(secretsCh) + values.addSecrets(secrets) }() } - settingClient := opts.settingClient settingFields, requiredSettings := getFields(v, settingTag) if len(settingFields) > 0 { if settingClient == nil { @@ -87,50 +77,30 @@ func parse(ctx context.Context, d any, opts parseOptions) error { errCh <- fmt.Errorf("%w: %s", ErrSettingRetrieval, err.Error()) return } - settingsCh <- settings - close(settingsCh) + values.addSettings(settings) }() } go func() { wg.Wait() - done <- struct{}{} - close(done) close(errCh) }() var errs []error - for { - select { - case secrets := <-secretsCh: - if len(secrets) > 0 { - if err := setFields(v, secrets, secretTag); err != nil { - if errors.Is(err, errRequired) { - err = requiredSecretsError{message: requiredErrorMessage(secrets, requiredSecrets, "secret")} - } - errs = append(errs, err) - } - } - case settings := <-settingsCh: - if len(settings) > 0 { - if err := setFields(v, settings, settingTag); err != nil { - if errors.Is(err, errRequired) { - err = requiredSettingsError{message: requiredErrorMessage(settings, requiredSettings, "setting")} - } - errs = append(errs, err) - } - } - case err := <-errCh: - if err != nil { - errs = append(errs, err) - } - case <-done: - if len(errs) > 0 { - return buildErr(errs...) - } - return nil + for err := range errCh { + errs = append(errs, err) + } + if len(errs) > 0 { + return newError(errs...) + } + + if err := setFields(v, values.v); err != nil { + if errors.Is(err, errRequired) { + return newRequiredFieldsError(values.v, requiredFields{f: requiredSecrets, t: "secret"}, requiredFields{f: requiredSettings, t: "setting"}) } + return newError(err) } + return nil } // getFields gets fields with the specified tag. @@ -160,49 +130,54 @@ func getFields(v reflect.Value, tag string) ([]string, []string) { return fields, required } -// hasValue wraps around method GetValue, -type hasValue interface { - GetValue() string +// fieldFromTag gets the field from the tag. +func fieldFromTag(f reflect.StructField, tags ...string) (string, bool) { + for _, tag := range tags { + if v, ok := f.Tag.Lookup(tag); ok { + return v, true + } + } + return "", false } // setFields sets the values from the map into the struct fields. -func setFields[V hasValue](v reflect.Value, values map[string]V, tag string) error { +func setFields(v reflect.Value, values map[string]string) error { t := v.Type() for i := 0; i < v.NumField(); i++ { if !v.Field(i).CanSet() { continue } if v.Field(i).Kind() == reflect.Pointer && v.Field(i).Elem().Kind() == reflect.Struct { - if err := setFields(v.Field(i).Elem(), values, tag); err != nil { + if err := setFields(v.Field(i).Elem(), values); err != nil { return err } } else if v.Field(i).Kind() == reflect.Struct { - if err := setFields(v.Field(i), values, tag); err != nil { + if err := setFields(v.Field(i), values); err != nil { return err } } else { - value, ok := t.Field(i).Tag.Lookup(tag) + field, ok := fieldFromTag(t.Field(i), secretTag, settingTag) if !ok { continue } - tags := strings.Split(value, ",") - if val, ok := values[tags[0]]; ok { - if len(val.GetValue()) == 0 && isRequired(tags) { + tags := strings.Split(field, ",") + if value, ok := values[tags[0]]; ok { + if len(value) == 0 && isRequired(tags) { return errRequired - } else if len(val.GetValue()) == 0 { + } else if len(value) == 0 { continue } if v.Field(i).Kind() == reflect.Slice { - vals := splitTrim(val.GetValue(), ",") - sl := reflect.MakeSlice(v.Field(i).Type(), len(vals), len(vals)) - for j := 0; j < sl.Cap(); j++ { - if err := setValue(sl.Index(j), vals[j]); err != nil { + values := splitTrim(value, ",") + slice := reflect.MakeSlice(v.Field(i).Type(), len(values), len(values)) + for j := 0; j < slice.Cap(); j++ { + if err := setValue(slice.Index(j), values[j]); err != nil { return fmt.Errorf("%w: field %s: %s", ErrSetValue, t.Field(i).Name, err.Error()) } } - v.Field(i).Set(sl) + v.Field(i).Set(slice) } else { - if err := setValue(v.Field(i), val.GetValue()); err != nil { + if err := setValue(v.Field(i), value); err != nil { return fmt.Errorf("%w: field %s: %s", ErrSetValue, t.Field(i).Name, err.Error()) } } @@ -296,10 +271,44 @@ func isRequired(values []string) bool { if len(values) == 1 { return false } - return values[1] == requiredTag + return values[1] == "required" } // parseError returns a new error with the provided type. func parseError(typ string) error { return fmt.Errorf("could not parse value into type %s", typ) } + +// values contains the values of the secrets and settings. Used to +// create a common thread-safe map for the values to be used +// when setting the fields. +type values struct { + v map[string]string + mu sync.RWMutex +} + +// addSecrets adds the secrets to the values. +func (v *values) addSecrets(secrets map[string]Secret) { + v.mu.Lock() + defer v.mu.Unlock() + + for k, val := range secrets { + v.v[k] = val.Value + } +} + +// addSettings adds the settings to the values. +func (v *values) addSettings(settings map[string]Setting) { + v.mu.Lock() + defer v.mu.Unlock() + + for k, val := range settings { + v.v[k] = val.Value + } +} + +// requiredFields contains the fields and the type of the required fields. +type requiredFields struct { + f []string + t string +} diff --git a/azcfg_test.go b/azcfg_test.go index 80b4a5e..397a52f 100644 --- a/azcfg_test.go +++ b/azcfg_test.go @@ -194,7 +194,7 @@ func TestParse(t *testing.T) { secretClient := newMockSecretClient(responseSecrets, test.wantErr) settingClient := newMockSettingClient(responseSettings, test.wantErr) - gotErr := parse(context.Background(), &test.input, parseOptions{secretClient: secretClient, settingClient: settingClient}) + gotErr := parse(context.Background(), &test.input, secretClient, settingClient) if diff := cmp.Diff(test.want, test.input, cmp.AllowUnexported(Struct{})); diff != "" { t.Errorf("parse() = unexpected result, (-want, +got)\n%s\n", diff) } @@ -216,10 +216,9 @@ func TestParseRequired(t *testing.T) { name: "required", input: StructWithRequired{}, wantErr: &RequiredFieldsError{ - errors: []error{ - requiredSecretsError{message: requiredErrorMessage(map[string]Secret{"empty": {}, "empty-float64": {}}, []string{"empty", "empty-float64"}, "secret")}, - requiredSettingsError{message: requiredErrorMessage(map[string]Setting{"empty-setting": {}}, []string{"empty-setting"}, "setting")}, - }, + message: "secrets: empty and empty-float64 are required\nsetting: empty-setting is required", + required: []string{"empty", "empty-float64", "empty-setting"}, + missing: []string{"empty", "empty-float64", "empty-setting"}, }, }, } @@ -229,7 +228,7 @@ func TestParseRequired(t *testing.T) { secretClient := mockSecretClient{secrets: responseSecrets} settingClient := mockSettingClient{settings: responseSettings} - gotErr := parse(context.Background(), &test.input, parseOptions{secretClient: secretClient, settingClient: settingClient}) + gotErr := parse(context.Background(), &test.input, secretClient, settingClient) if test.wantErr != nil && gotErr == nil { t.Errorf("Unexpected result, should return error\n") } diff --git a/errors.go b/errors.go index 3ba6f44..f40f4d0 100644 --- a/errors.go +++ b/errors.go @@ -33,7 +33,7 @@ type Error struct { // Error returns the combined error messages from the errors // contained in Error. -func (e Error) Error() string { +func (e *Error) Error() string { var errs []string for _, err := range e.errors { errs = append(errs, err.Error()) @@ -42,18 +42,18 @@ func (e Error) Error() string { } // Errors returns the errors contained in Error. -func (e Error) Errors() []error { +func (e *Error) Errors() []error { return e.errors } // Len returns the number of errors contained in Error. -func (e Error) Len() int { +func (e *Error) Len() int { return len(e.errors) } // Has returns true if the provided error type is found in the errors. // If found, the first error of the provided type is returned. -func (e Error) Has(err error) (error, bool) { +func (e *Error) Has(err error) (error, bool) { for _, e := range e.errors { if errors.Is(e, err) { return e, true @@ -62,98 +62,87 @@ func (e Error) Has(err error) (error, bool) { return nil, false } +// newError creates a new Error with the provided errors. +func newError(errs ...error) *Error { + if len(errs) == 0 { + return nil + } + + e := &Error{} + e.errors = append(e.errors, errs...) + return e +} + // RequiredFieldsError represents an error when either secrets or settings // are required but not set. type RequiredFieldsError struct { - errors []error + message string + required []string + missing []string } // Error returns the combined error messages from the errors // contained in RequiredFieldsError. -func (e RequiredFieldsError) Error() string { - var msgs []string - for _, err := range e.errors { - msgs = append(msgs, err.Error()) - } - return strings.Join(msgs, "\n") -} - -// requiredSecretsError represents an error when secrets are required -// but not set. -type requiredSecretsError struct { - message string -} - -// Error returns the message set in requiredSecretsError. -func (e requiredSecretsError) Error() string { +func (e *RequiredFieldsError) Error() string { return e.message } -// requiredSettingsError represents an error when settings are required -// but not set. -type requiredSettingsError struct { - message string +// Required returns the fields that are required. +func (e *RequiredFieldsError) Required() []string { + return e.required } -// Error returns the message set in requiredSettingsError. -func (e requiredSettingsError) Error() string { - return e.message +// Missing returns the fields that are missing the required values. +func (e *RequiredFieldsError) Missing() []string { + return e.missing } -// requiredErrorMessage builds a message based on the provided map[string]V (HasValue) -// and []string (required). -func requiredErrorMessage[V hasValue](values map[string]V, required []string, t string) string { - if len(required) == 0 { - return "" - } - - req := make([]string, 0) - l := 0 - for _, r := range required { - if len(values[r].GetValue()) == 0 { - req = append(req, r) - l++ - } +// newRequiredFieldsError creates a new RequiredFieldsError. +func newRequiredFieldsError(values map[string]string, requiredFields ...requiredFields) *RequiredFieldsError { + if len(requiredFields) == 0 { + return nil } - var message strings.Builder - if l == 1 { - message.WriteString(t + ": " + req[0] + " is required") - return message.String() - } - message.WriteString(t + "s: ") - for i, r := range req { - message.WriteString(r) - if i < l-1 && l > 2 && i != l-2 { - message.WriteString(", ") + var messages []string + var required []string + var missing []string + for _, rfs := range requiredFields { + if len(rfs.f) == 0 { + continue } - if i == l-2 { - message.WriteString(" and ") + required = append(required, rfs.f...) + l := 0 + mfs := make([]string, 0) + for _, r := range rfs.f { + if len(values[r]) == 0 { + mfs = append(mfs, r) + l++ + } } - } - message.WriteString(" are required") - return message.String() -} + missing = append(missing, mfs...) -// buildErr builds the resulting error and returns it. -func buildErr(errs ...error) error { - if len(errs) == 0 { - return nil - } - - var reqErr RequiredFieldsError - var e Error - for _, err := range errs { - var reqSecretsErr requiredSecretsError - var reqSettingsErr requiredSettingsError - if errors.As(err, &reqSecretsErr) || errors.As(err, &reqSettingsErr) { - reqErr.errors = append(reqErr.errors, err) + var message strings.Builder + if l == 1 { + message.WriteString(rfs.t + ": " + mfs[0] + " is required") } else { - e.errors = append(e.errors, err) + message.WriteString(rfs.t + "s: ") + for i, r := range mfs { + message.WriteString(r) + if i < l-1 && l > 2 && i != l-2 { + message.WriteString(", ") + } + if i == l-2 { + message.WriteString(" and ") + } + } + message.WriteString(" are required") } + messages = append(messages, message.String()) } - if len(reqErr.errors) > 0 { - return reqErr + + return &RequiredFieldsError{ + message: strings.Join(messages, "\n"), + required: required, + missing: missing, } - return e } diff --git a/errors_test.go b/errors_test.go index 215f6ba..f54e95c 100644 --- a/errors_test.go +++ b/errors_test.go @@ -4,171 +4,336 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" ) -func TestRequiredErrorMessage_Secret(t *testing.T) { +func TestNewError(t *testing.T) { + var tests = []struct { + name string + input []error + want *Error + }{ + { + name: "no errors", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := newError(test.input...) + + if diff := cmp.Diff(test.want, got, cmp.AllowUnexported(Error{})); diff != "" { + t.Errorf("newError() = unexpected result (-want +got)\n%s\n", diff) + } + + if test.want != nil { + if test.want.Len() != got.Len() { + t.Errorf("Len() = unexpected result, want: %d, got: %d\n", test.want.Len(), got.Len()) + } + } + }) + } +} + +func TestError_Has(t *testing.T) { + var tests = []struct { + name string + input error + want struct { + err error + ok bool + } + }{ + { + name: "has error", + input: ErrCredential, + want: struct { + err error + ok bool + }{ + err: ErrCredential, + ok: true, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := newError(test.input) + got, gotOk := err.Has(test.input) + + if diff := cmp.Diff(test.want.err, got, cmpopts.EquateErrors()); diff != "" { + t.Errorf("Has() = unexpected result (-want +got)\n%s\n", diff) + } + + if test.want.ok != gotOk { + t.Errorf("Has() = unexpected result, want: %t, got: %t\n", test.want.ok, gotOk) + } + }) + } +} + +func TestNewRequiredFieldsError(t *testing.T) { var tests = []struct { name string input struct { - secrets map[string]Secret - required []string + values map[string]string + requiredFields []requiredFields } - want string + want *RequiredFieldsError }{ { name: "no required", input: struct { - secrets map[string]Secret - required []string + values map[string]string + requiredFields []requiredFields }{ - secrets: map[string]Secret{}, - required: []string{}, + values: map[string]string{}, + requiredFields: []requiredFields{}, }, - want: "", + want: nil, }, { - name: "1 required", + name: "1 secret is required", input: struct { - secrets map[string]Secret - required []string + values map[string]string + requiredFields []requiredFields }{ - secrets: map[string]Secret{ - "secret1": {Value: ""}, + values: map[string]string{ + "secret1": "", }, - required: []string{ - "secret1", + requiredFields: []requiredFields{ + { + f: []string{"secret1"}, + t: "secret", + }, }, }, - want: "secret: secret1 is required", + want: &RequiredFieldsError{ + message: "secret: secret1 is required", + required: []string{"secret1"}, + missing: []string{"secret1"}, + }, }, { - name: "2 required", + name: "2 secrets are required", input: struct { - secrets map[string]Secret - required []string + values map[string]string + requiredFields []requiredFields }{ - secrets: map[string]Secret{ - "secret1": {Value: ""}, - "secret2": {Value: ""}, + values: map[string]string{ + "secret1": "", + "secret2": "", }, - required: []string{ - "secret1", - "secret2", + requiredFields: []requiredFields{ + { + f: []string{"secret1", "secret2"}, + t: "secret", + }, }, }, - want: "secrets: secret1 and secret2 are required", + want: &RequiredFieldsError{ + message: "secrets: secret1 and secret2 are required", + required: []string{"secret1", "secret2"}, + missing: []string{"secret1", "secret2"}, + }, }, { - name: "3 required", + name: "4 secrets are required", input: struct { - secrets map[string]Secret - required []string + values map[string]string + requiredFields []requiredFields }{ - secrets: map[string]Secret{ - "secret1": {Value: ""}, - "secret2": {Value: ""}, - "secret3": {Value: ""}, + values: map[string]string{ + "secret1": "", + "secret2": "", + "secret3": "", + "secret4": "", }, - required: []string{ - "secret1", - "secret2", - "secret3", + requiredFields: []requiredFields{ + { + f: []string{"secret1", "secret2", "secret3", "secret4"}, + t: "secret", + }, }, }, - want: "secrets: secret1, secret2 and secret3 are required", + want: &RequiredFieldsError{ + message: "secrets: secret1, secret2, secret3 and secret4 are required", + required: []string{"secret1", "secret2", "secret3", "secret4"}, + missing: []string{"secret1", "secret2", "secret3", "secret4"}, + }, }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := requiredErrorMessage(test.input.secrets, test.input.required, "secret") - - if diff := cmp.Diff(test.want, got); diff != "" { - t.Errorf("requiredErrorMessage() = unexpected result, (-want, +got)\n%s\n", diff) - } - }) - } -} - -func TestRequiredErrorMessage_Setting(t *testing.T) { - var tests = []struct { - name string - input struct { - settings map[string]Setting - required []string - } - want string - }{ { - name: "no required", + name: "4 secrets are required, 2 missing", + input: struct { + values map[string]string + requiredFields []requiredFields + }{ + values: map[string]string{ + "secret1": "", + "secret2": "value2", + "secret3": "value3", + "secret4": "", + }, + requiredFields: []requiredFields{ + { + f: []string{"secret1", "secret2", "secret3", "secret4"}, + t: "secret", + }, + }, + }, + want: &RequiredFieldsError{ + message: "secrets: secret1 and secret4 are required", + required: []string{"secret1", "secret2", "secret3", "secret4"}, + missing: []string{"secret1", "secret4"}, + }, + }, + { + name: "1 setting is required", + input: struct { + values map[string]string + requiredFields []requiredFields + }{ + values: map[string]string{ + "secret1": "", + }, + requiredFields: []requiredFields{ + { + f: []string{"setting1"}, + t: "setting", + }, + }, + }, + want: &RequiredFieldsError{ + message: "setting: setting1 is required", + required: []string{"setting1"}, + missing: []string{"setting1"}, + }, + }, + { + name: "2 settings are required", input: struct { - settings map[string]Setting - required []string + values map[string]string + requiredFields []requiredFields }{ - settings: map[string]Setting{}, - required: []string{}, + values: map[string]string{ + "setting1": "", + "setting2": "", + }, + requiredFields: []requiredFields{ + { + f: []string{"setting1", "setting2"}, + t: "setting", + }, + }, + }, + want: &RequiredFieldsError{ + message: "settings: setting1 and setting2 are required", + required: []string{"setting1", "setting2"}, + missing: []string{"setting1", "setting2"}, }, - want: "", }, { - name: "1 required", + name: "4 settings are required", input: struct { - settings map[string]Setting - required []string + values map[string]string + requiredFields []requiredFields }{ - settings: map[string]Setting{ - "setting1": {Value: ""}, + values: map[string]string{ + "setting1": "", + "setting2": "", + "setting3": "", + "setting4": "", }, - required: []string{ - "setting1", + requiredFields: []requiredFields{ + { + f: []string{"setting1", "setting2", "setting3", "setting4"}, + t: "setting", + }, }, }, - want: "setting: setting1 is required", + want: &RequiredFieldsError{ + message: "settings: setting1, setting2, setting3 and setting4 are required", + required: []string{"setting1", "setting2", "setting3", "setting4"}, + missing: []string{"setting1", "setting2", "setting3", "setting4"}, + }, }, { - name: "2 required", + name: "4 settings are required, 2 missing", input: struct { - settings map[string]Setting - required []string + values map[string]string + requiredFields []requiredFields }{ - settings: map[string]Setting{ - "setting1": {Value: ""}, - "setting2": {Value: ""}, + values: map[string]string{ + "setting1": "", + "setting2": "value2", + "setting3": "value3", + "setting4": "", }, - required: []string{ - "setting1", - "setting2", + requiredFields: []requiredFields{ + { + f: []string{"setting1", "setting2", "setting3", "setting4"}, + t: "setting", + }, }, }, - want: "settings: setting1 and setting2 are required", + want: &RequiredFieldsError{ + message: "settings: setting1 and setting4 are required", + required: []string{"setting1", "setting2", "setting3", "setting4"}, + missing: []string{"setting1", "setting4"}, + }, }, { - name: "3 required", + name: "settings and settings are required", input: struct { - settings map[string]Setting - required []string + values map[string]string + requiredFields []requiredFields }{ - settings: map[string]Setting{ - "setting1": {Value: ""}, - "setting2": {Value: ""}, - "setting3": {Value: ""}, + values: map[string]string{ + "secret1": "", + "secret2": "", + "secret3": "", + "secret4": "", + "setting1": "", + "setting2": "", + "setting3": "", + "setting4": "", }, - required: []string{ - "setting1", - "setting2", - "setting3", + requiredFields: []requiredFields{ + { + f: []string{"secret1", "secret2", "secret3", "secret4"}, + t: "secret", + }, + { + f: []string{"setting1", "setting2", "setting3", "setting4"}, + t: "setting", + }, }, }, - want: "settings: setting1, setting2 and setting3 are required", + want: &RequiredFieldsError{ + message: "secrets: secret1, secret2, secret3 and secret4 are required\nsettings: setting1, setting2, setting3 and setting4 are required", + required: []string{"secret1", "secret2", "secret3", "secret4", "setting1", "setting2", "setting3", "setting4"}, + missing: []string{"secret1", "secret2", "secret3", "secret4", "setting1", "setting2", "setting3", "setting4"}, + }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := requiredErrorMessage(test.input.settings, test.input.required, "setting") + got := newRequiredFieldsError(test.input.values, test.input.requiredFields...) + + if diff := cmp.Diff(test.want, got, cmp.AllowUnexported(RequiredFieldsError{})); diff != "" { + t.Errorf("newRequiredFieldsError() = unexpected result (-want +got)\n%s\n", diff) + } + + if test.want != nil { + if diff := cmp.Diff(test.want.Required(), got.Required()); diff != "" { + t.Errorf("Required() = unexpected result (-want +got)\n%s\n", diff) + } - if diff := cmp.Diff(test.want, got); diff != "" { - t.Errorf("requiredErrorMessage() = unexpected result, (-want, +got)\n%s\n", diff) + if diff := cmp.Diff(test.want.Missing(), got.Missing()); diff != "" { + t.Errorf("Missing() = unexpected result (-want +got)\n%s\n", diff) + } } }) } diff --git a/internal/secret/secret.go b/internal/secret/secret.go index 358fee8..a222876 100644 --- a/internal/secret/secret.go +++ b/internal/secret/secret.go @@ -31,11 +31,6 @@ type Secret struct { Value string `json:"value"` } -// GetValue returns the Value of the Secret. -func (s Secret) GetValue() string { - return s.Value -} - // Client contains methods to call the Azure Key Vault REST API and // base settings for handling the requests. type Client struct { diff --git a/internal/setting/setting.go b/internal/setting/setting.go index bd98f26..3e2ff6c 100644 --- a/internal/setting/setting.go +++ b/internal/setting/setting.go @@ -40,11 +40,6 @@ type Setting struct { Label string `json:"label"` } -// GetValue returns the Value of the Setting. -func (s Setting) GetValue() string { - return s.Value -} - // AccessKey contains the id and secret for access key // authentication. type AccessKey struct { diff --git a/parser.go b/parser.go index be27f5d..21706f5 100644 --- a/parser.go +++ b/parser.go @@ -19,6 +19,11 @@ import ( "github.com/KarlGW/azcfg/internal/setting" ) +// Parser provides a method to parse secrets and settings into a struct. +type Parser interface { + Parse(ctx context.Context, v any) error +} + // HTTPClient is an HTTP client with a Do method. type HTTPClient interface { Do(req *http.Request) (*http.Response, error) @@ -51,9 +56,9 @@ type parser struct { timeout time.Duration } -// NewParser creates and returns a *Parser. This suits situations +// NewParser creates and returns a Parser. This suits situations // where multiple calls to Parse are needed with the same settings. -func NewParser(options ...Option) (*parser, error) { +func NewParser(options ...Option) (Parser, error) { opts := defaultOptions() for _, option := range options { option(&opts) @@ -155,10 +160,7 @@ func NewParser(options ...Option) (*parser, error) { // Parse secrets from an Azure Key Vault and settings from an // Azure App Configuration into the provided struct. func (p *parser) Parse(ctx context.Context, v any) error { - return parse(ctx, v, parseOptions{ - secretClient: p.secretClient, - settingClient: p.settingClient, - }) + return parse(ctx, v, p.secretClient, p.settingClient) } // setupCredential configures credential based on the provided diff --git a/parser_test.go b/parser_test.go index ff45c9c..ae4dbe0 100644 --- a/parser_test.go +++ b/parser_test.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "crypto/x509" "encoding/base64" + "fmt" "os" "path/filepath" "testing" @@ -29,7 +30,7 @@ func TestNewParser(t *testing.T) { options []Option envs map[string]string } - want *parser + want Parser wantErr error }{ { @@ -289,12 +290,10 @@ func TestParser_Parse(t *testing.T) { var tests = []struct { name string input struct { - s Struct - options []Option - secrets map[string]Secret - secretErr error - settings map[string]Setting - settingErr error + s Struct + options []Option + secretClient secretClient + settingClient settingClient } want Struct wantErr error @@ -302,33 +301,45 @@ func TestParser_Parse(t *testing.T) { { name: "parse secrets and settings", input: struct { - s Struct - options []Option - secrets map[string]Secret - secretErr error - settings map[string]Setting - settingErr error + s Struct + options []Option + secretClient secretClient + settingClient settingClient }{ s: Struct{}, - secrets: map[string]Secret{ + secretClient: newMockSecretClient(map[string]Secret{ "string": {Value: "new string"}, - }, - settings: map[string]Setting{ + }, nil), + settingClient: newMockSettingClient(map[string]Setting{ "string-setting": {Value: "new string setting"}, - }, + }, nil), }, want: Struct{ String: "new string", StringSetting: "new string setting", }, }, + { + name: "parse error getting secrets and settings", + input: struct { + s Struct + options []Option + secretClient secretClient + settingClient settingClient + }{ + s: Struct{}, + secretClient: newMockSecretClient(nil, fmt.Errorf("secret error")), + settingClient: newMockSettingClient(nil, fmt.Errorf("setting error")), + }, + wantErr: cmpopts.AnyError, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { p := &parser{ - secretClient: newMockSecretClient(test.input.secrets, test.input.secretErr), - settingClient: newMockSettingClient(test.input.settings, test.input.settingErr), + secretClient: test.input.secretClient, + settingClient: test.input.settingClient, } gotErr := p.Parse(context.Background(), &test.input.s) diff --git a/version/version.go b/version/version.go index dd1947c..0d0d531 100644 --- a/version/version.go +++ b/version/version.go @@ -2,7 +2,7 @@ package version var ( // version contains version of azcfg. - version = "0.22.0" + version = "0.23.0" ) // Version returns the version of azcfg.