From 586c0e75b1c349042767f24f4504b8c323edc13b Mon Sep 17 00:00:00 2001 From: Adam Bouqdib Date: Fri, 25 Oct 2024 01:36:27 +0100 Subject: [PATCH] feat(gen): support custom time format --- _testdata/positive/time_extension.yml | 102 ++++++++++++++++++++ gen/_template/client.tmpl | 6 +- gen/_template/defaults/set.tmpl | 6 +- gen/_template/faker.tmpl | 2 +- gen/_template/handlers.tmpl | 6 +- gen/_template/json.tmpl | 2 +- gen/_template/json/decode.tmpl | 26 +++-- gen/_template/json/encode.tmpl | 25 +++-- gen/_template/json/encoders_generic.tmpl | 10 +- gen/_template/json/stdmarshaler.tmpl | 12 ++- gen/_template/uri/decode.tmpl | 6 +- gen/_template/uri/encode.tmpl | 6 +- gen/ir/json.go | 10 ++ internal/integration/generate.go | 1 + internal/integration/time_extension_test.go | 62 ++++++++++++ json/time.go | 100 ++++++++++--------- json/time_test.go | 36 +------ jsonschema/parser.go | 6 ++ jsonschema/parser_test.go | 8 ++ jsonschema/schema.go | 2 + 20 files changed, 323 insertions(+), 111 deletions(-) create mode 100644 _testdata/positive/time_extension.yml create mode 100644 internal/integration/time_extension_test.go diff --git a/_testdata/positive/time_extension.yml b/_testdata/positive/time_extension.yml new file mode 100644 index 000000000..62828de30 --- /dev/null +++ b/_testdata/positive/time_extension.yml @@ -0,0 +1,102 @@ +openapi: 3.0.3 +info: + title: API + version: 0.1.0 +paths: + /optional: + get: + operationId: default + parameters: + - name: date + in: query + schema: + type: string + format: date + x-ogen-time-format: 02/01/2006 + default: 04/03/2001 + - name: time + in: query + schema: + type: string + format: time + x-ogen-time-format: 3:04PM + default: 1:23AM + - name: dateTime + in: query + schema: + type: string + format: date-time + x-ogen-time-format: 2006-01-02T15:04:05.999999999Z07:00 + default: 2001-03-04T01:23:45.123456789-07:00 + responses: + '200': + description: Test + content: + application/json: + schema: + type: object + properties: + date: + type: string + format: date + x-ogen-time-format: 02/01/2006 + default: 04/03/2001 + time: + type: string + format: time + x-ogen-time-format: 3:04PM + default: 1:23AM + dateTime: + type: string + format: date-time + x-ogen-time-format: 2006-01-02T15:04:05.999999999Z07:00 + default: 2001-03-04T01:23:45.123456789-07:00 + /required: + get: + operationId: required + parameters: + - name: date + in: query + required: true + schema: + type: string + format: date + x-ogen-time-format: 02/01/2006 + - name: time + in: query + required: true + schema: + type: string + format: time + x-ogen-time-format: 3:04PM + - name: dateTime + in: query + required: true + schema: + type: string + format: date-time + x-ogen-time-format: 2006-01-02T15:04:05.999999999Z07:00 + responses: + '200': + description: Test + content: + application/json: + schema: + type: object + required: + - date + - time + - dateTime + properties: + date: + type: string + format: date + x-ogen-time-format: 02/01/2006 + time: + type: string + format: time + x-ogen-time-format: 3:04PM + dateTime: + type: string + format: date + x-ogen-time-format: 2006-01-02T15:04:05.999999999Z07:00 diff --git a/gen/_template/client.tmpl b/gen/_template/client.tmpl index 14826684d..f9873cd53 100644 --- a/gen/_template/client.tmpl +++ b/gen/_template/client.tmpl @@ -124,7 +124,7 @@ func NewWebhookClient(opts ...ClientOption) (*WebhookClient, error) { } {{- range $op := $ops }} - {{ template "client/operation" op_elem $op $ }} + {{ template "client/operation" op_elem $op $ }} {{- end }} {{- end }} @@ -273,7 +273,7 @@ func (c *{{ if $op.WebhookInfo }}Webhook{{ end }}Client) send{{ $op.Name }}(ctx {{ if $op.HasCookieParams }} {{ if $otel }}stage = "EncodeCookieParams"{{ end }} {{- template "encode_cookie_parameters" $op }} - {{- end }} + {{- end }} {{- with $securities := $op.Security.Securities }} { @@ -299,7 +299,7 @@ func (c *{{ if $op.WebhookInfo }}Webhook{{ end }}Client) send{{ $op.Name }}(ctx for _, requirement := range []bitset{ {{- range $req := $op.Security.Requirements }} { - {{- range $mask := $req }}{{ printf "%#08b" $mask }},{{ end -}} + {{- range $mask := $req }}{{ printf "%#08b" $mask }},{{ end -}} }, {{- end }} } { diff --git a/gen/_template/defaults/set.tmpl b/gen/_template/defaults/set.tmpl index 25bf9a36b..3b0487f07 100644 --- a/gen/_template/defaults/set.tmpl +++ b/gen/_template/defaults/set.tmpl @@ -16,7 +16,7 @@ {{ $.Var }}.SetTo(val) {{- end }} {{- else if $t.IsPointer -}} - {{- template "defaults/val" default_elem $t.PointerTo $.Var $.Default }} + {{- template "defaults/val" default_elem $t.PointerTo $.Var $.Default }} {{ $.Var }} = &val {{- else -}} {{ errorf "unsupported %#v: %s" $.Default.Value $t }} @@ -26,7 +26,9 @@ {{- define "defaults/val" -}} {{ $t := $.Type }}{{ $j := $t.JSON }}{{- $val := print_go $.Default.Value }} -{{- if $j.Format }} +{{- if $j.TimeFormat }} + val, _ := json.DecodeTimeFormat(jx.DecodeStr({{ quote $val }}), {{ $j.TimeFormat }}) +{{- else if $j.Format }} val, _ := json.Decode{{ $j.Format }}(jx.DecodeStr({{ quote $val }})) {{- else if $j.IsBase64 }} val, _ := jx.DecodeStr({{ quote $val }}).Base64() diff --git a/gen/_template/faker.tmpl b/gen/_template/faker.tmpl index 5f942c902..2faef6a0e 100644 --- a/gen/_template/faker.tmpl +++ b/gen/_template/faker.tmpl @@ -2,7 +2,7 @@ {{ template "header" $ }} {{- range $_, $s := $.Types }}{{- if $s.HasFeature "json" }} - {{ template "faker/fakers" $s }} + {{ template "faker/fakers" $s }} {{- end }}{{- end }} {{- end }} diff --git a/gen/_template/handlers.tmpl b/gen/_template/handlers.tmpl index d8ee9ade7..27ab59da1 100644 --- a/gen/_template/handlers.tmpl +++ b/gen/_template/handlers.tmpl @@ -15,7 +15,7 @@ func recordError(string, error) {} {{- if $.WebhookServerEnabled }} {{- range $op := $.Webhooks }} - {{- template "handlers/operation" op_elem $op $ }} + {{- template "handlers/operation" op_elem $op $ }} {{ end }} {{- end }} @@ -128,7 +128,7 @@ func (s *{{ if $op.WebhookInfo }}Webhook{{ end }}Server) handle{{ $op.Name }}Req for _, requirement := range []bitset{ {{- range $req := $op.Security.Requirements }} { - {{- range $mask := $req }}{{ printf "%#08b" $mask }},{{ end }} + {{- range $mask := $req }}{{ printf "%#08b" $mask }},{{ end }} }, {{- end }} } { @@ -156,7 +156,7 @@ func (s *{{ if $op.WebhookInfo }}Webhook{{ end }}Server) handle{{ $op.Name }}Req return } } - {{- end }} + {{- end }} {{- if $op.Params }} params, err := decode{{ $op.Name }}Params(args, argsEscaped, r) diff --git a/gen/_template/json.tmpl b/gen/_template/json.tmpl index d64472253..2597d3925 100644 --- a/gen/_template/json.tmpl +++ b/gen/_template/json.tmpl @@ -4,7 +4,7 @@ {{- range $_, $t := $.Types }} {{- if $t.HasFeature "json" }} {{- template "json/encoders" $t }} - {{- template "json/stdmarshaler" $t }} + {{- template "json/stdmarshaler" $t }} {{- end }} {{- end }} {{ end }} diff --git a/gen/_template/json/decode.tmpl b/gen/_template/json/decode.tmpl index c07ccfb8a..a8731b1d0 100644 --- a/gen/_template/json/decode.tmpl +++ b/gen/_template/json/decode.tmpl @@ -44,9 +44,15 @@ if err := d.Arr(func(d *jx.Decoder) error { {{ $.Var }}.Reset() {{- end }} {{- if $g.Format }} - if err := {{ $.Var }}.Decode(d, json.Decode{{ $g.JSON.Format }}); err != nil { - return err - } + {{ if $g.JSON.TimeFormat -}} + if err := {{ $.Var }}.Decode(d, json.NewTimeDecoder({{ $g.JSON.TimeFormat }})); err != nil { + return err + } + {{- else -}} + if err := {{ $.Var }}.Decode(d, json.Decode{{ $g.JSON.Format }}); err != nil { + return err + } + {{- end }} {{- else }} if err := {{ $.Var }}.Decode(d); err != nil { return err @@ -60,8 +66,8 @@ if err := d.Arr(func(d *jx.Decoder) error { {{- $j := $t.JSON -}} {{- if $t.IsPointer }} {{- template "json/dec_pointer" $ }} - {{- else if $t.IsGeneric }} - {{- template "json/dec_generic" $ }} + {{- else if $t.IsGeneric }} + {{- template "json/dec_generic" $ }} {{- else if $t.IsArray }} {{ template "json/dec_array" $ }} {{- else if $t.IsMap }} @@ -72,10 +78,16 @@ if err := d.Arr(func(d *jx.Decoder) error { if err := {{ $.Var }}.Decode(d); err != nil { return err } - {{- else if $t.IsNull }} + {{- else if $t.IsNull }} if err := d.Null(); err != nil { return err } + {{- else if $j.TimeFormat }} + v, err := json.DecodeTimeFormat(d, {{ $j.TimeFormat }}) + {{ $.Var }} = v + if err != nil { + return err + } {{- else if $j.Format }} v, err := json.Decode{{ $j.Format }}(d) {{ $.Var }} = v @@ -89,6 +101,6 @@ if err := d.Arr(func(d *jx.Decoder) error { return err } {{- else }} - {{ errorf "unsupported kind: %s" $t.Kind }} + {{ errorf "unsupported kind: %s" $t.Kind }} {{- end }} {{- end -}} diff --git a/gen/_template/json/encode.tmpl b/gen/_template/json/encode.tmpl index 5369a1d36..631f6952a 100644 --- a/gen/_template/json/encode.tmpl +++ b/gen/_template/json/encode.tmpl @@ -17,7 +17,7 @@ if {{ $.Var }}.Set { {{- template "json/enc_generic_field" $ }} } {{- else }} - {{- template "json/enc_generic_field" $ }} + {{- template "json/enc_generic_field" $ }} {{- end -}} {{- end }} @@ -28,9 +28,13 @@ if {{ $.Var }}.Set { {{- template "json/enc_field" $ }} {{- if $g.Format }} - {{ $.Var }}.Encode(e, json.Encode{{ $g.JSON.Format }}) + {{ if $g.JSON.TimeFormat -}} + {{ $.Var }}.Encode(e, json.NewTimeEncoder({{ $g.JSON.TimeFormat }})) + {{- else -}} + {{ $.Var }}.Encode(e, json.Encode{{ $g.JSON.Format }}) + {{- end }} {{- else }} - {{ $.Var }}.Encode(e) + {{ $.Var }}.Encode(e) {{- end }} {{- end }} @@ -98,17 +102,20 @@ if {{ $.Var }}.Set { {{- template "json/enc_generic" $ }} {{- else if $t.IsArray -}} {{- template "json/enc_array" $ }} - {{- else if $t.IsAny }} + {{- else if $t.IsAny }} if len({{ $.Var }}) != 0 { - {{- template "json/enc_field" $ }} + {{- template "json/enc_field" $ }} e.{{ $j.Fn }}({{ $.Var }}) } - {{- else if $t.IsNull }} + {{- else if $t.IsNull }} _ = {{ $.Var }} - {{- template "json/enc_field" $ }} + {{- template "json/enc_field" $ }} e.Null() - {{- else if $j.Format -}} - {{- template "json/enc_field" $ }} + {{- else if $j.TimeFormat }} + {{- template "json/enc_field" $ }} + json.EncodeTimeFormat(e, {{ $.Var }}, {{ $j.TimeFormat }}) + {{- else if $j.Format -}} + {{- template "json/enc_field" $ }} json.Encode{{ $j.Format }}(e, {{ $.Var }}) {{- else if $j.Fn -}} {{- template "json/enc_field" $ }} diff --git a/gen/_template/json/encoders_generic.tmpl b/gen/_template/json/encoders_generic.tmpl index 1004f8e5b..c0ef43b52 100644 --- a/gen/_template/json/encoders_generic.tmpl +++ b/gen/_template/json/encoders_generic.tmpl @@ -17,6 +17,8 @@ func (o {{ $.ReadOnlyReceiver }}) Encode(e *jx.Encoder{{ if $g.Format }}, format {{- end }} {{- if $g.Format }} format(e, o.Value) +{{- else if $g.JSON.TimeFormat }} + json.EncodeTimeFormat(e, o.Value, {{ $g.JSON.TimeFormat }}) {{- else if $g.JSON.Format }} json.Encode{{ $g.JSON.Format }}(e, o.Value) {{- else if $g.JSON.Fn }} @@ -31,7 +33,7 @@ func (o {{ $.ReadOnlyReceiver }}) Encode(e *jx.Encoder{{ if $g.Format }}, format {{- else if or ($g.IsStruct) ($g.IsMap) ($g.IsEnum) ($g.IsPointer) ($g.IsSum) ($g.IsAlias) }} o.Value.Encode(e) {{- else }} - {{ errorf "unexpected kind %s" $g.Kind }} + {{ errorf "unexpected kind %s" $g.Kind }} {{- end }} } @@ -68,6 +70,12 @@ func (o *{{ $.Name }}) Decode(d *jx.Decoder{{ if $g.Format }}, format func(*jx.D return err } o.Value = v + {{- else if $g.JSON.TimeFormat }} + v, err := json.DecodeTimeFormat(d, {{ $g.JSON.TimeFormat }}) + if err != nil { + return err + } + o.Value = v {{- else if $g.JSON.Format }} v, err := json.Decode{{ $g.JSON.Format }}(d) if err != nil { diff --git a/gen/_template/json/stdmarshaler.tmpl b/gen/_template/json/stdmarshaler.tmpl index b35f3ef1a..35e385825 100644 --- a/gen/_template/json/stdmarshaler.tmpl +++ b/gen/_template/json/stdmarshaler.tmpl @@ -6,7 +6,11 @@ func (s {{ $.ReadOnlyReceiver }}) MarshalJSON() ([]byte, error) { e := jx.Encoder{} {{- if $.IsGeneric }} {{- if $g.Format }} - s.Encode(&e, json.Encode{{ $g.JSON.Format }}) + {{ if $g.JSON.TimeFormat -}} + s.Encode(&e, json.NewTimeEncoder({{ $g.JSON.TimeFormat }})) + {{- else -}} + s.Encode(&e, json.Encode{{ $g.JSON.Format }}) + {{- end }} {{- else }} s.Encode(&e) {{- end }} @@ -21,7 +25,11 @@ func (s *{{ $.Name }}) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) {{- if $.IsGeneric }} {{- if $g.Format }} - return s.Decode(d, json.Decode{{ $g.JSON.Format }}) + {{ if $g.JSON.TimeFormat -}} + return s.Decode(d, json.NewTimeDecoder({{ $g.JSON.TimeFormat }})) + {{- else -}} + return s.Decode(d, json.Decode{{ $g.JSON.Format }}) + {{- end }} {{- else }} return s.Decode(d) {{- end }} diff --git a/gen/_template/uri/decode.tmpl b/gen/_template/uri/decode.tmpl index 012aa9e8d..677a4487f 100644 --- a/gen/_template/uri/decode.tmpl +++ b/gen/_template/uri/decode.tmpl @@ -9,7 +9,11 @@ return err } - c, err := conv.{{ $t.FromString }}(val) + {{ if $t.JSON.TimeFormat -}} + c, err := time.Parse({{ $t.JSON.TimeFormat }}, val) + {{- else -}} + c, err := conv.{{ $t.FromString }}(val) + {{- end }} if err != nil { return err } diff --git a/gen/_template/uri/encode.tmpl b/gen/_template/uri/encode.tmpl index 538eec1db..f62f763c2 100644 --- a/gen/_template/uri/encode.tmpl +++ b/gen/_template/uri/encode.tmpl @@ -3,7 +3,11 @@ {{- $t := $.Type }} {{- $var := $.Var }} {{- if $t.IsPrimitive }} - return e.EncodeValue(conv.{{ $t.ToString }}({{ $var }})) + {{ if $t.JSON.TimeFormat -}} + return e.EncodeValue({{ $var }}.Format({{ $t.JSON.TimeFormat }})) + {{- else -}} + return e.EncodeValue(conv.{{ $t.ToString }}({{ $var }})) + {{- end }} {{- else if $t.IsEnum }} return e.EncodeValue(conv.{{ $t.ToString }}({{ $t.Primitive.String }}({{ $var }}))) {{- else if $t.IsArray }} diff --git a/gen/ir/json.go b/gen/ir/json.go index 4767acfd3..156160b9b 100644 --- a/gen/ir/json.go +++ b/gen/ir/json.go @@ -2,6 +2,7 @@ package ir import ( "slices" + "strconv" "strings" "github.com/ogen-go/ogen/internal/bitset" @@ -318,6 +319,15 @@ func (j JSON) IsBase64() bool { return j.t.Primitive == ByteSlice } +// TimeFormat returns time format for json encoding and decoding. +func (j JSON) TimeFormat() string { + s := j.t.Schema + if s == nil || s.XOgenTimeFormat == "" { + return "" + } + return strconv.Quote(s.XOgenTimeFormat) +} + // Sum returns specification for parsing value as sum type. func (j JSON) Sum() SumJSON { if j.t.SumSpec.Discriminator != "" { diff --git a/internal/integration/generate.go b/internal/integration/generate.go index 39bd8e222..e035851f9 100644 --- a/internal/integration/generate.go +++ b/internal/integration/generate.go @@ -32,6 +32,7 @@ package integration // //go:generate go run ../../cmd/ogen -v --clean -target test_enum_naming ../../_testdata/positive/enum_naming.yml //go:generate go run ../../cmd/ogen -v --clean -target test_naming_extensions ../../_testdata/positive/naming_extensions.json +//go:generate go run ../../cmd/ogen -v --clean -target test_time_extension ../../_testdata/positive/time_extension.yml // // Regression test. // diff --git a/internal/integration/time_extension_test.go b/internal/integration/time_extension_test.go new file mode 100644 index 000000000..1e4fd1502 --- /dev/null +++ b/internal/integration/time_extension_test.go @@ -0,0 +1,62 @@ +package integration + +import ( + "testing" + "time" + + "github.com/go-faster/jx" + "github.com/stretchr/testify/require" + + api "github.com/ogen-go/ogen/internal/integration/test_time_extension" +) + +func TestTimeExtension(t *testing.T) { + input := `{ "date": "04/03/2001", "time": "1:23AM", "dateTime": "2001-03-04T01:23:45.123456789-07:00" }` + + t.Run("Required", func(t *testing.T) { + expected := api.RequiredOK{ + Date: time.Date(2001, 3, 4, 0, 0, 0, 0, time.UTC), + Time: time.Date(0, 1, 1, 1, 23, 0, 0, time.UTC), + DateTime: time.Date(2001, 3, 4, 1, 23, 45, 123456789, time.FixedZone("", -7*60*60)), + } + + a := require.New(t) + var p api.RequiredOK + a.NoError(p.Decode(jx.DecodeStr(input))) + a.Equal(p, expected) + + out, err := p.MarshalJSON() + a.NoError(err) + a.JSONEq(input, string(out)) + }) + + t.Run("Optional", func(t *testing.T) { + expected := api.DefaultOK{ + Date: api.NewOptDate(time.Date(2001, 3, 4, 0, 0, 0, 0, time.UTC)), + Time: api.NewOptTime(time.Date(0, 1, 1, 1, 23, 0, 0, time.UTC)), + DateTime: api.NewOptDateTime(time.Date(2001, 3, 4, 1, 23, 45, 123456789, time.FixedZone("", -7*60*60))), + } + + a := require.New(t) + var p api.DefaultOK + a.NoError(p.Decode(jx.DecodeStr(input))) + a.Equal(p, expected) + + out, err := p.MarshalJSON() + a.NoError(err) + a.JSONEq(input, string(out)) + }) + + t.Run("Defaults", func(t *testing.T) { + expected := api.DefaultOK{ + Date: api.NewOptDate(time.Date(2001, 3, 4, 0, 0, 0, 0, time.UTC)), + Time: api.NewOptTime(time.Date(0, 1, 1, 1, 23, 0, 0, time.UTC)), + DateTime: api.NewOptDateTime(time.Date(2001, 3, 4, 1, 23, 45, 123456789, time.FixedZone("", -7*60*60))), + } + + a := require.New(t) + var p api.DefaultOK + a.NoError(p.Decode(jx.DecodeStr(`{}`))) + a.Equal(p, expected) + }) +} diff --git a/json/time.go b/json/time.go index 6485e2dd1..67bb6dbaa 100644 --- a/json/time.go +++ b/json/time.go @@ -11,72 +11,78 @@ const ( timeLayout = "15:04:05" ) -// DecodeDate decodes date from json. -func DecodeDate(i *jx.Decoder) (v time.Time, err error) { - s, err := i.Str() +// DecodeTimeFormat decodes date, time & date-time from json using a custom layout. +func DecodeTimeFormat(d *jx.Decoder, layout string) (v time.Time, err error) { + s, err := d.Str() if err != nil { return v, err } - return time.Parse(dateLayout, s) + return time.Parse(layout, s) +} + +// EncodeTimeFormat encodes date, time & date-time to json using a custom layout. +func EncodeTimeFormat(e *jx.Encoder, v time.Time, layout string) { + const stackThreshold = 64 + + var buf []byte + if len(layout) > stackThreshold { + buf = make([]byte, len(layout)) + } else { + // Allocate buf on stack, if we can. + buf = make([]byte, stackThreshold) + } + + buf = v.AppendFormat(buf[:0], layout) + e.ByteStr(buf) +} + +// NewTimeDecoder returns a new time decoder using a custom layout. +func NewTimeDecoder(layout string) func(i *jx.Decoder) (time.Time, error) { + return func(d *jx.Decoder) (time.Time, error) { + return DecodeTimeFormat(d, layout) + } +} + +// NewTimeEncoder returns a new time encoder using a custom layout. +func NewTimeEncoder(layout string) func(e *jx.Encoder, v time.Time) { + return func(e *jx.Encoder, v time.Time) { + EncodeTimeFormat(e, v, layout) + } +} + +// DecodeDate decodes date from json. +func DecodeDate(d *jx.Decoder) (v time.Time, err error) { + return DecodeTimeFormat(d, dateLayout) } // EncodeDate encodes date to json. -func EncodeDate(s *jx.Encoder, v time.Time) { - const ( - roundTo = 8 - length = len(dateLayout) - allocate = ((length + roundTo - 1) / roundTo) * roundTo - ) - b := make([]byte, allocate) - b = v.AppendFormat(b[:0], dateLayout) - s.ByteStr(b) +func EncodeDate(e *jx.Encoder, v time.Time) { + EncodeTimeFormat(e, v, dateLayout) } // DecodeTime decodes time from json. -func DecodeTime(i *jx.Decoder) (v time.Time, err error) { - s, err := i.Str() - if err != nil { - return v, err - } - return time.Parse(timeLayout, s) +func DecodeTime(d *jx.Decoder) (v time.Time, err error) { + return DecodeTimeFormat(d, timeLayout) } // EncodeTime encodes time to json. -func EncodeTime(s *jx.Encoder, v time.Time) { - const ( - roundTo = 8 - length = len(timeLayout) - allocate = ((length + roundTo - 1) / roundTo) * roundTo - ) - b := make([]byte, allocate) - b = v.AppendFormat(b[:0], timeLayout) - s.ByteStr(b) +func EncodeTime(e *jx.Encoder, v time.Time) { + EncodeTimeFormat(e, v, timeLayout) } // DecodeDateTime decodes date-time from json. -func DecodeDateTime(i *jx.Decoder) (v time.Time, err error) { - s, err := i.Str() - if err != nil { - return v, err - } - return time.Parse(time.RFC3339, s) +func DecodeDateTime(d *jx.Decoder) (v time.Time, err error) { + return DecodeTimeFormat(d, time.RFC3339) } // EncodeDateTime encodes date-time to json. -func EncodeDateTime(s *jx.Encoder, v time.Time) { - const ( - roundTo = 8 - length = len(time.RFC3339) - allocate = ((length + roundTo - 1) / roundTo) * roundTo - ) - b := make([]byte, allocate) - b = v.AppendFormat(b[:0], time.RFC3339) - s.ByteStr(b) +func EncodeDateTime(e *jx.Encoder, v time.Time) { + EncodeTimeFormat(e, v, time.RFC3339) } // DecodeDuration decodes duration from json. -func DecodeDuration(i *jx.Decoder) (v time.Duration, err error) { - s, err := i.Str() +func DecodeDuration(d *jx.Decoder) (v time.Duration, err error) { + s, err := d.Str() if err != nil { return v, err } @@ -84,8 +90,8 @@ func DecodeDuration(i *jx.Decoder) (v time.Duration, err error) { } // EncodeDuration encodes duration to json. -func EncodeDuration(s *jx.Encoder, v time.Duration) { +func EncodeDuration(e *jx.Encoder, v time.Duration) { var buf [32]byte w := formatDuration(&buf, v) - s.ByteStr(buf[w:]) + e.ByteStr(buf[w:]) } diff --git a/json/time_test.go b/json/time_test.go index c06a5c19b..7d097ea1a 100644 --- a/json/time_test.go +++ b/json/time_test.go @@ -9,48 +9,18 @@ import ( "github.com/stretchr/testify/require" ) -func BenchmarkEncodeDate(b *testing.B) { +func BenchmarkEncodeTimeLayout(b *testing.B) { t := time.Now() e := jx.GetEncoder() // Preallocate internal buffer. - EncodeDate(e, t) + EncodeTimeFormat(e, t, time.RFC3339) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { e.Reset() - EncodeDate(e, t) - } -} - -func BenchmarkEncodeTime(b *testing.B) { - t := time.Now() - e := jx.GetEncoder() - // Preallocate internal buffer. - EncodeTime(e, t) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - e.Reset() - EncodeTime(e, t) - } -} - -func BenchmarkEncodeDateTime(b *testing.B) { - t := time.Now() - e := jx.GetEncoder() - // Preallocate internal buffer. - EncodeDateTime(e, t) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - e.Reset() - EncodeDateTime(e, t) + EncodeTimeFormat(e, t, time.RFC3339) } } diff --git a/jsonschema/parser.go b/jsonschema/parser.go index 526ca60e2..ec1007b5e 100644 --- a/jsonschema/parser.go +++ b/jsonschema/parser.go @@ -20,6 +20,7 @@ import ( const ( xOgenName = "x-ogen-name" xOgenProperties = "x-ogen-properties" + xOgenTimeFormat = "x-ogen-time-format" xOapiExtraTags = "x-oapi-codegen-extra-tags" ) @@ -178,6 +179,11 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun s.Properties[idx].X = x } + case xOgenTimeFormat: + if err := val.Decode(&s.XOgenTimeFormat); err != nil { + return err + } + case xOapiExtraTags: if err := val.Decode(&s.ExtraTags); err != nil { return err diff --git a/jsonschema/parser_test.go b/jsonschema/parser_test.go index c13ce4c45..981921abf 100644 --- a/jsonschema/parser_test.go +++ b/jsonschema/parser_test.go @@ -381,6 +381,14 @@ func TestSchemaExtensions(t *testing.T) { {`{"type": "string", "x-ogen-name": "foo"}`, nil, true}, // Invalid type. {`{"type": "string", "x-ogen-name": {}}`, nil, true}, + { + `{"type": "string", "x-ogen-time-format": "2006-01-02T15:04:05.999999999Z07:00"}`, + &Schema{ + Type: String, + XOgenTimeFormat: "2006-01-02T15:04:05.999999999Z07:00", + }, + false, + }, } for i, tt := range tests { diff --git a/jsonschema/schema.go b/jsonschema/schema.go index 046c48ff1..379ba2396 100644 --- a/jsonschema/schema.go +++ b/jsonschema/schema.go @@ -103,6 +103,8 @@ type Schema struct { // ExtraTags is a map of extra struct field tags ExtraTags map[string]string + XOgenTimeFormat string // Time format for time.Time. + location.Pointer `json:"-" yaml:"-"` }