-
Notifications
You must be signed in to change notification settings - Fork 2
feat: allow optional field is empty when pass field #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Summary by CodeRabbit
WalkthroughReplaced exported IsNil with a reflection-based IsEmpty(v any) that detects nil, nil pointers, zero-length arrays/slices/maps, and other zero values; scanner logic updated to use IsEmpty/reflect zero checks and to avoid recursing into nil pointers; tests renamed/extended to cover these cases. Changes
Sequence Diagram(s)sequenceDiagram
actor Caller
participant Scanner as validator.Scanner
participant Field as Struct Field
participant Util as validator.IsEmpty
Caller->>Scanner: Scan(struct)
loop per field
Scanner->>Field: read value & tags
alt omitempty present
Scanner->>Field: reflect.Value.IsZero()
alt Zero
Scanner-->>Caller: skip field
else Non-zero
Scanner-->>Caller: continue
end
else required
Scanner->>Util: IsEmpty(value)
alt Empty
Scanner-->>Caller: record error
else Not empty
Scanner-->>Caller: continue
end
end
alt validate:"nested"
alt Field is pointer
Scanner->>Field: is nil?
alt Nil
Scanner-->>Caller: skip nested
else Non-nil
Scanner->>Scanner: recurse on pointee
end
else Non-pointer
Scanner->>Scanner: recurse on value
end
end
end
Scanner-->>Caller: return result
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (2)
dto/validator/common_test.go (1)
236-244: Tests correctly cover IsEmpty for nil, empty slices, and maps; consider renaming for clarity.The assertions look good and align with the new IsEmpty semantics. Minor nit: the test function is still named Test_IsNil, which can be confusing now that the API is IsEmpty.
Example rename:
func Test_IsEmpty(t *testing.T) { t.Parallel() assert.True(t, validator.IsEmpty(nil)) assert.True(t, validator.IsEmpty([]string{})) a := []interface{}{} assert.True(t, validator.IsEmpty(a)) b := make(map[string]interface{}) assert.True(t, validator.IsEmpty(b)) }dto/validator/scanner_test.go (1)
225-249: Usevalidatetag key consistently (notvalidator) to avoid confusion.The scanner reads the
validatestruct tag. Here, ContactName usesvalidator:"isAlphanumeric", which is ignored. While this test still passes becauseContactis nil, using a consistent tag key reduces surprises.type Contact struct { - ContactName string `json:"name" validator:"isAlphanumeric"` + ContactName string `json:"name" validate:"isAlphanumeric"` ContactEmail string `json:"email" validate:"isEmail"` ContactPhone string `json:"phone"` }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
dto/validator/common.go(1 hunks)dto/validator/common_test.go(1 hunks)dto/validator/scanner.go(2 hunks)dto/validator/scanner_test.go(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
dto/validator/scanner_test.go (1)
dto/validator/scanner.go (1)
Scanner(14-184)
dto/validator/scanner.go (1)
dto/validator/common.go (1)
IsEmpty(305-320)
dto/validator/common_test.go (1)
dto/validator/common.go (1)
IsEmpty(305-320)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (go)
🔇 Additional comments (2)
dto/validator/scanner.go (2)
41-43: Behavior change: optional zero values are now skipped. Verify this is intended across all types.Switching to
reflect.ValueOf(value).IsZero()means non-required fields with zero values (e.g., 0, false, "", nil pointer, empty slice/map, zero time) will skip validation. This matches “optional can be empty” but also means, for example, a non-requiredisInton anintfield set to 0 won’t be validated.Confirm this is desired for all validators.
47-49: Required + pointer-to-zero-value may pass unless IsEmpty recurses (ensure fixed).If
IsEmptyonly checks pointer nil-ness,requiredon*stringwhere the pointer is non-nil but points to "" would incorrectly pass. After applying theIsEmptyfix to recurse into pointers, this is addressed.Please confirm that required semantics should treat a non-nil pointer to a zero value as empty.
| func IsEmpty(v any) bool { | ||
| if v == nil { | ||
| return true | ||
| } | ||
| val := reflect.ValueOf(v) | ||
| switch val.Kind() { | ||
| case reflect.Pointer: | ||
| return val.IsNil() | ||
| case reflect.Array, reflect.Slice: | ||
| return val.Len() == 0 | ||
| case reflect.Map: | ||
| return val.Len() == 0 | ||
| default: | ||
| return val == nil | ||
| return val.IsZero() | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Fix reflect.Kind constant and make IsEmpty traverse pointers/interfaces (compile-time bug + semantics).
- reflect.Kind has Ptr, not Pointer. Using reflect.Pointer will not compile.
- Current pointer logic treats any non-nil pointer as “non-empty”, even if it points to a zero value (e.g., *string -> ""). This undermines “required” semantics for pointer fields.
Apply the following diff to fix both issues and make emptiness checks robust:
func IsEmpty(v any) bool {
if v == nil {
return true
}
val := reflect.ValueOf(v)
switch val.Kind() {
- case reflect.Pointer:
- return val.IsNil()
- case reflect.Array, reflect.Slice:
- return val.Len() == 0
- case reflect.Map:
- return val.Len() == 0
+ case reflect.Ptr, reflect.Interface:
+ if val.IsNil() {
+ return true
+ }
+ // Recurse into the underlying value so that pointers to zero values count as empty.
+ return IsEmpty(val.Elem().Interface())
+ case reflect.Array, reflect.Slice, reflect.Map:
+ return val.Len() == 0
default:
return val.IsZero()
}
}Run the following to ensure no other occurrences of the invalid constant remain:
🏁 Script executed:
#!/bin/bash
rg -n -C2 -g '!**/vendor/**' -P '\breflect\.Pointer\b'Length of output: 617
Fix reflect.Kind constant and expand IsEmpty pointer handling
The code currently uses the non-existent reflect.Pointer constant (it should be reflect.Ptr) and only treats top-level pointers as non-empty, even if they point to a zero value. Update both common.go and scanner.go:
• dto/validator/common.go:
- Replace
reflect.Pointerwithreflect.Ptrand addreflect.Interfaceto recurse into underlying values so that pointers to zero values count as empty.
• dto/validator/scanner.go: - Replace
reflect.Pointerwithreflect.Ptr.
Apply the following diffs:
--- a/dto/validator/common.go
+++ b/dto/validator/common.go
@@ -308,13 +308,18 @@ func IsEmpty(v any) bool {
val := reflect.ValueOf(v)
switch val.Kind() {
- case reflect.Pointer:
- return val.IsNil()
- case reflect.Array, reflect.Slice:
- return val.Len() == 0
- case reflect.Map:
- return val.Len() == 0
+ case reflect.Ptr, reflect.Interface:
+ if val.IsNil() {
+ return true
+ }
+ // Recurse into the underlying value so pointers to zero values are empty
+ return IsEmpty(val.Elem().Interface())
+ case reflect.Array, reflect.Slice, reflect.Map:
+ return val.Len() == 0
default:
return val.IsZero()
}
}--- a/dto/validator/scanner.go
+++ b/dto/validator/scanner.go
@@ -113,7 +113,7 @@ func scanFields(ct reflect.Type, data any, fields []fieldInfo) {
}
case "nested":
- if field.Type.Kind() == reflect.Pointer {
+ if field.Type.Kind() == reflect.Ptr {
ptr := ct.Field(i).Interface()
if reflect.ValueOf(ptr).IsNil() {
continue🤖 Prompt for AI Agents
In dto/validator/common.go around lines 305-320 and dto/validator/scanner.go
(where reflect.Pointer is used), replace the incorrect reflect.Pointer constant
with reflect.Ptr; in common.go also add a case for reflect.Interface and handle
both Ptr and Interface by recursing into the underlying value (checking for
IsNil first and using Elem()) so that pointers or interfaces that point to zero
values are treated as empty; update scanner.go usages to use reflect.Ptr instead
of reflect.Pointer.
| if field.Type.Kind() == reflect.Pointer { | ||
| err := Scanner(ct.Field(i).Interface()) | ||
| ptr := ct.Field(i).Interface() | ||
| if reflect.ValueOf(ptr).IsNil() { | ||
| continue | ||
| } | ||
| err := Scanner(ptr) | ||
| if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix pointer kind check and simplify nil guard in nested validation (compile-time bug).
reflect.Pointer is not a valid Kind; use reflect.Ptr. Also, use the field’s reflect.Value directly for the nil check to avoid interface boxing.
- if field.Type.Kind() == reflect.Pointer {
- ptr := ct.Field(i).Interface()
- if reflect.ValueOf(ptr).IsNil() {
- continue
- }
- err := Scanner(ptr)
+ if field.Type.Kind() == reflect.Ptr {
+ fv := ct.Field(i)
+ if fv.IsNil() {
+ continue
+ }
+ err := Scanner(fv.Interface())
if err != nil {
errMsg = append(errMsg, err.Error())
}
} else if field.Type.Kind() == reflect.Slice {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if field.Type.Kind() == reflect.Pointer { | |
| err := Scanner(ct.Field(i).Interface()) | |
| ptr := ct.Field(i).Interface() | |
| if reflect.ValueOf(ptr).IsNil() { | |
| continue | |
| } | |
| err := Scanner(ptr) | |
| if err != nil { | |
| if field.Type.Kind() == reflect.Ptr { | |
| fv := ct.Field(i) | |
| if fv.IsNil() { | |
| continue | |
| } | |
| err := Scanner(fv.Interface()) | |
| if err != nil { | |
| errMsg = append(errMsg, err.Error()) | |
| } | |
| } else if field.Type.Kind() == reflect.Slice { |
🤖 Prompt for AI Agents
In dto/validator/scanner.go around lines 115 to 121, the code incorrectly uses
reflect.Pointer (should be reflect.Ptr) and checks nil by boxing the pointer
into an interface; change the kind check to field.Type.Kind() == reflect.Ptr and
perform the nil guard using the field's reflect.Value (e.g., v := ct.Field(i);
if v.IsNil() { continue }) before calling Scanner on the underlying
interface/value to avoid the compile-time/type-safety bug.
584964a to
6d2df78
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
dto/validator/common_test.go (1)
236-247: Broaden IsEmpty test coverage (nil slice vs empty slice, primitives, non-empty cases, arrays, interface-typed nils)To lock in intended semantics and prevent regressions, add assertions for:
- nil slice vs empty slice
- non-empty slice/map (negative cases)
- empty primitives (string/number/bool) and zero time.Time
- non-nil pointer to zero-value struct (should be non-empty per current code)
- typed nil hidden in an interface
- arrays: zero-length vs zero-valued non-zero-length
This also documents the intended behavior difference from reflect.Value.IsZero for arrays.
func Test_IsEmpty(t *testing.T) { t.Parallel() assert.True(t, validator.IsEmpty(nil)) assert.True(t, validator.IsEmpty([]string{})) a := []interface{}{} assert.True(t, validator.IsEmpty(a)) b := make(map[string]interface{}) assert.True(t, validator.IsEmpty(b)) type Pointer struct{} var c *Pointer assert.True(t, validator.IsEmpty(c)) + + // Nil slice vs empty slice + var s []string = nil + assert.True(t, validator.IsEmpty(s)) + s2 := []string{"x"} + assert.False(t, validator.IsEmpty(s2)) + + // Non-empty map + b["k"] = "v" + assert.False(t, validator.IsEmpty(b)) + + // Empty primitives and zero-values + assert.True(t, validator.IsEmpty("")) + assert.True(t, validator.IsEmpty(0)) + assert.True(t, validator.IsEmpty(0.0)) + assert.True(t, validator.IsEmpty(false)) + assert.True(t, validator.IsEmpty(time.Time{})) + + // Non-nil pointer should be non-empty per current implementation + d := &Pointer{} + assert.False(t, validator.IsEmpty(d)) + + // Typed nil inside interface + var p *int = nil + var anyP interface{} = p + assert.True(t, validator.IsEmpty(anyP)) + + // Arrays: document semantics + var arr0 [0]int + assert.True(t, validator.IsEmpty(arr0)) + var arr2 [2]int + assert.False(t, validator.IsEmpty(arr2)) // note: reflect.Value.IsZero(arr2) would be true }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
dto/validator/common.go(1 hunks)dto/validator/common_test.go(1 hunks)dto/validator/scanner.go(2 hunks)dto/validator/scanner_test.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- dto/validator/scanner_test.go
- dto/validator/scanner.go
- dto/validator/common.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
dto/validator/common_test.go (1)
dto/validator/common.go (1)
IsEmpty(305-320)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (go)
🔇 Additional comments (2)
dto/validator/common_test.go (2)
233-248: Rename to IsEmpty and pointer coverage look goodThe test now aligns with the new IsEmpty API and correctly covers nil, empty containers, and a nil pointer case. This matches the implementation in dto/validator/common.go.
233-248: Confirm numeric and array “empty” semanticsIsEmpty currently uses
reflect.Value.IsZerofor scalars—so0andfalseare treated as empty—while arrays and slices are only checked viaLen(), meaning a non-zero-length, zero-valued array is not empty.Our searches show:
- No calls to
validator.IsNil(...)remain.- No direct uses of
IsEmpty(0|0.0|false)in the codebase.Please confirm this behavior is intentional for the scanner logic, especially for optional numeric fields where
0may be a meaningful value.
6d2df78 to
c0d780a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
dto/validator/common.go (2)
311-318: Fix compile-time bug and pointer/interface semantics in IsEmpty
- reflect.Kind has Ptr, not Pointer. This won’t compile.
- Non-nil pointers that point to zero values (e.g., *string to "") should be treated as empty for typical “omitempty” semantics.
- Interfaces wrapping nil/pointers need the same handling.
Apply:
switch val.Kind() { - case reflect.Pointer: - return val.IsNil() - case reflect.Array, reflect.Slice: - return val.Len() == 0 - case reflect.Map: - return val.Len() == 0 + case reflect.Ptr, reflect.Interface: + if val.IsNil() { + return true + } + // Recurse into underlying value so pointers/interfaces to zero values are empty. + return IsEmpty(val.Elem().Interface()) + case reflect.Array, reflect.Slice, reflect.Map: + return val.Len() == 0 default: return val.IsZero() }
311-312: Replace allreflect.Pointerusages withreflect.PtrTo avoid build errors and ensure consistency, update the two remaining occurrences of the invalid
reflect.Pointerconstant:• dto/validator/common.go (around line 311)
– case reflect.Pointer: – return val.IsNil() + case reflect.Ptr: + return val.IsNil()• dto/validator/scanner.go (around line 115)
– if field.Type.Kind() == reflect.Pointer { + if field.Type.Kind() == reflect.Ptr { ptr := ct.Field(i).Interface() err := Scanner(ptr)Ensure any other scanner or validator logic also uses
reflect.Ptrconsistently.
🧹 Nitpick comments (1)
dto/validator/common.go (1)
305-319: Add tests for non-nil pointers to zero values and interface-wrapped pointersYour current tests cover nil pointers; add cases for:
- p := new(string) (points to ""), expect IsEmpty(p) == true
- var v any = p, expect IsEmpty(v) == true
- Non-empty pointer, e.g., s := "x"; p := &s, expect IsEmpty(p) == false
Example tests (dto/validator/common_test.go):
func Test_IsEmpty_PointerToZeroString(t *testing.T) { var s string p := &s if !validator.IsEmpty(p) { t.Fatalf("expected pointer to zero string to be empty") } } func Test_IsEmpty_InterfaceWrappingPointer(t *testing.T) { var s string p := &s var v any = p if !validator.IsEmpty(v) { t.Fatalf("expected interface-wrapped pointer to zero value to be empty") } } func Test_IsEmpty_PointerToNonZeroString(t *testing.T) { s := "x" p := &s if validator.IsEmpty(p) { t.Fatalf("expected pointer to non-zero string to be non-empty") } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
dto/validator/common.go(1 hunks)dto/validator/common_test.go(1 hunks)dto/validator/scanner.go(2 hunks)dto/validator/scanner_test.go(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- dto/validator/common_test.go
- dto/validator/scanner_test.go
- dto/validator/scanner.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
dto/validator/common.go (2)
dto/validator/common_test.go (1)
Pointer(245-245)core/ctx.go (1)
Map(386-386)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (go)
No description provided.