Skip to content

Commit

Permalink
Merge pull request basgys#16 from dtelyukh/master
Browse files Browse the repository at this point in the history
Improved plugin system + added new plugins and type
  • Loading branch information
basgys authored Oct 31, 2018
2 parents cbfce0b + baf6ce0 commit 996d9fc
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 57 deletions.
9 changes: 3 additions & 6 deletions converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 25 additions & 10 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type Decoder struct {
err error
attributePrefix string
contentPrefix string
excludeAttrs map[string]bool
formatters []nodeFormatter
}

type element struct {
Expand All @@ -35,28 +37,34 @@ 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) DecodeWithCustomPrefixes(root *Node, contentPrefix string, attributePrefix string) error {
dec.contentPrefix = contentPrefix
dec.attributePrefix = attributePrefix
return dec.Decode(root)
}

// NewDecoder returns a new decoder that reads from r.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
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.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
Expand Down Expand Up @@ -85,6 +93,9 @@ func (dec *Decoder) Decode(root *Node) error {

// Extract attributes as children
for _, a := range se.Attr {
if _, ok := dec.excludeAttrs[a.Name.Local]; ok {
continue
}
elem.n.AddChild(dec.attributePrefix+a.Name.Local, &Node{Data: a.Value})
}
case xml.CharData:
Expand All @@ -101,6 +112,10 @@ func (dec *Decoder) Decode(root *Node) error {
}
}

for _, formatter := range dec.formatters {
formatter.Format(root)
}

return nil
}

Expand Down
30 changes: 25 additions & 5 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)

// TestDecode ensures that decode does not return any errors (not that useful)
func TestDecode(t *testing.T) {
assert := assert.New(t)

s := `<?xml version="1.0" encoding="UTF-8"?>
var s = `<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="CGImap 0.0.2">
<bounds minlat="54.0889580" minlon="12.2487570" maxlat="54.0913900" maxlon="12.2524800"/>
<node id="298884269" lat="54.0901746" lon="12.2482632" user="SvenHRO" uid="46882" visible="true" version="1" changeset="676636" timestamp="2008-09-21T21:37:45Z"/>
Expand All @@ -23,6 +19,10 @@ func TestDecode(t *testing.T) {
<foo>bar</foo>
</osm>`

// TestDecode ensures that decode does not return any errors (not that useful)
func TestDecode(t *testing.T) {
assert := assert.New(t)

// Decode XML document
root := &Node{}
var err error
Expand All @@ -38,6 +38,26 @@ func TestDecode(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), WithAttrPrefix(""), ExcludeAttributes([]string{"version", "generator"}))
err = dec.Decode(root)
assert.NoError(err)

// Check that some attribute`s name has no prefix and has expected value
assert.Exactly(root.Children["osm"][0].Children["bounds"][0].Children["minlat"][0].Data, "54.0889580")
// Check that some attributes are not present
_, exists := root.Children["osm"][0].Children["version"]
assert.False(exists)
_, exists = root.Children["osm"][0].Children["generator"]
assert.False(exists)
}

func TestTrim(t *testing.T) {
table := []struct {
in string
Expand Down
14 changes: 4 additions & 10 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)

Expand Down Expand Up @@ -73,7 +67,7 @@ func (enc *Encoder) format(n *Node, lvl int) error {
enc.write(label)
enc.write("\": ")

if len(children) > 1 {
if n.ChildrenAlwaysAsArray || len(children) > 1 {
// Array
enc.write("[")
for j, c := range children {
Expand Down
53 changes: 51 additions & 2 deletions encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package xml2json

import (
"bytes"
"encoding/json"
"fmt"
"testing"

Expand Down Expand Up @@ -70,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)
Expand All @@ -99,3 +100,51 @@ func TestEncode(t *testing.T) {
enc.err = fmt.Errorf("Testing if error provided is returned")
assert.Error(enc.Encode(nil))
}

// TestEncodeWithChildrenAsExplicitArray ensures that ChildrenAlwaysAsArray flag works as expected.
func TestEncodeWithChildrenAsExplicitArray(t *testing.T) {
type hobbies struct {
Hobbies []string `json:"hobbies"`
}

var (
testBio hobbies
err error
)
assert := assert.New(t)

author := bio{
Hobbies: []string{"DJ"},
}

// ChildrenAlwaysAsArray is not set
root := &Node{}
for _, h := range author.Hobbies {
root.AddChild("hobbies", &Node{
Data: h,
})
}
var enc *Encoder

buf := new(bytes.Buffer)
enc = NewEncoder(buf)

err = enc.Encode(root)
assert.NoError(err)

json.Unmarshal(buf.Bytes(), &testBio)
assert.Equal(0, len(testBio.Hobbies))

// ChildrenAlwaysAsArray is set
root.ChildrenAlwaysAsArray = true
testBio = hobbies{}

buf = new(bytes.Buffer)
enc = NewEncoder(buf)

err = enc.Encode(root)
assert.NoError(err)

json.Unmarshal(buf.Bytes(), &testBio)
assert.Equal(1, len(testBio.Hobbies))
}
7 changes: 7 additions & 0 deletions jstype.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
Int
Float
String
Null
)

// Str2JSType extract a JavaScript type from a string
Expand All @@ -29,6 +30,8 @@ func Str2JSType(s string) JSType {
output = Float
case isInt(s):
output = Int
case isNull(s):
output = Null
default:
output = String // if all alternatives have been eliminated, the input is a string
}
Expand Down Expand Up @@ -65,3 +68,7 @@ func isInt(s string) bool {
}
return output
}

func isNull(s string) bool {
return s == "null"
}
39 changes: 23 additions & 16 deletions parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -32,19 +35,21 @@ const (
<id>42</id>
<price>13.32</price>
<deleted>true</deleted>
<nullable>null</nullable>
`
)

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) {
Expand All @@ -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) {
Expand All @@ -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")
}
Loading

0 comments on commit 996d9fc

Please sign in to comment.