Files

86 lines
1.9 KiB
Go

package dto
import (
"reflect"
"golang.org/x/text/unicode/norm"
)
// Normalize iterates through an object and performs Unicode normalization on all string fields with the `unorm` tag
func Normalize(obj any) {
normalizeValue(reflect.ValueOf(obj))
}
func normalizeValue(value reflect.Value) {
if !value.IsValid() {
return
}
// Unwrap interfaces and pointers so nested DTOs share the same traversal
kind := value.Kind()
if kind == reflect.Interface || kind == reflect.Pointer {
if !value.IsNil() {
normalizeValue(value.Elem())
}
return
}
// Walk collections because request DTOs may contain nested slices or arrays
if kind == reflect.Slice || kind == reflect.Array {
for i := range value.Len() {
normalizeValue(value.Index(i))
}
return
}
// Ignore scalar values because normalization is opt-in through struct field tags
if kind != reflect.Struct {
return
}
// Normalize tagged fields directly and recursively inspect untagged nested values
valueType := value.Type()
for i := range value.NumField() {
field := value.Field(i)
form, tagged := normalizationForm(valueType.Field(i).Tag.Get("unorm"))
if tagged {
normalizeString(field, form)
continue
}
normalizeValue(field)
}
}
func normalizationForm(tag string) (norm.Form, bool) {
switch tag {
case "nfc":
return norm.NFC, true
case "nfkc":
return norm.NFKC, true
case "nfd":
return norm.NFD, true
case "nfkd":
return norm.NFKD, true
default:
return 0, false
}
}
func normalizeString(value reflect.Value, form norm.Form) {
// Dereference optional string fields while leaving nil values unchanged
if value.Kind() == reflect.Pointer {
if value.IsNil() {
return
}
value = value.Elem()
}
// Ignore incompatible or read-only fields because reflection cannot safely update them
if value.Kind() != reflect.String || !value.CanSet() {
return
}
value.SetString(form.String(value.String()))
}