Skip to content

Commit 088fa82

Browse files
committed
initial support for marshaling self-closing XML tags
add ,selfclosing flag to xml struct tags support ,selfclosing flag on XMLName fields
1 parent 0a901bc commit 088fa82

File tree

4 files changed

+83
-6
lines changed

4 files changed

+83
-6
lines changed

marshal.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,12 @@ func (enc *Encoder) EncodeToken(t Token) error {
211211

212212
p := &enc.p
213213
switch t := t.(type) {
214+
case SelfClosingElement:
215+
if err := p.writeStart((*StartElement)(&t), true); err != nil {
216+
return err
217+
}
214218
case StartElement:
215-
if err := p.writeStart(&t); err != nil {
219+
if err := p.writeStart(&t, false); err != nil {
216220
return err
217221
}
218222
case EndElement:
@@ -593,7 +597,16 @@ func (p *printer) marshalValue(val reflect.Value, finfo *fieldInfo, startTemplat
593597
start.Attr = append(start.Attr, Attr{Name{Space: "", Local: xmlnsPrefix}, ""})
594598
}
595599

596-
if err := p.writeStart(&start); err != nil {
600+
// If this is a self-closing tag, write the tag and return.
601+
if finfo != nil && finfo.flags&fSelfClosing != 0 ||
602+
tinfo.xmlname != nil && tinfo.xmlname.flags&fSelfClosing != 0 {
603+
if err := p.writeStart(&start, true); err != nil {
604+
return err
605+
}
606+
return p.cachedWriteError()
607+
}
608+
609+
if err := p.writeStart(&start, false); err != nil {
597610
return err
598611
}
599612

@@ -748,7 +761,7 @@ func (p *printer) marshalInterface(val Marshaler, start StartElement) error {
748761

749762
// marshalTextInterface marshals a TextMarshaler interface value.
750763
func (p *printer) marshalTextInterface(val encoding.TextMarshaler, start StartElement) error {
751-
if err := p.writeStart(&start); err != nil {
764+
if err := p.writeStart(&start, false); err != nil {
752765
return err
753766
}
754767
text, err := val.MarshalText()
@@ -760,7 +773,8 @@ func (p *printer) marshalTextInterface(val encoding.TextMarshaler, start StartEl
760773
}
761774

762775
// writeStart writes the given start element.
763-
func (p *printer) writeStart(start *StartElement) error {
776+
// If close is true, it is written as a self-closing element.
777+
func (p *printer) writeStart(start *StartElement, close bool) error {
764778
if start.Name.Local == "" {
765779
return fmt.Errorf("xml: start tag with no name")
766780
}
@@ -858,7 +872,13 @@ func (p *printer) writeStart(start *StartElement) error {
858872
p.EscapeString(attr.Value)
859873
p.WriteByte('"')
860874
}
861-
p.WriteByte('>')
875+
if close {
876+
p.WriteString("/>")
877+
// Pop elements stack
878+
p.elements = p.elements[:len(p.elements)-1]
879+
} else {
880+
p.WriteByte('>')
881+
}
862882
return nil
863883
}
864884

@@ -1217,7 +1237,7 @@ func (s *parentStack) trim(parents []string) error {
12171237
// push adds parent elements to the stack and writes open tags.
12181238
func (s *parentStack) push(parents []string) error {
12191239
for i := 0; i < len(parents); i++ {
1220-
if err := s.p.writeStart(&StartElement{Name: Name{Local: parents[i]}}); err != nil {
1240+
if err := s.p.writeStart(&StartElement{Name: Name{Local: parents[i]}}, false); err != nil {
12211241
return err
12221242
}
12231243
}

marshal_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,17 @@ type Generic[T any] struct {
530530

531531
type EPP struct {
532532
XMLName struct{} `xml:"urn:ietf:params:xml:ns:epp-1.0 epp"`
533+
Hello *Hello `xml:"hello,omitempty,selfclosing"`
533534
Command *Command `xml:"command,omitempty"`
534535
}
535536

537+
type Hello struct{}
538+
539+
type Closer struct {
540+
XMLName struct{} `xml:"closer,selfclosing"`
541+
Beverage string `xml:"beverage,attr,omitempty"`
542+
}
543+
536544
type Command struct {
537545
Check *Check `xml:"urn:ietf:params:xml:ns:epp-1.0 check,omitempty"`
538546
}
@@ -1698,11 +1706,25 @@ var marshalTests = []struct {
16981706
UnmarshalOnly: true,
16991707
},
17001708

1709+
// Test self-closing tags
1710+
{
1711+
ExpectXML: `<closer/>`,
1712+
Value: &Closer{},
1713+
},
1714+
{
1715+
ExpectXML: `<closer beverage="coffee"/>`,
1716+
Value: &Closer{Beverage: "coffee"},
1717+
},
1718+
17011719
// Test namespace prefixes
17021720
{
17031721
ExpectXML: `<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"></epp>`,
17041722
Value: &EPP{},
17051723
},
1724+
{
1725+
ExpectXML: `<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"><hello/></epp>`,
1726+
Value: &EPP{Hello: &Hello{}},
1727+
},
17061728
{
17071729
ExpectXML: `<epp xmlns="urn:ietf:params:xml:ns:epp-1.0"><command></command></epp>`,
17081730
Value: &EPP{Command: &Command{}},
@@ -2065,6 +2087,22 @@ var encodeTokenTests = []struct {
20652087
StartElement{Name{"space", "local"}, nil},
20662088
},
20672089
want: `<local xmlns="space">`,
2090+
}, {
2091+
desc: "self-closing element with namespace",
2092+
toks: []Token{
2093+
SelfClosingElement{Name{"space", "local"}, nil},
2094+
},
2095+
want: `<local xmlns="space"/>`,
2096+
}, {
2097+
desc: "self-closing elements inside other elements",
2098+
toks: []Token{
2099+
StartElement{Name{"", "outer"}, nil},
2100+
SelfClosingElement{Name{"", "a"}, nil},
2101+
SelfClosingElement{Name{"", "b"}, nil},
2102+
SelfClosingElement{Name{"", "c"}, nil},
2103+
EndElement{Name{"", "outer"}},
2104+
},
2105+
want: `<outer><a/><b/><c/></outer>`,
20682106
}, {
20692107
desc: "start element with no name",
20702108
toks: []Token{

typeinfo.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const (
4040

4141
fOmitEmpty
4242

43+
fSelfClosing
44+
4345
fMode = fElement | fAttr | fCDATA | fCharData | fInnerXML | fComment | fAny
4446

4547
xmlName = "XMLName"
@@ -140,6 +142,8 @@ func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, erro
140142
finfo.flags |= fAny
141143
case "omitempty":
142144
finfo.flags |= fOmitEmpty
145+
case "selfclosing":
146+
finfo.flags |= fSelfClosing
143147
}
144148
}
145149

@@ -162,6 +166,9 @@ func structFieldInfo(typ reflect.Type, f *reflect.StructField) (*fieldInfo, erro
162166
if finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {
163167
valid = false
164168
}
169+
if finfo.flags&fSelfClosing != 0 && finfo.flags&fElement == 0 {
170+
valid = false
171+
}
165172
if !valid {
166173
return nil, fmt.Errorf("xml: invalid tag in field %s of type %s: %q",
167174
f.Name, typ, f.Tag.Get("xml"))

xml.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ func (e StartElement) End() EndElement {
7373
return EndElement{e.Name}
7474
}
7575

76+
// A SelfClosingElement represents a self-closing XML element.
77+
// It is otherwise identical to StartElement.
78+
type SelfClosingElement StartElement
79+
80+
// Copy creates a new copy of SelfClosingElement.
81+
func (e SelfClosingElement) Copy() SelfClosingElement {
82+
attrs := make([]Attr, len(e.Attr))
83+
copy(attrs, e.Attr)
84+
e.Attr = attrs
85+
return e
86+
}
87+
7688
// An EndElement represents an XML end element.
7789
type EndElement struct {
7890
Name Name

0 commit comments

Comments
 (0)