Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Iterate Over String Fields in Struct

Tags:

go

I'm looking to iterate over the string fields of a struct so I can do some clean-up/validation (with strings.TrimSpace, strings.Trim, etc).

Right now I have a messy switch-case that's not really scalable, and as this isn't in a hot spot of my application (a web form) it seems leveraging reflect is a good choice here.

I'm at a bit of a roadblock for how to implement this however, and the reflect docs are a little confusing to me (I've been digging through some other validation packages, but they're way too heavyweight + I'm using gorilla/schema for the unmarshalling part already):

  • Iterate over the struct
  • For each field of type string, apply whatever I need to from the strings package i.e. field = strings.TrimSpace(field)
  • If there exists a field.Tag.Get("max"), we'll use that value (strconv.Atoi, then unicode.RuneCountInString)
  • Provide an error slice that's also compatible with the error interface type

    type FormError []string         
    
    type Listing struct {
            Title string `max:"50"`
            Location string `max:"100"`
            Description string `max:"10000"`
            ExpiryDate time.Time
            RenderedDesc template.HTML
            Contact string `max:"255"`
        }
    
        // Iterate over our struct, fix whitespace/formatting where possible
        // and return errors encountered
        func (l *Listing) Validate() error {
    
           typ := l.Elem().Type()
    
           var invalid FormError
           for i = 0; i < typ.NumField(); i++ {
               // Iterate over fields
               // For StructFields of type string, field = strings.TrimSpace(field)
               // if field.Tag.Get("max") != "" {
               //     check max length/convert to int/utf8.RuneCountInString
                      if max length exceeded, invalid = append(invalid, "errormsg")
           }
    
           if len(invalid) > 0 {
               return invalid
           } 
    
           return nil
       }
    
    
       func (f FormError) Error() string {
           var fullError string
           for _, v := range f {
               fullError =+ v + "\n"
           }
           return "Errors were encountered during form processing: " + fullError
       }
    

Thanks in advance.

like image 478
elithrar Avatar asked Jan 21 '14 00:01

elithrar


3 Answers

What you want is primarily the methods on reflect.Value called NumFields() int and Field(int). The only thing you're really missing is the string check and SetString method.

package main

import "fmt"
import "reflect"
import "strings"

type MyStruct struct {
    A,B,C string
    I int
    D string
    J int
}

func main() {
    ms := MyStruct{"Green ", " Eggs", " and ", 2, " Ham      ", 15}
    // Print it out now so we can see the difference
    fmt.Printf("%s%s%s%d%s%d\n", ms.A, ms.B, ms.C, ms.I, ms.D, ms.J)

    // We need a pointer so that we can set the value via reflection
    msValuePtr := reflect.ValueOf(&ms)
    msValue := msValuePtr.Elem()

    for i := 0; i < msValue.NumField(); i++ {
        field := msValue.Field(i)

        // Ignore fields that don't have the same type as a string
        if field.Type() != reflect.TypeOf("") {
            continue
        }

        str := field.Interface().(string)
        str = strings.TrimSpace(str)
        field.SetString(str)
    }
    fmt.Printf("%s%s%s%d%s%d\n", ms.A, ms.B, ms.C, ms.I, ms.D, ms.J)
}

(Playground link)

There are two caveats here:

  1. You need a pointer to what you're going to change. If you have a value, you'll need to return the modified result.

  2. Attempts to modify unexported fields generally will cause reflect to panic. If you plan on modifying unexported fields, make sure to do this trick inside the package.

This code is rather flexible, you can use switch statements or type switches (on the value returned by field.Interface()) if you need differing behavior depending on the type.

Edit: As for the tag behavior, you seem to already have that figured out. Once you have field and have checked that it's a string, you can just use field.Tag.Get("max") and parse it from there.

Edit2: I made a small error on the tag. Tags are part of the reflect.Type of a struct, so to get them you can use (this is a bit long-winded) msValue.Type().Field(i).Tag.Get("max")

