diff --git a/converter.go b/converter.go
index a85314d..a1311ab 100644
--- a/converter.go
+++ b/converter.go
@@ -6,20 +6,17 @@ import (
)
// Convert converts the given XML document to JSON
-func Convert(r io.Reader, ps ...encoderPlugin) (*bytes.Buffer, error) {
+func Convert(r io.Reader, ps ...plugin) (*bytes.Buffer, error) {
// Decode XML document
root := &Node{}
- err := NewDecoder(r).Decode(root)
+ err := NewDecoder(r, ps...).Decode(root)
if err != nil {
return nil, err
}
// Then encode it in JSON
buf := new(bytes.Buffer)
- e := NewEncoder(buf)
- for _, p := range ps {
- e = p.AddTo(e)
- }
+ e := NewEncoder(buf, ps...)
err = e.Encode(root)
if err != nil {
return nil, err
diff --git a/decoder.go b/decoder.go
index c811de6..a45079f 100644
--- a/decoder.go
+++ b/decoder.go
@@ -15,12 +15,12 @@ const (
// A Decoder reads and decodes XML objects from an input stream.
type Decoder struct {
- r io.Reader
- err error
- attributePrefix string
- contentPrefix string
- turnOffDefaultPrefixes bool
- excludeAttrs map[string]bool
+ r io.Reader
+ err error
+ attributePrefix string
+ contentPrefix string
+ excludeAttrs map[string]bool
+ formatters []nodeFormatter
}
type element struct {
@@ -37,16 +37,16 @@ func (dec *Decoder) SetContentPrefix(prefix string) {
dec.contentPrefix = prefix
}
+func (dec *Decoder) AddFormatters(formatters []nodeFormatter) {
+ dec.formatters = formatters
+}
+
func (dec *Decoder) ExcludeAttributes(attrs []string) {
for _, attr := range attrs {
dec.excludeAttrs[attr] = true
}
}
-func (dec *Decoder) TurnOffDefaultPrefixes() {
- dec.turnOffDefaultPrefixes = true
-}
-
func (dec *Decoder) DecodeWithCustomPrefixes(root *Node, contentPrefix string, attributePrefix string) error {
dec.contentPrefix = contentPrefix
dec.attributePrefix = attributePrefix
@@ -54,23 +54,17 @@ func (dec *Decoder) DecodeWithCustomPrefixes(root *Node, contentPrefix string, a
}
// NewDecoder returns a new decoder that reads from r.
-func NewDecoder(r io.Reader) *Decoder {
- return &Decoder{r: r, excludeAttrs: map[string]bool{}}
+func NewDecoder(r io.Reader, plugins ...plugin) *Decoder {
+ d := &Decoder{r: r, contentPrefix: contentPrefix, attributePrefix: attrPrefix, excludeAttrs: map[string]bool{}}
+ for _, p := range plugins {
+ d = p.AddToDecoder(d)
+ }
+ return d
}
// Decode reads the next JSON-encoded value from its
// input and stores it in the value pointed to by v.
func (dec *Decoder) Decode(root *Node) error {
-
- if !dec.turnOffDefaultPrefixes {
- if dec.contentPrefix == "" {
- dec.contentPrefix = contentPrefix
- }
- if dec.attributePrefix == "" {
- dec.attributePrefix = attrPrefix
- }
- }
-
xmlDec := xml.NewDecoder(dec.r)
// That will convert the charset if the provided XML is non-UTF-8
@@ -118,6 +112,10 @@ func (dec *Decoder) Decode(root *Node) error {
}
}
+ for _, formatter := range dec.formatters {
+ formatter.Format(root)
+ }
+
return nil
}
diff --git a/decoder_test.go b/decoder_test.go
index e756347..27c09d2 100644
--- a/decoder_test.go
+++ b/decoder_test.go
@@ -38,16 +38,14 @@ func TestDecode(t *testing.T) {
}
-func TestDecodeWithoutDefaultsAndSomeAttributes(t *testing.T) {
+func TestDecodeWithoutDefaultsAndExcludeAttributes(t *testing.T) {
assert := assert.New(t)
// Decode XML document
root := &Node{}
var err error
var dec *Decoder
- dec = NewDecoder(strings.NewReader(s))
- dec.TurnOffDefaultPrefixes()
- dec.ExcludeAttributes([]string{"version", "generator"})
+ dec = NewDecoder(strings.NewReader(s), WithAttrPrefix(""), ExcludeAttributes([]string{"version", "generator"}))
err = dec.Decode(root)
assert.NoError(err)
diff --git a/encoder.go b/encoder.go
index 61d2d55..61fafc5 100644
--- a/encoder.go
+++ b/encoder.go
@@ -16,10 +16,10 @@ type Encoder struct {
}
// NewEncoder returns a new encoder that writes to w.
-func NewEncoder(w io.Writer, plugins ...encoderPlugin) *Encoder {
- e := &Encoder{w: w}
+func NewEncoder(w io.Writer, plugins ...plugin) *Encoder {
+ e := &Encoder{w: w, contentPrefix: contentPrefix, attributePrefix: attrPrefix}
for _, p := range plugins {
- e = p.AddTo(e)
+ e = p.AddToEncoder(e)
}
return e
}
@@ -32,12 +32,6 @@ func (enc *Encoder) Encode(root *Node) error {
if root == nil {
return nil
}
- if enc.contentPrefix == "" {
- enc.contentPrefix = contentPrefix
- }
- if enc.attributePrefix == "" {
- enc.attributePrefix = attrPrefix
- }
enc.err = enc.format(root, 0)
diff --git a/encoder_test.go b/encoder_test.go
index 5037122..0a5aada 100644
--- a/encoder_test.go
+++ b/encoder_test.go
@@ -71,9 +71,9 @@ func TestEncode(t *testing.T) {
assert.NoError(err)
attr := WithAttrPrefix("test")
- attr.AddTo(enc)
+ attr.AddToEncoder(enc)
content := WithContentPrefix("test2")
- content.AddTo(enc)
+ content.AddToEncoder(enc)
err = enc.Encode(root)
assert.NoError(err)
diff --git a/parse_test.go b/parse_test.go
index add71ee..bd56d4d 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -9,21 +9,24 @@ import (
)
type Product struct {
- ID int `json:"id"`
- Price float64 `json:"price"`
- Deleted bool `json:"deleted"`
+ ID int `json:"id"`
+ Price float64 `json:"price"`
+ Deleted bool `json:"deleted"`
+ Nullable interface{} `json:"nullable"`
}
type StringProduct struct {
- ID string `json:"id"`
- Price string `json:"price"`
- Deleted string `json:"deleted"`
+ ID string `json:"id"`
+ Price string `json:"price"`
+ Deleted string `json:"deleted"`
+ Nullable string `json:"nullable"`
}
type MixedProduct struct {
- ID string `json:"id"`
- Price float64 `json:"price"`
- Deleted string `json:"deleted"`
+ ID string `json:"id"`
+ Price float64 `json:"price"`
+ Deleted string `json:"deleted"`
+ Nullable string `json:"nullable"`
}
const (
@@ -32,19 +35,21 @@ const (
42
13.32
true
+ null
`
)
func TestAllJSTypeParsing(t *testing.T) {
xml := strings.NewReader(productString)
- jsBuf, err := Convert(xml, WithTypeConverter(Bool, Int, Float))
+ jsBuf, err := Convert(xml, WithTypeConverter(Bool, Int, Float, Null))
assert.NoError(t, err, "could not parse test xml")
product := Product{}
err = json.Unmarshal(jsBuf.Bytes(), &product)
assert.NoError(t, err, "could not unmarshal test json")
- assert.Equal(t, 42, product.ID, "price should match")
+ assert.Equal(t, 42, product.ID, "ID should match")
assert.Equal(t, 13.32, product.Price, "price should match")
- assert.Equal(t, true, product.Deleted, "price should match")
+ assert.Equal(t, true, product.Deleted, "deleted should match")
+ assert.Equal(t, nil, product.Nullable, "nullable should match")
}
func TestStringParsing(t *testing.T) {
@@ -54,9 +59,10 @@ func TestStringParsing(t *testing.T) {
product := StringProduct{}
err = json.Unmarshal(jsBuf.Bytes(), &product)
assert.NoError(t, err, "could not unmarshal test json")
- assert.Equal(t, "42", product.ID, "price should match")
+ assert.Equal(t, "42", product.ID, "ID should match")
assert.Equal(t, "13.32", product.Price, "price should match")
- assert.Equal(t, "true", product.Deleted, "price should match")
+ assert.Equal(t, "true", product.Deleted, "deleted should match")
+ assert.Equal(t, "null", product.Nullable, "nullable should match")
}
func TestMixedParsing(t *testing.T) {
@@ -66,7 +72,8 @@ func TestMixedParsing(t *testing.T) {
product := MixedProduct{}
err = json.Unmarshal(jsBuf.Bytes(), &product)
assert.NoError(t, err, "could not unmarshal test json")
- assert.Equal(t, "42", product.ID, "price should match")
+ assert.Equal(t, "42", product.ID, "ID should match")
assert.Equal(t, 13.32, product.Price, "price should match")
- assert.Equal(t, "true", product.Deleted, "price should match")
+ assert.Equal(t, "true", product.Deleted, "deleted should match")
+ assert.Equal(t, "null", product.Nullable, "nullable should match")
}
diff --git a/plugins.go b/plugins.go
index 6b93ffe..60137f0 100644
--- a/plugins.go
+++ b/plugins.go
@@ -5,9 +5,10 @@ import (
)
type (
- // an encodePlugin is added to an encoder to allow custom functionality at runtime
- encoderPlugin interface {
- AddTo(*Encoder) *Encoder
+ // an plugin is added to an encoder or/and to an decoder to allow custom functionality at runtime
+ plugin interface {
+ AddToEncoder(*Encoder) *Encoder
+ AddToDecoder(*Decoder) *Decoder
}
// a type converter overides the default string sanitization for encoding json
encoderTypeConverter interface {
@@ -21,6 +22,22 @@ type (
attrPrefixer string
contentPrefixer string
+
+ excluder []string
+
+ nodesFormatter struct {
+ list []nodeFormatter
+ }
+ nodeFormatter struct {
+ path string
+ plugin nodePlugin
+ }
+
+ nodePlugin interface {
+ AddTo(*Node)
+ }
+
+ arrayFormatter struct{}
)
// WithTypeConverter allows customized js type conversion behavior by passing in the desired JSTypes
@@ -41,11 +58,15 @@ func (tc *customTypeConverter) parseAsString(t JSType) bool {
}
// Adds the type converter to the encoder
-func (tc *customTypeConverter) AddTo(e *Encoder) *Encoder {
+func (tc *customTypeConverter) AddToEncoder(e *Encoder) *Encoder {
e.tc = tc
return e
}
+func (tc *customTypeConverter) AddToDecoder(d *Decoder) *Decoder {
+ return d
+}
+
func (tc *customTypeConverter) Convert(s string) string {
// remove quotes if they exists
if strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`) {
@@ -65,8 +86,14 @@ func WithAttrPrefix(prefix string) *attrPrefixer {
return &ap
}
-func (a *attrPrefixer) AddTo(e *Encoder) {
+func (a *attrPrefixer) AddToEncoder(e *Encoder) *Encoder {
e.attributePrefix = string((*a))
+ return e
+}
+
+func (a *attrPrefixer) AddToDecoder(d *Decoder) *Decoder {
+ d.attributePrefix = string((*a))
+ return d
}
// WithContentPrefix appends the given prefix to the json output of xml content fields to preserve namespaces
@@ -75,6 +102,60 @@ func WithContentPrefix(prefix string) *contentPrefixer {
return &c
}
-func (c *contentPrefixer) AddTo(e *Encoder) {
+func (c *contentPrefixer) AddToEncoder(e *Encoder) *Encoder {
e.contentPrefix = string((*c))
+ return e
+}
+
+func (c *contentPrefixer) AddToDecoder(d *Decoder) *Decoder {
+ d.contentPrefix = string((*c))
+ return d
+}
+
+// ExcludeAttributes excludes some xml attributes, for example, xmlns:xsi, xsi:noNamespaceSchemaLocation
+func ExcludeAttributes(attrs []string) *excluder {
+ ex := excluder(attrs)
+ return &ex
+}
+
+func (ex *excluder) AddToEncoder(e *Encoder) *Encoder {
+ return e
+}
+
+func (ex *excluder) AddToDecoder(d *Decoder) *Decoder {
+ d.ExcludeAttributes([]string((*ex)))
+ return d
+}
+
+// WithNodes formats specific nodes
+func WithNodes(n ...nodeFormatter) *nodesFormatter {
+ return &nodesFormatter{list: n}
+}
+
+func (nf *nodesFormatter) AddToEncoder(e *Encoder) *Encoder {
+ return e
+}
+
+func (nf *nodesFormatter) AddToDecoder(d *Decoder) *Decoder {
+ d.AddFormatters(nf.list)
+ return d
+}
+
+func NodePlugin(path string, plugin nodePlugin) nodeFormatter {
+ return nodeFormatter{path: path, plugin: plugin}
+}
+
+func (nf *nodeFormatter) Format(node *Node) {
+ child := node.GetChild(nf.path)
+ if child != nil {
+ nf.plugin.AddTo(child)
+ }
+}
+
+func ToArray() *arrayFormatter {
+ return &arrayFormatter{}
+}
+
+func (af *arrayFormatter) AddTo(n *Node) {
+ n.ChildrenAlwaysAsArray = true
}
diff --git a/struct.go b/struct.go
index b18397f..350e1ac 100644
--- a/struct.go
+++ b/struct.go
@@ -1,5 +1,9 @@
package xml2json
+import (
+ "strings"
+)
+
// Node is a data element on a tree
type Node struct {
Children map[string]Nodes
@@ -24,3 +28,20 @@ func (n *Node) AddChild(s string, c *Node) {
func (n *Node) IsComplex() bool {
return len(n.Children) > 0
}
+
+// GetChild returns child by path if exists. Path looks like "grandparent.parent.child.grandchild"
+func (n *Node) GetChild(path string) *Node {
+ result := n
+ names := strings.Split(path, ".")
+ for _, name := range names {
+ children, exists := result.Children[name]
+ if !exists {
+ return nil
+ }
+ if len(children) == 0 {
+ return nil
+ }
+ result = children[0]
+ }
+ return result
+}
diff --git a/struct_test.go b/struct_test.go
index 08579df..4bc7a17 100644
--- a/struct_test.go
+++ b/struct_test.go
@@ -19,6 +19,18 @@ func TestAddChild(t *testing.T) {
assert.Len(n.Children, 2)
}
+func TestGetChild(t *testing.T) {
+ assert := assert.New(t)
+
+ n := Node{}
+ child := Node{}
+ child.AddChild("b", &Node{Data: "foobar"})
+ n.AddChild("a", &child)
+
+ bNode := n.GetChild("a.b")
+ assert.Equal("foobar", bNode.Data)
+}
+
func TestIsComplex(t *testing.T) {
assert := assert.New(t)