fix(validation-gen): Correct ratcheting for uncorrelated old values

The validation ratcheting logic failed to distinguish between a field that was explicitly nil and a field that was absent in the old object (uncorrelated). safe.Field() returns nil in both scenarios.

This caused validation to be incorrectly skipped for oldObj that cannot be found during an update, as the logic treated the (nil, nil) old/new value pair as unchanged.

This commit introduces an oldValueCorrelated boolean flag to the generated validation functions. This flag is set to false when the parent of the old object is nil, signaling that a corresponding old value could not be found. The ratcheting check is now conditioned on this flag, ensuring that validation proceeds correctly.
This commit is contained in:
yongruilin 2025-11-05 01:57:08 +00:00
parent 030d72959e
commit a5a2cfdb35
5 changed files with 133 additions and 115 deletions

View file

@ -176,11 +176,11 @@ func Test_StructEmbedded(t *testing.T) {
st.Value(mkTest()).OldValue(mkTest()).ExpectValid()
}
// This test is to prove the bug of ratcheting behavior mistakenly skip validation on nil vs not found.
// TODO: update this test once the ratcheting behavior is fixed.
func Test_Mix(t *testing.T) {
st := localSchemeBuilder.Test(t)
st.Value(&MixComparableStruct{
Primitive: "a",
}).OldValue(nil).ExpectValid()
}).OldValue(nil).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Invalid(field.NewPath("NonComparable"), "", ""),
})
}

View file

