Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tricky Go xml.Unmarshal() case

I'm trying to unmarshal XML like this in Go:

<property>
  <code value="abc"/>
  <valueBoolean value="true"/>
</property>

or this

<property>
  <code value="abc"/>
  <valueString value="apple"/>
</property>

or this

<property>
  <code value="abc"/>
  <valueDecimal value="3.14159"/>
</property>

etc., into this:

type Property struct {
    Code  string      `xml:"code>value,attr"`
    Value interface{}
}

where the tag (valueBoolean, valueString, etc.) tells me what the type of the value attribute is. The XML that I'm trying to parse is part of an international standard, so I don't have any control over its definition. It wouldn't be hard to implement parsing these things, something like:

var value string
for a := range se.Attr {
    if a.Name.Local == "value" {
        value = a.Value
    } else {
        // Invalid attribute
    }
}
switch se.Name.Local {
case "code":
case "valueBoolean":
    property.Value = value == "true"
case "valueString":
    property.Value = value
case "valueInteger":
    property.Value, err = strconv.ParseInteger(value)
case "valueDecimal":
    property.Value, err = strconv.ParseFloat(value)
...
}

but I can't figure out how to tell the XML package to find it, and these things are buried in other XML that I'd really rather use xml.Unmarshal to handle. Alternately, I could redefine the type as:

type Property struct {
    Code         string `xml:"code>value,attr"`
    ValueBoolean bool   `xml:"valueBoolean>value,attr"`
    ValueString  string `xml:"valueString>value,attr"`
    ValueInteger int    `xml:"valueInteger>value,attr"`
    ValueDecimal float  `xml:"valueDecimal>value,attr"`
}

but that's pretty inefficient, particularly given that I'll have a large number of instances of these things, and this leaves me no way to derive the type without adding another attribute to indicate the type.

Can I somehow tie this into the normal XML unmarshalling method, just handling the tricky part by hand, or do I need to write the whole unmarshaller for this type from scratch?

like image 620
Scott Deerwester Avatar asked Apr 29 '16 13:04

Scott Deerwester


1 Answers

Thanks to the pointer from OneOfOne, here's an implementation that works well with the standard XML unmarshaler:

package main

import (
    "encoding/xml"
    "fmt"
    "strconv"
    "strings"
)

type Property struct {
    Code  string `xml:"code"`
    Value interface{}
}

const xmldata = `<properties>
  <property>
<code value="a"/>
<valueBoolean value="true"/>
  </property>
  <property>
<code value="b"/>
<valueString value="apple"/>
  </property>
  <property>
<code value="c"/>
<valueDecimal value="3.14159"/>
  </property>
</properties>
`

func (p *Property) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    if start.Name.Local != "property" {
        return fmt.Errorf("Invalid start tag for Property")
    }

    for {
        tok, err := d.Token()

        if tok == nil {
            break
        }

        if err != nil {
            return err
        }

        switch se := tok.(type) {
        case xml.StartElement:
            var value string
            var valueAssigned bool

            for _, attr := range se.Attr {
                if attr.Name.Local == "value" {
                    value = attr.Value
                    valueAssigned = true
                } else {
                    return fmt.Errorf("Invalid attribute %s", attr.Name.Local)
                }
            }

            if !valueAssigned {
                return fmt.Errorf("Valid attribute missing")
            }

            switch se.Name.Local {
            case "code":
                p.Code = value
            case "valueBoolean":
                if value == "true" {
                    p.Value = true
                } else if value == "false" {
                    p.Value = false
                } else {
                    return fmt.Errorf("Invalid string %s for Boolean value", value)
                }
            case "valueString", "valueCode", "valueUri":
                p.Value = value
            case "valueInteger":
                if ival, err := strconv.ParseInt(value, 10, 32); err != nil {
                    return err
                } else {
                    p.Value = ival
                }
            case "valueDecimal":
                if dval, err := strconv.ParseFloat(value, 64); err != nil {
                    return err
                } else {
                    p.Value = dval
                }
            default:
                return fmt.Errorf("Invalid tag %s for property", se.Name.Local)
            }
        }
    }

    return nil
}

func main() {
    r := strings.NewReader(xmldata)

    type Properties struct {
        List []Property `xml:"property"`
    }

    var properties Properties

    d := xml.NewDecoder(r)

    if err := d.Decode(&properties); err != nil {
        fmt.Println(err.Error())
    }

    for _, p := range properties.List {
        switch p.Value.(type) {
        case bool:
            if p.Value.(bool) {
                fmt.Println(p.Code, "is true")
            } else {
                fmt.Println(p.Code, "is false")
            }
        default:
            fmt.Println(p.Code, "=", p.Value)
        }
    }
}

Output is:

a is true
b = apple
c = 3.14159
like image 112
Scott Deerwester Avatar answered Nov 11 '22 09:11

Scott Deerwester