mirror of https://github.com/hashicorp/consul
133 lines
3.4 KiB
Go
133 lines
3.4 KiB
Go
package jsonx
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/Jeffail/gabs"
|
|
)
|
|
|
|
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, err := cont.Children()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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, err := cont.ChildrenMap()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|