@ -116,15 +116,15 @@ func Validate_DirectComparableStruct(ctx context.Context, op operation.Operation
// field DirectComparableStruct.IntField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field intField")...)
return
}(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *DirectComparableStruct) *int { return &oldObj.IntField }))...)
}(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *DirectComparableStruct) *int { return &oldObj.IntField }), oldObj != nil)...)
return errs
}
@ -137,15 +137,15 @@ func Validate_MixComparableStruct(ctx context.Context, op operation.Operation, f
// field MixComparableStruct.NonComparable
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field NonComparable")...)
return
}(fldPath.Child("NonComparable"), obj.NonComparable, safe.Field(oldObj, func(oldObj *MixComparableStruct) []string { return oldObj.NonComparable }))...)
}(fldPath.Child("NonComparable"), obj.NonComparable, safe.Field(oldObj, func(oldObj *MixComparableStruct) []string { return oldObj.NonComparable }), oldObj != nil)...)
return errs
}
@ -165,9 +165,9 @@ func Validate_NestedDirectComparableStruct(ctx context.Context, op operation.Ope
// field NestedDirectComparableStruct.DirectComparableStructField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
@ -177,7 +177,7 @@ func Validate_NestedDirectComparableStruct(ctx context.Context, op operation.Ope
return
}(fldPath.Child("directComparableStructField"), &obj.DirectComparableStructField, safe.Field(oldObj, func(oldObj *NestedDirectComparableStruct) *DirectComparableStruct {
return &oldObj.DirectComparableStructField
}))...)
}), oldObj != nil)...)
return errs
}
@ -189,9 +189,9 @@ func Validate_NestedNonDirectComparableStruct(ctx context.Context, op operation.
// field NestedNonDirectComparableStruct.NonDirectComparableStructField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -201,7 +201,7 @@ func Validate_NestedNonDirectComparableStruct(ctx context.Context, op operation.
return
}(fldPath.Child("nonDirectComparableStructField"), &obj.NonDirectComparableStructField, safe.Field(oldObj, func(oldObj *NestedNonDirectComparableStruct) *NonDirectComparableStruct {
return &oldObj.NonDirectComparableStructField
}))...)
}), oldObj != nil)...)
return errs
}
@ -213,15 +213,15 @@ func Validate_NonDirectComparableStruct(ctx context.Context, op operation.Operat
// field NonDirectComparableStruct.IntPtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field intPtrField")...)
return
}(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *NonDirectComparableStruct) *int { return oldObj.IntPtrField }))...)
}(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *NonDirectComparableStruct) *int { return oldObj.IntPtrField }), oldObj != nil)...)
return errs
}
@ -241,9 +241,9 @@ func Validate_StructEmbedded(ctx context.Context, op operation.Operation, fldPat
// field StructEmbedded.DirectComparableStruct
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
@ -251,13 +251,13 @@ func Validate_StructEmbedded(ctx context.Context, op operation.Operation, fldPat
// call the type's validation function
errs = append(errs, Validate_DirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("directComparableStruct"), &obj.DirectComparableStruct, safe.Field(oldObj, func(oldObj *StructEmbedded) *DirectComparableStruct { return &oldObj.DirectComparableStruct }))...)
}(fldPath.Child("directComparableStruct"), &obj.DirectComparableStruct, safe.Field(oldObj, func(oldObj *StructEmbedded) *DirectComparableStruct { return &oldObj.DirectComparableStruct }), oldObj != nil)...)
// field StructEmbedded.NonDirectComparableStruct
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -265,13 +265,13 @@ func Validate_StructEmbedded(ctx context.Context, op operation.Operation, fldPat
// call the type's validation function
errs = append(errs, Validate_NonDirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("nonDirectComparableStruct"), &obj.NonDirectComparableStruct, safe.Field(oldObj, func(oldObj *StructEmbedded) *NonDirectComparableStruct { return &oldObj.NonDirectComparableStruct }))...)
}(fldPath.Child("nonDirectComparableStruct"), &obj.NonDirectComparableStruct, safe.Field(oldObj, func(oldObj *StructEmbedded) *NonDirectComparableStruct { return &oldObj.NonDirectComparableStruct }), oldObj != nil)...)
// field StructEmbedded.NestedDirectComparableStructField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NestedDirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *NestedDirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
@ -281,13 +281,13 @@ func Validate_StructEmbedded(ctx context.Context, op operation.Operation, fldPat
return
}(fldPath.Child("nestedDirectComparableStructField"), &obj.NestedDirectComparableStructField, safe.Field(oldObj, func(oldObj *StructEmbedded) *NestedDirectComparableStruct {
return &oldObj.NestedDirectComparableStructField
}))...)
}), oldObj != nil)...)
// field StructEmbedded.NestedNonDirectComparableStructField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NestedNonDirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *NestedNonDirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -297,7 +297,7 @@ func Validate_StructEmbedded(ctx context.Context, op operation.Operation, fldPat
return
}(fldPath.Child("nestedNonDirectComparableStructField"), &obj.NestedNonDirectComparableStructField, safe.Field(oldObj, func(oldObj *StructEmbedded) *NestedNonDirectComparableStruct {
return &oldObj.NestedNonDirectComparableStructField
}))...)
}), oldObj != nil)...)
return errs
}
@ -309,9 +309,9 @@ func Validate_StructMap(ctx context.Context, op operation.Operation, fldPath *fi
// field StructMap.MapKeyField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj map[S]string) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj map[S]string, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -319,13 +319,13 @@ func Validate_StructMap(ctx context.Context, op operation.Operation, fldPath *fi
// iterate the map and call the key type's validation function
errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, Validate_S)...)
return
}(fldPath.Child("mapKeyField"), obj.MapKeyField, safe.Field(oldObj, func(oldObj *StructMap) map[S]string { return oldObj.MapKeyField }))...)
}(fldPath.Child("mapKeyField"), obj.MapKeyField, safe.Field(oldObj, func(oldObj *StructMap) map[S]string { return oldObj.MapKeyField }), oldObj != nil)...)
// field StructMap.MapValueField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj map[string]S) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj map[string]S, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -333,13 +333,13 @@ func Validate_StructMap(ctx context.Context, op operation.Operation, fldPath *fi
// iterate the map and call the value type's validation function
errs = append(errs, validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, Validate_S)...)
return
}(fldPath.Child("mapValueField"), obj.MapValueField, safe.Field(oldObj, func(oldObj *StructMap) map[string]S { return oldObj.MapValueField }))...)
}(fldPath.Child("mapValueField"), obj.MapValueField, safe.Field(oldObj, func(oldObj *StructMap) map[string]S { return oldObj.MapValueField }), oldObj != nil)...)
// field StructMap.AliasMapKeyTypeField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj AliasMapKeyType) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj AliasMapKeyType, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -347,13 +347,13 @@ func Validate_StructMap(ctx context.Context, op operation.Operation, fldPath *fi
// call the type's validation function
errs = append(errs, Validate_AliasMapKeyType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("aliasMapKeyTypeField"), obj.AliasMapKeyTypeField, safe.Field(oldObj, func(oldObj *StructMap) AliasMapKeyType { return oldObj.AliasMapKeyTypeField }))...)
}(fldPath.Child("aliasMapKeyTypeField"), obj.AliasMapKeyTypeField, safe.Field(oldObj, func(oldObj *StructMap) AliasMapKeyType { return oldObj.AliasMapKeyTypeField }), oldObj != nil)...)
// field StructMap.AliasMapValueTypeField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj AliasMapValueType) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj AliasMapValueType, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -361,7 +361,7 @@ func Validate_StructMap(ctx context.Context, op operation.Operation, fldPath *fi
// call the type's validation function
errs = append(errs, Validate_AliasMapValueType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("aliasMapValueTypeField"), obj.AliasMapValueTypeField, safe.Field(oldObj, func(oldObj *StructMap) AliasMapValueType { return oldObj.AliasMapValueTypeField }))...)
}(fldPath.Child("aliasMapValueTypeField"), obj.AliasMapValueTypeField, safe.Field(oldObj, func(oldObj *StructMap) AliasMapValueType { return oldObj.AliasMapValueTypeField }), oldObj != nil)...)
return errs
}
@ -373,21 +373,21 @@ func Validate_StructPrimitive(ctx context.Context, op operation.Operation, fldPa
// field StructPrimitive.IntField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field intField")...)
return
}(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *StructPrimitive) *int { return &oldObj.IntField }))...)
}(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *StructPrimitive) *int { return &oldObj.IntField }), oldObj != nil)...)
// field StructPrimitive.IntPtrField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
@ -400,7 +400,7 @@ func Validate_StructPrimitive(ctx context.Context, op operation.Operation, fldPa
}
errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field intPtrField")...)
return
}(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *StructPrimitive) *int { return oldObj.IntPtrField }))...)
}(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *StructPrimitive) *int { return oldObj.IntPtrField }), oldObj != nil)...)
return errs
}
@ -412,9 +412,9 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// field StructSlice.SliceField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []S) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []S, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -422,13 +422,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_S)...)
return
}(fldPath.Child("sliceField"), obj.SliceField, safe.Field(oldObj, func(oldObj *StructSlice) []S { return oldObj.SliceField }))...)
}(fldPath.Child("sliceField"), obj.SliceField, safe.Field(oldObj, func(oldObj *StructSlice) []S { return oldObj.SliceField }), oldObj != nil)...)
// field StructSlice.TypeDefSliceField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj MySlice) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj MySlice, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -436,7 +436,7 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// call the type's validation function
errs = append(errs, Validate_MySlice(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("typedefSliceField"), obj.TypeDefSliceField, safe.Field(oldObj, func(oldObj *StructSlice) MySlice { return oldObj.TypeDefSliceField }))...)
}(fldPath.Child("typedefSliceField"), obj.TypeDefSliceField, safe.Field(oldObj, func(oldObj *StructSlice) MySlice { return oldObj.TypeDefSliceField }), oldObj != nil)...)
return errs
}
@ -448,9 +448,9 @@ func Validate_StructStruct(ctx context.Context, op operation.Operation, fldPath
// field StructStruct.DirectComparableStructField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
@ -458,13 +458,13 @@ func Validate_StructStruct(ctx context.Context, op operation.Operation, fldPath
// call the type's validation function
errs = append(errs, Validate_DirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("directComparableStructField"), &obj.DirectComparableStructField, safe.Field(oldObj, func(oldObj *StructStruct) *DirectComparableStruct { return &oldObj.DirectComparableStructField }))...)
}(fldPath.Child("directComparableStructField"), &obj.DirectComparableStructField, safe.Field(oldObj, func(oldObj *StructStruct) *DirectComparableStruct { return &oldObj.DirectComparableStructField }), oldObj != nil)...)
// field StructStruct.NonDirectComparableStructField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -472,13 +472,13 @@ func Validate_StructStruct(ctx context.Context, op operation.Operation, fldPath
// call the type's validation function
errs = append(errs, Validate_NonDirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("nonDirectComparableStructField"), &obj.NonDirectComparableStructField, safe.Field(oldObj, func(oldObj *StructStruct) *NonDirectComparableStruct { return &oldObj.NonDirectComparableStructField }))...)
}(fldPath.Child("nonDirectComparableStructField"), &obj.NonDirectComparableStructField, safe.Field(oldObj, func(oldObj *StructStruct) *NonDirectComparableStruct { return &oldObj.NonDirectComparableStructField }), oldObj != nil)...)
// field StructStruct.DirectComparableStructPtr
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
@ -486,13 +486,13 @@ func Validate_StructStruct(ctx context.Context, op operation.Operation, fldPath
// call the type's validation function
errs = append(errs, Validate_DirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("directComparableStructPtrField"), obj.DirectComparableStructPtr, safe.Field(oldObj, func(oldObj *StructStruct) *DirectComparableStruct { return oldObj.DirectComparableStructPtr }))...)
}(fldPath.Child("directComparableStructPtrField"), obj.DirectComparableStructPtr, safe.Field(oldObj, func(oldObj *StructStruct) *DirectComparableStruct { return oldObj.DirectComparableStructPtr }), oldObj != nil)...)
// field StructStruct.NonDirectComparableStructPtr
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -500,13 +500,13 @@ func Validate_StructStruct(ctx context.Context, op operation.Operation, fldPath
// call the type's validation function
errs = append(errs, Validate_NonDirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("nonDirectComparableStructPtrField"), obj.NonDirectComparableStructPtr, safe.Field(oldObj, func(oldObj *StructStruct) *NonDirectComparableStruct { return oldObj.NonDirectComparableStructPtr }))...)
}(fldPath.Child("nonDirectComparableStructPtrField"), obj.NonDirectComparableStructPtr, safe.Field(oldObj, func(oldObj *StructStruct) *NonDirectComparableStruct { return oldObj.NonDirectComparableStructPtr }), oldObj != nil)...)
// field StructStruct.DirectComparableStruct
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *DirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
@ -514,13 +514,13 @@ func Validate_StructStruct(ctx context.Context, op operation.Operation, fldPath
// call the type's validation function
errs = append(errs, Validate_DirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(safe.Value(fldPath, func() *field.Path { return fldPath.Child("DirectComparableStruct") }), &obj.DirectComparableStruct, safe.Field(oldObj, func(oldObj *StructStruct) *DirectComparableStruct { return &oldObj.DirectComparableStruct }))...)
}(safe.Value(fldPath, func() *field.Path { return fldPath.Child("DirectComparableStruct") }), &obj.DirectComparableStruct, safe.Field(oldObj, func(oldObj *StructStruct) *DirectComparableStruct { return &oldObj.DirectComparableStruct }), oldObj != nil)...)
// field StructStruct.NonDirectComparableStruct
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj *NonDirectComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -528,7 +528,7 @@ func Validate_StructStruct(ctx context.Context, op operation.Operation, fldPath
// call the type's validation function
errs = append(errs, Validate_NonDirectComparableStruct(ctx, op, fldPath, obj, oldObj)...)
return
}(safe.Value(fldPath, func() *field.Path { return fldPath.Child("NonDirectComparableStruct") }), &obj.NonDirectComparableStruct, safe.Field(oldObj, func(oldObj *StructStruct) *NonDirectComparableStruct { return &oldObj.NonDirectComparableStruct }))...)
}(safe.Value(fldPath, func() *field.Path { return fldPath.Child("NonDirectComparableStruct") }), &obj.NonDirectComparableStruct, safe.Field(oldObj, func(oldObj *StructStruct) *NonDirectComparableStruct { return &oldObj.NonDirectComparableStruct }), oldObj != nil)...)
return errs
}

