-
Notifications
You must be signed in to change notification settings - Fork 1
/
jsonx.go
126 lines (103 loc) · 3.32 KB
/
jsonx.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package jsonx
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"sort"
"github.com/Jeffail/gabs/v2"
)
const (
XMLHeader = `<?xml version="1.0" encoding="UTF-8"?>`
Header = `<json:object xsi:schemaLocation="http://www.datapower.com/schemas/json jsonx.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:json="http://www.ibm.com/xmlns/prod/2009/jsonx">`
Footer = `</json:object>`
)
// namedContainer wraps a gabs.Container to carry name information with it
type namedContainer struct {
name string
*gabs.Container
}
// Marshal marshals the input data into JSONx.
func Marshal(input interface{}) (string, error) {
jsonBytes, err := json.Marshal(input)
if err != nil {
return "", err
}
xmlBytes, err := EncodeJSONBytes(jsonBytes)
if err != nil {
return "", err
}
return fmt.Sprintf("%s%s%s%s", XMLHeader, Header, string(xmlBytes), Footer), nil
}
// EncodeJSONBytes encodes JSON-formatted bytes into JSONx. It is designed to
// be used for multiple entries so does not prepend the JSONx header tag or
// append the JSONx footer tag. You can use jsonx.Header and jsonx.Footer to
// easily add these when necessary.
func EncodeJSONBytes(input []byte) ([]byte, error) {
o := bytes.NewBuffer(nil)
reader := bytes.NewReader(input)
dec := json.NewDecoder(reader)
dec.UseNumber()
cont, err := gabs.ParseJSONDecoder(dec)
if err != nil {
return nil, err
}
if err := sortAndTransformObject(o, &namedContainer{Container: cont}); err != nil {
return nil, err
}
return o.Bytes(), nil
}
func transformContainer(o *bytes.Buffer, cont *namedContainer) error {
var printName string
if cont.name != "" {
escapedNameBuf := bytes.NewBuffer(nil)
err := xml.EscapeText(escapedNameBuf, []byte(cont.name))
if err != nil {
return err
}
printName = fmt.Sprintf(" name=\"%s\"", escapedNameBuf.String())
}
data := cont.Data()
switch data.(type) {
case nil:
o.WriteString(fmt.Sprintf("<json:null%s />", printName))
case bool:
o.WriteString(fmt.Sprintf("<json:boolean%s>%t</json:boolean>", printName, data))
case json.Number:
o.WriteString(fmt.Sprintf("<json:number%s>%v</json:number>", printName, data))
case string:
o.WriteString(fmt.Sprintf("<json:string%s>%v</json:string>", printName, data))
case []interface{}:
o.WriteString(fmt.Sprintf("<json:array%s>", printName))
arrayChildren := cont.Children()
for _, child := range arrayChildren {
if err := transformContainer(o, &namedContainer{Container: child}); err != nil {
return err
}
}
o.WriteString("</json:array>")
case map[string]interface{}:
o.WriteString(fmt.Sprintf("<json:object%s>", printName))
if err := sortAndTransformObject(o, cont); err != nil {
return err
}
o.WriteString("</json:object>")
}
return nil
}
// sortAndTransformObject sorts object keys to make the output predictable so
// the package can be tested; logic is here to prevent code duplication
func sortAndTransformObject(o *bytes.Buffer, cont *namedContainer) error {
objectChildren := cont.ChildrenMap()
sortedNames := make([]string, 0, len(objectChildren))
for name, _ := range objectChildren {
sortedNames = append(sortedNames, name)
}
sort.Strings(sortedNames)
for _, name := range sortedNames {
if err := transformContainer(o, &namedContainer{name: name, Container: objectChildren[name]}); err != nil {
return err
}
}
return nil
}