diff --git a/.golangci.yml b/.golangci.yml index 0ba64cb..3261131 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,6 +20,7 @@ linters-settings: - .Errorf( - errors.New( - errors.Unwrap( + - errors.Join( - .Wrap( - .Wrapf( - .WithMessage( diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..ec9789f --- /dev/null +++ b/config/config.go @@ -0,0 +1,217 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package config provides types for specifying the expected configuration of a +// Conduit plugin (connector or processor). It also provides utilities to +// validate the configuration based on the specifications. +package config + +import ( + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "github.com/mitchellh/mapstructure" +) + +// Config is a map of configuration values. The keys are the configuration +// parameter names and the values are the configuration parameter values. +type Config map[string]string + +// Sanitize removes leading and trailing spaces from all keys and values in the +// configuration. +func (c Config) Sanitize() Config { + for key, val := range c { + delete(c, key) + key = strings.TrimSpace(key) + val = strings.TrimSpace(val) + c[key] = val + } + return c +} + +// ApplyDefaults applies the default values defined in the parameter +// specifications to the configuration. If a parameter is not present in the +// configuration, the default value is applied. +func (c Config) ApplyDefaults(params Parameters) Config { + for key, param := range params { + if strings.TrimSpace(c[key]) == "" { + c[key] = param.Default + } + } + return c +} + +// Validate is a utility function that applies all the validations defined in +// the parameter specifications. It checks for unrecognized parameters, type +// validations, and value validations. It returns all encountered errors. +func (c Config) Validate(params Parameters) error { + errs := c.validateUnrecognizedParameters(params) + + for key := range params { + err := c.validateParamType(key, params[key]) + if err != nil { + // append error and continue with next parameter + errs = append(errs, err) + continue + } + err = c.validateParamValue(key, params[key]) + if err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} + +// validateUnrecognizedParameters validates that the config only contains +// parameters specified in the parameters. +func (c Config) validateUnrecognizedParameters(params Parameters) []error { + var errs []error + for key := range c { + if _, ok := params[key]; !ok { + errs = append(errs, fmt.Errorf("%q: %w", key, ErrUnrecognizedParameter)) + } + } + return errs +} + +// validateParamType validates that a parameter value is parsable to its assigned type. +func (c Config) validateParamType(key string, param Parameter) error { + value := c[key] + // empty value is valid for all types + if c[key] == "" { + return nil + } + + //nolint:exhaustive // type ParameterTypeFile and ParameterTypeString don't need type validations (both are strings or byte slices) + switch param.Type { + case ParameterTypeInt: + _, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("error validating %q: %q value is not an integer: %w", key, value, ErrInvalidParameterType) + } + case ParameterTypeFloat: + _, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("error validating %q: %q value is not a float: %w", key, value, ErrInvalidParameterType) + } + case ParameterTypeDuration: + _, err := time.ParseDuration(value) + if err != nil { + return fmt.Errorf("error validating %q: %q value is not a duration: %w", key, value, ErrInvalidParameterType) + } + case ParameterTypeBool: + _, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("error validating %q: %q value is not a boolean: %w", key, value, ErrInvalidParameterType) + } + } + return nil +} + +// validateParamValue validates that a configuration value matches all the +// validations required for the parameter. +func (c Config) validateParamValue(key string, param Parameter) error { + value := c[key] + var errs []error + + isRequired := false + for _, v := range param.Validations { + if _, ok := v.(ValidationRequired); ok { + isRequired = true + } + err := v.Validate(value) + if err != nil { + errs = append(errs, fmt.Errorf("error validating %q: %w", key, err)) + continue + } + } + if value == "" && !isRequired { + return nil // empty optional parameter is valid + } + + return errors.Join(errs...) +} + +// DecodeInto copies configuration values into the target object. +// Under the hood, this function uses github.com/mitchellh/mapstructure, with +// the "mapstructure" tag renamed to "json". To rename a key, use the "json" +// tag. To embed structs, append ",squash" to your tag. For more details and +// docs, see https://pkg.go.dev/github.com/mitchellh/mapstructure. +func (c Config) DecodeInto(target any, hookFunc ...mapstructure.DecodeHookFunc) error { + dConfig := &mapstructure.DecoderConfig{ + WeaklyTypedInput: true, + Result: &target, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + append( + hookFunc, + emptyStringToZeroValueHookFunc(), + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + )..., + ), + TagName: "json", + Squash: true, + } + + decoder, err := mapstructure.NewDecoder(dConfig) + if err != nil { + return fmt.Errorf("failed to create decoder: %w", err) + } + err = decoder.Decode(c.breakUp()) + if err != nil { + return fmt.Errorf("failed to decode configuration map into target: %w", err) + } + + return nil +} + +// breakUp breaks up the configuration into a map of maps based on the dot separator. +func (c Config) breakUp() map[string]any { + const sep = "." + + brokenUp := make(map[string]any) + for k, v := range c { + // break up based on dot and put in maps in case target struct is broken up + tokens := strings.Split(k, sep) + remain := k + current := brokenUp + for _, t := range tokens { + current[remain] = v // we don't care if we overwrite a map here, the string has precedence + if _, ok := current[t]; !ok { + current[t] = map[string]any{} + } + var ok bool + current, ok = current[t].(map[string]any) + if !ok { + break // this key is a string, leave it as it is + } + _, remain, _ = strings.Cut(remain, sep) + } + } + return brokenUp +} + +func emptyStringToZeroValueHookFunc() mapstructure.DecodeHookFunc { + return func(f reflect.Type, t reflect.Type, data any) (any, error) { + if f.Kind() != reflect.String || data != "" { + return data, nil + } + return reflect.New(t).Elem().Interface(), nil + } +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..217974a --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,559 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "errors" + "regexp" + "testing" + "time" + + "github.com/matryer/is" +) + +func TestConfig_Sanitize(t *testing.T) { + is := is.New(t) + have := Config{" key ": " value "} + want := Config{"key": "value"} + + have.Sanitize() + is.Equal(have, want) +} + +func TestConfig_Validate_ParameterType(t *testing.T) { + tests := []struct { + name string + config Config + params Parameters + wantErr bool + }{{ + name: "valid type number", + config: Config{"param1": "3"}, + params: Parameters{"param1": {Default: "3.3", Type: ParameterTypeFloat}}, + wantErr: false, + }, { + name: "invalid type float", + config: Config{"param1": "not-a-number"}, + params: Parameters{"param1": {Default: "3.3", Type: ParameterTypeFloat}}, + wantErr: true, + }, { + name: "valid default type float", + config: Config{"param1": ""}, + params: Parameters{"param1": {Default: "3", Type: ParameterTypeFloat}}, + wantErr: false, + }, { + name: "valid type int", + config: Config{"param1": "3"}, + params: Parameters{"param1": {Type: ParameterTypeInt}}, + wantErr: false, + }, { + name: "invalid type int", + config: Config{"param1": "3.3"}, + params: Parameters{"param1": {Type: ParameterTypeInt}}, + wantErr: true, + }, { + name: "valid type bool", + config: Config{"param1": "1"}, + params: Parameters{"param1": {Type: ParameterTypeBool}}, + wantErr: false, + }, { + name: "valid type bool", + config: Config{"param1": "true"}, + params: Parameters{"param1": {Type: ParameterTypeBool}}, + wantErr: false, + }, { + name: "invalid type bool", + config: Config{"param1": "not-a-bool"}, + params: Parameters{"param1": {Type: ParameterTypeBool}}, + wantErr: true, + }, { + name: "valid type duration", + config: Config{"param1": "1s"}, + params: Parameters{"param1": {Type: ParameterTypeDuration}}, + wantErr: false, + }, { + name: "empty value is valid for all types", + config: Config{"param1": ""}, + params: Parameters{"param1": {Type: ParameterTypeDuration}}, + wantErr: false, + }, { + name: "invalid type duration", + config: Config{"param1": "not-a-duration"}, + params: Parameters{"param1": {Type: ParameterTypeDuration}}, + wantErr: true, + }, { + name: "valid type string", + config: Config{"param1": "param"}, + params: Parameters{"param1": {Type: ParameterTypeString}}, + wantErr: false, + }, { + name: "valid type file", + config: Config{"param1": "some-data"}, + params: Parameters{"param1": {Type: ParameterTypeFile}}, + wantErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + is := is.New(t) + err := tt.config.Sanitize(). + ApplyDefaults(tt.params). + Validate(tt.params) + + if err != nil && tt.wantErr { + is.True(errors.Is(err, ErrInvalidParameterType)) + } else if err != nil || tt.wantErr { + t.Errorf("UtilityFunc() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestConfig_Validate_Validations(t *testing.T) { + tests := []struct { + name string + config Config + params Parameters + wantErr bool + err error + }{{ + name: "required validation failed", + config: Config{"param1": ""}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + }}, + }, + wantErr: true, + err: ErrRequiredParameterMissing, + }, { + name: "required validation pass", + config: Config{"param1": "value"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + }}, + }, + wantErr: false, + }, { + name: "less than validation failed", + config: Config{"param1": "20"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationLessThan{10}, + }}, + }, + wantErr: true, + err: ErrLessThanValidationFail, + }, { + name: "less than validation pass", + config: Config{"param1": "0"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationLessThan{10}, + }}, + }, + wantErr: false, + }, { + name: "greater than validation failed", + config: Config{"param1": "0"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationGreaterThan{10}, + }}, + }, + wantErr: true, + err: ErrGreaterThanValidationFail, + }, { + name: "greater than validation failed", + config: Config{"param1": "20"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationGreaterThan{10}, + }}, + }, + wantErr: false, + }, { + name: "inclusion validation failed", + config: Config{"param1": "three"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationInclusion{[]string{"one", "two"}}, + }}, + }, + wantErr: true, + err: ErrInclusionValidationFail, + }, { + name: "inclusion validation pass", + config: Config{"param1": "two"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationInclusion{[]string{"one", "two"}}, + }}, + }, + wantErr: false, + }, { + name: "exclusion validation failed", + config: Config{"param1": "one"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationExclusion{[]string{"one", "two"}}, + }}, + }, + wantErr: true, + err: ErrExclusionValidationFail, + }, { + name: "exclusion validation pass", + config: Config{"param1": "three"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationExclusion{[]string{"one", "two"}}, + }}, + }, + wantErr: false, + }, { + name: "regex validation failed", + config: Config{"param1": "a-a"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationRegex{regexp.MustCompile("[a-z]-[1-9]")}, + }}, + }, + wantErr: true, + err: ErrRegexValidationFail, + }, { + name: "regex validation pass", + config: Config{"param1": "a-8"}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationRequired{}, + ValidationRegex{regexp.MustCompile("[a-z]-[1-9]")}, + }}, + }, + wantErr: false, + }, { + name: "optional validation pass", + config: Config{"param1": ""}, + params: Parameters{ + "param1": {Validations: []Validation{ + ValidationInclusion{[]string{"one", "two"}}, + ValidationExclusion{[]string{"three", "four"}}, + ValidationRegex{regexp.MustCompile("[a-z]")}, + ValidationGreaterThan{10}, + ValidationLessThan{20}, + }}, + }, + wantErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + is := is.New(t) + err := tt.config.Sanitize(). + ApplyDefaults(tt.params). + Validate(tt.params) + + if err != nil && tt.wantErr { + is.True(errors.Is(err, tt.err)) + } else if err != nil || tt.wantErr { + t.Errorf("UtilityFunc() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestConfig_Validate_MultiError(t *testing.T) { + is := is.New(t) + + params := map[string]Parameter{ + "limit": { + Type: ParameterTypeInt, + Validations: []Validation{ + ValidationGreaterThan{0}, + ValidationRegex{regexp.MustCompile("^[0-9]")}, + }, + }, + "option": { + Type: ParameterTypeString, + Validations: []Validation{ + ValidationInclusion{[]string{"one", "two", "three"}}, + ValidationExclusion{[]string{"one", "five"}}, + }, + }, + "name": { + Type: ParameterTypeString, + Validations: []Validation{ + ValidationRequired{}, + }, + }, + } + cfg := Config{ + "limit": "-1", + "option": "five", + } + + err := cfg.Sanitize(). + ApplyDefaults(params). + Validate(params) + is.True(err != nil) + + errs := unwrapErrors(err) + want := []error{ + ErrRequiredParameterMissing, + ErrInclusionValidationFail, + ErrExclusionValidationFail, + ErrGreaterThanValidationFail, + ErrRegexValidationFail, + } + +OUTER: + for _, gotErr := range errs { + for j, wantErr := range want { + if errors.Is(gotErr, wantErr) { + // remove error from want and continue asserting the rest + want = append(want[:j], want[j+1:]...) + continue OUTER + } + } + t.Fatalf("unexpected error: %v", gotErr) + } + if len(want) != 0 { + t.Fatalf("expected more errors: %v", want) + } +} + +// unwrapErrors recursively unwraps all errors combined using errors.Join. +func unwrapErrors(err error) []error { + errs, ok := err.(interface{ Unwrap() []error }) + if !ok { + return []error{err} + } + // unwrap recursively all sub-errors + var out []error + for _, err := range errs.Unwrap() { + out = append(out, unwrapErrors(err)...) + } + return out +} + +func TestParseConfig_Simple_Struct(t *testing.T) { + is := is.New(t) + + type Person struct { + Name string `json:"person_name"` + Age int + Dur time.Duration + } + + input := Config{ + "person_name": "meroxa", + "age": "91", + "dur": "", // empty value should result in zero value + } + want := Person{ + Name: "meroxa", + Age: 91, + } + + var got Person + err := input.DecodeInto(&got) + is.NoErr(err) + is.Equal(want, got) +} + +func TestParseConfig_Embedded_Struct(t *testing.T) { + is := is.New(t) + + type Family struct { + LastName string `json:"last.name"` + } + type Location struct { + City string + } + type Person struct { + Family // last.name + Location // City + F1 Family // F1.last.name + // City + L1 Location `json:",squash"` //nolint:staticcheck // json here is a rename for the mapstructure tag + L2 Location // L2.City + L3 Location `json:"loc3"` // loc3.City + FirstName string `json:"First.Name"` // First.Name + First string // First + } + + input := Config{ + "last.name": "meroxa", + "F1.last.name": "turbine", + "City": "San Francisco", + "L2.City": "Paris", + "loc3.City": "London", + "First.Name": "conduit", + "First": "Mickey", + } + want := Person{ + Family: Family{LastName: "meroxa"}, + F1: Family{LastName: "turbine"}, + Location: Location{City: "San Francisco"}, + L1: Location{City: "San Francisco"}, + L2: Location{City: "Paris"}, + L3: Location{City: "London"}, + FirstName: "conduit", + First: "Mickey", + } + + var got Person + err := input.DecodeInto(&got) + is.NoErr(err) + is.Equal(want, got) +} + +func TestParseConfig_All_Types(t *testing.T) { + is := is.New(t) + type testCfg struct { + MyString string + MyBool1 bool + MyBool2 bool + MyBool3 bool + MyBoolDefault bool + + MyInt int + MyUint uint + MyInt8 int8 + MyUint8 uint8 + MyInt16 int16 + MyUint16 uint16 + MyInt32 int32 + MyUint32 uint32 + MyInt64 int64 + MyUint64 uint64 + + MyByte byte + MyRune rune + + MyFloat32 float32 + MyFloat64 float64 + + MyDuration time.Duration + MyDurationDefault time.Duration + + MySlice []string + MyIntSlice []int + MyFloatSlice []float32 + } + + input := Config{ + "mystring": "string", + "mybool1": "t", + "mybool2": "true", + "mybool3": "1", // 1 is true + "myInt": "-1", + "myuint": "1", + "myint8": "-1", + "myuint8": "1", + "myInt16": "-1", + "myUint16": "1", + "myint32": "-1", + "myuint32": "1", + "myint64": "-1", + "myuint64": "1", + + "mybyte": "99", // 99 fits in one byte + "myrune": "4567", + + "myfloat32": "1.1122334455", + "myfloat64": "1.1122334455", + + "myduration": "1s", + + "myslice": "1,2,3,4", + "myIntSlice": "1,2,3,4", + "myFloatSlice": "1.1,2.2", + } + want := testCfg{ + MyString: "string", + MyBool1: true, + MyBool2: true, + MyBool3: true, + MyBoolDefault: false, // default + MyInt: -1, + MyUint: 0x1, + MyInt8: -1, + MyUint8: 0x1, + MyInt16: -1, + MyUint16: 0x1, + MyInt32: -1, + MyUint32: 0x1, + MyInt64: -1, + MyUint64: 0x1, + MyByte: 0x63, + MyRune: 4567, + MyFloat32: 1.1122334, + MyFloat64: 1.1122334455, + MyDuration: 1000000000, + MyDurationDefault: 0, + MySlice: []string{"1", "2", "3", "4"}, + MyIntSlice: []int{1, 2, 3, 4}, + MyFloatSlice: []float32{1.1, 2.2}, + } + + var result testCfg + err := input.DecodeInto(&result) + is.NoErr(err) + is.Equal(want, result) +} + +func TestBreakUpConfig(t *testing.T) { + is := is.New(t) + + input := Config{ + "foo.bar.baz": "1", + "test": "2", + } + want := map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": map[string]interface{}{ + "baz": "1", + }, + "bar.baz": "1", + }, + "foo.bar.baz": "1", + "test": "2", + } + got := input.breakUp() + is.Equal(want, got) +} + +func TestBreakUpConfig_Conflict_Value(t *testing.T) { + is := is.New(t) + + input := Config{ + "foo": "1", + "foo.bar.baz": "1", // key foo is already taken, will not be broken up + } + want := map[string]interface{}{ + "foo": "1", + "foo.bar.baz": "1", + } + got := input.breakUp() + is.Equal(want, got) +} diff --git a/config/errors.go b/config/errors.go new file mode 100644 index 0000000..edd2a9f --- /dev/null +++ b/config/errors.go @@ -0,0 +1,31 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import "errors" + +var ( + ErrUnrecognizedParameter = errors.New("unrecognized parameter") + ErrInvalidParameterValue = errors.New("invalid parameter value") + ErrInvalidParameterType = errors.New("invalid parameter type") + ErrInvalidValidationType = errors.New("invalid validation type") + ErrRequiredParameterMissing = errors.New("required parameter is not provided") + + ErrLessThanValidationFail = errors.New("less-than validation failed") + ErrGreaterThanValidationFail = errors.New("greater-than validation failed") + ErrInclusionValidationFail = errors.New("inclusion validation failed") + ErrExclusionValidationFail = errors.New("exclusion validation failed") + ErrRegexValidationFail = errors.New("regex validation failed") +) diff --git a/config/parameter.go b/config/parameter.go new file mode 100644 index 0000000..4931535 --- /dev/null +++ b/config/parameter.go @@ -0,0 +1,43 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate stringer -type=ParameterType -linecomment + +package config + +// Parameters is a map of all configuration parameters. +type Parameters map[string]Parameter + +// Parameter defines a single configuration parameter. +type Parameter struct { + // Default is the default value of the parameter, if any. + Default string + // Description holds a description of the field and how to configure it. + Description string + // Type defines the parameter data type. + Type ParameterType + // Validations list of validations to check for the parameter. + Validations []Validation +} + +type ParameterType int + +const ( + ParameterTypeString ParameterType = iota + 1 // string + ParameterTypeInt // int + ParameterTypeFloat // float + ParameterTypeBool // bool + ParameterTypeFile // file + ParameterTypeDuration // duration +) diff --git a/config/parametertype_string.go b/config/parametertype_string.go new file mode 100644 index 0000000..1b8249a --- /dev/null +++ b/config/parametertype_string.go @@ -0,0 +1,29 @@ +// Code generated by "stringer -type=ParameterType -linecomment"; DO NOT EDIT. + +package config + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ParameterTypeString-1] + _ = x[ParameterTypeInt-2] + _ = x[ParameterTypeFloat-3] + _ = x[ParameterTypeBool-4] + _ = x[ParameterTypeFile-5] + _ = x[ParameterTypeDuration-6] +} + +const _ParameterType_name = "stringintfloatboolfileduration" + +var _ParameterType_index = [...]uint8{0, 6, 9, 14, 18, 22, 30} + +func (i ParameterType) String() string { + i -= 1 + if i < 0 || i >= ParameterType(len(_ParameterType_index)-1) { + return "ParameterType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _ParameterType_name[_ParameterType_index[i]:_ParameterType_index[i+1]] +} diff --git a/config/proto.go b/config/proto.go new file mode 100644 index 0000000..defd048 --- /dev/null +++ b/config/proto.go @@ -0,0 +1,182 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + configv1 "github.com/conduitio/conduit-commons/proto/config/v1" +) + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + var cTypes [1]struct{} + _ = cTypes[int(ParameterTypeString)-int(configv1.Parameter_TYPE_STRING)] + _ = cTypes[int(ParameterTypeInt)-int(configv1.Parameter_TYPE_INT)] + _ = cTypes[int(ParameterTypeFloat)-int(configv1.Parameter_TYPE_FLOAT)] + _ = cTypes[int(ParameterTypeBool)-int(configv1.Parameter_TYPE_BOOL)] + _ = cTypes[int(ParameterTypeFile)-int(configv1.Parameter_TYPE_FILE)] + _ = cTypes[int(ParameterTypeDuration)-int(configv1.Parameter_TYPE_DURATION)] +} + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + var cTypes [1]struct{} + _ = cTypes[int(ValidationTypeRequired)-int(configv1.Validation_TYPE_REQUIRED)] + _ = cTypes[int(ValidationTypeGreaterThan)-int(configv1.Validation_TYPE_GREATER_THAN)] + _ = cTypes[int(ValidationTypeLessThan)-int(configv1.Validation_TYPE_LESS_THAN)] + _ = cTypes[int(ValidationTypeInclusion)-int(configv1.Validation_TYPE_INCLUSION)] + _ = cTypes[int(ValidationTypeExclusion)-int(configv1.Validation_TYPE_EXCLUSION)] + _ = cTypes[int(ValidationTypeRegex)-int(configv1.Validation_TYPE_REGEX)] +} + +// -- From Proto To Parameter -------------------------------------------------- + +// FromProto takes data from the supplied proto object and populates the +// receiver. If the proto object is nil, the receiver is set to its zero value. +// If the function returns an error, the receiver could be partially populated. +func (p *Parameters) FromProto(proto map[string]*configv1.Parameter) error { + if proto == nil { + *p = nil + return nil + } + + clear(*p) + for k, v := range proto { + var param Parameter + err := param.FromProto(v) + if err != nil { + return fmt.Errorf("error converting parameter: %w", err) + } + (*p)[k] = param + } + return nil +} + +// FromProto takes data from the supplied proto object and populates the +// receiver. If the proto object is nil, the receiver is set to its zero value. +// If the function returns an error, the receiver could be partially populated. +func (p *Parameter) FromProto(proto *configv1.Parameter) error { + if proto == nil { + *p = Parameter{} + return nil + } + + var err error + p.Validations, err = validationsFromProto(proto.Validations) + if err != nil { + return err + } + + p.Default = proto.Default + p.Description = proto.Description + p.Type = ParameterType(proto.Type) + return nil +} + +func validationsFromProto(proto []*configv1.Validation) ([]Validation, error) { + if proto == nil { + return nil, nil + } + + validations := make([]Validation, len(proto)) + for i, v := range proto { + var err error + validations[i], err = validationFromProto(v) + if err != nil { + return nil, fmt.Errorf("error converting validation: %w", err) + } + } + return validations, nil +} + +func validationFromProto(proto *configv1.Validation) (Validation, error) { + if proto == nil { + return nil, nil //nolint:nilnil // This is the expected behavior. + } + + switch proto.Type { + case configv1.Validation_TYPE_REQUIRED: + return ValidationRequired{}, nil + case configv1.Validation_TYPE_GREATER_THAN: + v, err := strconv.ParseFloat(proto.Value, 64) + if err != nil { + return nil, fmt.Errorf("error parsing greater than value: %w", err) + } + return ValidationGreaterThan{V: v}, nil + case configv1.Validation_TYPE_LESS_THAN: + v, err := strconv.ParseFloat(proto.Value, 64) + if err != nil { + return nil, fmt.Errorf("error parsing less than value: %w", err) + } + return ValidationLessThan{V: v}, nil + case configv1.Validation_TYPE_INCLUSION: + return ValidationInclusion{List: strings.Split(proto.Value, ",")}, nil + case configv1.Validation_TYPE_EXCLUSION: + return ValidationExclusion{List: strings.Split(proto.Value, ",")}, nil + case configv1.Validation_TYPE_REGEX: + regex, err := regexp.Compile(proto.Value) + if err != nil { + return nil, fmt.Errorf("error compiling regex: %w", err) + } + return ValidationRegex{Regex: regex}, nil + case configv1.Validation_TYPE_UNSPECIFIED: + fallthrough + default: + return nil, fmt.Errorf("%v: %w", proto.Type, ErrInvalidValidationType) + } +} + +// -- From Parameter To Proto -------------------------------------------------- + +// ToProto takes data from the receiver and populates the supplied proto object. +func (p Parameters) ToProto(proto map[string]*configv1.Parameter) { + clear(proto) + for k, param := range p { + var v configv1.Parameter + param.ToProto(&v) + proto[k] = &v + } +} + +// ToProto takes data from the receiver and populates the supplied proto object. +func (p Parameter) ToProto(proto *configv1.Parameter) { + proto.Default = p.Default + proto.Description = p.Description + proto.Type = configv1.Parameter_Type(p.Type) + proto.Validations = validationsToProto(p.Validations) +} + +func validationsToProto(validations []Validation) []*configv1.Validation { + if validations == nil { + return nil + } + + proto := make([]*configv1.Validation, len(validations)) + for i, v := range validations { + proto[i] = validationToProto(v) + } + return proto +} + +func validationToProto(validation Validation) *configv1.Validation { + return &configv1.Validation{ + Type: configv1.Validation_Type(validation.Type()), + Value: validation.Value(), + } +} diff --git a/config/proto_test.go b/config/proto_test.go new file mode 100644 index 0000000..fcb2c70 --- /dev/null +++ b/config/proto_test.go @@ -0,0 +1,215 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "errors" + "regexp" + "testing" + + configv1 "github.com/conduitio/conduit-commons/proto/config/v1" + "github.com/matryer/is" +) + +func TestParameter_FromProto(t *testing.T) { + is := is.New(t) + + have := &configv1.Parameter{ + Description: "test-description", + Default: "test-default", + Type: configv1.Parameter_TYPE_STRING, + Validations: []*configv1.Validation{ + {Type: configv1.Validation_TYPE_REQUIRED}, + {Type: configv1.Validation_TYPE_GREATER_THAN, Value: "1.2"}, + {Type: configv1.Validation_TYPE_LESS_THAN, Value: "3.4"}, + {Type: configv1.Validation_TYPE_INCLUSION, Value: "1,2,3"}, + {Type: configv1.Validation_TYPE_EXCLUSION, Value: "4,5,6"}, + {Type: configv1.Validation_TYPE_REGEX, Value: "test-regex"}, + }, + } + + want := Parameter{ + Description: "test-description", + Default: "test-default", + Type: ParameterTypeString, + Validations: []Validation{ + ValidationRequired{}, + ValidationGreaterThan{V: 1.2}, + ValidationLessThan{V: 3.4}, + ValidationInclusion{List: []string{"1", "2", "3"}}, + ValidationExclusion{List: []string{"4", "5", "6"}}, + ValidationRegex{Regex: regexp.MustCompile("test-regex")}, + }, + } + + var got Parameter + err := got.FromProto(have) + is.NoErr(err) + is.Equal(want, got) +} + +func TestParameter_ToProto(t *testing.T) { + is := is.New(t) + + have := Parameter{ + Description: "test-description", + Default: "test-default", + Type: ParameterTypeString, + Validations: []Validation{ + ValidationRequired{}, + ValidationRegex{Regex: regexp.MustCompile("test-regex")}, + }, + } + + want := &configv1.Parameter{ + Description: "test-description", + Default: "test-default", + Type: configv1.Parameter_TYPE_STRING, + Validations: []*configv1.Validation{ + {Type: configv1.Validation_TYPE_REQUIRED}, + {Type: configv1.Validation_TYPE_REGEX, Value: "test-regex"}, + }, + } + + got := &configv1.Parameter{} + have.ToProto(got) + is.Equal(want, got) +} + +func TestParameter_ParameterTypes(t *testing.T) { + testCases := []struct { + protoType configv1.Parameter_Type + goType ParameterType + }{ + {protoType: configv1.Parameter_TYPE_UNSPECIFIED, goType: 0}, + {protoType: configv1.Parameter_TYPE_STRING, goType: ParameterTypeString}, + {protoType: configv1.Parameter_TYPE_INT, goType: ParameterTypeInt}, + {protoType: configv1.Parameter_TYPE_FLOAT, goType: ParameterTypeFloat}, + {protoType: configv1.Parameter_TYPE_BOOL, goType: ParameterTypeBool}, + {protoType: configv1.Parameter_TYPE_FILE, goType: ParameterTypeFile}, + {protoType: configv1.Parameter_TYPE_DURATION, goType: ParameterTypeDuration}, + {protoType: configv1.Parameter_Type(100), goType: 100}, + } + + t.Run("FromProto", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.goType.String(), func(*testing.T) { + is := is.New(t) + have := &configv1.Parameter{Type: tc.protoType} + want := Parameter{Type: tc.goType} + + var got Parameter + err := got.FromProto(have) + is.NoErr(err) + is.Equal(want, got) + }) + } + }) + + t.Run("ToProto", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.goType.String(), func(t *testing.T) { + is := is.New(t) + have := Parameter{Type: tc.goType} + want := &configv1.Parameter{Type: tc.protoType} + + got := &configv1.Parameter{} + have.ToProto(got) + is.Equal(want, got) + }) + } + }) +} + +func TestParameter_Validation(t *testing.T) { + testCases := []struct { + protoType *configv1.Validation + goType Validation + }{ + { + protoType: &configv1.Validation{Type: configv1.Validation_TYPE_REQUIRED}, + goType: ValidationRequired{}, + }, + { + protoType: &configv1.Validation{Type: configv1.Validation_TYPE_GREATER_THAN, Value: "1.2"}, + goType: ValidationGreaterThan{V: 1.2}, + }, + { + protoType: &configv1.Validation{Type: configv1.Validation_TYPE_LESS_THAN, Value: "3.4"}, + goType: ValidationLessThan{V: 3.4}, + }, + { + protoType: &configv1.Validation{Type: configv1.Validation_TYPE_INCLUSION, Value: "1,2,3"}, + goType: ValidationInclusion{List: []string{"1", "2", "3"}}, + }, + { + protoType: &configv1.Validation{Type: configv1.Validation_TYPE_EXCLUSION, Value: "4,5,6"}, + goType: ValidationExclusion{List: []string{"4", "5", "6"}}, + }, + { + protoType: &configv1.Validation{Type: configv1.Validation_TYPE_REGEX, Value: "test-regex"}, + goType: ValidationRegex{Regex: regexp.MustCompile("test-regex")}, + }, + } + + t.Run("FromProto", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.goType.Type().String(), func(t *testing.T) { + is := is.New(t) + have := &configv1.Parameter{ + Validations: []*configv1.Validation{tc.protoType}, + } + want := Parameter{ + Validations: []Validation{tc.goType}, + } + + var got Parameter + err := got.FromProto(have) + is.NoErr(err) + is.Equal(want, got) + }) + } + }) + + t.Run("ToProto", func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.goType.Type().String(), func(t *testing.T) { + is := is.New(t) + have := Parameter{ + Validations: []Validation{tc.goType}, + } + want := &configv1.Parameter{ + Validations: []*configv1.Validation{tc.protoType}, + } + + got := &configv1.Parameter{} + have.ToProto(got) + is.Equal(want, got) + }) + } + }) +} + +func TestParameter_Validation_InvalidType(t *testing.T) { + is := is.New(t) + have := &configv1.Parameter{ + Validations: []*configv1.Validation{ + {Type: configv1.Validation_TYPE_UNSPECIFIED}, + }, + } + var got Parameter + err := got.FromProto(have) + is.True(errors.Is(err, ErrInvalidValidationType)) +} diff --git a/config/validation.go b/config/validation.go new file mode 100644 index 0000000..1425022 --- /dev/null +++ b/config/validation.go @@ -0,0 +1,129 @@ +// Copyright © 2024 Meroxa, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate stringer -type=ValidationType -linecomment + +package config + +import ( + "fmt" + "regexp" + "slices" + "strconv" + "strings" +) + +type Validation interface { + Type() ValidationType + Value() string + + Validate(string) error +} + +type ValidationType int64 + +const ( + ValidationTypeRequired ValidationType = iota + 1 // required + ValidationTypeGreaterThan // greater-than + ValidationTypeLessThan // less-than + ValidationTypeInclusion // inclusion + ValidationTypeExclusion // exclusion + ValidationTypeRegex // regex +) + +type ValidationRequired struct{} + +func (v ValidationRequired) Type() ValidationType { return ValidationTypeRequired } +func (v ValidationRequired) Value() string { return "" } +func (v ValidationRequired) Validate(value string) error { + if value == "" { + return ErrRequiredParameterMissing + } + return nil +} + +type ValidationGreaterThan struct { + V float64 +} + +func (v ValidationGreaterThan) Type() ValidationType { return ValidationTypeGreaterThan } +func (v ValidationGreaterThan) Value() string { return strconv.FormatFloat(v.V, 'f', -1, 64) } +func (v ValidationGreaterThan) Validate(value string) error { + val, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("%q value should be a number: %w", value, ErrInvalidParameterValue) + } + if !(val > v.V) { + formatted := strconv.FormatFloat(v.V, 'f', -1, 64) + return fmt.Errorf("%q should be greater than %s: %w", value, formatted, ErrGreaterThanValidationFail) + } + return nil +} + +type ValidationLessThan struct { + V float64 +} + +func (v ValidationLessThan) Type() ValidationType { return ValidationTypeLessThan } +func (v ValidationLessThan) Value() string { return strconv.FormatFloat(v.V, 'f', -1, 64) } +func (v ValidationLessThan) Validate(value string) error { + val, err := strconv.ParseFloat(value, 64) + if err != nil { + return fmt.Errorf("%q value should be a number: %w", value, ErrInvalidParameterValue) + } + if !(val < v.V) { + formatted := strconv.FormatFloat(v.V, 'f', -1, 64) + return fmt.Errorf("%q should be less than %s: %w", value, formatted, ErrLessThanValidationFail) + } + return nil +} + +type ValidationInclusion struct { + List []string +} + +func (v ValidationInclusion) Type() ValidationType { return ValidationTypeInclusion } +func (v ValidationInclusion) Value() string { return strings.Join(v.List, ",") } +func (v ValidationInclusion) Validate(value string) error { + if !slices.Contains(v.List, value) { + return fmt.Errorf("%q value must be included in the list [%s]: %w", value, strings.Join(v.List, ","), ErrInclusionValidationFail) + } + return nil +} + +type ValidationExclusion struct { + List []string +} + +func (v ValidationExclusion) Type() ValidationType { return ValidationTypeExclusion } +func (v ValidationExclusion) Value() string { return strings.Join(v.List, ",") } +func (v ValidationExclusion) Validate(value string) error { + if slices.Contains(v.List, value) { + return fmt.Errorf("%q value must be excluded from the list [%s]: %w", value, strings.Join(v.List, ","), ErrExclusionValidationFail) + } + return nil +} + +type ValidationRegex struct { + Regex *regexp.Regexp +} + +func (v ValidationRegex) Type() ValidationType { return ValidationTypeRegex } +func (v ValidationRegex) Value() string { return v.Regex.String() } +func (v ValidationRegex) Validate(value string) error { + if !v.Regex.MatchString(value) { + return fmt.Errorf("%q should match the regex %q: %w", value, v.Regex.String(), ErrRegexValidationFail) + } + return nil +} diff --git a/config/validationtype_string.go b/config/validationtype_string.go new file mode 100644 index 0000000..e15a33c --- /dev/null +++ b/config/validationtype_string.go @@ -0,0 +1,29 @@ +// Code generated by "stringer -type=ValidationType -linecomment"; DO NOT EDIT. + +package config + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ValidationTypeRequired-1] + _ = x[ValidationTypeGreaterThan-2] + _ = x[ValidationTypeLessThan-3] + _ = x[ValidationTypeInclusion-4] + _ = x[ValidationTypeExclusion-5] + _ = x[ValidationTypeRegex-6] +} + +const _ValidationType_name = "requiredgreater-thanless-thaninclusionexclusionregex" + +var _ValidationType_index = [...]uint8{0, 8, 20, 29, 38, 47, 52} + +func (i ValidationType) String() string { + i -= 1 + if i < 0 || i >= ValidationType(len(_ValidationType_index)-1) { + return "ValidationType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _ValidationType_name[_ValidationType_index[i]:_ValidationType_index[i+1]] +} diff --git a/go.mod b/go.mod index b54c6e5..0eb093f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/matryer/is v1.4.1 + github.com/mitchellh/mapstructure v1.5.0 go.uber.org/goleak v1.3.0 golang.org/x/tools v0.18.0 google.golang.org/protobuf v1.32.0 @@ -149,7 +150,6 @@ require ( github.com/mbilski/exhaustivestruct v1.2.0 // indirect github.com/mgechev/revive v1.3.7 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/moricho/tparallel v0.3.1 // indirect github.com/morikuni/aec v1.0.0 // indirect diff --git a/proto/buf.md b/proto/buf.md index 0affdef..36fd6c0 100644 --- a/proto/buf.md +++ b/proto/buf.md @@ -1,5 +1,5 @@ # Conduit Commons This repository contains the proto definitions for common Conduit types, -specifically the OpenCDC record. For more info see the +specifically the OpenCDC record and plugin parameter. For more info see the [source repository](https://github.com/ConduitIO/conduit-commons). diff --git a/proto/config/v1/parameter.pb.go b/proto/config/v1/parameter.pb.go new file mode 100644 index 0000000..dd1a031 --- /dev/null +++ b/proto/config/v1/parameter.pb.go @@ -0,0 +1,426 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc (unknown) +// source: config/v1/parameter.proto + +package configv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Type shows the parameter type. +type Parameter_Type int32 + +const ( + Parameter_TYPE_UNSPECIFIED Parameter_Type = 0 + // Parameter is a string. + Parameter_TYPE_STRING Parameter_Type = 1 + // Parameter is an integer. + Parameter_TYPE_INT Parameter_Type = 2 + // Parameter is a float. + Parameter_TYPE_FLOAT Parameter_Type = 3 + // Parameter is a boolean. + Parameter_TYPE_BOOL Parameter_Type = 4 + // Parameter is a file. + Parameter_TYPE_FILE Parameter_Type = 5 + // Parameter is a duration. + Parameter_TYPE_DURATION Parameter_Type = 6 +) + +// Enum value maps for Parameter_Type. +var ( + Parameter_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "TYPE_STRING", + 2: "TYPE_INT", + 3: "TYPE_FLOAT", + 4: "TYPE_BOOL", + 5: "TYPE_FILE", + 6: "TYPE_DURATION", + } + Parameter_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "TYPE_STRING": 1, + "TYPE_INT": 2, + "TYPE_FLOAT": 3, + "TYPE_BOOL": 4, + "TYPE_FILE": 5, + "TYPE_DURATION": 6, + } +) + +func (x Parameter_Type) Enum() *Parameter_Type { + p := new(Parameter_Type) + *p = x + return p +} + +func (x Parameter_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Parameter_Type) Descriptor() protoreflect.EnumDescriptor { + return file_config_v1_parameter_proto_enumTypes[0].Descriptor() +} + +func (Parameter_Type) Type() protoreflect.EnumType { + return &file_config_v1_parameter_proto_enumTypes[0] +} + +func (x Parameter_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Parameter_Type.Descriptor instead. +func (Parameter_Type) EnumDescriptor() ([]byte, []int) { + return file_config_v1_parameter_proto_rawDescGZIP(), []int{0, 0} +} + +type Validation_Type int32 + +const ( + Validation_TYPE_UNSPECIFIED Validation_Type = 0 + // Parameter must be present. + Validation_TYPE_REQUIRED Validation_Type = 1 + // Parameter must be greater than {value}. + Validation_TYPE_GREATER_THAN Validation_Type = 2 + // Parameter must be less than {value}. + Validation_TYPE_LESS_THAN Validation_Type = 3 + // Parameter must be included in the comma separated list {value}. + Validation_TYPE_INCLUSION Validation_Type = 4 + // Parameter must not be included in the comma separated list {value}. + Validation_TYPE_EXCLUSION Validation_Type = 5 + // Parameter must match the regex {value}. + Validation_TYPE_REGEX Validation_Type = 6 +) + +// Enum value maps for Validation_Type. +var ( + Validation_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "TYPE_REQUIRED", + 2: "TYPE_GREATER_THAN", + 3: "TYPE_LESS_THAN", + 4: "TYPE_INCLUSION", + 5: "TYPE_EXCLUSION", + 6: "TYPE_REGEX", + } + Validation_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "TYPE_REQUIRED": 1, + "TYPE_GREATER_THAN": 2, + "TYPE_LESS_THAN": 3, + "TYPE_INCLUSION": 4, + "TYPE_EXCLUSION": 5, + "TYPE_REGEX": 6, + } +) + +func (x Validation_Type) Enum() *Validation_Type { + p := new(Validation_Type) + *p = x + return p +} + +func (x Validation_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Validation_Type) Descriptor() protoreflect.EnumDescriptor { + return file_config_v1_parameter_proto_enumTypes[1].Descriptor() +} + +func (Validation_Type) Type() protoreflect.EnumType { + return &file_config_v1_parameter_proto_enumTypes[1] +} + +func (x Validation_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Validation_Type.Descriptor instead. +func (Validation_Type) EnumDescriptor() ([]byte, []int) { + return file_config_v1_parameter_proto_rawDescGZIP(), []int{1, 0} +} + +// Parameter describes a single config parameter. +type Parameter struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Default is the default value of the parameter. If there is no default + // value use an empty string. + Default string `protobuf:"bytes,1,opt,name=default,proto3" json:"default,omitempty"` + // Description explains what the parameter does and how to configure it. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // Type defines the parameter data type. + Type Parameter_Type `protobuf:"varint,3,opt,name=type,proto3,enum=config.v1.Parameter_Type" json:"type,omitempty"` + // Validations are validations to be made on the parameter. + Validations []*Validation `protobuf:"bytes,4,rep,name=validations,proto3" json:"validations,omitempty"` +} + +func (x *Parameter) Reset() { + *x = Parameter{} + if protoimpl.UnsafeEnabled { + mi := &file_config_v1_parameter_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Parameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Parameter) ProtoMessage() {} + +func (x *Parameter) ProtoReflect() protoreflect.Message { + mi := &file_config_v1_parameter_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Parameter.ProtoReflect.Descriptor instead. +func (*Parameter) Descriptor() ([]byte, []int) { + return file_config_v1_parameter_proto_rawDescGZIP(), []int{0} +} + +func (x *Parameter) GetDefault() string { + if x != nil { + return x.Default + } + return "" +} + +func (x *Parameter) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Parameter) GetType() Parameter_Type { + if x != nil { + return x.Type + } + return Parameter_TYPE_UNSPECIFIED +} + +func (x *Parameter) GetValidations() []*Validation { + if x != nil { + return x.Validations + } + return nil +} + +// Validation to be made on the parameter. +type Validation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type Validation_Type `protobuf:"varint,1,opt,name=type,proto3,enum=config.v1.Validation_Type" json:"type,omitempty"` + // The value to be compared with the parameter, + // or a comma separated list in case of Validation.TYPE_INCLUSION or Validation.TYPE_EXCLUSION. + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +} + +func (x *Validation) Reset() { + *x = Validation{} + if protoimpl.UnsafeEnabled { + mi := &file_config_v1_parameter_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Validation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Validation) ProtoMessage() {} + +func (x *Validation) ProtoReflect() protoreflect.Message { + mi := &file_config_v1_parameter_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Validation.ProtoReflect.Descriptor instead. +func (*Validation) Descriptor() ([]byte, []int) { + return file_config_v1_parameter_proto_rawDescGZIP(), []int{1} +} + +func (x *Validation) GetType() Validation_Type { + if x != nil { + return x.Type + } + return Validation_TYPE_UNSPECIFIED +} + +func (x *Validation) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +var File_config_v1_parameter_proto protoreflect.FileDescriptor + +var file_config_v1_parameter_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x22, 0xad, 0x02, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x12, 0x20, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, + 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x37, 0x0a, 0x0b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, + 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x7c, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, + 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x49, 0x4e, 0x54, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x4c, + 0x4f, 0x41, 0x54, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x4f, + 0x4f, 0x4c, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x49, 0x4c, + 0x45, 0x10, 0x05, 0x12, 0x11, 0x0a, 0x0d, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x55, 0x52, 0x41, + 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x06, 0x22, 0xe7, 0x01, 0x0a, 0x0a, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x2e, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x92, 0x01, 0x0a, 0x04, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x10, 0x01, 0x12, 0x15, 0x0a, + 0x11, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x52, 0x45, 0x41, 0x54, 0x45, 0x52, 0x5f, 0x54, 0x48, + 0x41, 0x4e, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4c, 0x45, 0x53, + 0x53, 0x5f, 0x54, 0x48, 0x41, 0x4e, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x49, 0x4e, 0x43, 0x4c, 0x55, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x45, 0x58, 0x43, 0x4c, 0x55, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x05, + 0x12, 0x0e, 0x0a, 0x0a, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x47, 0x45, 0x58, 0x10, 0x06, + 0x42, 0xa3, 0x01, 0x0a, 0x0d, 0x63, 0x6f, 0x6d, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x76, 0x31, 0x42, 0x0e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x50, 0x72, 0x6f, + 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x75, 0x69, 0x74, 0x69, 0x6f, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x75, + 0x69, 0x74, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x43, 0x58, 0x58, 0xaa, 0x02, 0x09, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x09, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, 0x56, + 0x31, 0xe2, 0x02, 0x15, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, + 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0a, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_config_v1_parameter_proto_rawDescOnce sync.Once + file_config_v1_parameter_proto_rawDescData = file_config_v1_parameter_proto_rawDesc +) + +func file_config_v1_parameter_proto_rawDescGZIP() []byte { + file_config_v1_parameter_proto_rawDescOnce.Do(func() { + file_config_v1_parameter_proto_rawDescData = protoimpl.X.CompressGZIP(file_config_v1_parameter_proto_rawDescData) + }) + return file_config_v1_parameter_proto_rawDescData +} + +var file_config_v1_parameter_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_config_v1_parameter_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_config_v1_parameter_proto_goTypes = []interface{}{ + (Parameter_Type)(0), // 0: config.v1.Parameter.Type + (Validation_Type)(0), // 1: config.v1.Validation.Type + (*Parameter)(nil), // 2: config.v1.Parameter + (*Validation)(nil), // 3: config.v1.Validation +} +var file_config_v1_parameter_proto_depIdxs = []int32{ + 0, // 0: config.v1.Parameter.type:type_name -> config.v1.Parameter.Type + 3, // 1: config.v1.Parameter.validations:type_name -> config.v1.Validation + 1, // 2: config.v1.Validation.type:type_name -> config.v1.Validation.Type + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_config_v1_parameter_proto_init() } +func file_config_v1_parameter_proto_init() { + if File_config_v1_parameter_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_config_v1_parameter_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Parameter); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_config_v1_parameter_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Validation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_config_v1_parameter_proto_rawDesc, + NumEnums: 2, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_config_v1_parameter_proto_goTypes, + DependencyIndexes: file_config_v1_parameter_proto_depIdxs, + EnumInfos: file_config_v1_parameter_proto_enumTypes, + MessageInfos: file_config_v1_parameter_proto_msgTypes, + }.Build() + File_config_v1_parameter_proto = out.File + file_config_v1_parameter_proto_rawDesc = nil + file_config_v1_parameter_proto_goTypes = nil + file_config_v1_parameter_proto_depIdxs = nil +} diff --git a/proto/config/v1/parameter.proto b/proto/config/v1/parameter.proto new file mode 100644 index 0000000..90439fc --- /dev/null +++ b/proto/config/v1/parameter.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package config.v1; + +// Parameter describes a single config parameter. +message Parameter { + // Type shows the parameter type. + enum Type { + TYPE_UNSPECIFIED = 0; + // Parameter is a string. + TYPE_STRING = 1; + // Parameter is an integer. + TYPE_INT = 2; + // Parameter is a float. + TYPE_FLOAT = 3; + // Parameter is a boolean. + TYPE_BOOL = 4; + // Parameter is a file. + TYPE_FILE = 5; + // Parameter is a duration. + TYPE_DURATION = 6; + } + + // Default is the default value of the parameter. If there is no default + // value use an empty string. + string default = 1; + // Description explains what the parameter does and how to configure it. + string description = 2; + // Type defines the parameter data type. + Type type = 3; + // Validations are validations to be made on the parameter. + repeated Validation validations = 4; +} + +// Validation to be made on the parameter. +message Validation { + enum Type { + TYPE_UNSPECIFIED = 0; + // Parameter must be present. + TYPE_REQUIRED = 1; + // Parameter must be greater than {value}. + TYPE_GREATER_THAN = 2; + // Parameter must be less than {value}. + TYPE_LESS_THAN = 3; + // Parameter must be included in the comma separated list {value}. + TYPE_INCLUSION = 4; + // Parameter must not be included in the comma separated list {value}. + TYPE_EXCLUSION = 5; + // Parameter must match the regex {value}. + TYPE_REGEX = 6; + } + + Type type = 1; + // The value to be compared with the parameter, + // or a comma separated list in case of Validation.TYPE_INCLUSION or Validation.TYPE_EXCLUSION. + string value = 2; +} \ No newline at end of file