xmlbuilder2 icon indicating copy to clipboard operation
xmlbuilder2 copied to clipboard

Maintain ordering

Open vjpr opened this issue 3 years ago • 6 comments

Is your feature request related to a problem? Please describe.

In order to do xml -> json -> xml without any re-ordering, children must always be in an array, not grouped by their tag type.

<foo>
  <bar></bar>
  <baz></baz>
  <bar></bar>
</foo>

Describe the solution you'd like

Like xml-js does with {compact: false}. An option where all children are always an array.

Describe alternatives you've considered

Manipulating the DOM instead which maintains ordering.

vjpr avatar Sep 08 '21 21:09 vjpr

Did you try the verbose option?

https://oozcitak.github.io/xmlbuilder2/serialization.html#js-object-and-map-serializers

oozcitak avatar Sep 09 '21 06:09 oozcitak

Yeh, but if you have a node that has children of different element types interspersed, it groups by the element type. Which loses ordering.

vjpr avatar Sep 09 '21 10:09 vjpr

Can you create a test case to reproduce?

oozcitak avatar Sep 09 '21 11:09 oozcitak

The structure of the object seems to change depending on whether there are intersperesed children of different types or not. In order to use the serialized object, I need a consistent structure.

    <?xml version="1.0"?>
    <root>
      <person age="35"/>
      <person age="30"/>
      <ele>simple element</ele>
    </root>
      {
        "root": [
          {
            "person": [
              {
                "@age": "35"
              },
              {
                "@age": "30"
              }
            ],
            "ele": [
              "simple element"
            ]
          }
        ]
      }
    <?xml version="1.0"?>
    <root>
      <person age="35"/>
      <ele>simple element</ele>
      <person age="30"/>
    </root>
      {
        "root": [
          {
            "#": [
              {
                "person": [
                  {
                    "@age": "35"
                  }
                ]
              },
              {
                "ele": [
                  "simple element"
                ]
              },
              {
                "person": [
                  {
                    "@age": "30"
                  }
                ]
              }
            ]
          }
        ]
      }

Test case:

// @ts-nocheck

import $$ from '../TestHelpers'

describe('Replicate issue', () => {

  test('#104 - Maintain ordering for interspersed children of different element type', () => {
    const xmlA = $$.t`
    <?xml version="1.0"?>
    <root>
      <person age="35"/>
      <ele>simple element</ele>
      <person age="30"/>
    </root>
    `

    const xmlB = $$.t`
    <?xml version="1.0"?>
    <root>
      <person age="35"/>
      <person age="30"/>
      <ele>simple element</ele>
    </root>
    `

    const expectedObjA = {
      'root': {
        '#': [
          {
            'person': {
              '@age': '35'
            }
          },
          {
            'ele': 'simple element'
          },
          {
            'person': {
              '@age': '30'
            }
          }
        ]
      }
    }

    const expectedObjB = {
      'root': {
        '#': [
          {
            'person': {
              '@age': '35'
            }
          },
          {
            'person': {
              '@age': '30'
            }
          },
          {
            'ele': 'simple element'
          },
        ]
      }
    }

    const verbose = true
    const group = false
    const format = 'map'

    const docA = $$.create(xmlA)
    const docB = $$.create(xmlB)
    const objA = docA.end({format, verbose, group})
    const objB = docB.end({format, verbose, group})

    console.log(JSON.stringify(toObject(objA), null, 2))
    console.log(JSON.stringify(toObject(objB), null, 2))

    expect(toObject(objA)).toEqual(expectedObjA)
    expect(toObject(objB)).toEqual(expectedObjB)

    const xmlFromObjA = $$.create(objA).end({prettyPrint: true})
    const xmlFromObjB = $$.create(objB).end({prettyPrint: true})

    expect(xmlFromObjA).toEqual(xmlA)
    expect(xmlFromObjB).toEqual(xmlB)
  })

})

const toObject = (map = new Map) => {
  if (!(map instanceof Map)) return map
  return Object.fromEntries(Array.from(map.entries(), ([k, v]) => {
    if (v instanceof Array) {
      return [k, v.map(toObject)]
    } else if (v instanceof Map) {
      return [k, toObject(v)]
    } else {
      return [k, v]
    }
  }))
}


vjpr avatar Sep 09 '21 12:09 vjpr

So actually, it doesn't lose ordering...but the serialized object is inconsistent, making it difficult to traverse.

~I think format=map actually works as expected.~

I need to work with a JSON object because I want to modify things, and then diff, which is more difficult with the DOM.

I think verbose should force all children under a # - so that regardless of interspersal of children, you get the same object structure. Or could make it a separate option.

vjpr avatar Sep 09 '21 13:09 vjpr

@oozcitak Hello! Any update on this? We can't trust that object keys are going to remain in the same order, children need to be contained inside arrays.

iMrDJAi avatar Aug 06 '22 21:08 iMrDJAi