@@ -1698,3 +1698,178 @@ func TestTimeFormatBinding(t *testing.T) {
16981698 })
16991699 }
17001700}
1701+
1702+ func TestOpenAPIFormatTimeBinding (t * testing.T ) {
1703+ type TestStruct struct {
1704+ DateTime time.Time `param:"date_time" query:"date_time" header:"date_time" form:"date_time" format:"date-time"`
1705+ DateTimePtr * time.Time `param:"date_time_ptr" query:"date_time_ptr" header:"date_time_ptr" form:"date_time_ptr" format:"date-time"`
1706+ DateTimeLocal time.Time `param:"date_time_local" query:"date_time_local" header:"date_time_local" form:"date_time_local" format:"date-time-local"`
1707+ DateTimeLocalPtr * time.Time `param:"date_time_local_ptr" query:"date_time_local_ptr" header:"date_time_local_ptr" form:"date_time_local_ptr" format:"date-time-local"`
1708+ Date time.Time `param:"full_date" query:"full_date" header:"full_date" form:"full_date" format:"date"`
1709+ DatePtr * time.Time `param:"full_date_ptr" query:"full_date_ptr" header:"full_date_ptr" form:"full_date_ptr" format:"date"`
1710+ }
1711+
1712+ utc := time .Date (2023 , 12 , 25 , 14 , 30 , 45 , 0 , time .UTC )
1713+ testCases := []struct {
1714+ name , field , value string
1715+ want time.Time
1716+ wantError bool
1717+ }{
1718+ {name : "ok, date-time UTC" , field : "date_time" , value : "2023-12-25T14:30:45Z" , want : utc },
1719+ {name : "ok, date-time offset and fraction" , field : "date_time" , value : "2023-12-25T14:30:45.123456789+02:00" , want : time .Date (2023 , 12 , 25 , 14 , 30 , 45 , 123456789 , time .FixedZone ("" , 2 * 60 * 60 ))},
1720+ {name : "nok, date-time requires timezone" , field : "date_time" , value : "2023-12-25T14:30:45" , wantError : true },
1721+ {name : "nok, date-time invalid date" , field : "date_time" , value : "2023-02-30T14:30:45Z" , wantError : true },
1722+ {name : "ok, local date-time" , field : "date_time_local" , value : "2023-12-25T14:30:45" , want : utc },
1723+ {name : "ok, local date-time fraction" , field : "date_time_local" , value : "2023-12-25T14:30:45.123456789" , want : utc .Add (123456789 * time .Nanosecond )},
1724+ {name : "nok, local date-time rejects UTC suffix" , field : "date_time_local" , value : "2023-12-25T14:30:45Z" , wantError : true },
1725+ {name : "nok, local date-time rejects offset" , field : "date_time_local" , value : "2023-12-25T14:30:45+02:00" , wantError : true },
1726+ {name : "nok, local date-time requires seconds" , field : "date_time_local" , value : "2023-12-25T14:30" , wantError : true },
1727+ {name : "nok, local date-time invalid date" , field : "date_time_local" , value : "2023-02-30T14:30:45" , wantError : true },
1728+ {name : "ok, date" , field : "full_date" , value : "2023-12-25" , want : time .Date (2023 , 12 , 25 , 0 , 0 , 0 , 0 , time .UTC )},
1729+ {name : "nok, date rejects time" , field : "full_date" , value : "2023-12-25T14:30:45Z" , wantError : true },
1730+ {name : "nok, date invalid date" , field : "full_date" , value : "2023-02-30" , wantError : true },
1731+ }
1732+ for _ , source := range []string {"param" , "query" , "header" , "form" , "multipart" } {
1733+ for _ , tc := range testCases {
1734+ for _ , pointer := range []bool {false , true } {
1735+ t .Run (fmt .Sprintf ("%s/%s/pointer=%t" , source , tc .name , pointer ), func (t * testing.T ) {
1736+ field := tc .field
1737+ if pointer {
1738+ field += "_ptr"
1739+ }
1740+ req := httptest .NewRequest (http .MethodGet , "/" , nil )
1741+ bind := BindQueryParams
1742+ switch source {
1743+ case "param" :
1744+ bind = BindPathValues
1745+ case "query" :
1746+ req .URL .RawQuery = url.Values {field : {tc .value }}.Encode ()
1747+ case "header" :
1748+ req .Header .Set (field , tc .value )
1749+ bind = BindHeaders
1750+ case "form" :
1751+ req = httptest .NewRequest (http .MethodPost , "/" , strings .NewReader (url.Values {field : {tc .value }}.Encode ()))
1752+ req .Header .Set (HeaderContentType , MIMEApplicationForm )
1753+ bind = BindBody
1754+ case "multipart" :
1755+ var body bytes.Buffer
1756+ writer := multipart .NewWriter (& body )
1757+ if ! assert .NoError (t , writer .WriteField (field , tc .value )) || ! assert .NoError (t , writer .Close ()) {
1758+ return
1759+ }
1760+ req = httptest .NewRequest (http .MethodPost , "/" , & body )
1761+ req .Header .Set (HeaderContentType , writer .FormDataContentType ())
1762+ bind = BindBody
1763+ }
1764+ c := New ().NewContext (req , httptest .NewRecorder ())
1765+ c .SetPathValues (PathValues {{Name : field , Value : tc .value }})
1766+ var result TestStruct
1767+ err := bind (c , & result )
1768+ if tc .wantError {
1769+ assert .Error (t , err )
1770+ assert .Equal (t , http .StatusBadRequest , StatusCode (err ))
1771+ return
1772+ }
1773+ if ! assert .NoError (t , err ) {
1774+ return
1775+ }
1776+ actual , actualPtr := result .DateTime , result .DateTimePtr
1777+ switch tc .field {
1778+ case "date_time_local" :
1779+ actual , actualPtr = result .DateTimeLocal , result .DateTimeLocalPtr
1780+ case "full_date" :
1781+ actual , actualPtr = result .Date , result .DatePtr
1782+ }
1783+ if pointer {
1784+ if ! assert .NotNil (t , actualPtr ) {
1785+ return
1786+ }
1787+ actual = * actualPtr
1788+ }
1789+ assert .True (t , tc .want .Equal (actual ), "expected %v, got %v" , tc .want , actual )
1790+ _ , wantOffset := tc .want .Zone ()
1791+ _ , actualOffset := actual .Zone ()
1792+ assert .Equal (t , wantOffset , actualOffset )
1793+ if tc .field != "date_time" {
1794+ assert .Same (t , time .UTC , actual .Location ())
1795+ }
1796+ })
1797+ }
1798+ }
1799+ }
1800+ }
1801+
1802+ func TestOpenAPIFormatTimeBindingJSON (t * testing.T ) {
1803+ for _ , tc := range []struct {
1804+ name , value string
1805+ wantError bool
1806+ }{
1807+ {name : "ok, RFC3339 decoding ignores format tag" , value : "2023-12-25T14:30:45Z" },
1808+ {name : "nok, local format tag does not enable timezone-free JSON" , value : "2023-12-25T14:30:45" , wantError : true },
1809+ } {
1810+ t .Run (tc .name , func (t * testing.T ) {
1811+ var result struct {
1812+ Time * time.Time `json:"time" format:"date-time-local"`
1813+ }
1814+ req := httptest .NewRequest (http .MethodPost , "/" , strings .NewReader (fmt .Sprintf (`{"time":%q}` , tc .value )))
1815+ req .Header .Set (HeaderContentType , MIMEApplicationJSON )
1816+ err := New ().NewContext (req , httptest .NewRecorder ()).Bind (& result )
1817+ if tc .wantError {
1818+ assert .Error (t , err )
1819+ assert .Equal (t , http .StatusBadRequest , StatusCode (err ))
1820+ return
1821+ }
1822+ if assert .NoError (t , err ) && assert .NotNil (t , result .Time ) {
1823+ assert .True (t , time .Date (2023 , 12 , 25 , 14 , 30 , 45 , 0 , time .UTC ).Equal (* result .Time ))
1824+ }
1825+ })
1826+ }
1827+ }
1828+
1829+ func TestOpenAPIFormatTimeBindingCustomTypes (t * testing.T ) {
1830+ var result struct {
1831+ Time Timestamp `query:"time" format:"date-time-local"`
1832+ Text string `query:"text" format:"date-time-local"`
1833+ }
1834+ value := "2023-12-25T14:30:45Z"
1835+ req := httptest .NewRequest (http .MethodGet , "/?" + url.Values {"time" : {value }, "text" : {value }}.Encode (), nil )
1836+ err := New ().NewContext (req , httptest .NewRecorder ()).Bind (& result )
1837+ if assert .NoError (t , err ) {
1838+ assert .True (t , time .Date (2023 , 12 , 25 , 14 , 30 , 45 , 0 , time .UTC ).Equal (time .Time (result .Time )))
1839+ assert .Equal (t , value , result .Text )
1840+ }
1841+ }
1842+
1843+ func TestOpenAPIFormatTimeBindingEdgeCases (t * testing.T ) {
1844+ type TestStruct struct {
1845+ DateTime time.Time `query:"date_time" format:"date-time"`
1846+ DateTimeLocal time.Time `query:"date_time_local" format:"date-time-local"`
1847+ Date time.Time `query:"full_date" format:"date"`
1848+ Slice []time.Time `query:"slice" format:"date-time-local"`
1849+ }
1850+ testCases := []struct {
1851+ name , query string
1852+ wantError bool
1853+ }{
1854+ {name : "nok, literal date-time name is not a value" , query : "date_time=date-time" , wantError : true },
1855+ {name : "nok, literal date-time-local name is not a value" , query : "date_time_local=date-time-local" , wantError : true },
1856+ {name : "nok, literal date name is not a value" , query : "full_date=date" , wantError : true },
1857+ {name : "ok, slices ignore the format tag" , query : "slice=2023-12-25T14%3A30%3A45Z" },
1858+ {name : "nok, slices ignore the format tag" , query : "slice=2023-12-25T14%3A30%3A45" , wantError : true },
1859+ }
1860+ for _ , tc := range testCases {
1861+ t .Run (tc .name , func (t * testing.T ) {
1862+ req := httptest .NewRequest (http .MethodGet , "/?" + tc .query , nil )
1863+ c := New ().NewContext (req , httptest .NewRecorder ())
1864+ var result TestStruct
1865+ err := BindQueryParams (c , & result )
1866+ if tc .wantError {
1867+ assert .Error (t , err )
1868+ return
1869+ }
1870+ if assert .NoError (t , err ) && assert .Len (t , result .Slice , 1 ) {
1871+ assert .True (t , time .Date (2023 , 12 , 25 , 14 , 30 , 45 , 0 , time .UTC ).Equal (result .Slice [0 ]))
1872+ }
1873+ })
1874+ }
1875+ }
0 commit comments