diff --git a/util/gconv/gconv_scan.go b/util/gconv/gconv_scan.go index 8d8724208..9467725b8 100644 --- a/util/gconv/gconv_scan.go +++ b/util/gconv/gconv_scan.go @@ -25,3 +25,35 @@ func Scan(srcValue any, dstPointer any, paramKeyToAttrMap ...map[string]string) } return defaultConverter.Scan(srcValue, dstPointer, option) } + +// ScanWithOptions automatically checks the type of `dstPointer` and converts `srcValue` to `dstPointer`. +// It is the same as Scan function, but accepts one or more ScanOption values for additional conversion control. +// +// When using ScanWithOptions, the term "omit" means that the assignment from the source to the destination +// is skipped, so the existing value in the destination field is preserved. +// +// - option.OmitEmpty, when set to true, skips assignment of empty source values (for example: empty strings, +// zero numeric values, zero time values, empty slices or maps), preserving any existing non-empty values +// in the destination. +// +// - option.OmitNil, when set to true, skips assignment of nil source values, preserving the existing values +// in the destination when the source contains nil. +// +// Example: +// +// type User struct { +// Name string +// Email string +// } +// +// dst := &User{Name: "Alice", Email: "alice@example.com"} +// src := map[string]any{ +// "Name": "", +// "Email": nil, +// } +// +// // With OmitEmpty and OmitNil, empty and nil values in src will not overwrite dst. +// err := ScanWithOptions(src, dst, ScanOption{OmitEmpty: true, OmitNil: true}) +func ScanWithOptions(srcValue any, dstPointer any, option ...ScanOption) (err error) { + return defaultConverter.Scan(srcValue, dstPointer, option...) +} diff --git a/util/gconv/gconv_z_unit_scan_omit_test.go b/util/gconv/gconv_z_unit_scan_omit_test.go new file mode 100644 index 000000000..8fa8e75fa --- /dev/null +++ b/util/gconv/gconv_z_unit_scan_omit_test.go @@ -0,0 +1,146 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gconv_test + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gconv" +) + +type User struct { + Name string + Age int + Email string +} + +type User2 struct { + Name *string + Age int + Email string +} + +type Person struct { + Name string + Age int + Email string +} + +func TestScan_OmitEmpty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "", Age: 20, Email: ""} + person := Person{Name: "zhangsan", Age: 0, Email: "old@example.com"} + + err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{ + OmitEmpty: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "zhangsan") + t.Assert(person.Age, 20) + t.Assert(person.Email, "old@example.com") + }) +} + +func TestScan_AllOmitEmpty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "", Age: 0, Email: ""} + person := Person{Name: "zhangsan", Age: 100, Email: "old@example.com"} + + err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{ + OmitEmpty: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "zhangsan") + t.Assert(person.Age, 100) + t.Assert(person.Email, "old@example.com") + }) +} + +func TestScan_OmitNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + data := map[string]any{ + "Name": nil, + "Age": 30, + "Email": nil, + } + person := Person{Name: "lisi", Age: 0, Email: "old@example.com"} + + err := gconv.ScanWithOptions(data, &person, gconv.ScanOption{ + OmitNil: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "lisi") + t.Assert(person.Age, 30) + t.Assert(person.Email, "old@example.com") + }) +} + +func TestScan_OmitEmptyAndOmitNil(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + data := map[string]any{ + "Name": "", + "Age": 25, + "Email": nil, + } + person := Person{Name: "wangwu", Age: 0, Email: "old2@example.com"} + + err := gconv.ScanWithOptions(data, &person, gconv.ScanOption{ + OmitEmpty: true, + OmitNil: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "wangwu") + t.Assert(person.Age, 25) + t.Assert(person.Email, "old2@example.com") + }) +} + +func TestScan_NoOmitOptions(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "", Age: 20, Email: ""} + person := Person{Name: "zhangsan", Age: 30, Email: "old@example.com"} + + err := gconv.ScanWithOptions(user, &person, gconv.ScanOption{ + OmitEmpty: false, + OmitNil: false, + }) + t.AssertNil(err) + t.Assert(person.Name, "") + t.Assert(person.Age, 20) + t.Assert(person.Email, "") + }) +} + +func TestScan_OriginalBehavior(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user := User{Name: "newname", Age: 25, Email: "new@example.com"} + person := Person{Name: "", Age: 0, Email: ""} + + err := gconv.Scan(user, &person) + t.AssertNil(err) + t.Assert(person.Name, "newname") + t.Assert(person.Age, 25) + t.Assert(person.Email, "new@example.com") + }) +} + +func TestScan_StructOmitEmptyAndOmitNilOptions(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + user2 := User2{Name: nil, Age: 25, Email: ""} + person := Person{Name: "wangwu", Age: 0, Email: "old2@example.com"} + + err := gconv.ScanWithOptions(user2, &person, gconv.ScanOption{ + OmitEmpty: true, + OmitNil: true, + }) + t.AssertNil(err) + t.Assert(person.Name, "wangwu") + t.Assert(person.Age, 25) + t.Assert(person.Email, "old2@example.com") + }) +} diff --git a/util/gconv/internal/converter/converter_scan.go b/util/gconv/internal/converter/converter_scan.go index 6a8e8c4b4..ad06d3848 100644 --- a/util/gconv/internal/converter/converter_scan.go +++ b/util/gconv/internal/converter/converter_scan.go @@ -23,6 +23,15 @@ type ScanOption struct { // ContinueOnError specifies whether to continue converting the next element // if one element converting fails. ContinueOnError bool + + // OmitEmpty specifies whether to skip assignment when the source value is empty + // (empty string, zero value, etc.), preserving the existing value in the + // destination field. + OmitEmpty bool + + // OmitNil specifies whether to skip assignment when the source value is nil, + // preserving the existing value in the destination field. + OmitNil bool } func (c *Converter) getScanOption(option ...ScanOption) ScanOption { @@ -292,6 +301,8 @@ func (c *Converter) doScanForComplicatedTypes( mapOption = StructOption{ ParamKeyToAttrMap: keyToAttributeNameMapping, ContinueOnError: option.ContinueOnError, + OmitEmpty: option.OmitEmpty, + OmitNil: option.OmitNil, } ) return c.Structs(srcValue, dstPointer, StructsOption{ @@ -304,6 +315,8 @@ func (c *Converter) doScanForComplicatedTypes( ParamKeyToAttrMap: keyToAttributeNameMapping, PriorityTag: "", ContinueOnError: option.ContinueOnError, + OmitEmpty: option.OmitEmpty, + OmitNil: option.OmitNil, } return c.Struct(srcValue, dstPointer, structOption) } diff --git a/util/gconv/internal/converter/converter_struct.go b/util/gconv/internal/converter/converter_struct.go index b8b10d29a..063977af6 100644 --- a/util/gconv/internal/converter/converter_struct.go +++ b/util/gconv/internal/converter/converter_struct.go @@ -30,6 +30,15 @@ type StructOption struct { // ContinueOnError specifies whether to continue converting the next element // if one element converting fails. ContinueOnError bool + + // OmitEmpty specifies whether to skip assignment when the source value is empty + // (empty string, zero value, etc.), preserving the existing value in the + // destination field. + OmitEmpty bool + + // OmitNil specifies whether to skip assignment when the source value is nil, + // preserving the existing value in the destination field. + OmitNil bool } func (c *Converter) getStructOption(option ...StructOption) StructOption { @@ -363,6 +372,13 @@ func (c *Converter) bindVarToStructField( } } }() + // Check if the value should be omitted based on OmitEmpty or OmitNil options + if option.OmitNil && empty.IsNil(srcValue) { + return nil + } + if option.OmitEmpty && empty.IsEmpty(srcValue) { + return nil + } // Directly converting. if empty.IsNil(srcValue) { fieldValue.Set(reflect.Zero(fieldValue.Type()))