From 76f65a88aff38fed79c66d2774d63322204be23b Mon Sep 17 00:00:00 2001 From: Rhyanz46 Date: Wed, 11 Sep 2024 11:44:51 +0700 Subject: [PATCH] using tree chain to collect error on unique values --- README.md | 4 +- go.mod | 2 +- go.sum | 2 + .../{chain_test.go => chain_tree_test.go} | 32 ++++- map_validator/functions.go | 42 ++++--- map_validator/implements.go | 7 ++ map_validator/interfaces.go | 11 ++ map_validator/models.go | 54 +++++++- .../{chaining.go => tree_chaining.go} | 115 ++++++++++++++++++ test/unique_values_test.go | 94 ++++++++++++-- 10 files changed, 329 insertions(+), 34 deletions(-) rename map_validator/{chain_test.go => chain_tree_test.go} (70%) rename map_validator/{chaining.go => tree_chaining.go} (58%) diff --git a/README.md b/README.md index fc620b5..ce22bd6 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,8 @@ go get github.com/Rhyanz46/go-map-validator/map_validator - on invalid regex message : ✅ ready - on type not match message : ✅ ready - on min/max data message : ✅ ready + - on unique values error : ✅ ready - on null data message : ⌛ not ready - - on unique values error : ⌛ not ready - on enum value not match : ⌛ not ready - on `RequiredWithout` error : ⌛ not ready @@ -57,7 +57,7 @@ go get github.com/Rhyanz46/go-map-validator/map_validator ## Road Map - +- errors detail mode - get from urls params http - validation for `base64` - handle file size on multipart diff --git a/go.mod b/go.mod index c418de5..8f4f8ba 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/Rhyanz46/go-map-validator go 1.20 -require github.com/google/uuid v1.4.0 +require github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index fef9ecd..d2fc251 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/map_validator/chain_test.go b/map_validator/chain_tree_test.go similarity index 70% rename from map_validator/chain_test.go rename to map_validator/chain_tree_test.go index f20ae03..8afee00 100644 --- a/map_validator/chain_test.go +++ b/map_validator/chain_tree_test.go @@ -1,6 +1,8 @@ package map_validator -import "testing" +import ( + "testing" +) func TestSetKey(t *testing.T) { root := newChainer().SetKey("root") @@ -108,3 +110,31 @@ func TestMultipleLevelChildren(t *testing.T) { root.GetResult().GetAllKeys() } + +func TestChainValues(T *testing.T) { + root := newChainer().SetKey("root") + childa_1 := root.AddChild().SetKeyValue("childa_1", "value+childa_1") + childa_2 := root.AddChild().SetKeyValue("childa_2", "value+childa_2") + root.AddChild().SetKeyValue("childa_3", "value+childa_3") + + childa_1.AddChild().SetKeyValue("childb_1_d", "value+childb_d") + childa_2.AddChild().SetKeyValue("childa_2_x", "value+a").SetUniques([]string{"childa_2_z", "childa_2_y"}) + childa_2.AddChild().SetKeyValue("childa_2_z", "value+a") + childa_2.AddChild().SetKeyValue("childa_2_y", "value+ka") + childa_2.AddChild().SetKeyValue("childa_2_sssy", nil) + childa_2.AddChild().SetKeyValue("childa_2_m", "value+a") + childa_2.AddChild().SetKeyValue("childa_2_g", "value+childa_2_g") + childa_2.AddChild().SetKeyValue("childa_2_s", "value+childa_2_s") + childa_2.AddChild().SetKeyValue("childa_2_e", "value+childa_2_e") + + childa_1.AddChild().SetKeyValue("childa_1_e", "value+childa_1_e") + childa_1.AddChild().SetKeyValue("childa_1_f", "value+childa_1_f").SetUniques([]string{"childa_1_t"}) + childa_1.AddChild().SetKeyValue("childa_1_t", "value+childa_1_e").SetUniques([]string{"childa_1_e"}) + + root.GetResult().RunUniqueChecker() + res := root.GetResult() + errors := res.GetErrors() + if len(errors) != 2 { + T.Errorf("Expected have two errors, but we got %d error", len(errors)) + } +} diff --git a/map_validator/functions.go b/map_validator/functions.go index 181c6bb..a23b689 100644 --- a/map_validator/functions.go +++ b/map_validator/functions.go @@ -70,6 +70,8 @@ func buildMessage(msg string, meta MessageMeta) error { actualLengthVar := "${actual_length}" expectedMinLengthVar := "${expected_min_length}" expectedMaxLengthVar := "${expected_max_length}" + uniqueOriginVar := "${unique_origin}" + uniqueTargetVar := "${unique_target}" if strings.Contains(msg, fieldVar) { if meta.Field != nil { v := *meta.Field @@ -106,6 +108,18 @@ func buildMessage(msg string, meta MessageMeta) error { msg = strings.ReplaceAll(msg, expectedMaxLengthVar, fmt.Sprintf("%v", v)) } } + if strings.Contains(msg, uniqueOriginVar) { + if meta.Field != nil { + v := *meta.UniqueOrigin + msg = strings.ReplaceAll(msg, uniqueOriginVar, v) + } + } + if strings.Contains(msg, uniqueTargetVar) { + if meta.Field != nil { + v := *meta.UniqueTarget + msg = strings.ReplaceAll(msg, uniqueTargetVar, v) + } + } return errors.New(msg) } @@ -135,28 +149,18 @@ func validateRecursive(pChain ChainerType, wrapper *RulesWrapper, key string, da cChain.SetValue(res) } - // check unique values - if wrapper != nil && res != nil && len(rule.Unique) > 0 { - for _, unique := range rule.Unique { - if wrapper.uniqueValues == nil { - wrapper.uniqueValues = &map[string]map[string]interface{}{} - } - - if _, exists := (*wrapper.uniqueValues)[unique]; !exists { - (*wrapper.uniqueValues)[unique] = make(map[string]interface{}) - } + if wrapper != nil { + // add unique values + if res != nil && len(rule.Unique) > 0 { + cChain.SetUniques(rule.Unique) + } - for keyX, val := range (*wrapper.uniqueValues)[unique] { - if val == res { - return nil, fmt.Errorf("value of '%s' and '%s' fields must be different", keyX, key) - } - } - (*wrapper.uniqueValues)[unique][key] = res + // add custom message values + if res != nil && rule.CustomMsg.isNotNil() { + cChain.SetCustomMsg(&rule.CustomMsg) } - } - // put filled and null fields - if wrapper != nil { + // put filled and null fields if wrapper.filledField == nil { wrapper.filledField = &[]string{} } diff --git a/map_validator/implements.go b/map_validator/implements.go index 32aa741..55fb255 100644 --- a/map_validator/implements.go +++ b/map_validator/implements.go @@ -214,6 +214,13 @@ func (state *finalOperation) RunValidate() (*ExtraOperationData, error) { return nil, err } + chainRes.RunUniqueChecker() + for _, err = range chainRes.GetErrors() { + if err != nil { + return nil, err + } + } + manipulatedData := chainRes.ToMap() extraData := &ExtraOperationData{ rules: state.rules, diff --git a/map_validator/interfaces.go b/map_validator/interfaces.go index 4520f19..735084d 100644 --- a/map_validator/interfaces.go +++ b/map_validator/interfaces.go @@ -42,6 +42,8 @@ type ChainResultType interface { PrintHierarchyWithSeparator(separator string, currentPath string) ToMap() map[string]interface{} RunManipulator() error + RunUniqueChecker() + GetErrors() []error } type ChainerType interface { @@ -51,11 +53,20 @@ type ChainerType interface { Forward(index int) ChainerType SetKey(name string) ChainerType GetKey() string + SetKeyValue(key string, value interface{}) ChainerType GetParentKeys() []string AddChild() ChainerType LoadFromMap(data map[string]interface{}) SetValue(value interface{}) ChainerType GetValue() interface{} SetManipulator(manipulator *func(interface{}) (interface{}, error)) ChainerType + SetUniques(uniques []string) ChainerType + SetCustomMsg(customMsg *CustomMsg) ChainerType + GetUniques() []string + AddError(err error) ChainerType GetResult() ChainResultType + + GetChildren() []ChainerType + GetParent() ChainerType + GetBrothers() []ChainerType } diff --git a/map_validator/models.go b/map_validator/models.go index 5aa5af6..204022f 100644 --- a/map_validator/models.go +++ b/map_validator/models.go @@ -17,6 +17,8 @@ type MessageMeta struct { ExpectedMinLength *int64 ExpectedMaxLength *int64 ActualType *reflect.Kind + UniqueOrigin *string + UniqueTarget *string } type EnumField[T any] struct { @@ -31,6 +33,49 @@ type CustomMsg struct { OnMax *string OnMin *string OnRegexString *string + OnUnique *string +} + +func (cm *CustomMsg) uniqueNotNil() bool { + return cm.OnUnique != nil +} + +func (cm *CustomMsg) maxNotNil() bool { + return cm.OnMax != nil +} + +func (cm *CustomMsg) minNotNil() bool { + return cm.OnMin != nil +} + +func (cm *CustomMsg) regexNotNil() bool { + return cm.OnRegexString != nil +} + +func (cm *CustomMsg) typeNotMatchNotNil() bool { + return cm.OnTypeNotMatch != nil +} + +func (cm *CustomMsg) isNotNil() (notNil bool) { + if cm == nil { + return + } + if cm.OnTypeNotMatch != nil { + notNil = true + } + if cm.OnMax != nil { + notNil = true + } + if cm.OnMin != nil { + notNil = true + } + if cm.OnRegexString != nil { + notNil = true + } + if cm.OnUnique != nil { + notNil = true + } + return notNil } type Setting struct { @@ -66,10 +111,11 @@ type Rules struct { File bool RegexString string Unique []string - RequiredWithout []string - RequiredIf []string - Object *RulesWrapper - ListObject *RulesWrapper + + RequiredWithout []string + RequiredIf []string + Object *RulesWrapper + ListObject *RulesWrapper CustomMsg CustomMsg // will support soon } diff --git a/map_validator/chaining.go b/map_validator/tree_chaining.go similarity index 58% rename from map_validator/chaining.go rename to map_validator/tree_chaining.go index 0f05c86..61799c1 100644 --- a/map_validator/chaining.go +++ b/map_validator/tree_chaining.go @@ -8,10 +8,36 @@ type chainState struct { key string manipulator *func(interface{}) (interface{}, error) value interface{} + CustomMsg *CustomMsg + uniques []string + errs []error parent *chainState children []*chainState } +func (cs *chainState) AddError(err error) ChainerType { + cs.errs = append(cs.errs, err) + return cs +} + +func (cs *chainState) SetCustomMsg(customMsg *CustomMsg) ChainerType { + cs.CustomMsg = customMsg + return cs +} + +func (cs *chainState) GetErrors() []error { + return cs.recursiveGetErrors() +} + +func (cs *chainState) recursiveGetErrors() []error { + var errors []error + errors = append(errors, cs.errs...) + for _, child := range cs.children { + errors = append(errors, child.recursiveGetErrors()...) + } + return errors +} + func (cs *chainState) SetManipulator(manipulator *func(interface{}) (interface{}, error)) ChainerType { cs.manipulator = manipulator return cs @@ -54,6 +80,12 @@ func (cs *chainState) SetKey(name string) ChainerType { return cs } +func (cs *chainState) SetKeyValue(key string, value interface{}) ChainerType { + cs.value = value + cs.key = key + return cs +} + func (cs *chainState) GetParentKeys() []string { var keys []string current := cs @@ -141,12 +173,95 @@ func (cs *chainState) ToMap() map[string]interface{} { return result } +func (cs *chainState) GetUniques() []string { + return cs.uniques +} + +func (cs *chainState) GetBrothers() []ChainerType { + var brothers []ChainerType + if cs.GetParent() == nil { + return nil + } + + for _, child := range cs.GetParent().GetChildren() { + if child == nil || cs.GetKey() == child.GetKey() { + continue + } + brothers = append(brothers, child) + } + return brothers +} + +func (cs *chainState) GetParent() ChainerType { + return cs.parent +} + +func (cs *chainState) GetChildren() []ChainerType { + var children []ChainerType + if cs == nil || cs.children == nil { + return []ChainerType{} + } + for _, child := range cs.children { + children = append(children, child) + } + return children +} + +func (cs *chainState) SetUniques(uniques []string) ChainerType { + var removedDuplicated []string + for _, unique := range uniques { + var found bool + for _, removed := range removedDuplicated { + if removed == unique { + found = true + break + } + } + if !found { + removedDuplicated = append(removedDuplicated, unique) + } + } + cs.uniques = removedDuplicated + return cs +} + +func (cs *chainState) RunUniqueChecker() { + if cs.value != nil && len(cs.uniques) > 0 { + brothers := cs.GetBrothers() + for _, bro := range brothers { + if bro.GetValue() == nil { + continue + } + for _, unique := range cs.uniques { + originKey := cs.GetKey() + targetKey := bro.GetKey() + if targetKey == unique && bro.GetValue() == cs.GetValue() { + msgError := fmt.Errorf("value of '%s' and '%s' fields must be different", originKey, targetKey) + if cs.CustomMsg != nil && cs.CustomMsg.uniqueNotNil() { + msgError = buildMessage(*cs.CustomMsg.OnUnique, MessageMeta{ + Field: &originKey, + UniqueOrigin: &originKey, + UniqueTarget: &targetKey, + }) + } + cs.AddError(msgError) + } + } + } + } + for _, child := range cs.children { + child.RunUniqueChecker() + } +} + func (cs *chainState) RunManipulator() (err error) { return cs.runManipulate() } func (cs *chainState) runManipulate() (err error) { + // check if current value is not nil and has manipulator if cs.value != nil && cs.manipulator != nil { + // run manipulator cs.value, err = (*cs.manipulator)(cs.value) if err != nil { return err diff --git a/test/unique_values_test.go b/test/unique_values_test.go index 493d11c..ae3b995 100644 --- a/test/unique_values_test.go +++ b/test/unique_values_test.go @@ -9,7 +9,7 @@ import ( func TestUniqueValue(t *testing.T) { role := map_validator.RulesWrapper{ Rules: map[string]map_validator.Rules{ - "password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, + "password": {Type: reflect.String, Null: true}, "new_password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, }, } @@ -25,30 +25,71 @@ func TestUniqueValue(t *testing.T) { expected := "value of 'password' and 'new_password' fields must be different" expectedOr := "value of 'new_password' and 'password' fields must be different" _, err = check.RunValidate() - if !(err.Error() == expected || err.Error() == expectedOr) { + if err == nil { + t.Error("Expected error, but got no error :") + } + if err != nil && !(err.Error() == expected || err.Error() == expectedOr) { t.Errorf("Expected :%s. But you got : %s", expected, err) } } -func TestNonUniqueValue(t *testing.T) { +func TestUniqueValueInNested(t *testing.T) { role := map_validator.RulesWrapper{ Rules: map[string]map_validator.Rules{ - "password": {Type: reflect.String, Unique: []string{"a"}, Null: true}, + "password": {Type: reflect.String, Null: true}, "new_password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, + "data": {Object: &map_validator.RulesWrapper{ + Rules: map[string]map_validator.Rules{ + "dt_password": {Type: reflect.String, Null: true}, + "dt_new_password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, + }, + }}, }, } payload := map[string]interface{}{ "password": "sabalong", "new_password": "sabalong", + "data": map[string]interface{}{ + "dt_password": "golang@123", + "dt_new_password": "golang@123", + }, } check, err := map_validator.NewValidateBuilder().SetRules(role).Load(payload) if err != nil { t.Errorf("Expected not have error, but got error : %s", err) return } + expected := "value of 'password' and 'new_password' fields must be different" + expectedOr := "value of 'new_password' and 'password' fields must be different" _, err = check.RunValidate() + if err == nil { + t.Error("Expected error, but got no error :") + } + if err != nil && !(err.Error() == expected || err.Error() == expectedOr) { + t.Errorf("Expected :%s. But you got : %s", expected, err) + } +} + +func TestNonUniqueValue(t *testing.T) { + role := map_validator.RulesWrapper{ + Rules: map[string]map_validator.Rules{ + "password": {Type: reflect.String, Null: true}, + "new_password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, + }, + } + payload := map[string]interface{}{ + "password": "sabalong", + "new_password": "sabalong", + } + check, err := map_validator.NewValidateBuilder().SetRules(role).Load(payload) if err != nil { t.Errorf("Expected not have error, but got error : %s", err) + return + } + expected := "value of 'new_password' and 'password' fields must be different" + _, err = check.RunValidate() + if !(err.Error() == expected) { + t.Errorf("Expected :%s. But you got : %s", expected, err) } } @@ -58,7 +99,7 @@ func TestChildUniqueValue(t *testing.T) { "data": {Object: &map_validator.RulesWrapper{ Rules: map[string]map_validator.Rules{ "name": {Type: reflect.String, Null: true}, - "password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, + "password": {Type: reflect.String, Null: true}, "new_password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, }, }}, @@ -86,9 +127,9 @@ func TestChildUniqueValue(t *testing.T) { func TestUniqueManyValue(t *testing.T) { role := map_validator.RulesWrapper{ Rules: map[string]map_validator.Rules{ - "name": {Type: reflect.String, Unique: []string{"basic", "password"}, Null: true}, + "name": {Type: reflect.String, Unique: []string{"basic", "password", "new_password"}, Null: true}, "hoby": {Type: reflect.String, Unique: []string{"basic"}, Null: true}, - "password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, + "password": {Type: reflect.String, Null: true}, "new_password": {Type: reflect.String, Unique: []string{"password"}, Null: true}, }, } @@ -110,3 +151,42 @@ func TestUniqueManyValue(t *testing.T) { t.Errorf("Expected :%s. But you got : %s", expected, err) } } + +func TestChildUniqueValueWithCustomMsg(t *testing.T) { + role := map_validator.RulesWrapper{ + Rules: map[string]map_validator.Rules{ + "data": {Object: &map_validator.RulesWrapper{ + Rules: map[string]map_validator.Rules{ + "name": {Type: reflect.String, Null: true}, + "password": {Type: reflect.String, Null: true}, + "new_password": { + Type: reflect.String, Unique: []string{"password"}, Null: true, + CustomMsg: map_validator.CustomMsg{ + OnUnique: map_validator.SetMessage("Nilai dari '${unique_origin}' tidak boleh sama dengan nilai '${unique_target}'"), + }, + }, + }, + }}, + }, + } + payload := map[string]interface{}{ + "data": map[string]interface{}{ + "password": "sabalong", + "new_password": "sabalong", + }, + } + check, err := map_validator.NewValidateBuilder().SetRules(role).Load(payload) + if err != nil { + t.Errorf("Expected not have error, but got error : %s", err) + return + } + expected := "Nilai dari 'new_password' tidak boleh sama dengan nilai 'password'" + _, err = check.RunValidate() + if err == nil { + t.Error("Expected have an error, but got no error ") + return + } + if err.Error() != expected { + t.Errorf("Expected :%s. But you got : %s", expected, err) + } +}