View file

@ -100,8 +100,6 @@ func Test_StructSlice(t *testing.T) {
}).ExpectValid()
}
// This test is to prove the bug of ratcheting behavior mistakenly skip validation on nil vs not found.
// TODO: update this test once the ratcheting behavior is fixed.
func Test_Items(t *testing.T) {
st := localSchemeBuilder.Test(t)
@ -113,5 +111,7 @@ func Test_Items(t *testing.T) {
Items: []Item{
{Key: "valid1"},
},
}).ExpectValid()
}).ExpectValidateFalseByPath(map[string][]string{
"items[0].data": {"field Data"},
})
}

View file

@ -64,15 +64,15 @@ func Validate_Item(ctx context.Context, op operation.Operation, fldPath *field.P
// field Item.Data
errs = append(errs,
func(fldPath *field.Path, obj, oldObj map[string]string) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Data")...)
return
}(fldPath.Child("data"), obj.Data, safe.Field(oldObj, func(oldObj *Item) map[string]string { return oldObj.Data }))...)
}(fldPath.Child("data"), obj.Data, safe.Field(oldObj, func(oldObj *Item) map[string]string { return oldObj.Data }), oldObj != nil)...)
return errs
}
@ -84,9 +84,9 @@ func Validate_ItemList(ctx context.Context, op operation.Operation, fldPath *fie
// field ItemList.Items
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []Item) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []Item, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -95,7 +95,7 @@ func Validate_ItemList(ctx context.Context, op operation.Operation, fldPath *fie
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key == b.Key }, validate.SemanticDeepEqual, Validate_Item)...)
return
}(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *ItemList) []Item { return oldObj.Items }))...)
}(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *ItemList) []Item { return oldObj.Items }), oldObj != nil)...)
return errs
}
@ -147,9 +147,9 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// field StructSlice.AtomicSliceStringField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []StringType) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []StringType, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -157,13 +157,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field AtomicSliceStringField[*]")
})...)
return
}(fldPath.Child("atomicSliceStringField"), obj.AtomicSliceStringField, safe.Field(oldObj, func(oldObj *StructSlice) []StringType { return oldObj.AtomicSliceStringField }))...)
}(fldPath.Child("atomicSliceStringField"), obj.AtomicSliceStringField, safe.Field(oldObj, func(oldObj *StructSlice) []StringType { return oldObj.AtomicSliceStringField }), oldObj != nil)...)
// field StructSlice.AtomicSliceTypeField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj IntSliceType) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj IntSliceType, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -171,13 +171,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field AtomicSliceTypeField[*]")
})...)
return
}(fldPath.Child("atomicSliceTypeField"), obj.AtomicSliceTypeField, safe.Field(oldObj, func(oldObj *StructSlice) IntSliceType { return oldObj.AtomicSliceTypeField }))...)
}(fldPath.Child("atomicSliceTypeField"), obj.AtomicSliceTypeField, safe.Field(oldObj, func(oldObj *StructSlice) IntSliceType { return oldObj.AtomicSliceTypeField }), oldObj != nil)...)
// field StructSlice.AtomicSliceComparableField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []ComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []ComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -185,13 +185,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field AtomicSliceComparableField[*]")
})...)
return
}(fldPath.Child("atomicSliceComparableField"), obj.AtomicSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStruct { return oldObj.AtomicSliceComparableField }))...)
}(fldPath.Child("atomicSliceComparableField"), obj.AtomicSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStruct { return oldObj.AtomicSliceComparableField }), oldObj != nil)...)
// field StructSlice.AtomicSliceNonComparableField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []NonComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []NonComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -201,13 +201,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, nil, nil, Validate_NonComparableStruct)...)
return
}(fldPath.Child("atomicSliceNonComparableField"), obj.AtomicSliceNonComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []NonComparableStruct { return oldObj.AtomicSliceNonComparableField }))...)
}(fldPath.Child("atomicSliceNonComparableField"), obj.AtomicSliceNonComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []NonComparableStruct { return oldObj.AtomicSliceNonComparableField }), oldObj != nil)...)
// field StructSlice.SetSliceComparableField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []ComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []ComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -217,13 +217,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// lists with set semantics require unique values
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...)
return
}(fldPath.Child("setSliceComparableField"), obj.SetSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStruct { return oldObj.SetSliceComparableField }))...)
}(fldPath.Child("setSliceComparableField"), obj.SetSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStruct { return oldObj.SetSliceComparableField }), oldObj != nil)...)
// field StructSlice.SetSliceNonComparableField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []NonComparableStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []NonComparableStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -235,13 +235,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual, nil, Validate_NonComparableStruct)...)
return
}(fldPath.Child("setSliceNonComparableField"), obj.SetSliceNonComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []NonComparableStruct { return oldObj.SetSliceNonComparableField }))...)
}(fldPath.Child("setSliceNonComparableField"), obj.SetSliceNonComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []NonComparableStruct { return oldObj.SetSliceNonComparableField }), oldObj != nil)...)
// field StructSlice.MapSliceComparableField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []ComparableStructWithKey) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []ComparableStructWithKey, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -251,13 +251,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a ComparableStructWithKey, b ComparableStructWithKey) bool { return a.Key == b.Key })...)
return
}(fldPath.Child("mapSliceComparableField"), obj.MapSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStructWithKey { return oldObj.MapSliceComparableField }))...)
}(fldPath.Child("mapSliceComparableField"), obj.MapSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStructWithKey { return oldObj.MapSliceComparableField }), oldObj != nil)...)
// field StructSlice.MapSliceNonComparableField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []NonComparableStructWithKey) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []NonComparableStructWithKey, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -269,13 +269,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a NonComparableStructWithKey, b NonComparableStructWithKey) bool { return a.Key == b.Key }, validate.SemanticDeepEqual, Validate_NonComparableStructWithKey)...)
return
}(fldPath.Child("mapSliceNonComparableField"), obj.MapSliceNonComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []NonComparableStructWithKey { return oldObj.MapSliceNonComparableField }))...)
}(fldPath.Child("mapSliceNonComparableField"), obj.MapSliceNonComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []NonComparableStructWithKey { return oldObj.MapSliceNonComparableField }), oldObj != nil)...)
// field StructSlice.MapSlicePtrKeyField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []PtrKeyStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []PtrKeyStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -293,13 +293,13 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
return ((a.Key == nil && b.Key == nil) || (a.Key != nil && b.Key != nil && *a.Key == *b.Key))
}, validate.SemanticDeepEqual, Validate_PtrKeyStruct)...)
return
}(fldPath.Child("mapSlicePtrKeyField"), obj.MapSlicePtrKeyField, safe.Field(oldObj, func(oldObj *StructSlice) []PtrKeyStruct { return oldObj.MapSlicePtrKeyField }))...)
}(fldPath.Child("mapSlicePtrKeyField"), obj.MapSlicePtrKeyField, safe.Field(oldObj, func(oldObj *StructSlice) []PtrKeyStruct { return oldObj.MapSlicePtrKeyField }), oldObj != nil)...)
// field StructSlice.MapSliceMixedKeyField
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []MixedKeyStruct) (errs field.ErrorList) {
func(fldPath *field.Path, obj, oldObj []MixedKeyStruct, oldValueCorrelated bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
@ -317,7 +317,7 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
return ((a.Key1 == nil && b.Key1 == nil) || (a.Key1 != nil && b.Key1 != nil && *a.Key1 == *b.Key1)) && a.Key2 == b.Key2
}, validate.SemanticDeepEqual, Validate_MixedKeyStruct)...)
return
}(fldPath.Child("mapSliceMixedKeyField"), obj.MapSliceMixedKeyField, safe.Field(oldObj, func(oldObj *StructSlice) []MixedKeyStruct { return oldObj.MapSliceMixedKeyField }))...)
}(fldPath.Child("mapSliceMixedKeyField"), obj.MapSliceMixedKeyField, safe.Field(oldObj, func(oldObj *StructSlice) []MixedKeyStruct { return oldObj.MapSliceMixedKeyField }), oldObj != nil)...)
return errs
}

