Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,17 @@ func TestCDATASupport(t *testing.T) {
assert.Nil(t, err)

result := utils.NodeToJSON(doc, 10)
expected := map[string]interface{}{"root": "1 & 2"}

assert.Equal(t, expected, result)
// Marshal and unmarshal to compare structure
jsonData, err := json.Marshal(result)
assert.Nil(t, err)

var resultMap map[string]interface{}
err = json.Unmarshal(jsonData, &resultMap)
assert.Nil(t, err)

expected := map[string]interface{}{"root": "1 & 2"}
assert.Equal(t, expected, resultMap)
}

func TestProcessAsJSON(t *testing.T) {
Expand Down
85 changes: 73 additions & 12 deletions internal/utils/jsonutil.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,72 @@
package utils

import (
"bytes"
"encoding/json"
"strings"

"github.com/antchfx/xmlquery"
)

// OrderedMap preserves insertion order for JSON output
type OrderedMap struct {
keys []string
values map[string]interface{}
}

// NewOrderedMap creates a new OrderedMap
func NewOrderedMap() *OrderedMap {
return &OrderedMap{
keys: make([]string, 0),
values: make(map[string]interface{}),
}
}

// Set adds or updates a key-value pair
func (om *OrderedMap) Set(key string, value interface{}) {
if _, exists := om.values[key]; !exists {
om.keys = append(om.keys, key)
}
om.values[key] = value
}

// Get retrieves a value by key
func (om *OrderedMap) Get(key string) (interface{}, bool) {
val, ok := om.values[key]
return val, ok
}

// Len returns the number of entries
func (om *OrderedMap) Len() int {
return len(om.keys)
}

// MarshalJSON implements json.Marshaler to preserve order
func (om *OrderedMap) MarshalJSON() ([]byte, error) {
buf := &bytes.Buffer{}
buf.WriteByte('{')
for i, key := range om.keys {
if i > 0 {
buf.WriteByte(',')
}
// Marshal the key
keyBytes, err := json.Marshal(key)
if err != nil {
return nil, err
}
buf.Write(keyBytes)
buf.WriteByte(':')
// Marshal the value
valBytes, err := json.Marshal(om.values[key])
if err != nil {
return nil, err
}
buf.Write(valBytes)
}
buf.WriteByte('}')
return buf.Bytes(), nil
}

// NodeToJSON converts an xmlquery.Node to a JSON object. The depth parameter
// specifies how many levels of children to include in the result. A depth of 0 means
// only the text content of the node is included. A depth of 1 means the node's children
Expand All @@ -17,7 +78,7 @@ func NodeToJSON(node *xmlquery.Node, depth int) interface{} {

switch node.Type {
case xmlquery.DocumentNode:
result := make(map[string]interface{})
result := NewOrderedMap()
var textParts []string

// Process the next sibling of the document node first (if any)
Expand All @@ -33,7 +94,7 @@ func NodeToJSON(node *xmlquery.Node, depth int) interface{} {
switch child.Type {
case xmlquery.ElementNode:
childResult := nodeToJSONInternal(child, depth)
result[child.Data] = childResult
result.Set(child.Data, childResult)
case xmlquery.TextNode, xmlquery.CharDataNode:
text := strings.TrimSpace(child.Data)
if text != "" {
Expand All @@ -43,7 +104,7 @@ func NodeToJSON(node *xmlquery.Node, depth int) interface{} {
}

if len(textParts) > 0 {
result["#text"] = strings.Join(textParts, "\n")
result.Set("#text", strings.Join(textParts, "\n"))
}
return result

Expand All @@ -63,9 +124,9 @@ func nodeToJSONInternal(node *xmlquery.Node, depth int) interface{} {
return getTextContent(node)
}

result := make(map[string]interface{})
result := NewOrderedMap()
for _, attr := range node.Attr {
result["@"+attr.Name.Local] = attr.Value
result.Set("@"+attr.Name.Local, attr.Value)
}

var textParts []string
Expand All @@ -83,10 +144,10 @@ func nodeToJSONInternal(node *xmlquery.Node, depth int) interface{} {
}

if len(textParts) > 0 {
if len(result) == 0 {
if result.Len() == 0 {
return strings.Join(textParts, "\n")
}
result["#text"] = strings.Join(textParts, "\n")
result.Set("#text", strings.Join(textParts, "\n"))
}

return result
Expand All @@ -108,18 +169,18 @@ func getTextContent(node *xmlquery.Node) string {
return strings.Join(parts, "\n")
}

func addToResult(result map[string]interface{}, key string, value interface{}) {
func addToResult(result *OrderedMap, key string, value interface{}) {
if key == "" {
return
}
if existing, ok := result[key]; ok {
if existing, ok := result.Get(key); ok {
switch existing := existing.(type) {
case []interface{}:
result[key] = append(existing, value)
result.Set(key, append(existing, value))
default:
result[key] = []interface{}{existing, value}
result.Set(key, []interface{}{existing, value})
}
} else {
result[key] = value
result.Set(key, value)
}
}
31 changes: 31 additions & 0 deletions internal/utils/jsonutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,34 @@ func TestXmlToJSON(t *testing.T) {
assert.Equal(t, expectedJson, output.String())
}
}

func TestElementOrderInJSON(t *testing.T) {
// Test that element order is preserved in JSON output
xmlInput := `<root>
<command-name>/status</command-name>
<command-message>status</command-message>
<command-args></command-args>
</root>`

node, err := xmlquery.Parse(strings.NewReader(xmlInput))
assert.NoError(t, err)

result := NodeToJSON(node, -1)
assert.NotNil(t, result)

// Marshal to JSON and check order
jsonData, err := json.Marshal(result)
assert.NoError(t, err)

jsonStr := string(jsonData)
t.Logf("JSON output: %s", jsonStr)

// Find positions of each key in the JSON string
namePos := strings.Index(jsonStr, "command-name")
messagePos := strings.Index(jsonStr, "command-message")
argsPos := strings.Index(jsonStr, "command-args")

// Check that they appear in the original order
assert.True(t, namePos < messagePos, "command-name should appear before command-message")
assert.True(t, messagePos < argsPos, "command-message should appear before command-args")
}
4 changes: 2 additions & 2 deletions test/data/xml2json/formatted3.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"root": {
"#text": "text\nmore text",
"child": "value"
"child": "value",
"#text": "text\nmore text"
}
}