diff --git a/structs/structs.go b/structs/structs.go new file mode 100644 index 0000000..81bce61 --- /dev/null +++ b/structs/structs.go @@ -0,0 +1,77 @@ +// Package structs provides a function to unmarshal properties to structs. +// +// Use Go struct tag to define property mappings. For example: +// +// type Data struct { +// String string `cfg:"string"` +// Integer int `cfg:"integer"` +// Uinteger uint `cfg:"uinteger"` +// Bool bool `cfg:"bool"` +// Float float64 `cfg:"float"` +// } +// +// See strconv package to see how string value is converted to int, bool +// or float. +package structs + +import ( + "fmt" + "reflect" + "strconv" +) + +const ( + tagName = "cfg" +) + +// Unmarshal unmarshals properties p into v. +// v must be a pointer to a struct. +func Unmarshal(p map[string]string, v interface{}) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr { + return fmt.Errorf("value must be pointer to a struct: %+v", reflect.TypeOf(v)) + } + val := rv.Elem() + + for i := 0; i < val.NumField(); i++ { + tag := val.Type().Field(i).Tag.Get(tagName) + if tag == "" { + continue + } + strVal := p[tag] + if strVal == "" { + continue + } + field := val.Field(i) + switch field.Kind() { + case reflect.String: + field.SetString(strVal) + case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: + x, err := strconv.ParseInt(strVal, 10, 0) + if err != nil { + return err + } + field.SetInt(x) + case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: + x, err := strconv.ParseUint(strVal, 10, 0) + if err != nil { + return err + } + field.SetUint(x) + case reflect.Bool: + x, err := strconv.ParseBool(strVal) + if err != nil { + return err + } + field.SetBool(x) + case reflect.Float64, reflect.Float32: + x, err := strconv.ParseFloat(strVal, 0) + if err != nil { + return err + } + field.SetFloat(x) + } + + } + return nil +} diff --git a/structs/structs_test.go b/structs/structs_test.go new file mode 100644 index 0000000..6024e2a --- /dev/null +++ b/structs/structs_test.go @@ -0,0 +1,79 @@ +package structs + +import ( + "fmt" + "math" + "testing" +) + +func TestUnmarshal(t *testing.T) { + in := map[string]string{ + "string": "a string", + "integer": "-1234", + "uinteger": "1234", + "bool": "true", + "float": "1.234", + } + var out struct { + String string `cfg:"string"` + Integer int `cfg:"integer"` + Uinteger uint `cfg:"uinteger"` + Bool bool `cfg:"bool"` + Float float64 `cfg:"float"` + } + + err := Unmarshal(in, &out) + if err != nil { + t.Fatal(err) + } + if out.String != "a string" { + t.Errorf("unexpected string: %v", out.String) + } + if out.Integer != -1234 { + t.Errorf("unexpected integer: %v", out.Integer) + } + if out.Uinteger != 1234 { + t.Errorf("unexpected uinteger: %v", out.Uinteger) + } + if out.Bool != true { + t.Errorf("unexpected bool: %v", out.Bool) + } + if math.Abs(out.Float-1.234) > 0.001 { + t.Errorf("unexpected float: %v", out.Float) + } +} + +func ExampleUnmarshal() { + in := map[string]string{ + "name": "Foo Bar", + "age": "54", + "married": "true", + "height": "1.75", + } + var out struct { + Name string `cfg:"name"` + Age int `cfg:"age"` + Married bool `cfg:"married"` + Height float64 `cfg:"height"` + } + err := Unmarshal(in, &out) + if err != nil { + panic(err) + } + fmt.Printf("%+v\n", out) + // Output: + // {Name:Foo Bar Age:54 Married:true Height:1.75} +} + +func TestUnmarshalInvalidInt(t *testing.T) { + in := map[string]string{ + "integer": "abc", + } + var out struct { + Integer int `cfg:"integer"` + } + err := Unmarshal(in, &out) + if err == nil { + t.Fatal("error expected") + } +}