From e6aa9c3eeb8312fd6c37b74e371343467f35ca7d Mon Sep 17 00:00:00 2001 From: Denis Telyukh Date: Tue, 16 Oct 2018 18:15:54 +0700 Subject: [PATCH 1/5] Two features: 1) turning off attributes` names default prefixes; 2) excluding some attributes from decoder (for example, xmlns:xsi or xsi:noNamespaceSchemaLocation) --- decoder.go | 37 +++++++++++++++++++++++++++---------- decoder_test.go | 32 +++++++++++++++++++++++++++----- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/decoder.go b/decoder.go index af03356..c811de6 100644 --- a/decoder.go +++ b/decoder.go @@ -15,10 +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 + r io.Reader + err error + attributePrefix string + contentPrefix string + turnOffDefaultPrefixes bool + excludeAttrs map[string]bool } type element struct { @@ -35,6 +37,16 @@ func (dec *Decoder) SetContentPrefix(prefix string) { dec.contentPrefix = prefix } +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 @@ -43,18 +55,20 @@ 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} + return &Decoder{r: r, excludeAttrs: map[string]bool{}} } // 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 + if !dec.turnOffDefaultPrefixes { + if dec.contentPrefix == "" { + dec.contentPrefix = contentPrefix + } + if dec.attributePrefix == "" { + dec.attributePrefix = attrPrefix + } } xmlDec := xml.NewDecoder(dec.r) @@ -85,6 +99,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: diff --git a/decoder_test.go b/decoder_test.go index 97eb54c..e756347 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -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 := ` +var s = ` @@ -23,6 +19,10 @@ func TestDecode(t *testing.T) { bar ` +// 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 @@ -38,6 +38,28 @@ func TestDecode(t *testing.T) { } +func TestDecodeWithoutDefaultsAndSomeAttributes(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"}) + 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 From a9b2ae605096ad7b9ae837807bfc56869b146ba2 Mon Sep 17 00:00:00 2001 From: Denis Telyukh Date: Wed, 17 Oct 2018 13:36:26 +0700 Subject: [PATCH 2/5] Added ChildrenAlwaysAsArray flag for the encoder --- encoder.go | 2 +- encoder_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ struct.go | 5 +++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/encoder.go b/encoder.go index be69046..c097e80 100644 --- a/encoder.go +++ b/encoder.go @@ -74,7 +74,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 { diff --git a/encoder_test.go b/encoder_test.go index 1288d16..5037122 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -2,6 +2,7 @@ package xml2json import ( "bytes" + "encoding/json" "fmt" "testing" @@ -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)) +} diff --git a/struct.go b/struct.go index 1f423b1..b18397f 100644 --- a/struct.go +++ b/struct.go @@ -2,8 +2,9 @@ package xml2json // Node is a data element on a tree type Node struct { - Children map[string]Nodes - Data string + Children map[string]Nodes + Data string + ChildrenAlwaysAsArray bool } // Nodes is a list of nodes From 1dfd788a8f788332718e9070b980176ab4ef0359 Mon Sep 17 00:00:00 2001 From: Denis Telyukh Date: Fri, 19 Oct 2018 17:01:54 +0700 Subject: [PATCH 3/5] type Null --- encoder.go | 2 -- jstype.go | 7 +++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/encoder.go b/encoder.go index c097e80..61d2d55 100644 --- a/encoder.go +++ b/encoder.go @@ -2,7 +2,6 @@ package xml2json import ( "bytes" - "fmt" "io" "unicode/utf8" ) @@ -102,7 +101,6 @@ func (enc *Encoder) format(n *Node, lvl int) error { if enc.tc == nil { // do nothing } else { - fmt.Println(s) s = enc.tc.Convert(s) } enc.write(s) diff --git a/jstype.go b/jstype.go index cfbf3b7..e19bfec 100644 --- a/jstype.go +++ b/jstype.go @@ -14,6 +14,7 @@ const ( Int Float String + Null ) // Str2JSType extract a JavaScript type from a string @@ -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 } @@ -65,3 +68,7 @@ func isInt(s string) bool { } return output } + +func isNull(s string) bool { + return s == "null" +} From 606a033f2579526da2680633e4b5bb21a3a6004f Mon Sep 17 00:00:00 2001 From: Denis Telyukh Date: Tue, 16 Oct 2018 18:15:54 +0700 Subject: [PATCH 4/5] Two features: 1) turning off attributes` names default prefixes; 2) excluding some attributes from decoder (for example, xmlns:xsi or xsi:noNamespaceSchemaLocation) --- decoder.go | 37 +++++++++++++++++++++++++++---------- decoder_test.go | 32 +++++++++++++++++++++++++++----- encoder.go | 4 +--- encoder_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ jstype.go | 7 +++++++ struct.go | 5 +++-- 6 files changed, 114 insertions(+), 20 deletions(-) diff --git a/decoder.go b/decoder.go index af03356..c811de6 100644 --- a/decoder.go +++ b/decoder.go @@ -15,10 +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 + r io.Reader + err error + attributePrefix string + contentPrefix string + turnOffDefaultPrefixes bool + excludeAttrs map[string]bool } type element struct { @@ -35,6 +37,16 @@ func (dec *Decoder) SetContentPrefix(prefix string) { dec.contentPrefix = prefix } +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 @@ -43,18 +55,20 @@ 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} + return &Decoder{r: r, excludeAttrs: map[string]bool{}} } // 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 + if !dec.turnOffDefaultPrefixes { + if dec.contentPrefix == "" { + dec.contentPrefix = contentPrefix + } + if dec.attributePrefix == "" { + dec.attributePrefix = attrPrefix + } } xmlDec := xml.NewDecoder(dec.r) @@ -85,6 +99,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: diff --git a/decoder_test.go b/decoder_test.go index 97eb54c..e756347 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -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 := ` +var s = ` @@ -23,6 +19,10 @@ func TestDecode(t *testing.T) { bar ` +// 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 @@ -38,6 +38,28 @@ func TestDecode(t *testing.T) { } +func TestDecodeWithoutDefaultsAndSomeAttributes(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"}) + 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 diff --git a/encoder.go b/encoder.go index be69046..61d2d55 100644 --- a/encoder.go +++ b/encoder.go @@ -2,7 +2,6 @@ package xml2json import ( "bytes" - "fmt" "io" "unicode/utf8" ) @@ -74,7 +73,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 { @@ -102,7 +101,6 @@ func (enc *Encoder) format(n *Node, lvl int) error { if enc.tc == nil { // do nothing } else { - fmt.Println(s) s = enc.tc.Convert(s) } enc.write(s) diff --git a/encoder_test.go b/encoder_test.go index 1288d16..5037122 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -2,6 +2,7 @@ package xml2json import ( "bytes" + "encoding/json" "fmt" "testing" @@ -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)) +} diff --git a/jstype.go b/jstype.go index cfbf3b7..e19bfec 100644 --- a/jstype.go +++ b/jstype.go @@ -14,6 +14,7 @@ const ( Int Float String + Null ) // Str2JSType extract a JavaScript type from a string @@ -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 } @@ -65,3 +68,7 @@ func isInt(s string) bool { } return output } + +func isNull(s string) bool { + return s == "null" +} diff --git a/struct.go b/struct.go index 1f423b1..b18397f 100644 --- a/struct.go +++ b/struct.go @@ -2,8 +2,9 @@ package xml2json // Node is a data element on a tree type Node struct { - Children map[string]Nodes - Data string + Children map[string]Nodes + Data string + ChildrenAlwaysAsArray bool } // Nodes is a list of nodes From c55258dfe3906b0416fa0e4c7d05174b0f8c78ef Mon Sep 17 00:00:00 2001 From: Denis Telyukh Date: Wed, 24 Oct 2018 20:40:31 +0700 Subject: [PATCH 5/5] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20code=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- converter.go | 9 ++--- decoder.go | 42 +++++++++++----------- decoder_test.go | 6 ++-- encoder.go | 12 ++----- encoder_test.go | 4 +-- parse_test.go | 39 ++++++++++++--------- plugins.go | 93 +++++++++++++++++++++++++++++++++++++++++++++---- struct.go | 21 +++++++++++ struct_test.go | 12 +++++++ 9 files changed, 173 insertions(+), 65 deletions(-) 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)