1package xml
2
3import (
4	"encoding/base64"
5	"fmt"
6	"math/big"
7	"strconv"
8
9	"github.com/aws/smithy-go/encoding"
10)
11
12// Value represents an XML Value type
13// XML Value types: Object, Array, Map, String, Number, Boolean.
14type Value struct {
15	w       writer
16	scratch *[]byte
17
18	// xml start element is the associated start element for the Value
19	startElement StartElement
20
21	// indicates if the Value represents a flattened shape
22	isFlattened bool
23}
24
25// newFlattenedValue returns a Value encoder. newFlattenedValue does NOT write the start element tag
26func newFlattenedValue(w writer, scratch *[]byte, startElement StartElement) Value {
27	return Value{
28		w:            w,
29		scratch:      scratch,
30		startElement: startElement,
31	}
32}
33
34// newValue writes the start element xml tag and returns a Value
35func newValue(w writer, scratch *[]byte, startElement StartElement) Value {
36	writeStartElement(w, startElement)
37	return Value{w: w, scratch: scratch, startElement: startElement}
38}
39
40// writeStartElement takes in a start element and writes it.
41// It handles namespace, attributes in start element.
42func writeStartElement(w writer, el StartElement) error {
43	if el.isZero() {
44		return fmt.Errorf("xml start element cannot be nil")
45	}
46
47	w.WriteRune(leftAngleBracket)
48
49	if len(el.Name.Space) != 0 {
50		escapeString(w, el.Name.Space)
51		w.WriteRune(colon)
52	}
53	escapeString(w, el.Name.Local)
54	for _, attr := range el.Attr {
55		w.WriteRune(' ')
56		writeAttribute(w, &attr)
57	}
58
59	w.WriteRune(rightAngleBracket)
60	return nil
61}
62
63// writeAttribute writes an attribute from a provided Attribute
64// For a namespace attribute, the attr.Name.Space must be defined as "xmlns".
65// https://www.w3.org/TR/REC-xml-names/#NT-DefaultAttName
66func writeAttribute(w writer, attr *Attr) {
67	// if local, space both are not empty
68	if len(attr.Name.Space) != 0 && len(attr.Name.Local) != 0 {
69		escapeString(w, attr.Name.Space)
70		w.WriteRune(colon)
71	}
72
73	// if prefix is empty, the default `xmlns` space should be used as prefix.
74	if len(attr.Name.Local) == 0 {
75		attr.Name.Local = attr.Name.Space
76	}
77
78	escapeString(w, attr.Name.Local)
79	w.WriteRune(equals)
80	w.WriteRune(quote)
81	escapeString(w, attr.Value)
82	w.WriteRune(quote)
83}
84
85// writeEndElement takes in a end element and writes it.
86func writeEndElement(w writer, el EndElement) error {
87	if el.isZero() {
88		return fmt.Errorf("xml end element cannot be nil")
89	}
90
91	w.WriteRune(leftAngleBracket)
92	w.WriteRune(forwardSlash)
93
94	if len(el.Name.Space) != 0 {
95		escapeString(w, el.Name.Space)
96		w.WriteRune(colon)
97	}
98	escapeString(w, el.Name.Local)
99	w.WriteRune(rightAngleBracket)
100
101	return nil
102}
103
104// String encodes v as a XML string.
105// It will auto close the parent xml element tag.
106func (xv Value) String(v string) {
107	escapeString(xv.w, v)
108	xv.Close()
109}
110
111// Byte encodes v as a XML number.
112// It will auto close the parent xml element tag.
113func (xv Value) Byte(v int8) {
114	xv.Long(int64(v))
115}
116
117// Short encodes v as a XML number.
118// It will auto close the parent xml element tag.
119func (xv Value) Short(v int16) {
120	xv.Long(int64(v))
121}
122
123// Integer encodes v as a XML number.
124// It will auto close the parent xml element tag.
125func (xv Value) Integer(v int32) {
126	xv.Long(int64(v))
127}
128
129// Long encodes v as a XML number.
130// It will auto close the parent xml element tag.
131func (xv Value) Long(v int64) {
132	*xv.scratch = strconv.AppendInt((*xv.scratch)[:0], v, 10)
133	xv.w.Write(*xv.scratch)
134
135	xv.Close()
136}
137
138// Float encodes v as a XML number.
139// It will auto close the parent xml element tag.
140func (xv Value) Float(v float32) {
141	xv.float(float64(v), 32)
142	xv.Close()
143}
144
145// Double encodes v as a XML number.
146// It will auto close the parent xml element tag.
147func (xv Value) Double(v float64) {
148	xv.float(v, 64)
149	xv.Close()
150}
151
152func (xv Value) float(v float64, bits int) {
153	*xv.scratch = encoding.EncodeFloat((*xv.scratch)[:0], v, bits)
154	xv.w.Write(*xv.scratch)
155}
156
157// Boolean encodes v as a XML boolean.
158// It will auto close the parent xml element tag.
159func (xv Value) Boolean(v bool) {
160	*xv.scratch = strconv.AppendBool((*xv.scratch)[:0], v)
161	xv.w.Write(*xv.scratch)
162
163	xv.Close()
164}
165
166// Base64EncodeBytes writes v as a base64 value in XML string.
167// It will auto close the parent xml element tag.
168func (xv Value) Base64EncodeBytes(v []byte) {
169	encodeByteSlice(xv.w, (*xv.scratch)[:0], v)
170	xv.Close()
171}
172
173// BigInteger encodes v big.Int as XML value.
174// It will auto close the parent xml element tag.
175func (xv Value) BigInteger(v *big.Int) {
176	xv.w.Write([]byte(v.Text(10)))
177	xv.Close()
178}
179
180// BigDecimal encodes v big.Float as XML value.
181// It will auto close the parent xml element tag.
182func (xv Value) BigDecimal(v *big.Float) {
183	if i, accuracy := v.Int64(); accuracy == big.Exact {
184		xv.Long(i)
185		return
186	}
187
188	xv.w.Write([]byte(v.Text('e', -1)))
189	xv.Close()
190}
191
192// Write writes v directly to the xml document
193// if escapeXMLText is set to true, write will escape text.
194// It will auto close the parent xml element tag.
195func (xv Value) Write(v []byte, escapeXMLText bool) {
196	// escape and write xml text
197	if escapeXMLText {
198		escapeText(xv.w, v)
199	} else {
200		// write xml directly
201		xv.w.Write(v)
202	}
203
204	xv.Close()
205}
206
207// MemberElement does member element encoding. It returns a Value.
208// Member Element method should be used for all shapes except flattened shapes.
209//
210// A call to MemberElement will write nested element tags directly using the
211// provided start element. The value returned by MemberElement should be closed.
212func (xv Value) MemberElement(element StartElement) Value {
213	return newValue(xv.w, xv.scratch, element)
214}
215
216// FlattenedElement returns flattened element encoding. It returns a Value.
217// This method should be used for flattened shapes.
218//
219// Unlike MemberElement, flattened element will NOT write element tags
220// directly for the associated start element.
221//
222// The value returned by the FlattenedElement does not need to be closed.
223func (xv Value) FlattenedElement(element StartElement) Value {
224	v := newFlattenedValue(xv.w, xv.scratch, element)
225	v.isFlattened = true
226	return v
227}
228
229// Array returns an array encoder. By default, the members of array are
230// wrapped with `<member>` element tag.
231// If value is marked as flattened, the start element is used to wrap the members instead of
232// the `<member>` element.
233func (xv Value) Array() *Array {
234	return newArray(xv.w, xv.scratch, arrayMemberWrapper, xv.startElement, xv.isFlattened)
235}
236
237/*
238ArrayWithCustomName returns an array encoder.
239
240It takes named start element as an argument, the named start element will used to wrap xml array entries.
241for eg, `<someList><customName>entry1</customName></someList>`
242Here `customName` named start element will be wrapped on each array member.
243*/
244func (xv Value) ArrayWithCustomName(element StartElement) *Array {
245	return newArray(xv.w, xv.scratch, element, xv.startElement, xv.isFlattened)
246}
247
248/*
249Map returns a map encoder. By default, the map entries are
250wrapped with `<entry>` element tag.
251
252If value is marked as flattened, the start element is used to wrap the entry instead of
253the `<member>` element.
254*/
255func (xv Value) Map() *Map {
256	// flattened map
257	if xv.isFlattened {
258		return newFlattenedMap(xv.w, xv.scratch, xv.startElement)
259	}
260
261	// un-flattened map
262	return newMap(xv.w, xv.scratch)
263}
264
265// encodeByteSlice is modified copy of json encoder's encodeByteSlice.
266// It is used to base64 encode a byte slice.
267func encodeByteSlice(w writer, scratch []byte, v []byte) {
268	if v == nil {
269		return
270	}
271
272	encodedLen := base64.StdEncoding.EncodedLen(len(v))
273	if encodedLen <= len(scratch) {
274		// If the encoded bytes fit in e.scratch, avoid an extra
275		// allocation and use the cheaper Encoding.Encode.
276		dst := scratch[:encodedLen]
277		base64.StdEncoding.Encode(dst, v)
278		w.Write(dst)
279	} else if encodedLen <= 1024 {
280		// The encoded bytes are short enough to allocate for, and
281		// Encoding.Encode is still cheaper.
282		dst := make([]byte, encodedLen)
283		base64.StdEncoding.Encode(dst, v)
284		w.Write(dst)
285	} else {
286		// The encoded bytes are too long to cheaply allocate, and
287		// Encoding.Encode is no longer noticeably cheaper.
288		enc := base64.NewEncoder(base64.StdEncoding, w)
289		enc.Write(v)
290		enc.Close()
291	}
292}
293
294// IsFlattened returns true if value is for flattened shape.
295func (xv Value) IsFlattened() bool {
296	return xv.isFlattened
297}
298
299// Close closes the value.
300func (xv Value) Close() {
301	writeEndElement(xv.w, xv.startElement.End())
302}
303