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)