(Playground version of the code you posted in the comments with a working Tag get).

like image 194
Linear Avatar answered Oct 17 '22 06:10

Linear


I got beat to the punch, but since I went to the work, here's a solution:

type FormError []*string

type Listing struct {
    Title        string `max:"50"`
    Location     string `max:"100"`
    Description  string `max:"10000"`
    ExpiryDate   time.Time
    RenderedDesc template.HTML
    Contact      string `max:"255"`
}

// Iterate over our struct, fix whitespace/formatting where possible
// and return errors encountered
func (l *Listing) Validate() error {
    listingType := reflect.TypeOf(*l)
    listingValue := reflect.ValueOf(l)
    listingElem := listingValue.Elem()

    var invalid FormError = []*string{}
    // Iterate over fields
    for i := 0; i < listingElem.NumField(); i++ {
        fieldValue := listingElem.Field(i)
        // For StructFields of type string, field = strings.TrimSpace(field)
        if fieldValue.Type().Name() == "string" {
            newFieldValue := strings.TrimSpace(fieldValue.Interface().(string))
            fieldValue.SetString(newFieldValue)

            fieldType := listingType.Field(i)
            maxLengthStr := fieldType.Tag.Get("max")
            if maxLengthStr != "" {
                maxLength, err := strconv.Atoi(maxLengthStr)
                if err != nil {
                    panic("Field 'max' must be an integer")
                }
                //     check max length/convert to int/utf8.RuneCountInString
                if utf8.RuneCountInString(newFieldValue) > maxLength {
                    //     if max length exceeded, invalid = append(invalid, "errormsg")
                    invalidMessage := `"`+fieldType.Name+`" is too long (max allowed: `+maxLengthStr+`)`
                    invalid = append(invalid, &invalidMessage)
                }
            }
        }
    }

    if len(invalid) > 0 {
        return invalid
    }

    return nil
}

func (f FormError) Error() string {
    var fullError string
    for _, v := range f {
        fullError = *v + "\n"
    }
    return "Errors were encountered during form processing: " + fullError
}

I see you asked about how to do the tags. Reflection has two components: a type and a value. The tag is associated with the type, so you have to get it separately than the field: listingType := reflect.TypeOf(*l). Then you can get the indexed field and the tag from that.

like image 5
Tyson Avatar answered Oct 17 '22 07:10

Tyson


I don't know if it's a good way, but I use it like this.

https://play.golang.org/p/aQ_hG2BYmMD

You can send the address of a struct to this function. Sorry for My English is not very good.

trimStruct(&someStruct)

func trimStruct(v interface{}) {
    bytes, err := json.Marshal(v)
    if err != nil {
        fmt.Println("[trimStruct] Marshal Error :", err)
    }
    var mapSI map[string]interface{}
    if err := json.Unmarshal(bytes, &mapSI); err != nil {
        fmt.Println("[trimStruct] Unmarshal to byte Error :", err)
    }
    mapSI = trimMapStringInterface(mapSI).(map[string]interface{})
    bytes2, err := json.Marshal(mapSI)
    if err != nil {
        fmt.Println("[trimStruct] Marshal Error :", err)
    }
    if err := json.Unmarshal(bytes2, v); err != nil {
        fmt.Println("[trimStruct] Unmarshal to b Error :", err)
    }
}

func trimMapStringInterface(data interface{}) interface{} {
    if values, valid := data.([]interface{}); valid {
        for i := range values {
            data.([]interface{})[i] = trimMapStringInterface(values[i])
        }
    } else if values, valid := data.(map[string]interface{}); valid {
        for k, v := range values {
            data.(map[string]interface{})[k] = trimMapStringInterface(v)
        }
    } else if value, valid := data.(string); valid {
        data = strings.TrimSpace(value)
    }
    return data
}
like image 1
Pongsapak Pankhao Avatar answered Oct 17 '22 07:10

Pongsapak Pankhao