Jon Saw
Jon Saw

Reputation: 8092

Dynamically unmarshal child XML with attributes from parent

How do we dynamically unmarshal child XML with attributes from parent?

We have the the following XMLs:

<!-- Report I -->
<report type="YYYY-MM-DD">
  <created_at>2016-01-01</created_at>
</report>

<!-- Report II -->
<report type="DD-MM-YYYY">
  <created_at>01-01-2016</created_at>
</report>

And we have the following structure:

type Report struct {
  XMLName   xml.Name    `xml:"report"`
  Type      string      `xml:"type,attr"`
  CreatedAt *ReportDate `xml:"created_at"`
}

type ReportDate struct {
    time.Time
}

func (c *ReportDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    const format = "02-01-2006" // or "2016-01-02" depending on parent's "type"
    var v string
    d.DecodeElement(&v, &start)
    parse, err := time.Parse(format, v)
    if err != nil {
        return err
    }
    *c = ReportDate{parse}
    return nil
}

Will it be possible for ReportDate to obtain type="?" from it's parent in UnmarshalXML? Or will it be possible for Report to pass down attribute values to all child tags? If it is possible, how do we accomplish this?

Upvotes: 3

Views: 1934

Answers (2)

Josh Lubawy
Josh Lubawy

Reputation: 396

I'm not sure if there's anything more idiomatic for Golang, but...

If you added more elements to Report (as in 'name') the code would look like this:

https://play.golang.org/p/5VpzXM5F95

package main

import (
    "encoding/xml"
    "fmt"
    "io"
    "time"
)

var typeMap = map[string]string{
    "YYYY-MM-DD": "2006-01-02",
    "DD-MM-YYYY": "02-01-2006",
}

var reportStrings = []string{
    `<!-- Report I -->
<report type="YYYY-MM-DD">
  <created_at>2016-01-01</created_at>
  <name>Awesome Report I</name>
</report>`,

    `<!-- Report II -->
<report type="DD-MM-YYYY">
  <created_at>01-01-2016</created_at>
  <name>Awesome Report II</name>
</report>`,
}

type Report struct {
    XMLName xml.Name `xml:"report"`
    Type    string   `xml:"type,attr"`

    Name      string     `xml:"name"`
    CreatedAt ReportDate `xml:"created_at"`
}

type ReportDate struct {
    dateFormatStr string // lower-case field is ignored by decoder/encoder

    Time time.Time
}

func (r *Report) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    for _, attr := range start.Attr {
        if attr.Name.Local == "type" {
            r.Type = attr.Value
            dateFormatStr, ok := typeMap[attr.Value]
            if !ok {
                return fmt.Errorf("unknown date type '%s'", attr.Value)
            }
            r.CreatedAt.dateFormatStr = dateFormatStr
        }
    }

    for {
        tok, err := d.Token()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        switch tok.(type) {
        case xml.StartElement:
            nextStart := tok.(xml.StartElement)
            local := nextStart.Name.Local
            if local == "created_at" {
                d.DecodeElement(&r.CreatedAt, &nextStart)
            } else if local == "name" {
                d.DecodeElement(&r.Name, &nextStart)
            }
        }
    }

    return nil
}

func (c *ReportDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    var s string
    d.DecodeElement(&s, &start)
    t, err := time.Parse(c.dateFormatStr, s)
    if err != nil {
        return err
    }
    c.Time = t
    return nil
}

func main() {
    for i, reportStr := range reportStrings {
        var report Report
        if err := xml.Unmarshal([]byte(reportStr), &report); err != nil {
            panic(err)
        }

        fmt.Printf("[%d] %v\n", i, report)
    }
}

Upvotes: 1

Josh Lubawy
Josh Lubawy

Reputation: 396

While parsing the parent you can set a 'private' field within the child element that let's it know the time format string to use.

Here's a working example https://play.golang.org/p/CEqjWoDQR3.

And here's the code:

package main

import (
    "encoding/xml"
    "fmt"
    "io"
    "time"
)

// TypeMap converts the XML date format string to a valid Go date format string
var typeMap = map[string]string{
    "YYYY-MM-DD": "2006-01-02",
    "DD-MM-YYYY": "02-01-2006",
}

// Example XML documents
var reportStrings = []string{
    `<!-- Report I -->
<report type="YYYY-MM-DD">
  <created_at>2016-01-01</created_at>
</report>`,

    `<!-- Report II -->
<report type="DD-MM-YYYY">
  <created_at>01-01-2016</created_at>
</report>`,
}

type Report struct {
    XMLName   xml.Name   `xml:"report"`
    Type      string     `xml:"type,attr"`
    CreatedAt ReportDate `xml:"created_at"`
}

type ReportDate struct {
    dateFormatStr string // lower-case field is ignored by decoder/encoder

    Time time.Time
}

func (r *Report) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    for _, attr := range start.Attr {
        if attr.Name.Local == "type" {
            dateFormatStr, ok := typeMap[attr.Value]
            if !ok {
                return fmt.Errorf("unknown date type '%s'", attr.Value)
            }
            r.CreatedAt.dateFormatStr = dateFormatStr
        }
    }

    for {
        tok, err := d.Token()
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        switch tok.(type) {
        case xml.StartElement:
            nextStart := tok.(xml.StartElement)
            if nextStart.Name.Local == "created_at" {
                d.DecodeElement(&r.CreatedAt, &nextStart)
            }
        }
    }

    return nil
}

func (c *ReportDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    var s string
    d.DecodeElement(&s, &start)
    t, err := time.Parse(c.dateFormatStr, s)
    if err != nil {
        return err
    }
    c.Time = t
    return nil
}

func main() {
    for i, reportStr := range reportStrings {
        var report Report
        if err := xml.Unmarshal([]byte(reportStr), &report); err != nil {
            panic(err)
        }

        fmt.Printf("[%d] %s\n", i, report.CreatedAt.Time)
    }
}

Upvotes: 3

Related Questions