From 3426b80dd9483a25d8d12fa0f39be7686f26c97b Mon Sep 17 00:00:00 2001 From: Gustavo Niemeyer Date: Tue, 18 Jan 2011 15:39:38 -0500 Subject: [PATCH] xml: support for > in tags This introduces support for selecting which subelement to unmarshal into a given struct field by providing a nesting path separated by the > character. R=rsc CC=golang-dev https://golang.org/cl/4066041 --- src/pkg/xml/read.go | 88 ++++++++++++++++++++++++++++++++++++++-- src/pkg/xml/read_test.go | 68 +++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/src/pkg/xml/read.go b/src/pkg/xml/read.go index 9175659b23e..1f50e0b86b8 100644 --- a/src/pkg/xml/read.go +++ b/src/pkg/xml/read.go @@ -39,6 +39,7 @@ import ( // Name string // Phone string // Email []Email +// Groups []string "group>value" // } // // result := Result{Name: "name", Phone: "phone", Email: nil} @@ -53,6 +54,10 @@ import ( // gre@work.com // // Grace R. Emlin +// +// Friends +// Squash +// //
123 Main Street
// // @@ -65,10 +70,13 @@ import ( // Email{"home", "gre@example.com"}, // Email{"work", "gre@work.com"}, // }, +// []string{"Friends", "Squash"}, // } // // Note that the field r.Phone has not been modified and -// that the XML
element was discarded. +// that the XML
element was discarded. Also, the field +// Groups was assigned considering the element path provided in the +// field tag. // // Because Unmarshal uses the reflect package, it can only // assign to upper case fields. Unmarshal uses a case-insensitive @@ -97,6 +105,13 @@ import ( // The struct field may have type []byte or string. // If there is no such field, the character data is discarded. // +// * If the XML element contains a sub-element whose name matches +// the prefix of a struct field tag formatted as "a>b>c", unmarshal +// will descend into the XML structure looking for elements with the +// given names, and will map the innermost elements to that struct field. +// A struct field tag starting with ">" is equivalent to one starting +// with the field name followed by ">". +// // * If the XML element contains a sub-element whose name // matches a struct field whose tag is neither "attr" nor "chardata", // Unmarshal maps the sub-element to that struct field. @@ -104,7 +119,7 @@ import ( // maps the sub-element to that struct field. // // Unmarshal maps an XML element to a string or []byte by saving the -// concatenation of that elements character data in the string or []byte. +// concatenation of that element's character data in the string or []byte. // // Unmarshal maps an XML element to a slice by extending the length // of the slice and mapping the element to the newly created value. @@ -211,7 +226,9 @@ func (p *Parser) unmarshal(val reflect.Value, start *StartElement) os.Error { saveXMLData []byte sv *reflect.StructValue styp *reflect.StructType + fieldPaths map[string]fieldPath ) + switch v := val.(type) { default: return os.ErrorString("unknown type " + v.Type().String()) @@ -330,6 +347,23 @@ func (p *Parser) unmarshal(val reflect.Value, start *StartElement) os.Error { saveXMLIndex = p.savedOffset() } } + + default: + i := strings.Index(f.Tag, ">") + if i != -1 { + if fieldPaths == nil { + fieldPaths = make(map[string]fieldPath) + } + path := strings.ToLower(f.Tag) + if i == 0 { + path = strings.ToLower(f.Name) + path + } + if path[len(path)-1] == '>' { + path = path[:len(path)-1] + } + s := strings.Split(path, ">", -1) + fieldPaths[s[0]] = fieldPath{s[1:], f.Index} + } } } } @@ -351,10 +385,21 @@ Loop: // Sub-element. // Look up by tag name. if sv != nil { - k := fieldName(t.Name.Local) + k := strings.ToLower(fieldName(t.Name.Local)) + + if fieldPaths != nil { + if fp, ok := fieldPaths[k]; ok { + val := sv.FieldByIndex(fp.Index) + if err := p.unmarshalPath(val, &t, fp.Path); err != nil { + return err + } + continue Loop + } + } + match := func(s string) bool { // check if the name matches ignoring case - if strings.ToLower(s) != strings.ToLower(k) { + if strings.ToLower(s) != k { return false } // now check that it's public @@ -470,6 +515,41 @@ Loop: return nil } +type fieldPath struct { + Path []string + Index []int +} + +// unmarshalPath finds the nested elements matching the +// provided path and calls unmarshal on the tip elements. +func (p *Parser) unmarshalPath(val reflect.Value, start *StartElement, path []string) os.Error { + if len(path) == 0 { + return p.unmarshal(val, start) + } + for { + tok, err := p.Token() + if err != nil { + return err + } + switch t := tok.(type) { + case StartElement: + k := fieldName(t.Name.Local) + if k == path[0] { + if err := p.unmarshalPath(val, &t, path[1:]); err != nil { + return err + } + continue + } + if err := p.Skip(); err != nil { + return err + } + case EndElement: + return nil + } + } + panic("unreachable") +} + // Have already read a start element. // Read tokens until we find the end element. // Token is taking care of making sure the diff --git a/src/pkg/xml/read_test.go b/src/pkg/xml/read_test.go index 9ec1065c23d..72b907704ba 100644 --- a/src/pkg/xml/read_test.go +++ b/src/pkg/xml/read_test.go @@ -230,3 +230,71 @@ func TestFieldName(t *testing.T) { } } } + +const pathTestString = ` + + 1 + + + A + + + B + + + C + D + + + 2 + +` + +type PathTestItem struct { + Value string +} + +type PathTestA struct { + Items []PathTestItem ">item" + Before, After string +} + +type PathTestB struct { + Other []PathTestItem "items>Item" + Before, After string +} + +type PathTestC struct { + Values []string "items>item>value" + Before, After string +} + +type PathTestSet struct { + Item []PathTestItem +} + +type PathTestD struct { + Other PathTestSet "items>" + Before, After string +} + +var pathTests = []interface{}{ + &PathTestA{Items: []PathTestItem{{"A"}, {"D"}}, Before: "1", After: "2"}, + &PathTestB{Other: []PathTestItem{{"A"}, {"D"}}, Before: "1", After: "2"}, + &PathTestC{Values: []string{"A", "C", "D"}, Before: "1", After: "2"}, + &PathTestD{Other: PathTestSet{Item: []PathTestItem{{"A"}, {"D"}}}, Before: "1", After: "2"}, +} + +func TestUnmarshalPaths(t *testing.T) { + for _, pt := range pathTests { + p := reflect.MakeZero(reflect.NewValue(pt).Type()).(*reflect.PtrValue) + p.PointTo(reflect.MakeZero(p.Type().(*reflect.PtrType).Elem())) + v := p.Interface() + if err := Unmarshal(StringReader(pathTestString), v); err != nil { + t.Fatalf("Unmarshal: %s", err) + } + if !reflect.DeepEqual(v, pt) { + t.Fatalf("have %#v\nwant %#v", v, pt) + } + } +}