From e5b020b6d35acc7fed7bb7b3379cf6fd17ab782c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lovro=20Ma=C5=BEgon?= Date: Thu, 29 Feb 2024 15:15:46 +0100 Subject: [PATCH] add json functions for config parameters --- config/parameter.go | 49 +++++++++++- config/parameter_test.go | 156 +++++++++++++++++++++++++++++++++++++++ config/validation.go | 51 +++++++++++++ 3 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 config/parameter_test.go diff --git a/config/parameter.go b/config/parameter.go index 4931535..f8ca197 100644 --- a/config/parameter.go +++ b/config/parameter.go @@ -16,19 +16,25 @@ package config +import ( + "fmt" + "strconv" + "strings" +) + // 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 + Default string `json:"default"` // Description holds a description of the field and how to configure it. - Description string + Description string `json:"description"` // Type defines the parameter data type. - Type ParameterType + Type ParameterType `json:"type"` // Validations list of validations to check for the parameter. - Validations []Validation + Validations []Validation `json:"validations"` } type ParameterType int @@ -41,3 +47,38 @@ const ( ParameterTypeFile // file ParameterTypeDuration // duration ) + +func (pt ParameterType) MarshalText() ([]byte, error) { + return []byte(pt.String()), nil +} + +func (pt *ParameterType) UnmarshalText(b []byte) error { + if len(b) == 0 { + return nil // empty string, do nothing + } + + switch string(b) { + case ParameterTypeString.String(): + *pt = ParameterTypeString + case ParameterTypeInt.String(): + *pt = ParameterTypeInt + case ParameterTypeFloat.String(): + *pt = ParameterTypeFloat + case ParameterTypeBool.String(): + *pt = ParameterTypeBool + case ParameterTypeFile.String(): + *pt = ParameterTypeFile + case ParameterTypeDuration.String(): + *pt = ParameterTypeDuration + default: + // it may not be a known parameter type, but we also allow ParameterType(int) + valIntRaw := strings.TrimSuffix(strings.TrimPrefix(string(b), "ParameterType("), ")") + valInt, err := strconv.Atoi(valIntRaw) + if err != nil { + return fmt.Errorf("parameter type %q: %w", b, ErrInvalidParameterType) + } + *pt = ParameterType(valInt) + } + + return nil +} diff --git a/config/parameter_test.go b/config/parameter_test.go new file mode 100644 index 0000000..cc46542 --- /dev/null +++ b/config/parameter_test.go @@ -0,0 +1,156 @@ +// 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 ( + "regexp" + "testing" + + "github.com/goccy/go-json" + "github.com/matryer/is" +) + +func TestParameters_MarshalJSON(t *testing.T) { + is := is.New(t) + parameters := Parameters{ + "empty": {}, + "string": { + Default: "test-string", + Description: "test-description", + Type: ParameterTypeString, + Validations: []Validation{}, + }, + "int": { + Default: "test-string", + Description: "test-description", + Type: ParameterTypeInt, + Validations: []Validation{}, + }, + "float": { + Default: "test-float", + Description: "test-description", + Type: ParameterTypeFloat, + Validations: []Validation{}, + }, + "bool": { + Default: "test-bool", + Description: "test-description", + Type: ParameterTypeBool, + Validations: []Validation{}, + }, + "file": { + Default: "test-file", + Description: "test-description", + Type: ParameterTypeFile, + Validations: []Validation{}, + }, + "duration": { + Default: "test-duration", + Description: "test-description", + Type: ParameterTypeDuration, + Validations: []Validation{}, + }, + "validations": { + 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")}, + }, + }, + } + + want := `{ + "bool": { + "default": "test-bool", + "description": "test-description", + "type": "bool", + "validations": [] + }, + "duration": { + "default": "test-duration", + "description": "test-description", + "type": "duration", + "validations": [] + }, + "empty": { + "default": "", + "description": "", + "type": "ParameterType(0)", + "validations": null + }, + "file": { + "default": "test-file", + "description": "test-description", + "type": "file", + "validations": [] + }, + "float": { + "default": "test-float", + "description": "test-description", + "type": "float", + "validations": [] + }, + "int": { + "default": "test-string", + "description": "test-description", + "type": "int", + "validations": [] + }, + "string": { + "default": "test-string", + "description": "test-description", + "type": "string", + "validations": [] + }, + "validations": { + "default": "", + "description": "", + "type": "ParameterType(0)", + "validations": [ + { + "type": "required", + "value": "" + }, + { + "type": "greater-than", + "value": "1.2" + }, + { + "type": "less-than", + "value": "3.4" + }, + { + "type": "inclusion", + "value": "1,2,3" + }, + { + "type": "exclusion", + "value": "4,5,6" + }, + { + "type": "regex", + "value": "test-regex" + } + ] + } +}` + + got, err := json.MarshalIndent(parameters, "", " ") + is.NoErr(err) + is.Equal(want, string(got)) +} diff --git a/config/validation.go b/config/validation.go index 1425022..4b358a0 100644 --- a/config/validation.go +++ b/config/validation.go @@ -22,6 +22,8 @@ import ( "slices" "strconv" "strings" + + "github.com/goccy/go-json" ) type Validation interface { @@ -42,6 +44,41 @@ const ( ValidationTypeRegex // regex ) +func (vt ValidationType) MarshalText() ([]byte, error) { + return []byte(vt.String()), nil +} + +func (vt *ValidationType) UnmarshalText(b []byte) error { + if len(b) == 0 { + return nil // empty string, do nothing + } + + switch string(b) { + case ValidationTypeRequired.String(): + *vt = ValidationTypeRequired + case ValidationTypeGreaterThan.String(): + *vt = ValidationTypeGreaterThan + case ValidationTypeLessThan.String(): + *vt = ValidationTypeLessThan + case ValidationTypeInclusion.String(): + *vt = ValidationTypeInclusion + case ValidationTypeExclusion.String(): + *vt = ValidationTypeExclusion + case ValidationTypeRegex.String(): + *vt = ValidationTypeRegex + default: + // it may not be a known validation type, but we also allow ValidationType(int) + valIntRaw := strings.TrimSuffix(strings.TrimPrefix(string(b), "ValidationType("), ")") + valInt, err := strconv.Atoi(valIntRaw) + if err != nil { + return fmt.Errorf("validation type %q: %w", b, ErrInvalidValidationType) + } + *vt = ValidationType(valInt) + } + + return nil +} + type ValidationRequired struct{} func (v ValidationRequired) Type() ValidationType { return ValidationTypeRequired } @@ -52,6 +89,7 @@ func (v ValidationRequired) Validate(value string) error { } return nil } +func (v ValidationRequired) MarshalJSON() ([]byte, error) { return jsonMarshalValidation(v) } type ValidationGreaterThan struct { V float64 @@ -70,6 +108,7 @@ func (v ValidationGreaterThan) Validate(value string) error { } return nil } +func (v ValidationGreaterThan) MarshalJSON() ([]byte, error) { return jsonMarshalValidation(v) } type ValidationLessThan struct { V float64 @@ -88,6 +127,7 @@ func (v ValidationLessThan) Validate(value string) error { } return nil } +func (v ValidationLessThan) MarshalJSON() ([]byte, error) { return jsonMarshalValidation(v) } type ValidationInclusion struct { List []string @@ -101,6 +141,7 @@ func (v ValidationInclusion) Validate(value string) error { } return nil } +func (v ValidationInclusion) MarshalJSON() ([]byte, error) { return jsonMarshalValidation(v) } type ValidationExclusion struct { List []string @@ -114,6 +155,7 @@ func (v ValidationExclusion) Validate(value string) error { } return nil } +func (v ValidationExclusion) MarshalJSON() ([]byte, error) { return jsonMarshalValidation(v) } type ValidationRegex struct { Regex *regexp.Regexp @@ -127,3 +169,12 @@ func (v ValidationRegex) Validate(value string) error { } return nil } +func (v ValidationRegex) MarshalJSON() ([]byte, error) { return jsonMarshalValidation(v) } + +func jsonMarshalValidation(v Validation) ([]byte, error) { + //nolint:wrapcheck // no need to wrap this error, this will be called by the JSON lib itself + return json.Marshal(map[string]any{ + "type": v.Type(), + "value": v.Value(), + }) +}