View file

@ -1193,7 +1193,7 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild
}
sw.Do("// field $.inType|raw$.$.fieldName$\n", targs)
sw.Do("errs = append(errs,\n", targs)
sw.Do(" func(fldPath *$.field.Path|raw$, obj, oldObj $.fieldTypePfx$$.fieldType|raw$) (errs $.field.ErrorList|raw$) {\n", targs)
sw.Do(" func(fldPath *$.field.Path|raw$, obj, oldObj $.fieldTypePfx$$.fieldType|raw$, oldValueCorrelated bool) (errs $.field.ErrorList|raw$) {\n", targs)
if err := sw.Merge(buf, bufsw); err != nil {
panic(fmt.Sprintf("failed to merge buffer: %v", err))
}
@ -1207,10 +1207,28 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild
sw.Do("$.safe.Value|raw$(fldPath, func() *$.field.Path|raw$ { return fldPath.Child(\"$.fieldType|raw$\") }), ", targs)
}
sw.Do(" $.fieldExprPfx$obj.$.fieldName$, ", targs)
// safe.Field returns a nil if the old object does not have a correlatable
// value, such as a map.
// This is ambiguous with the case where the field exists and is nil.
// This ambiguity is a problem for ratcheting, which needs to distinguish
// these cases. For example, if a required field is removed from a map,
// safe.Field will return nil for the old value, and the new value is also
// nil (because it doesn't exist). Ratcheting would normally allow this,
// but it's a validation failure because a required field is missing.
//
// To solve this, we pass an extra boolean parameter to the validation
// function, indicating whether the old value was correlated. If the old
// value was uncorrelated, it means the field was not present in the old
// object, and we should not apply ratcheting logic. `oldObj != nil`
// provides this bit of information.
//
// This bit is not currently propagated down to deeper levels of
// validation, but since the code generator only ever looks one level
// down, this is sufficient for now.
sw.Do(" $.safe.Field|raw$(oldObj, ", targs)
sw.Do(" func(oldObj *$.inType|raw$) $.fieldTypePfx$$.fieldType|raw$ {", targs)
sw.Do(" return $.fieldExprPfx$oldObj.$.fieldName$", targs)
sw.Do(" }),", targs)
sw.Do(" }), oldObj != nil", targs)
sw.Do(" )...)\n", targs)
sw.Do("\n", nil)
} else {
@ -1257,10 +1275,10 @@ func emitRatchetingCheck(c *generator.Context, t *types.Type, sw *generator.Snip
// - obj != nil : handle optional fields which are updated to nil
// - oldObj != nil : handle optional fields which are updated from nil
// - *obj == *oldObj : compare values
sw.Do("if op.Type == $.operation.Update|raw$ && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {\n", targs)
sw.Do("if oldValueCorrelated && op.Type == $.operation.Update|raw$ && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {\n", targs)
} else {
targs["equality"] = mkSymbolArgs(c, equalityPkgSymbols)
sw.Do("if op.Type == $.operation.Update|raw$ && $.equality.Semantic|raw$.DeepEqual(obj, oldObj) {\n", targs)
sw.Do("if oldValueCorrelated && op.Type == $.operation.Update|raw$ && $.equality.Semantic|raw$.DeepEqual(obj, oldObj) {\n", targs)
}
sw.Do(" return nil\n", nil)
sw.Do("}\n", nil)