From 986fde759b8a63f1a0886cb3ce2971c50d09ae09 Mon Sep 17 00:00:00 2001 From: Nan Huang Date: Mon, 15 Feb 2021 23:01:53 +0000 Subject: [PATCH 1/5] Add xsd:date and xsd:datetime support --- gowsdl.go | 4 +-- soap/xsdDateTime.go | 70 +++++++++++++++++++++++++++++++++++++++++++++ xsd.go | 2 +- 3 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 soap/xsdDateTime.go diff --git a/gowsdl.go b/gowsdl.go index 6c2ff29..89bafa8 100644 --- a/gowsdl.go +++ b/gowsdl.go @@ -430,8 +430,8 @@ var xsd2GoTypes = map[string]string{ "byte": "int8", "long": "int64", "boolean": "bool", - "datetime": "time.Time", - "date": "time.Time", + "datetime": "soap.XsdDateTime", + "date": "soap.XsdDate", "time": "time.Time", "base64binary": "[]byte", "hexbinary": "[]byte", diff --git a/soap/xsdDateTime.go b/soap/xsdDateTime.go new file mode 100644 index 0000000..bb1ee04 --- /dev/null +++ b/soap/xsdDateTime.go @@ -0,0 +1,70 @@ +package soap + +import ( + "encoding/xml" + "time" +) + +const ( + dateLayout = "2006-01-02" +) + +// XsdDateTime is a type for representing xsd:datetime +type XsdDateTime struct { + time.Time +} + +// MarshalXML implementation on DateTimeg to skip "zero" time values +func (xdt XsdDateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if !xdt.IsZero() { + e.EncodeElement(xdt.Time.Format(time.RFC3339), start) + } + return nil +} + +// UnmarshalXML implementation on DateTimeg to use dateTimeLayout +func (xdt *XsdDateTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var err error + xdt.Time, err = unmarshalTime(d, start, time.RFC3339) + return err +} + +func unmarshalTime(d *xml.Decoder, start xml.StartElement, format string) (time.Time, error) { + var t time.Time + var content string + err := d.DecodeElement(&content, &start) + if err != nil { + return t, err + } + if content == "" { + return t, nil + } + if content == "0001-01-01T00:00:00Z" { + return t, nil + } + t, err = time.Parse(format, content) + if err != nil { + return t, err + } + return t, nil +} + +// XsdDate is a type for representing xsd:date +type XsdDate struct { + time.Time +} + +// MarshalXML implementation on DateTimeg to skip "zero" time values +func (xd XsdDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if !xd.IsZero() { + e.EncodeElement(xd.Time.Format(dateLayout), start) + } + return nil +} + +// UnmarshalXML implementation on DateTimeg to use dateTimeLayout +func (xd *XsdDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var err error + xd.Time, err = unmarshalTime(d, start, dateLayout) + return err +} diff --git a/xsd.go b/xsd.go index 28814f1..e34e590 100644 --- a/xsd.go +++ b/xsd.go @@ -137,7 +137,7 @@ type XSDElement struct { Groups []*XSDGroup `xml:"group"` } -// XSDElement represents a Schema element. +// XSDAny represents a Schema element. type XSDAny struct { XMLName xml.Name `xml:"any"` Doc string `xml:"annotation>documentation"` From 5c7b9edf095c8828eb0b2124c9ddc181a9ce3bff Mon Sep 17 00:00:00 2001 From: Nan Huang Date: Mon, 15 Feb 2021 23:02:17 +0000 Subject: [PATCH 2/5] Add nillable="true" support to xsd element --- gowsdl.go | 5 ++++- types_tmpl.go | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/gowsdl.go b/gowsdl.go index 89bafa8..a3ce76f 100644 --- a/gowsdl.go +++ b/gowsdl.go @@ -455,7 +455,7 @@ func removeNS(xsdType string) string { return r[0] } -func toGoType(xsdType string) string { +func toGoType(xsdType string, nillable bool) string { // Handles name space, ie. xsd:string, xs:string r := strings.Split(xsdType, ":") @@ -468,6 +468,9 @@ func toGoType(xsdType string) string { value := xsd2GoTypes[strings.ToLower(t)] if value != "" { + if nillable { + value = "*" + value + } return value } diff --git a/types_tmpl.go b/types_tmpl.go index d7b7b67..2fc2686 100644 --- a/types_tmpl.go +++ b/types_tmpl.go @@ -9,13 +9,13 @@ var typesTmpl = ` {{$type := replaceReservedWords .Name | makePublic}} {{if .Doc}} {{.Doc | comment}} {{end}} {{if ne .List.ItemType ""}} - type {{$type}} []{{toGoType .List.ItemType }} + type {{$type}} []{{toGoType .List.ItemType false}} {{else if ne .Union.MemberTypes ""}} type {{$type}} string {{else if .Union.SimpleType}} type {{$type}} string {{else if .Restriction.Base}} - type {{$type}} {{toGoType .Restriction.Base}} + type {{$type}} {{toGoType .Restriction.Base false}} {{else}} type {{$type}} interface{} {{end}} @@ -32,7 +32,7 @@ var typesTmpl = ` {{end}} {{define "ComplexContent"}} - {{$baseType := toGoType .Extension.Base}} + {{$baseType := toGoType .Extension.Base false}} {{ if $baseType }} {{$baseType}} {{end}} @@ -45,7 +45,7 @@ var typesTmpl = ` {{range .}} {{if .Doc}} {{.Doc | comment}} {{end}} {{ if ne .Type "" }} - {{ normalize .Name | makeFieldPublic}} {{toGoType .Type}} ` + "`" + `xml:"{{.Name}},attr,omitempty" json:"{{.Name}},omitempty"` + "`" + ` + {{ normalize .Name | makeFieldPublic}} {{toGoType .Type .Nillable}} ` + "`" + `xml:"{{.Name}},attr,omitempty" json:"{{.Name}},omitempty"` + "`" + ` {{ else }} {{ normalize .Name | makeFieldPublic}} string ` + "`" + `xml:"{{.Name}},attr,omitempty" json:"{{.Name}},omitempty"` + "`" + ` {{ end }} @@ -53,7 +53,7 @@ var typesTmpl = ` {{end}} {{define "SimpleContent"}} - Value {{toGoType .Extension.Base}} ` + "`xml:\",chardata\" json:\"-,\"`" + ` + Value {{toGoType .Extension.Base false}} ` + "`xml:\",chardata\" json:\"-,\"`" + ` {{template "Attributes" .Extension.Attributes}} {{end}} @@ -78,22 +78,22 @@ var typesTmpl = ` {{define "Elements"}} {{range .}} {{if ne .Ref ""}} - {{removeNS .Ref | replaceReservedWords | makePublic}} {{if eq .MaxOccurs "unbounded"}}[]{{end}}{{.Ref | toGoType}} ` + "`" + `xml:"{{.Ref | removeNS}},omitempty" json:"{{.Ref | removeNS}},omitempty"` + "`" + ` + {{removeNS .Ref | replaceReservedWords | makePublic}} {{if eq .MaxOccurs "unbounded"}}[]{{end}}{{toGoType .Ref .Nillable }} ` + "`" + `xml:"{{.Ref | removeNS}},omitempty" json:"{{.Ref | removeNS}},omitempty"` + "`" + ` {{else}} {{if not .Type}} {{if .SimpleType}} {{if .Doc}} {{.Doc | comment}} {{end}} {{if ne .SimpleType.List.ItemType ""}} - {{ normalize .Name | makeFieldPublic}} []{{toGoType .SimpleType.List.ItemType}} ` + "`" + `xml:"{{.Name}},omitempty" json:"{{.Name}},omitempty"` + "`" + ` + {{ normalize .Name | makeFieldPublic}} []{{toGoType .SimpleType.List.ItemType false}} ` + "`" + `xml:"{{.Name}},omitempty" json:"{{.Name}},omitempty"` + "`" + ` {{else}} - {{ normalize .Name | makeFieldPublic}} {{toGoType .SimpleType.Restriction.Base}} ` + "`" + `xml:"{{.Name}},omitempty" json:"{{.Name}},omitempty"` + "`" + ` + {{ normalize .Name | makeFieldPublic}} {{toGoType .SimpleType.Restriction.Base false}} ` + "`" + `xml:"{{.Name}},omitempty" json:"{{.Name}},omitempty"` + "`" + ` {{end}} {{else}} {{template "ComplexTypeInline" .}} {{end}} {{else}} {{if .Doc}}{{.Doc | comment}} {{end}} - {{replaceAttrReservedWords .Name | makeFieldPublic}} {{if eq .MaxOccurs "unbounded"}}[]{{end}}{{.Type | toGoType}} ` + "`" + `xml:"{{.Name}},omitempty" json:"{{.Name}},omitempty"` + "`" + ` {{end}} + {{replaceAttrReservedWords .Name | makeFieldPublic}} {{if eq .MaxOccurs "unbounded"}}[]{{end}}{{toGoType .Type .Nillable }} ` + "`" + `xml:"{{.Name}},omitempty" json:"{{.Name}},omitempty"` + "`" + ` {{end}} {{end}} {{end}} {{end}} @@ -133,8 +133,8 @@ var typesTmpl = ` } {{end}} {{else}} - {{if ne ($name | replaceReservedWords | makePublic) (toGoType .Type | removePointerFromType)}} - type {{$name | replaceReservedWords | makePublic}} {{toGoType .Type | removePointerFromType}} + {{if ne ($name | replaceReservedWords | makePublic) (toGoType .Type .Nillable | removePointerFromType)}} + type {{$name | replaceReservedWords | makePublic}} {{toGoType .Type .Nillable | removePointerFromType}} {{end}} {{end}} {{end}} @@ -142,7 +142,7 @@ var typesTmpl = ` {{range .ComplexTypes}} {{/* ComplexTypeGlobal */}} {{$name := replaceReservedWords .Name | makePublic}} - {{if eq (toGoType .SimpleContent.Extension.Base) "string"}} + {{if eq (toGoType .SimpleContent.Extension.Base false) "string"}} type {{$name}} string {{else}} type {{$name}} struct { From be06889ed156f3ddb3f1977c48063c16d1aca96c Mon Sep 17 00:00:00 2001 From: Nan Huang Date: Wed, 24 Feb 2021 00:11:05 +0000 Subject: [PATCH 3/5] add xsd date and time support --- gowsdl.go | 4 +- soap/soap_test.go | 209 ++++++++++++++++++++++++++++++++++++++++++++ soap/xsdDateTime.go | 182 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 soap/xsdDateTime.go diff --git a/gowsdl.go b/gowsdl.go index 6c2ff29..89bafa8 100644 --- a/gowsdl.go +++ b/gowsdl.go @@ -430,8 +430,8 @@ var xsd2GoTypes = map[string]string{ "byte": "int8", "long": "int64", "boolean": "bool", - "datetime": "time.Time", - "date": "time.Time", + "datetime": "soap.XsdDateTime", + "date": "soap.XsdDate", "time": "time.Time", "base64binary": "[]byte", "hexbinary": "[]byte", diff --git a/soap/soap_test.go b/soap/soap_test.go index baf5b7d..de90af5 100644 --- a/soap/soap_test.go +++ b/soap/soap_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" ) type Ping struct { @@ -145,3 +146,211 @@ func TestClient_MTOM(t *testing.T) { t.Errorf("got %s wanted %s", reply.Attachment.Bytes(), req.Attachment.ContentType()) } } + +// TestXsdDateTime checks the marshalled xsd datetime +func TestXsdDateTime(t *testing.T) { + type TestDateTime struct { + XMLName xml.Name `xml:"TestDateTime"` + Datetime XsdDateTime + } + + // test marshalling + { + testDateTime := TestDateTime{ + Datetime: XsdDateTime{ + Time: time.Date(1951, time.October, 22, 1, 2, 3, 4, time.FixedZone("UTC-8", -8*60*60)), + }, + } + if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22T01:02:03-08:00" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test marshalling of UTC + { + testDateTime := TestDateTime{ + Datetime: XsdDateTime{ + Time: time.Date(1951, time.October, 22, 1, 2, 3, 4, time.UTC), + }, + } + if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22T01:02:03Z" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test unmarshalling + { + dateTimes := map[string]time.Time{ + "1951-10-22T01:02:03-08:00": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.FixedZone("-0800", -8*60*60)), + "1951-10-22T01:02:03Z": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.UTC), + } + for dateTimeStr, dateTimeObj := range dateTimes { + parsedDt := TestDateTime{} + if err := xml.Unmarshal([]byte(dateTimeStr), &parsedDt); err != nil { + t.Error(err) + } else { + if !parsedDt.Datetime.Time.Equal(dateTimeObj) { + t.Errorf("Got: %#v\nExpected: %#v", parsedDt.Datetime.Time, dateTimeObj) + } + } + } + } +} + +// TestXsdDateTime checks the marshalled xsd datetime +func TestXsdDate(t *testing.T) { + type TestDate struct { + XMLName xml.Name `xml:"TestDate"` + Date XsdDate + } + + // test marshalling + { + testDate := TestDate{ + Date: XsdDate{ + Time: time.Date(1951, time.October, 22, 0, 0, 0, 0, time.FixedZone("UTC-8", -8*60*60)), + }, + } + if output, err := xml.MarshalIndent(testDate, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test marshalling of UTC + { + testDate := TestDate{ + Date: XsdDate{ + Time: time.Date(1951, time.October, 22, 0, 0, 0, 0, time.UTC), + }, + } + if output, err := xml.MarshalIndent(testDate, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test unmarshalling + { + dates := map[string]time.Time{ + "1951-10-22": time.Date(1951, time.October, 22, 0, 0, 0, 0, time.Local), + } + for dateStr, dateObj := range dates { + parsedDate := TestDate{} + if err := xml.Unmarshal([]byte(dateStr), &parsedDate); err != nil { + t.Error(err) + } else { + if !parsedDate.Date.Time.Equal(dateObj) { + t.Errorf("Got: %#v\nExpected: %#v", parsedDate.Date.Time, dateObj) + } + } + } + } +} + +// TestXsdTime checks the marshalled xsd datetime +func TestXsdTime(t *testing.T) { + type TestTime struct { + XMLName xml.Name `xml:"TestTime"` + Time XsdTime + } + + // test marshalling + { + testTime := TestTime{ + Time: XsdTime{ + Hour: 12, + Minute: 13, + Second: 14, + Fraction: 4, + Tz: time.FixedZone("Test", -19800), + }, + } + if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + { + testTime := TestTime{ + Time: XsdTime{ + Hour: 12, + Minute: 13, + Second: 14, + Fraction: 0, + Tz: time.FixedZone("UTC-8", -8*60*60), + }, + } + if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + { + testTime := TestTime{ + Time: XsdTime{ + Hour: 12, + Minute: 13, + Second: 14, + Fraction: 0, + Tz: nil, + }, + } + if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test unmarshalling + { + timeStr := "" + parsedTime := TestTime{} + if err := xml.Unmarshal([]byte(timeStr), &parsedTime); err != nil { + t.Error(err) + } else { + if parsedTime.Time.Hour != 12 || + parsedTime.Time.Minute != 13 || + parsedTime.Time.Second != 14 { + t.Errorf("Got: %#v\nExpected: %#v", parsedTime.Time, "12:13:14") + } + } + } +} diff --git a/soap/xsdDateTime.go b/soap/xsdDateTime.go new file mode 100644 index 0000000..dd0c3ea --- /dev/null +++ b/soap/xsdDateTime.go @@ -0,0 +1,182 @@ +package soap + +import ( + "encoding/xml" + "fmt" + "strconv" + "strings" + "time" + "unicode" +) + +const ( + dateLayout = "2006-01-02" + timeLayout = "12:13:14" + timeFracLayout = ".999999999" + timeZoneLayout = "Z07:00" +) + +// XsdDateTime is a type for representing xsd:datetime +type XsdDateTime struct { + time.Time +} + +// MarshalXML implementation on DateTimeg to skip "zero" time values +func (xdt XsdDateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if !xdt.IsZero() { + e.EncodeElement(xdt.Time.Format(time.RFC3339), start) + } + return nil +} + +// UnmarshalXML implementation on DateTimeg to use dateTimeLayout +func (xdt *XsdDateTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var err error + xdt.Time, err = unmarshalTime(d, start, time.RFC3339) + return err +} + +func unmarshalTime(d *xml.Decoder, start xml.StartElement, format string) (time.Time, error) { + var t time.Time + var content string + err := d.DecodeElement(&content, &start) + if err != nil { + return t, err + } + if content == "" { + return t, nil + } + if content == "0001-01-01T00:00:00Z" { + return t, nil + } + t, err = time.Parse(format, content) + if err != nil { + return t, err + } + return t, nil +} + +// XsdDate is a type for representing xsd:date +type XsdDate struct { + time.Time +} + +// MarshalXML implementation on DateTimeg to skip "zero" time values +func (xd XsdDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if !xd.IsZero() { + e.EncodeElement(xd.Time.Format(dateLayout), start) + } + return nil +} + +// UnmarshalXML implementation on DateTimeg to use dateTimeLayout +func (xd *XsdDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var err error + xd.Time, err = unmarshalTime(d, start, dateLayout) + return err +} + +// XsdTime is a type for representing xsd:time +type XsdTime struct { + Hour int + Minute int + Second int + Fraction int + Tz *time.Location +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +// MarshalXML implementation on DateTimeg to skip "zero" time values +func (xt XsdTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + timeString := fmt.Sprintf("%v:%v:%v", xt.Hour, xt.Minute, xt.Second) + if xt.Fraction != 0 { + timeString = fmt.Sprintf("%v.%v", timeString, xt.Fraction) + } + if xt.Tz != nil { + _, offset := time.Now().In(xt.Tz).Zone() + hrOffset := offset / 3600 + minOffset := abs((offset - hrOffset*3600) / 60) + if hrOffset == 0 && minOffset == 0 { + timeString = timeString + "Z" + } else { + timeString = fmt.Sprintf("%v%+03d:%02d", timeString, hrOffset, minOffset) + } + } + e.EncodeElement(timeString, start) + return nil +} + +// UnmarshalXML implementation on DateTimeg to use dateTimeLayout +func (xt *XsdTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var err error + var content string + err = d.DecodeElement(&content, &start) + if err != nil { + return err + } + tok1 := strings.SplitN(content, ":", 3) + if len(tok1) != 3 { + return fmt.Errorf("Failed to parse time %v", content) + } + xt.Hour, err = strconv.Atoi(tok1[0]) + if err != nil { + return err + } + xt.Minute, err = strconv.Atoi(tok1[1]) + if err != nil { + return err + } + xt.Second, err = strconv.Atoi(tok1[2][:2]) + if err != nil { + return err + } + reststr := tok1[2][2:] + if len(reststr) > 0 && reststr[0] == '.' { + // there is a fraction + frac := "" + for i, c := range reststr[1:] { + if unicode.IsDigit(c) { + frac = frac + string(c) + } else { + xt.Fraction, err = strconv.Atoi(frac) + if err != nil { + return err + } + reststr = reststr[i:] + break + } + } + } + if len(reststr) > 0 && reststr[0] == 'Z' { + xt.Tz = time.UTC + } else if len(reststr) > 0 { + sign := 1 + if reststr[0] == '+' { + sign = 1 + } else if reststr[0] == '-' { + sign = -1 + } else { + return fmt.Errorf("timezone format is incorrect %v", content) + } + reststr = reststr[1:] + tok2 := strings.Split(reststr, ":") + if len(tok2) > 1 { + hrOffset, err := strconv.Atoi(tok2[0]) + if err != nil { + return err + } + minOffset, err := strconv.Atoi(tok2[1]) + if err != nil { + return err + } + xt.Tz = time.FixedZone(reststr, sign*hrOffset*3600+sign*minOffset*60) + } + } + return nil +} From d98ec5e6442224ae82c55c0c939a9cca77b7c384 Mon Sep 17 00:00:00 2001 From: Nan Huang Date: Wed, 24 Feb 2021 00:11:05 +0000 Subject: [PATCH 4/5] add xsd date and time support --- soap/soap_test.go | 206 ++++++++++++++++++++++++++++++++++++++++++++ soap/xsdDateTime.go | 114 +++++++++++++++++++++++- 2 files changed, 319 insertions(+), 1 deletion(-) diff --git a/soap/soap_test.go b/soap/soap_test.go index 44435e0..803033c 100644 --- a/soap/soap_test.go +++ b/soap/soap_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" ) type Ping struct { @@ -305,5 +306,210 @@ func Test_Client_FaultDefault(t *testing.T) { t.Fatalf("call to ping() should have failed, but succeeded.") } }) +// TestXsdDateTime checks the marshalled xsd datetime +func TestXsdDateTime(t *testing.T) { + type TestDateTime struct { + XMLName xml.Name `xml:"TestDateTime"` + Datetime XsdDateTime + } + + // test marshalling + { + testDateTime := TestDateTime{ + Datetime: XsdDateTime{ + Time: time.Date(1951, time.October, 22, 1, 2, 3, 4, time.FixedZone("UTC-8", -8*60*60)), + }, + } + if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22T01:02:03-08:00" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test marshalling of UTC + { + testDateTime := TestDateTime{ + Datetime: XsdDateTime{ + Time: time.Date(1951, time.October, 22, 1, 2, 3, 4, time.UTC), + }, + } + if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22T01:02:03Z" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test unmarshalling + { + dateTimes := map[string]time.Time{ + "1951-10-22T01:02:03-08:00": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.FixedZone("-0800", -8*60*60)), + "1951-10-22T01:02:03Z": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.UTC), + } + for dateTimeStr, dateTimeObj := range dateTimes { + parsedDt := TestDateTime{} + if err := xml.Unmarshal([]byte(dateTimeStr), &parsedDt); err != nil { + t.Error(err) + } else { + if !parsedDt.Datetime.Time.Equal(dateTimeObj) { + t.Errorf("Got: %#v\nExpected: %#v", parsedDt.Datetime.Time, dateTimeObj) + } + } + } + } +} + +// TestXsdDateTime checks the marshalled xsd datetime +func TestXsdDate(t *testing.T) { + type TestDate struct { + XMLName xml.Name `xml:"TestDate"` + Date XsdDate + } + + // test marshalling + { + testDate := TestDate{ + Date: XsdDate{ + Time: time.Date(1951, time.October, 22, 0, 0, 0, 0, time.FixedZone("UTC-8", -8*60*60)), + }, + } + if output, err := xml.MarshalIndent(testDate, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test marshalling of UTC + { + testDate := TestDate{ + Date: XsdDate{ + Time: time.Date(1951, time.October, 22, 0, 0, 0, 0, time.UTC), + }, + } + if output, err := xml.MarshalIndent(testDate, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test unmarshalling + { + dates := map[string]time.Time{ + "1951-10-22": time.Date(1951, time.October, 22, 0, 0, 0, 0, time.Local), + } + for dateStr, dateObj := range dates { + parsedDate := TestDate{} + if err := xml.Unmarshal([]byte(dateStr), &parsedDate); err != nil { + t.Error(err) + } else { + if !parsedDate.Date.Time.Equal(dateObj) { + t.Errorf("Got: %#v\nExpected: %#v", parsedDate.Date.Time, dateObj) + } + } + } + } +} + +// TestXsdTime checks the marshalled xsd datetime +func TestXsdTime(t *testing.T) { + type TestTime struct { + XMLName xml.Name `xml:"TestTime"` + Time XsdTime + } + + // test marshalling + { + testTime := TestTime{ + Time: XsdTime{ + Hour: 12, + Minute: 13, + Second: 14, + Fraction: 4, + Tz: time.FixedZone("Test", -19800), + }, + } + if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + { + testTime := TestTime{ + Time: XsdTime{ + Hour: 12, + Minute: 13, + Second: 14, + Fraction: 0, + Tz: time.FixedZone("UTC-8", -8*60*60), + }, + } + if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + { + testTime := TestTime{ + Time: XsdTime{ + Hour: 12, + Minute: 13, + Second: 14, + Fraction: 0, + Tz: nil, + }, + } + if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test unmarshalling + { + timeStr := "" + parsedTime := TestTime{} + if err := xml.Unmarshal([]byte(timeStr), &parsedTime); err != nil { + t.Error(err) + } else { + if parsedTime.Time.Hour != 12 || + parsedTime.Time.Minute != 13 || + parsedTime.Time.Second != 14 { + t.Errorf("Got: %#v\nExpected: %#v", parsedTime.Time, "12:13:14") + } + } } } diff --git a/soap/xsdDateTime.go b/soap/xsdDateTime.go index bb1ee04..dd0c3ea 100644 --- a/soap/xsdDateTime.go +++ b/soap/xsdDateTime.go @@ -2,11 +2,18 @@ package soap import ( "encoding/xml" + "fmt" + "strconv" + "strings" "time" + "unicode" ) const ( - dateLayout = "2006-01-02" + dateLayout = "2006-01-02" + timeLayout = "12:13:14" + timeFracLayout = ".999999999" + timeZoneLayout = "Z07:00" ) // XsdDateTime is a type for representing xsd:datetime @@ -68,3 +75,108 @@ func (xd *XsdDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { xd.Time, err = unmarshalTime(d, start, dateLayout) return err } + +// XsdTime is a type for representing xsd:time +type XsdTime struct { + Hour int + Minute int + Second int + Fraction int + Tz *time.Location +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +// MarshalXML implementation on DateTimeg to skip "zero" time values +func (xt XsdTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + timeString := fmt.Sprintf("%v:%v:%v", xt.Hour, xt.Minute, xt.Second) + if xt.Fraction != 0 { + timeString = fmt.Sprintf("%v.%v", timeString, xt.Fraction) + } + if xt.Tz != nil { + _, offset := time.Now().In(xt.Tz).Zone() + hrOffset := offset / 3600 + minOffset := abs((offset - hrOffset*3600) / 60) + if hrOffset == 0 && minOffset == 0 { + timeString = timeString + "Z" + } else { + timeString = fmt.Sprintf("%v%+03d:%02d", timeString, hrOffset, minOffset) + } + } + e.EncodeElement(timeString, start) + return nil +} + +// UnmarshalXML implementation on DateTimeg to use dateTimeLayout +func (xt *XsdTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var err error + var content string + err = d.DecodeElement(&content, &start) + if err != nil { + return err + } + tok1 := strings.SplitN(content, ":", 3) + if len(tok1) != 3 { + return fmt.Errorf("Failed to parse time %v", content) + } + xt.Hour, err = strconv.Atoi(tok1[0]) + if err != nil { + return err + } + xt.Minute, err = strconv.Atoi(tok1[1]) + if err != nil { + return err + } + xt.Second, err = strconv.Atoi(tok1[2][:2]) + if err != nil { + return err + } + reststr := tok1[2][2:] + if len(reststr) > 0 && reststr[0] == '.' { + // there is a fraction + frac := "" + for i, c := range reststr[1:] { + if unicode.IsDigit(c) { + frac = frac + string(c) + } else { + xt.Fraction, err = strconv.Atoi(frac) + if err != nil { + return err + } + reststr = reststr[i:] + break + } + } + } + if len(reststr) > 0 && reststr[0] == 'Z' { + xt.Tz = time.UTC + } else if len(reststr) > 0 { + sign := 1 + if reststr[0] == '+' { + sign = 1 + } else if reststr[0] == '-' { + sign = -1 + } else { + return fmt.Errorf("timezone format is incorrect %v", content) + } + reststr = reststr[1:] + tok2 := strings.Split(reststr, ":") + if len(tok2) > 1 { + hrOffset, err := strconv.Atoi(tok2[0]) + if err != nil { + return err + } + minOffset, err := strconv.Atoi(tok2[1]) + if err != nil { + return err + } + xt.Tz = time.FixedZone(reststr, sign*hrOffset*3600+sign*minOffset*60) + } + } + return nil +} From 1ee9cf8d6961864ad1142248c05dade792c71ad7 Mon Sep 17 00:00:00 2001 From: Nan Huang Date: Fri, 26 Feb 2021 18:52:55 +0000 Subject: [PATCH 5/5] add unit test cases and refector implementation --- fixtures/epcis/epcisquery.src | 16 +- gowsdl.go | 2 +- soap/soap_test.go | 185 +++++++++++++++------ soap/xsdDateTime.go | 298 ++++++++++++++++++++++------------ 4 files changed, 336 insertions(+), 165 deletions(-) diff --git a/fixtures/epcis/epcisquery.src b/fixtures/epcis/epcisquery.src index e5f7e4f..0aff648 100644 --- a/fixtures/epcis/epcisquery.src +++ b/fixtures/epcis/epcisquery.src @@ -33,7 +33,7 @@ type Document struct { // The date the message was created. Used for auditing and logging. // - CreationDate time.Time `xml:"creationDate,attr,omitempty" json:"creationDate,omitempty"` + CreationDate soap.XsdDateTime `xml:"creationDate,attr,omitempty" json:"creationDate,omitempty"` } type EPC string @@ -49,7 +49,7 @@ type DocumentIdentification struct { MultipleType bool `xml:"MultipleType,omitempty" json:"MultipleType,omitempty"` - CreationDateAndTime time.Time `xml:"CreationDateAndTime,omitempty" json:"CreationDateAndTime,omitempty"` + CreationDateAndTime soap.XsdDateTime `xml:"CreationDateAndTime,omitempty" json:"CreationDateAndTime,omitempty"` } type Partner struct { @@ -118,11 +118,11 @@ type Scope struct { } type CorrelationInformation struct { - RequestingDocumentCreationDateTime time.Time `xml:"RequestingDocumentCreationDateTime,omitempty" json:"RequestingDocumentCreationDateTime,omitempty"` + RequestingDocumentCreationDateTime soap.XsdDateTime `xml:"RequestingDocumentCreationDateTime,omitempty" json:"RequestingDocumentCreationDateTime,omitempty"` RequestingDocumentInstanceIdentifier string `xml:"RequestingDocumentInstanceIdentifier,omitempty" json:"RequestingDocumentInstanceIdentifier,omitempty"` - ExpectedResponseDateTime time.Time `xml:"ExpectedResponseDateTime,omitempty" json:"ExpectedResponseDateTime,omitempty"` + ExpectedResponseDateTime soap.XsdDateTime `xml:"ExpectedResponseDateTime,omitempty" json:"ExpectedResponseDateTime,omitempty"` } type BusinessService struct { @@ -414,7 +414,7 @@ type CorrectiveEventIDsType struct { } type ErrorDeclarationType struct { - DeclarationTime time.Time `xml:"declarationTime,omitempty" json:"declarationTime,omitempty"` + DeclarationTime soap.XsdDateTime `xml:"declarationTime,omitempty" json:"declarationTime,omitempty"` Reason *ErrorReasonIDType `xml:"reason,omitempty" json:"reason,omitempty"` @@ -430,9 +430,9 @@ type ErrorDeclarationExtensionType struct { } type EPCISEventType struct { - EventTime time.Time `xml:"eventTime,omitempty" json:"eventTime,omitempty"` + EventTime soap.XsdDateTime `xml:"eventTime,omitempty" json:"eventTime,omitempty"` - RecordTime time.Time `xml:"recordTime,omitempty" json:"recordTime,omitempty"` + RecordTime soap.XsdDateTime `xml:"recordTime,omitempty" json:"recordTime,omitempty"` EventTimeZoneOffset string `xml:"eventTimeZoneOffset,omitempty" json:"eventTimeZoneOffset,omitempty"` @@ -764,7 +764,7 @@ type SubscriptionControls struct { Trigger AnyURI `xml:"trigger,omitempty" json:"trigger,omitempty"` - InitialRecordTime time.Time `xml:"initialRecordTime,omitempty" json:"initialRecordTime,omitempty"` + InitialRecordTime soap.XsdDateTime `xml:"initialRecordTime,omitempty" json:"initialRecordTime,omitempty"` ReportIfEmpty bool `xml:"reportIfEmpty,omitempty" json:"reportIfEmpty,omitempty"` diff --git a/gowsdl.go b/gowsdl.go index a3ce76f..b4a0ffb 100644 --- a/gowsdl.go +++ b/gowsdl.go @@ -432,7 +432,7 @@ var xsd2GoTypes = map[string]string{ "boolean": "bool", "datetime": "soap.XsdDateTime", "date": "soap.XsdDate", - "time": "time.Time", + "time": "soap.XsdTime", "base64binary": "[]byte", "hexbinary": "[]byte", "unsignedint": "uint32", diff --git a/soap/soap_test.go b/soap/soap_test.go index b8f0b5b..7a3f85c 100644 --- a/soap/soap_test.go +++ b/soap/soap_test.go @@ -316,13 +316,11 @@ func TestXsdDateTime(t *testing.T) { XMLName xml.Name `xml:"TestDateTime"` Datetime XsdDateTime } - // test marshalling { + // without nanosecond testDateTime := TestDateTime{ - Datetime: XsdDateTime{ - Time: time.Date(1951, time.October, 22, 1, 2, 3, 4, time.FixedZone("UTC-8", -8*60*60)), - }, + Datetime: CreateXsdDateTime(time.Date(1951, time.October, 22, 1, 2, 3, 0, time.FixedZone("UTC-8", -8*60*60)), true), } if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { t.Error(err) @@ -334,19 +332,48 @@ func TestXsdDateTime(t *testing.T) { } } } + { + // with nanosecond + testDateTime := TestDateTime{ + Datetime: CreateXsdDateTime(time.Date(1951, time.October, 22, 1, 2, 3, 4, time.FixedZone("UTC-8", -8*60*60)), true), + } + if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22T01:02:03.000000004-08:00" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } // test marshalling of UTC { testDateTime := TestDateTime{ - Datetime: XsdDateTime{ - Time: time.Date(1951, time.October, 22, 1, 2, 3, 4, time.UTC), - }, + Datetime: CreateXsdDateTime(time.Date(1951, time.October, 22, 1, 2, 3, 4, time.UTC), true), } if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { t.Error(err) } else { outputstr := string(output) - expected := "1951-10-22T01:02:03Z" + expected := "1951-10-22T01:02:03.000000004Z" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + + // test marshalling of XsdDateTime without TZ + { + testDateTime := TestDateTime{ + Datetime: CreateXsdDateTime(time.Date(1951, time.October, 22, 1, 2, 3, 4, time.UTC), false), + } + if output, err := xml.MarshalIndent(testDateTime, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22T01:02:03.000000004" if outputstr != expected { t.Errorf("Got: %v\nExpected: %v", outputstr, expected) } @@ -356,16 +383,17 @@ func TestXsdDateTime(t *testing.T) { // test unmarshalling { dateTimes := map[string]time.Time{ - "1951-10-22T01:02:03-08:00": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.FixedZone("-0800", -8*60*60)), - "1951-10-22T01:02:03Z": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.UTC), + "1951-10-22T01:02:03.000000004-08:00": time.Date(1951, time.October, 22, 1, 2, 3, 4, time.FixedZone("-0800", -8*60*60)), + "1951-10-22T01:02:03Z": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.UTC), + "1951-10-22T01:02:03": time.Date(1951, time.October, 22, 1, 2, 3, 0, time.Local), } for dateTimeStr, dateTimeObj := range dateTimes { parsedDt := TestDateTime{} if err := xml.Unmarshal([]byte(dateTimeStr), &parsedDt); err != nil { t.Error(err) } else { - if !parsedDt.Datetime.Time.Equal(dateTimeObj) { - t.Errorf("Got: %#v\nExpected: %#v", parsedDt.Datetime.Time, dateTimeObj) + if !parsedDt.Datetime.ToGoTime().Equal(dateTimeObj) { + t.Errorf("Got: %#v\nExpected: %#v", parsedDt.Datetime.ToGoTime(), dateTimeObj) } } } @@ -382,9 +410,7 @@ func TestXsdDate(t *testing.T) { // test marshalling { testDate := TestDate{ - Date: XsdDate{ - Time: time.Date(1951, time.October, 22, 0, 0, 0, 0, time.FixedZone("UTC-8", -8*60*60)), - }, + Date: CreateXsdDate(time.Date(1951, time.October, 22, 0, 0, 0, 0, time.FixedZone("UTC-8", -8*60*60)), false), } if output, err := xml.MarshalIndent(testDate, "", ""); err != nil { t.Error(err) @@ -397,18 +423,32 @@ func TestXsdDate(t *testing.T) { } } + // test marshalling + { + testDate := TestDate{ + Date: CreateXsdDate(time.Date(1951, time.October, 22, 0, 0, 0, 0, time.FixedZone("UTC-8", -8*60*60)), true), + } + if output, err := xml.MarshalIndent(testDate, "", ""); err != nil { + t.Error(err) + } else { + outputstr := string(output) + expected := "1951-10-22-08:00" + if outputstr != expected { + t.Errorf("Got: %v\nExpected: %v", outputstr, expected) + } + } + } + // test marshalling of UTC { testDate := TestDate{ - Date: XsdDate{ - Time: time.Date(1951, time.October, 22, 0, 0, 0, 0, time.UTC), - }, + Date: CreateXsdDate(time.Date(1951, time.October, 22, 0, 0, 0, 0, time.UTC), true), } if output, err := xml.MarshalIndent(testDate, "", ""); err != nil { t.Error(err) } else { outputstr := string(output) - expected := "1951-10-22" + expected := "1951-10-22Z" if outputstr != expected { t.Errorf("Got: %v\nExpected: %v", outputstr, expected) } @@ -418,15 +458,17 @@ func TestXsdDate(t *testing.T) { // test unmarshalling { dates := map[string]time.Time{ - "1951-10-22": time.Date(1951, time.October, 22, 0, 0, 0, 0, time.Local), + "1951-10-22": time.Date(1951, time.October, 22, 0, 0, 0, 0, time.Local), + "1951-10-22Z": time.Date(1951, time.October, 22, 0, 0, 0, 0, time.UTC), + "1951-10-22-08:00": time.Date(1951, time.October, 22, 0, 0, 0, 0, time.FixedZone("UTC-8", -8*60*60)), } for dateStr, dateObj := range dates { parsedDate := TestDate{} if err := xml.Unmarshal([]byte(dateStr), &parsedDate); err != nil { - t.Error(err) + t.Error(dateStr, err) } else { - if !parsedDate.Date.Time.Equal(dateObj) { - t.Errorf("Got: %#v\nExpected: %#v", parsedDate.Date.Time, dateObj) + if !parsedDate.Date.ToGoTime().Equal(dateObj) { + t.Errorf("Got: %#v\nExpected: %#v", parsedDate.Date.ToGoTime(), dateObj) } } } @@ -443,19 +485,13 @@ func TestXsdTime(t *testing.T) { // test marshalling { testTime := TestTime{ - Time: XsdTime{ - Hour: 12, - Minute: 13, - Second: 14, - Fraction: 4, - Tz: time.FixedZone("Test", -19800), - }, + Time: CreateXsdTime(12, 13, 14, 4, time.FixedZone("Test", -19800)), } if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { t.Error(err) } else { outputstr := string(output) - expected := "" + expected := "" if outputstr != expected { t.Errorf("Got: %v\nExpected: %v", outputstr, expected) } @@ -463,13 +499,7 @@ func TestXsdTime(t *testing.T) { } { testTime := TestTime{ - Time: XsdTime{ - Hour: 12, - Minute: 13, - Second: 14, - Fraction: 0, - Tz: time.FixedZone("UTC-8", -8*60*60), - }, + Time: CreateXsdTime(12, 13, 14, 0, time.FixedZone("UTC-8", -8*60*60)), } if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { t.Error(err) @@ -483,13 +513,7 @@ func TestXsdTime(t *testing.T) { } { testTime := TestTime{ - Time: XsdTime{ - Hour: 12, - Minute: 13, - Second: 14, - Fraction: 0, - Tz: nil, - }, + Time: CreateXsdTime(12, 13, 14, 0, nil), } if output, err := xml.MarshalIndent(testTime, "", ""); err != nil { t.Error(err) @@ -502,17 +526,76 @@ func TestXsdTime(t *testing.T) { } } - // test unmarshalling + // test unmarshalling without TZ { - timeStr := "" + timeStr := "" parsedTime := TestTime{} if err := xml.Unmarshal([]byte(timeStr), &parsedTime); err != nil { t.Error(err) } else { - if parsedTime.Time.Hour != 12 || - parsedTime.Time.Minute != 13 || - parsedTime.Time.Second != 14 { - t.Errorf("Got: %#v\nExpected: %#v", parsedTime.Time, "12:13:14") + if parsedTime.Time.Hour() != 12 { + t.Errorf("Got hour %#v\nExpected: %#v", parsedTime.Time.Hour(), 12) + } + if parsedTime.Time.Minute() != 13 { + t.Errorf("Got minute %#v\nExpected: %#v", parsedTime.Time.Minute(), 13) + } + if parsedTime.Time.Second() != 14 { + t.Errorf("Got second %#v\nExpected: %#v", parsedTime.Time.Second(), 14) + } + if parsedTime.Time.Nanosecond() != 4 { + t.Errorf("Got nsec %#v\nExpected: %#v", parsedTime.Time.Nanosecond(), 4) + } + if parsedTime.Time.Location() != nil { + t.Errorf("Got location %v\nExpected: Nil/Undetermined", parsedTime.Time.Location().String()) + } + } + } + // test unmarshalling with UTC + { + timeStr := "" + parsedTime := TestTime{} + if err := xml.Unmarshal([]byte(timeStr), &parsedTime); err != nil { + t.Error(err) + } else { + if parsedTime.Time.Hour() != 12 { + t.Errorf("Got hour %#v\nExpected: %#v", parsedTime.Time.Hour(), 12) + } + if parsedTime.Time.Minute() != 13 { + t.Errorf("Got minute %#v\nExpected: %#v", parsedTime.Time.Minute(), 13) + } + if parsedTime.Time.Second() != 14 { + t.Errorf("Got second %#v\nExpected: %#v", parsedTime.Time.Second(), 14) + } + if parsedTime.Time.Nanosecond() != 0 { + t.Errorf("Got nsec %#v\nExpected: %#v", parsedTime.Time.Nanosecond(), 0) + } + if parsedTime.Time.Location().String() != "UTC" { + t.Errorf("Got location %v\nExpected: UTC", parsedTime.Time.Location().String()) + } + } + } + // test unmarshalling with non-UTC Tz + { + timeStr := "" + parsedTime := TestTime{} + if err := xml.Unmarshal([]byte(timeStr), &parsedTime); err != nil { + t.Error(err) + } else { + if parsedTime.Time.Hour() != 12 { + t.Errorf("Got hour %#v\nExpected: %#v", parsedTime.Time.Hour(), 12) + } + if parsedTime.Time.Minute() != 13 { + t.Errorf("Got minute %#v\nExpected: %#v", parsedTime.Time.Minute(), 13) + } + if parsedTime.Time.Second() != 14 { + t.Errorf("Got second %#v\nExpected: %#v", parsedTime.Time.Second(), 14) + } + if parsedTime.Time.Nanosecond() != 0 { + t.Errorf("Got nsec %#v\nExpected: %#v", parsedTime.Time.Nanosecond(), 0) + } + _, tzOffset := parsedTime.Time.innerTime.Zone() + if tzOffset != -8*3600 { + t.Errorf("Got location offset %v\nExpected: %v", tzOffset, -8*3600) } } } diff --git a/soap/xsdDateTime.go b/soap/xsdDateTime.go index dd0c3ea..b0dbe20 100644 --- a/soap/xsdDateTime.go +++ b/soap/xsdDateTime.go @@ -1,30 +1,63 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + package soap import ( "encoding/xml" - "fmt" - "strconv" "strings" "time" - "unicode" ) const ( - dateLayout = "2006-01-02" - timeLayout = "12:13:14" - timeFracLayout = ".999999999" - timeZoneLayout = "Z07:00" + dateLayout = "2006-01-02Z07:00" + timeLayout = "15:04:05.999999999Z07:00" ) -// XsdDateTime is a type for representing xsd:datetime +// +// DateTime struct +// + +// XsdDateTime is a type for representing xsd:datetime in Golang type XsdDateTime struct { - time.Time + innerTime time.Time + hasTz bool } -// MarshalXML implementation on DateTimeg to skip "zero" time values +// StripTz removes TZ information from the datetime +func (xdt *XsdDateTime) StripTz() { + xdt.hasTz = false +} + +// ToGoTime converts the time to time.Time by checking if a TZ is specified. +// If there is a TZ, that TZ is used, otherwise local TZ is used +func (xdt *XsdDateTime) ToGoTime() time.Time { + if xdt.hasTz { + return xdt.innerTime + } + return time.Date(xdt.innerTime.Year(), xdt.innerTime.Month(), xdt.innerTime.Day(), + xdt.innerTime.Hour(), xdt.innerTime.Minute(), xdt.innerTime.Second(), + xdt.innerTime.Nanosecond(), time.Local) +} + +// MarshalXML implementation on DateTime to skip "zero" time values. It also checks if nanoseconds and TZ exist. func (xdt XsdDateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - if !xdt.IsZero() { - e.EncodeElement(xdt.Time.Format(time.RFC3339), start) + if !xdt.innerTime.IsZero() { + dateTimeLayout := time.RFC3339Nano + if xdt.innerTime.Nanosecond() == 0 { + dateTimeLayout = time.RFC3339 + } + dtString := xdt.innerTime.Format(dateTimeLayout) + if !xdt.hasTz { + // split off time portion + dateAndTime := strings.SplitN(dtString, "T", 2) + toks := strings.SplitN(dateAndTime[1], "Z", 2) + toks = strings.SplitN(toks[0], "+", 2) + toks = strings.SplitN(toks[0], "-", 2) + dtString = dateAndTime[0] + "T" + toks[0] + } + e.EncodeElement(dtString, start) } return nil } @@ -32,39 +65,98 @@ func (xdt XsdDateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error // UnmarshalXML implementation on DateTimeg to use dateTimeLayout func (xdt *XsdDateTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var err error - xdt.Time, err = unmarshalTime(d, start, time.RFC3339) + xdt.innerTime, xdt.hasTz, err = unmarshalTime(d, start, time.RFC3339Nano) return err } -func unmarshalTime(d *xml.Decoder, start xml.StartElement, format string) (time.Time, error) { +// CreateXsdDateTime creates an object represent xsd:datetime object in Golang +func CreateXsdDateTime(dt time.Time, hasTz bool) XsdDateTime { + return XsdDateTime{ + innerTime: dt, + hasTz: hasTz, + } +} + +func unmarshalTime(d *xml.Decoder, start xml.StartElement, format string) (time.Time, bool, error) { var t time.Time var content string err := d.DecodeElement(&content, &start) if err != nil { - return t, err + return t, true, err } if content == "" { - return t, nil + return t, true, nil } - if content == "0001-01-01T00:00:00Z" { - return t, nil + hasTz := false + if strings.Contains(content, "T") { // check if we have a time portion + // split into date and time portion + dateAndTime := strings.SplitN(content, "T", 2) + if len(dateAndTime) > 1 { + if strings.Contains(dateAndTime[1], "Z") || + strings.Contains(dateAndTime[1], "+") || + strings.Contains(dateAndTime[1], "-") { + hasTz = true + } + } + if !hasTz { + content += "Z" + } + if content == "0001-01-01T00:00:00Z" { + return t, true, nil + } + } else { + // we don't see to have a time portion, check timezone + if strings.Contains(content, "Z") || + strings.Contains(content, ":") { + hasTz = true + } + if !hasTz { + content += "Z" + } } t, err = time.Parse(format, content) - if err != nil { - return t, err - } - return t, nil + return t, hasTz, nil } -// XsdDate is a type for representing xsd:date +// XsdDate is a type for representing xsd:date in Golang type XsdDate struct { - time.Time + innerDate time.Time + hasTz bool +} + +// StripTz removes the TZ information from the date +func (xd *XsdDate) StripTz() { + xd.hasTz = false +} + +// ToGoTime converts the date to Golang time.Time by checking if a TZ is specified. +// If there is a TZ, that TZ is used, otherwise local TZ is used +func (xd *XsdDate) ToGoTime() time.Time { + if xd.hasTz { + return xd.innerDate + } + return time.Date(xd.innerDate.Year(), xd.innerDate.Month(), xd.innerDate.Day(), + 0, 0, 0, 0, time.Local) } // MarshalXML implementation on DateTimeg to skip "zero" time values func (xd XsdDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - if !xd.IsZero() { - e.EncodeElement(xd.Time.Format(dateLayout), start) + if !xd.innerDate.IsZero() { + dateString := xd.innerDate.Format(dateLayout) // serialize with TZ + if !xd.hasTz { + if strings.Contains(dateString, "Z") { + // UTC Tz + toks := strings.SplitN(dateString, "Z", 2) + dateString = toks[0] + } else { + // [+-]00:00 Tz, remove last 6 chars + if len(dateString) > 5 { // this should always be true + start := len(dateString) - 6 // locate at "-" + dateString = dateString[0:start] + } + } + } + e.EncodeElement(dateString, start) } return nil } @@ -72,111 +164,107 @@ func (xd XsdDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error { // UnmarshalXML implementation on DateTimeg to use dateTimeLayout func (xd *XsdDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var err error - xd.Time, err = unmarshalTime(d, start, dateLayout) + xd.innerDate, xd.hasTz, err = unmarshalTime(d, start, dateLayout) return err } -// XsdTime is a type for representing xsd:time -type XsdTime struct { - Hour int - Minute int - Second int - Fraction int - Tz *time.Location +// CreateXsdDate creates an object represent xsd:datetime object in Golang +func CreateXsdDate(date time.Time, hasTz bool) XsdDate { + return XsdDate{ + innerDate: date, + hasTz: hasTz, + } } -func abs(n int) int { - if n < 0 { - return -n - } - return n +// XsdTime is a type for representing xsd:time +type XsdTime struct { + innerTime time.Time + hasTz bool } // MarshalXML implementation on DateTimeg to skip "zero" time values func (xt XsdTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error { - timeString := fmt.Sprintf("%v:%v:%v", xt.Hour, xt.Minute, xt.Second) - if xt.Fraction != 0 { - timeString = fmt.Sprintf("%v.%v", timeString, xt.Fraction) - } - if xt.Tz != nil { - _, offset := time.Now().In(xt.Tz).Zone() - hrOffset := offset / 3600 - minOffset := abs((offset - hrOffset*3600) / 60) - if hrOffset == 0 && minOffset == 0 { - timeString = timeString + "Z" - } else { - timeString = fmt.Sprintf("%v%+03d:%02d", timeString, hrOffset, minOffset) + if !xt.innerTime.IsZero() { + dateTimeLayout := time.RFC3339Nano + if xt.innerTime.Nanosecond() == 0 { + dateTimeLayout = time.RFC3339 + } + // split off date portion + dateAndTime := strings.SplitN(xt.innerTime.Format(dateTimeLayout), "T", 2) + timeString := dateAndTime[1] + if !xt.hasTz { + toks := strings.SplitN(timeString, "Z", 2) + toks = strings.SplitN(toks[0], "+", 2) + toks = strings.SplitN(toks[0], "-", 2) + timeString = toks[0] } + e.EncodeElement(timeString, start) } - e.EncodeElement(timeString, start) return nil } // UnmarshalXML implementation on DateTimeg to use dateTimeLayout func (xt *XsdTime) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var t time.Time var err error var content string err = d.DecodeElement(&content, &start) if err != nil { return err } - tok1 := strings.SplitN(content, ":", 3) - if len(tok1) != 3 { - return fmt.Errorf("Failed to parse time %v", content) + if content == "" { + xt.innerTime = t + return nil } - xt.Hour, err = strconv.Atoi(tok1[0]) - if err != nil { - return err + xt.hasTz = false + if strings.Contains(content, "Z") || + strings.Contains(content, "+") || + strings.Contains(content, "-") { + xt.hasTz = true } - xt.Minute, err = strconv.Atoi(tok1[1]) - if err != nil { - return err + if !xt.hasTz { + content += "Z" } - xt.Second, err = strconv.Atoi(tok1[2][:2]) - if err != nil { - return err + xt.innerTime, err = time.Parse(timeLayout, content) + return err +} + +// Hour returns hour of the xsd:time +func (xt XsdTime) Hour() int { + return xt.innerTime.Hour() +} + +// Minute returns minutes of the xsd:time +func (xt XsdTime) Minute() int { + return xt.innerTime.Minute() +} + +// Second returns seconds of the xsd:time +func (xt XsdTime) Second() int { + return xt.innerTime.Second() +} + +// Nanosecond returns nanosecond of the xsd:time +func (xt XsdTime) Nanosecond() int { + return xt.innerTime.Nanosecond() +} + +// Location returns the TZ information of the xsd:time +func (xt XsdTime) Location() *time.Location { + if xt.hasTz { + return xt.innerTime.Location() } - reststr := tok1[2][2:] - if len(reststr) > 0 && reststr[0] == '.' { - // there is a fraction - frac := "" - for i, c := range reststr[1:] { - if unicode.IsDigit(c) { - frac = frac + string(c) - } else { - xt.Fraction, err = strconv.Atoi(frac) - if err != nil { - return err - } - reststr = reststr[i:] - break - } - } + return nil +} + +// CreateXsdTime creates an object representing xsd:time in Golang +func CreateXsdTime(hour int, min int, sec int, nsec int, loc *time.Location) XsdTime { + realLoc := loc + if loc == nil { + realLoc = time.Local } - if len(reststr) > 0 && reststr[0] == 'Z' { - xt.Tz = time.UTC - } else if len(reststr) > 0 { - sign := 1 - if reststr[0] == '+' { - sign = 1 - } else if reststr[0] == '-' { - sign = -1 - } else { - return fmt.Errorf("timezone format is incorrect %v", content) - } - reststr = reststr[1:] - tok2 := strings.Split(reststr, ":") - if len(tok2) > 1 { - hrOffset, err := strconv.Atoi(tok2[0]) - if err != nil { - return err - } - minOffset, err := strconv.Atoi(tok2[1]) - if err != nil { - return err - } - xt.Tz = time.FixedZone(reststr, sign*hrOffset*3600+sign*minOffset*60) - } + return XsdTime{ + innerTime: time.Date(1951, 10, 22, hour, min, sec, nsec, realLoc), + hasTz: loc != nil, } - return nil }