xml: add Marshal and MarshalIndent

I have written up a Marshal and MarshalIndent pair that should
closely reflect the way that Unmarshal works.  I would love feedback
on making this code more accessible and efficient... I haven't used
reflecton on this scale before, so there is probably a lot of work
that can be done on that.

Some potentially controversial things:
- All tag names are lower-cased by default.
- Zero-valued struct values are skipped.
- No namespace prefix (o:tag, etc) mechanism is supplied.
- You are allowed to marshal non-struct values (even though unmarshal
  cannot handle them).
- A tag for a non-XMLName struct field that isn't "attr", "chardata",
  or "innerxml" is used as the name of the tag.  This could wreak
  havoc if you try to marshal a protobuf struct.
- The "innerxml" and "chardata" are inserted verbatim.  If you try to
  marshal something straight from unmarshal, the results could be
  unexpected (remove "innerxml" support from Marshal would be one
  possible solution).

R=rsc
CC=golang-dev
https://golang.org/cl/4539082
This commit is contained in:
Kyle Lemons 2011-06-27 19:07:28 -04:00 committed by Russ Cox
parent 9ded2b3451
commit abd50de296
4 changed files with 578 additions and 0 deletions

299
src/pkg/xml/marshal_test.go Normal file
View file

@ -0,0 +1,299 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package xml
import (
"reflect"
"testing"
"os"
"bytes"
"strings"
"strconv"
)
type DriveType int
const (
HyperDrive DriveType = iota
ImprobabilityDrive
)
type Passenger struct {
Name []string "name"
Weight float32 "weight"
}
type Ship struct {
XMLName Name "spaceship"
Name string "attr"
Pilot string "attr"
Drive DriveType "drive"
Age uint "age"
Passenger []*Passenger "passenger"
secret string
}
type RawXML string
func (rx RawXML) MarshalXML() ([]byte, os.Error) {
return []byte(rx), nil
}
type NamedType string
type Port struct {
XMLName Name "port"
Type string "attr"
Number string "chardata"
}
type Domain struct {
XMLName Name "domain"
Country string "attr"
Name []byte "chardata"
}
type SecretAgent struct {
XMLName Name "agent"
Handle string "attr"
Identity string
Obfuscate string "innerxml"
}
var nilStruct *Ship
var marshalTests = []struct {
Value interface{}
ExpectXML string
}{
// Test nil marshals to nothing
{Value: nil, ExpectXML: ``},
{Value: nilStruct, ExpectXML: ``},
// Test value types (no tag name, so ???)
{Value: true, ExpectXML: `<???>true</???>`},
{Value: int(42), ExpectXML: `<???>42</???>`},
{Value: int8(42), ExpectXML: `<???>42</???>`},
{Value: int16(42), ExpectXML: `<???>42</???>`},
{Value: int32(42), ExpectXML: `<???>42</???>`},
{Value: uint(42), ExpectXML: `<???>42</???>`},
{Value: uint8(42), ExpectXML: `<???>42</???>`},
{Value: uint16(42), ExpectXML: `<???>42</???>`},
{Value: uint32(42), ExpectXML: `<???>42</???>`},
{Value: float32(1.25), ExpectXML: `<???>1.25</???>`},
{Value: float64(1.25), ExpectXML: `<???>1.25</???>`},
{Value: uintptr(0xFFDD), ExpectXML: `<???>65501</???>`},
{Value: "gopher", ExpectXML: `<???>gopher</???>`},
{Value: []byte("gopher"), ExpectXML: `<???>gopher</???>`},
{Value: "</>", ExpectXML: `<???>&lt;/&gt;</???>`},
{Value: []byte("</>"), ExpectXML: `<???>&lt;/&gt;</???>`},
{Value: [3]byte{'<', '/', '>'}, ExpectXML: `<???>&lt;/&gt;</???>`},
{Value: NamedType("potato"), ExpectXML: `<???>potato</???>`},
{Value: []int{1, 2, 3}, ExpectXML: `<???>1</???><???>2</???><???>3</???>`},
{Value: [3]int{1, 2, 3}, ExpectXML: `<???>1</???><???>2</???><???>3</???>`},
// Test innerxml
{Value: RawXML("</>"), ExpectXML: `</>`},
{
Value: &SecretAgent{
Handle: "007",
Identity: "James Bond",
Obfuscate: "<redacted/>",
},
//ExpectXML: `<agent handle="007"><redacted/></agent>`,
ExpectXML: `<agent handle="007"><Identity>James Bond</Identity><redacted/></agent>`,
},
// Test structs
{Value: &Port{Type: "ssl", Number: "443"}, ExpectXML: `<port type="ssl">443</port>`},
{Value: &Port{Number: "443"}, ExpectXML: `<port>443</port>`},
{Value: &Port{Type: "<unix>"}, ExpectXML: `<port type="&lt;unix&gt;"></port>`},
{Value: &Domain{Name: []byte("google.com&friends")}, ExpectXML: `<domain>google.com&amp;friends</domain>`},
{Value: atomValue, ExpectXML: atomXml},
{
Value: &Ship{
Name: "Heart of Gold",
Pilot: "Computer",
Age: 1,
Drive: ImprobabilityDrive,
Passenger: []*Passenger{
&Passenger{
Name: []string{"Zaphod", "Beeblebrox"},
Weight: 7.25,
},
&Passenger{
Name: []string{"Trisha", "McMillen"},
Weight: 5.5,
},
&Passenger{
Name: []string{"Ford", "Prefect"},
Weight: 7,
},
&Passenger{
Name: []string{"Arthur", "Dent"},
Weight: 6.75,
},
},
},
ExpectXML: `<spaceship name="Heart of Gold" pilot="Computer">` +
`<drive>` + strconv.Itoa(int(ImprobabilityDrive)) + `</drive>` +
`<age>1</age>` +
`<passenger>` +
`<name>Zaphod</name>` +
`<name>Beeblebrox</name>` +
`<weight>7.25</weight>` +
`</passenger>` +
`<passenger>` +
`<name>Trisha</name>` +
`<name>McMillen</name>` +
`<weight>5.5</weight>` +
`</passenger>` +
`<passenger>` +
`<name>Ford</name>` +
`<name>Prefect</name>` +
`<weight>7</weight>` +
`</passenger>` +
`<passenger>` +
`<name>Arthur</name>` +
`<name>Dent</name>` +
`<weight>6.75</weight>` +
`</passenger>` +
`</spaceship>`,
},
}
func TestMarshal(t *testing.T) {
for idx, test := range marshalTests {
buf := bytes.NewBuffer(nil)
err := Marshal(buf, test.Value)
if err != nil {
t.Errorf("#%d: Error: %s", idx, err)
continue
}
if got, want := buf.String(), test.ExpectXML; got != want {
if strings.Contains(want, "\n") {
t.Errorf("#%d: marshal(%#v) - GOT:\n%s\nWANT:\n%s", idx, test.Value, got, want)
} else {
t.Errorf("#%d: marshal(%#v) = %#q want %#q", idx, test.Value, got, want)
}
}
}
}
var marshalErrorTests = []struct {
Value interface{}
ExpectErr string
ExpectKind reflect.Kind
}{
{
Value: make(chan bool),
ExpectErr: "xml: unsupported type: chan bool",
ExpectKind: reflect.Chan,
},
{
Value: map[string]string{
"question": "What do you get when you multiply six by nine?",
"answer": "42",
},
ExpectErr: "xml: unsupported type: map[string] string",
ExpectKind: reflect.Map,
},
{
Value: map[*Ship]bool{nil: false},
ExpectErr: "xml: unsupported type: map[*xml.Ship] bool",
ExpectKind: reflect.Map,
},
}
func TestMarshalErrors(t *testing.T) {
for idx, test := range marshalErrorTests {
buf := bytes.NewBuffer(nil)
err := Marshal(buf, test.Value)
if got, want := err, test.ExpectErr; got == nil {
t.Errorf("#%d: want error %s", idx, want)
continue
} else if got.String() != want {
t.Errorf("#%d: marshal(%#v) = [error] %q, want %q", idx, test.Value, got, want)
}
if got, want := err.(*UnsupportedTypeError).Type.Kind(), test.ExpectKind; got != want {
t.Errorf("#%d: marshal(%#v) = [error kind] %s, want %s", idx, test.Value, got, want)
}
}
}
// Do invertibility testing on the various structures that we test
func TestUnmarshal(t *testing.T) {
for i, test := range marshalTests {
// Skip the nil pointers
if i <= 1 {
continue
}
var dest interface{}
switch test.Value.(type) {
case *Ship, Ship:
dest = &Ship{}
case *Port, Port:
dest = &Port{}
case *Domain, Domain:
dest = &Domain{}
case *Feed, Feed:
dest = &Feed{}
default:
continue
}
buffer := bytes.NewBufferString(test.ExpectXML)
err := Unmarshal(buffer, dest)
// Don't compare XMLNames
switch fix := dest.(type) {
case *Ship:
fix.XMLName = Name{}
case *Port:
fix.XMLName = Name{}
case *Domain:
fix.XMLName = Name{}
case *Feed:
fix.XMLName = Name{}
fix.Author.InnerXML = ""
for i := range fix.Entry {
fix.Entry[i].Author.InnerXML = ""
}
}
if err != nil {
t.Errorf("#%d: unexpected error: %#v", i, err)
} else if got, want := dest, test.Value; !reflect.DeepEqual(got, want) {
t.Errorf("#%d: unmarshal(%#s) = %#v, want %#v", i, test.ExpectXML, got, want)
}
}
}
func BenchmarkMarshal(b *testing.B) {
idx := len(marshalTests) - 1
test := marshalTests[idx]
buf := bytes.NewBuffer(nil)
for i := 0; i < b.N; i++ {
Marshal(buf, test.Value)
buf.Truncate(0)
}
}
func BenchmarkUnmarshal(b *testing.B) {
idx := len(marshalTests) - 1
test := marshalTests[idx]
sm := &Ship{}
xml := []byte(test.ExpectXML)
for i := 0; i < b.N; i++ {
buffer := bytes.NewBuffer(xml)
Unmarshal(buffer, sm)
}
}