Skip to content

Commit

Permalink
Fixes for XML marshaling/unmarshaling
Browse files Browse the repository at this point in the history
- Implement the encoding.TextMarshaler and encoding.TextUnmarshaler for
  Decimal type so we can use it as chardata in structs.
- Implement xml.Marshaler interface in Invoice so we can always generate
  valid canonical XML.
- Add MarshalXML and UnmarshalXML convenience functions for marshaling
  and unmarshaling structs from this package without the need to import
  the printesoi/xml-go package.
  • Loading branch information
printesoi committed Mar 24, 2024
1 parent ed356e1 commit 0b617a6
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 134 deletions.
12 changes: 12 additions & 0 deletions decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ func (d *Decimal) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(d.String(), start)
}

// MarshalText implements the encoding.TextMarshaler interface. This is needed
// so we can use Decimal as chardata.
func (d Decimal) MarshalText() (text []byte, err error) {
return d.Decimal.MarshalText()
}

// UnmarshalText implements the encoding.TextUnmarshaler interface. This is
// needed so we can use Decimal as chardata.
func (d *Decimal) UnmarshalText(text []byte) error {
return d.Decimal.UnmarshalText(text)
}

// Add returns d + d2.
func (d Decimal) Add(d2 Decimal) Decimal {
return DD(d.Decimal.Add(d2.Decimal))
Expand Down
12 changes: 11 additions & 1 deletion helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,17 @@ func xmlUnmarshalReader(r io.Reader, v any) error {
if err != nil {
return err
}
return xml.Unmarshal(data, v)
return UnmarshalXML(data, v)
}

// setupUBLXMLEncoder will configure the xml.Encoder to make it suitable for
// marshaling UBL objects to XML.
func setupUBLXMLEncoder(enc *xml.Encoder) *xml.Encoder {
enc.AddNamespaceBinding(XMLNSUBLcac, "cac")
enc.AddSkipNamespaceAttrForPrefix(XMLNSUBLcac, "cac")
enc.AddNamespaceBinding(XMLNSUBLcbc, "cbc")
enc.AddSkipNamespaceAttrForPrefix(XMLNSUBLcbc, "cbc")
return enc
}

// xmlMarshalReader returns the XML encoding of v as a io.Reader.
Expand Down
85 changes: 36 additions & 49 deletions invoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package efactura

import (
"bytes"
"fmt"

"github.com/printesoi/xml-go"
Expand Down Expand Up @@ -182,70 +181,56 @@ type Invoice struct {
// Cardinality: 1..n
InvoiceLines []InvoiceLine `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2 InvoiceLine"`

// Name of node. Will be automatically set in MarshalXML
// Name of node.
XMLName xml.Name `xml:"Invoice"`
// xmlns. Will be automatically set in MarshalXML
NS string `xml:"xmlns,attr"`
// xmlns:cac. Will be automatically set in MarshalXML
NScac string `xml:"xmlns:cac,attr"`
// xmlns:cbc. Will be automatically set in MarshalXML
NScbc string `xml:"xmlns:cbc,attr"`
// generated with... Will be automatically set in MarshalXML
// xmlns attr. Will be automatically set in MarshalXML
Namespace string `xml:"xmlns,attr"`
// xmlns:cac attr. Will be automatically set in MarshalXML
NamespaceCAC string `xml:"xmlns:cac,attr"`
// xmlns:cbc attr. Will be automatically set in MarshalXML
NamespaceCBC string `xml:"xmlns:cbc,attr"`
// generated with... Will be automatically set in MarshalXML if empty.
Comment string `xml:",comment"`
}

// Prefill sets the NS, NScac, NScbc and Comment properties for ensuring that
// the required attributes and properties are set vor a valid UBL XML.
// the required attributes and properties are set for a valid UBL XML.
func (iv *Invoice) Prefill() {
iv.NS = XMLNSInvoice2
iv.NScac = XMLNSUBLcac
iv.NScbc = XMLNSUBLcbc
iv.Namespace = XMLNSInvoice2
iv.NamespaceCAC = XMLNSUBLcac
iv.NamespaceCBC = XMLNSUBLcbc
iv.UBLVersionID = UBLVersionID
iv.CustomizationID = CIUSRO_v101
if iv.Comment == "" {
iv.Comment = "Generated with " + efacturaVersion
}
}

func (iv Invoice) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
type invoice Invoice
setupUBLXMLEncoder(e)
iv.Prefill()
return e.EncodeElement(invoice(iv), start)
}

// XML returns the XML encoding of the Invoice
func (iv Invoice) XML() ([]byte, error) {
return iv.XMLIndent("", "")
return MarshalXML(iv)
}

// XMLIndent works like XML, but each XML element begins on a new
// indented line that starts with prefix and is followed by one or more
// copies of indent according to the nesting depth.
func (iv Invoice) XMLIndent(prefix, indent string) ([]byte, error) {
var b bytes.Buffer
if _, err := b.WriteString(xml.Header); err != nil {
return nil, err
}

enc := xml.NewEncoder(&b)
enc.AddNamespaceBinding(XMLNSUBLcac, "cac")
enc.AddSkipNamespaceAttrForPrefix(XMLNSUBLcac, "cac")
enc.AddNamespaceBinding(XMLNSUBLcbc, "cbc")
enc.AddSkipNamespaceAttrForPrefix(XMLNSUBLcbc, "cbc")
enc.Indent(prefix, indent)

iv.Prefill()
if err := enc.Encode(iv); err != nil {
enc.Close()
return nil, err
}
if err := enc.Close(); err != nil {
return nil, err
}

return b.Bytes(), nil
return MarshalIndentXML(iv, prefix, indent)
}

// UnmarshalInvoice unmarshals an Invoice from XML data. Only use this method
// for unmarshaling an Invoice, since the standard encoding/xml cannot
// properly unmarshal a struct like Invoice due to namespace prefixes. This
// method does not check if the unmarshaled Invoice is valid.
func UnmarshalInvoice(xmlData []byte, invoice *Invoice) error {
return xml.Unmarshal(xmlData, invoice)
return UnmarshalXML(xmlData, invoice)
}

type InvoiceBillingReference struct {
Expand Down Expand Up @@ -982,6 +967,20 @@ type InvoiceLine struct {
Price InvoiceLinePrice `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2 Price"`
}

// InvoicedQuantity represents the quantity (of items) on an invoice line.
type InvoicedQuantity struct {
Quantity Decimal `xml:",chardata"`
// The unit of the quantity.
UnitCode UnitCodeType `xml:"unitCode,attr"`
// The quantity unit code list.
UnitCodeListID string `xml:"unitCodeListID,attr,omitempty"`
// The identification of the agency that maintains the quantity unit code
// list.
UnitCodeListAgencyID string `xml:"unitCodeListAgencyID,attr,omitempty"`
// The name of the agency which maintains the quantity unit code list.
UnitCodeListAgencyName string `xml:"unitCodeListAgencyName,attr,omitempty"`
}

type InvoiceLinePeriod struct {
// ID: BT-134
// Term: Data de început a perioadei de facturare a liniei facturii
Expand Down Expand Up @@ -1203,18 +1202,6 @@ func (n InvoiceNote) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
return nil
}

type IDNode struct {
ID string `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ID"`
}

func MakeIDNode(id string) IDNode {
return IDNode{ID: id}
}

func NewIDNode(id string) *IDNode {
return &IDNode{ID: id}
}

type TaxScheme struct {
ID TaxSchemeIDType `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ID,omitempty"`
}
Expand Down
9 changes: 5 additions & 4 deletions rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ func (r *MessagesListResponse) IsOk() bool {
}

// ValidateXML call the validate endpoint with the given standard and XML body
// reader.
func (c *Client) ValidateXML(ctx context.Context, xml io.Reader, st ValidateStandard) (*ValidateResponse, error) {
var response *ValidateResponse

Expand Down Expand Up @@ -775,26 +776,26 @@ func (c *Client) DownloadInvoiceParseZip(
XMLName xml.Name
}
var doc docName
if err = xml.Unmarshal(response.InvoiceXML, &doc); err != nil {
if err = UnmarshalXML(response.InvoiceXML, &doc); err != nil {
return
}
switch doc.XMLName.Space {
case XMLNSInvoice2:
iv := new(Invoice)
if err = xml.Unmarshal(response.InvoiceXML, iv); err != nil {
if err = UnmarshalXML(response.InvoiceXML, iv); err != nil {
return
}
response.Invoice = iv

case XMLNSMsgErrorV1:
ie := new(InvoiceErrorMessage)
if err = xml.Unmarshal(response.InvoiceXML, &ie); err != nil {
if err = UnmarshalXML(response.InvoiceXML, &ie); err != nil {
return
}
response.InvoiceError = ie

default:
err = fmt.Errorf("Invalid namespace for invoice/message '%q'", doc.XMLName.Space)
err = fmt.Errorf("Invalid namespace for invoice/message: %q", doc.XMLName.Space)
return
}

Expand Down
115 changes: 35 additions & 80 deletions xml_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ import (
"time"

"github.com/printesoi/xml-go"
"github.com/shopspring/decimal"
)

var (
// RoZoneLocation is the Romanian timezone location loaded in the init
// function. This library does not load the time/tzdata package for the
// embedded timezone database, so the user of this library is responsible
// to ensure the Europe/Bucharest location is available, otherwise UTC is
// used an may lead to unexpected results.
// used and may lead to unexpected results.
RoZoneLocation *time.Location

// Allow mocking and testing
Expand All @@ -50,7 +49,7 @@ type Date struct {
}

// MakeDate creates a date with the provided year, month and day in the
// Local time zone location.
// Romanian time zone location.
func MakeDate(year int, month time.Month, day int) Date {
return Date{time.Date(year, month, day, 0, 0, 0, 0, RoZoneLocation)}
}
Expand All @@ -60,7 +59,8 @@ func NewDate(year int, month time.Month, day int) *Date {
return MakeDate(year, month, day).Ptr()
}

// MakeDateFromTime creates a Date from the given time.Time.
// MakeDateFromTime creates a Date in Romanian time zone location from the
// given time.Time.
func MakeDateFromTime(t time.Time) Date {
return MakeDate(t.In(RoZoneLocation).Date())
}
Expand All @@ -70,11 +70,13 @@ func NewDateFromTime(t time.Time) *Date {
return MakeDate(t.In(RoZoneLocation).Date()).Ptr()
}

// MarshalXML implements the xml.Marshaler interface.
func (d Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
v := d.Format(time.DateOnly)
return e.EncodeElement(v, start)
}

// UnmarshalXML implements the xml.Unmarshaler interface.
func (dt *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var sd string
if err := d.DecodeElement(&sd, &start); err != nil {
Expand Down Expand Up @@ -117,41 +119,10 @@ func TimeInRomania(t time.Time) time.Time {
// chardata and the currency ID as the currencyID attribute. The name of the
// node must be controlled by the parent type.
type AmountWithCurrency struct {
Amount Decimal
CurrencyID CurrencyCodeType
}

// this type is a hack for a limitation of the encoding/xml package: it only
// supports []byte and string for a chardata.
type xmlAmountWithCurrency struct {
Amount string `xml:",chardata"`
Amount Decimal `xml:",chardata"`
CurrencyID CurrencyCodeType `xml:"currencyID,attr,omitempty"`
}

func (a AmountWithCurrency) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
xa := xmlAmountWithCurrency{
Amount: a.Amount.String(),
CurrencyID: a.CurrencyID,
}
return e.EncodeElement(xa, start)
}

func (a *AmountWithCurrency) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var xa xmlAmountWithCurrency
if err := d.DecodeElement(&xa, &start); err != nil {
return err
}

amount, err := decimal.NewFromString(xa.Amount)
if err != nil {
return err
}

a.Amount = DD(amount)
a.CurrencyID = xa.CurrencyID
return nil
}

// ValueWithAttrs represents and embeddable type that stores a string as
// chardata and a list of attributes. The name of the XML node must be
// controlled by the parent type.
Expand Down Expand Up @@ -203,56 +174,40 @@ func (v *ValueWithAttrs) GetAttrByName(name string) (attr xml.Attr) {
return
}

// InvoicedQuantity represents the quantity (of items) on an invoice line.
type InvoicedQuantity struct {
Quantity Decimal
// The unit of the quantity.
UnitCode UnitCodeType
// The quantity unit code list.
UnitCodeListID string
// The identification of the agency that maintains the quantity unit code
// list.
UnitCodeListAgencyID string
// The name of the agency which maintains the quantity unit code list.
UnitCodeListAgencyName string
// IDNote is a struct that encodes a node that only has a cbc:ID property.
type IDNode struct {
ID string `xml:"urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2 ID"`
}

// this type is a hack for a limitation of the encoding/xml package: it only
// supports []byte and string for a chardata.
type xmlInvoicedQuantity struct {
Quantity string `xml:",chardata"`
UnitCode UnitCodeType `xml:"unitCode,attr"`
UnitCodeListID string `xml:"unitCodeListID,attr,omitempty"`
UnitCodeListAgencyID string `xml:"unitCodeListAgencyID,attr,omitempty"`
UnitCodeListAgencyName string `xml:"unitCodeListAgencyName,attr,omitempty"`
// MakeIDNode creates a IDNode with the given id.
func MakeIDNode(id string) IDNode {
return IDNode{ID: id}
}

func (q InvoicedQuantity) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
xq := xmlInvoicedQuantity{
Quantity: q.Quantity.String(),
UnitCode: q.UnitCode,
UnitCodeListID: q.UnitCodeListID,
UnitCodeListAgencyID: q.UnitCodeListAgencyID,
UnitCodeListAgencyName: q.UnitCodeListAgencyName,
}
return e.EncodeElement(xq, start)
// NewIDNode creates a *IDNode with the given id.
func NewIDNode(id string) *IDNode {
return &IDNode{ID: id}
}

func (q *InvoicedQuantity) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var xq xmlInvoicedQuantity
if err := d.DecodeElement(&xq, &start); err != nil {
return err
}
// MarshalXML returns the XML encoding of v in Canonical XML form [XML-C14N].
// This method must be used for marshaling objects from this library, instead
// of encoding/xml.
func MarshalXML(v any) ([]byte, error) {
return xml.Marshal(v)
}

quantity, err := decimal.NewFromString(xq.Quantity)
if err != nil {
return err
}
// MarshalIndentXML works like MarshalXML, but each XML element begins on a new
// indented line that starts with prefix and is followed by one or more
// copies of indent according to the nesting depth.
func MarshalIndentXML(v any, prefix, indent string) ([]byte, error) {
return xml.MarshalIndent(v, prefix, indent)
}

q.Quantity = DD(quantity)
q.UnitCode = xq.UnitCode
q.UnitCodeListID = xq.UnitCodeListID
q.UnitCodeListAgencyID = xq.UnitCodeListAgencyID
q.UnitCodeListAgencyName = xq.UnitCodeListAgencyName
return nil
// Unmarshal parses the XML-encoded data and stores the result in
// the value pointed to by v, which must be an arbitrary struct,
// slice, or string. Well-formed data that does not fit into v is
// discarded. This method must be used for unmarshaling objects from this
// library, instead of encoding/xml.
func UnmarshalXML(data []byte, v any) error {
return xml.Unmarshal(data, v)
}

0 comments on commit 0b617a6

Please sign in to comment.