From 929105795ede7dc2be9bdfd615cec662c9110c93 Mon Sep 17 00:00:00 2001 From: Jefftree Date: Wed, 2 Aug 2023 17:16:11 +0000 Subject: [PATCH 1/3] Implement experimental marshaler for OpenAPI V3 --- pkg/handler3/handler_test.go | 2 +- pkg/internal/flags.go | 1 + pkg/spec3/benchmark_serialization_test.go | 148 ++++++++++++++++++++++ pkg/spec3/encoding.go | 21 +++ pkg/spec3/example.go | 14 ++ pkg/spec3/external_documentation.go | 13 ++ pkg/spec3/fuzz.go | 15 +++ pkg/spec3/header.go | 31 +++++ pkg/spec3/media_type.go | 20 +++ pkg/spec3/operation.go | 27 ++++ pkg/spec3/parameter.go | 31 +++++ pkg/spec3/path.go | 47 ++++++- pkg/spec3/request_body.go | 21 +++ pkg/spec3/response.go | 52 ++++++++ pkg/spec3/security_scheme.go | 17 +++ pkg/spec3/server.go | 26 ++++ pkg/spec3/spec.go | 9 ++ 17 files changed, 491 insertions(+), 4 deletions(-) diff --git a/pkg/handler3/handler_test.go b/pkg/handler3/handler_test.go index e7db4703a..647c053df 100644 --- a/pkg/handler3/handler_test.go +++ b/pkg/handler3/handler_test.go @@ -39,7 +39,7 @@ var returnedOpenAPI = []byte(`{ "title": "Kubernetes", "version": "v1.23.0" }, - "paths": {}}`) + "paths": {"x-kubernetes-extension": "val"}}`) func TestRegisterOpenAPIVersionedService(t *testing.T) { var s *spec3.OpenAPI diff --git a/pkg/internal/flags.go b/pkg/internal/flags.go index bef603782..da5485f6a 100644 --- a/pkg/internal/flags.go +++ b/pkg/internal/flags.go @@ -22,3 +22,4 @@ var UseOptimizedJSONUnmarshalingV3 bool = true // Used by tests to selectively disable experimental JSON marshaler var UseOptimizedJSONMarshaling bool = true +var UseOptimizedJSONMarshalingV3 bool = true diff --git a/pkg/spec3/benchmark_serialization_test.go b/pkg/spec3/benchmark_serialization_test.go index 919abf86d..84495a320 100644 --- a/pkg/spec3/benchmark_serialization_test.go +++ b/pkg/spec3/benchmark_serialization_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" fuzz "github.com/google/gofuzz" "k8s.io/kube-openapi/pkg/internal" + jsontesting "k8s.io/kube-openapi/pkg/util/jsontesting" "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -71,6 +72,95 @@ func TestOpenAPIV3Deserialize(t *testing.T) { } } +func TestOpenAPIV3Serialize(t *testing.T) { + swagFile, err := os.Open("./testdata/appsv1spec.json") + if err != nil { + t.Fatal(err) + } + defer swagFile.Close() + originalJSON, err := io.ReadAll(swagFile) + if err != nil { + t.Fatal(err) + } + var openapi *OpenAPI + if err := json.Unmarshal(originalJSON, &openapi); err != nil { + t.Fatal(err) + } + + internal.UseOptimizedJSONUnmarshalingV3 = false + want, err := json.Marshal(openapi) + if err != nil { + t.Fatal(err) + } + internal.UseOptimizedJSONUnmarshalingV3 = true + got, err := openapi.MarshalJSON() + if err != nil { + t.Fatal(err) + } + if err := jsontesting.JsonCompare(want, got); err != nil { + t.Errorf("marshal doesn't match: %v", err) + } +} + +func TestOpenAPIV3SerializeFuzzed(t *testing.T) { + var fuzzer *fuzz.Fuzzer + fuzzer = fuzz.NewWithSeed(1646791953) + fuzzer.MaxDepth(13).NilChance(0.075).NumElements(1, 2) + fuzzer.Funcs(OpenAPIV3FuzzFuncs...) + + for i := 0; i < 100; i++ { + openapi := &OpenAPI{} + fuzzer.Fuzz(openapi) + + internal.UseOptimizedJSONUnmarshalingV3 = false + want, err := json.Marshal(openapi) + if err != nil { + t.Fatal(err) + } + internal.UseOptimizedJSONUnmarshalingV3 = true + got, err := openapi.MarshalJSON() + if err != nil { + t.Fatal(err) + } + if err := jsontesting.JsonCompare(want, got); err != nil { + t.Errorf("fuzzed marshal doesn't match: %v", err) + } + } +} + +func TestOpenAPIV3SerializeStable(t *testing.T) { + swagFile, err := os.Open("./testdata/appsv1spec.json") + if err != nil { + t.Fatal(err) + } + defer swagFile.Close() + originalJSON, err := io.ReadAll(swagFile) + if err != nil { + t.Fatal(err) + } + var openapi *OpenAPI + if err := json.Unmarshal(originalJSON, &openapi); err != nil { + t.Fatal(err) + } + + internal.UseOptimizedJSONUnmarshalingV3 = true + for i := 0; i < 5; i++ { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + want, err := openapi.MarshalJSON() + if err != nil { + t.Fatal(err) + } + got, err := openapi.MarshalJSON() + if err != nil { + t.Fatal(err) + } + if err := jsontesting.JsonCompare(want, got); err != nil { + t.Errorf("marshal doesn't match: %v", err) + } + }) + } +} + func BenchmarkOpenAPIV3Deserialize(b *testing.B) { benchcases := []struct { file string @@ -142,3 +232,61 @@ func BenchmarkOpenAPIV3Deserialize(b *testing.B) { }) } } + +func BenchmarkOpenAPIV3Serialize(b *testing.B) { + benchcases := []struct { + file string + }{ + { + file: "appsv1spec.json", + }, + { + file: "authorizationv1spec.json", + }, + } + for _, bc := range benchcases { + swagFile, err := os.Open("./testdata/" + bc.file) + if err != nil { + b.Fatal(err) + } + defer swagFile.Close() + originalJSON, err := io.ReadAll(swagFile) + if err != nil { + b.Fatal(err) + } + var openapi *OpenAPI + if err := json.Unmarshal(originalJSON, &openapi); err != nil { + b.Fatal(err) + } + b.ResetTimer() + b.Run(fmt.Sprintf("%s jsonv1", bc.file), func(b2 *testing.B) { + b2.ReportAllocs() + internal.UseOptimizedJSONMarshalingV3 = false + for i := 0; i < b2.N; i++ { + if _, err := json.Marshal(openapi); err != nil { + b2.Fatal(err) + } + } + }) + + b.Run(fmt.Sprintf("%s jsonv2 via jsonv1 full spec", bc.file), func(b2 *testing.B) { + b2.ReportAllocs() + internal.UseOptimizedJSONMarshalingV3 = true + for i := 0; i < b2.N; i++ { + if _, err := json.Marshal(openapi); err != nil { + b2.Fatal(err) + } + } + }) + + b.Run("jsonv2", func(b2 *testing.B) { + b2.ReportAllocs() + internal.UseOptimizedJSONMarshalingV3 = true + for i := 0; i < b2.N; i++ { + if _, err := openapi.MarshalJSON(); err != nil { + b2.Fatal(err) + } + } + }) + } +} diff --git a/pkg/spec3/encoding.go b/pkg/spec3/encoding.go index 699291f1d..1f62c6e77 100644 --- a/pkg/spec3/encoding.go +++ b/pkg/spec3/encoding.go @@ -32,6 +32,9 @@ type Encoding struct { // MarshalJSON is a custom marshal function that knows how to encode Encoding as JSON func (e *Encoding) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(e) + } b1, err := json.Marshal(e.EncodingProps) if err != nil { return nil, err @@ -43,6 +46,16 @@ func (e *Encoding) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2), nil } +func (e *Encoding) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + EncodingProps encodingPropsOmitZero `json:",inline"` + spec.Extensions + } + x.Extensions = internal.SanitizeExtensions(e.Extensions) + x.EncodingProps = encodingPropsOmitZero(e.EncodingProps) + return opts.MarshalNext(enc, x) +} + func (e *Encoding) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, e) @@ -82,3 +95,11 @@ type EncodingProps struct { // AllowReserved determines whether the parameter value SHOULD allow reserved characters, as defined by RFC3986 AllowReserved bool `json:"allowReserved,omitempty"` } + +type encodingPropsOmitZero struct { + ContentType string `json:"contentType,omitempty"` + Headers map[string]*Header `json:"headers,omitempty"` + Style string `json:"style,omitempty"` + Explode bool `json:"explode,omitzero"` + AllowReserved bool `json:"allowReserved,omitzero"` +} diff --git a/pkg/spec3/example.go b/pkg/spec3/example.go index 03b872717..8834a92e6 100644 --- a/pkg/spec3/example.go +++ b/pkg/spec3/example.go @@ -36,6 +36,9 @@ type Example struct { // MarshalJSON is a custom marshal function that knows how to encode RequestBody as JSON func (e *Example) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(e) + } b1, err := json.Marshal(e.Refable) if err != nil { return nil, err @@ -50,6 +53,17 @@ func (e *Example) MarshalJSON() ([]byte, error) { } return swag.ConcatJSON(b1, b2, b3), nil } +func (e *Example) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + ExampleProps `json:",inline"` + spec.Extensions + } + x.Ref = e.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(e.Extensions) + x.ExampleProps = e.ExampleProps + return opts.MarshalNext(enc, x) +} func (e *Example) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { diff --git a/pkg/spec3/external_documentation.go b/pkg/spec3/external_documentation.go index e79956721..f0515496e 100644 --- a/pkg/spec3/external_documentation.go +++ b/pkg/spec3/external_documentation.go @@ -39,6 +39,9 @@ type ExternalDocumentationProps struct { // MarshalJSON is a custom marshal function that knows how to encode Responses as JSON func (e *ExternalDocumentation) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(e) + } b1, err := json.Marshal(e.ExternalDocumentationProps) if err != nil { return nil, err @@ -50,6 +53,16 @@ func (e *ExternalDocumentation) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2), nil } +func (e *ExternalDocumentation) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + ExternalDocumentationProps `json:",inline"` + spec.Extensions + } + x.Extensions = internal.SanitizeExtensions(e.Extensions) + x.ExternalDocumentationProps = e.ExternalDocumentationProps + return opts.MarshalNext(enc, x) +} + func (e *ExternalDocumentation) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, e) diff --git a/pkg/spec3/fuzz.go b/pkg/spec3/fuzz.go index bc19dd48e..db9eab79c 100644 --- a/pkg/spec3/fuzz.go +++ b/pkg/spec3/fuzz.go @@ -169,6 +169,21 @@ var OpenAPIV3FuzzFuncs []interface{} = []interface{}{ c.Fuzz(&v.ResponseProps) c.Fuzz(&v.VendorExtensible) }, + func(v *Operation, c fuzz.Continue) { + c.FuzzNoCustom(v) + // Do not fuzz null values into the array. + for i, val := range v.SecurityRequirement { + if val == nil { + v.SecurityRequirement[i] = make(map[string][]string) + } + + for k, v := range val { + if v == nil { + val[k] = make([]string, 0) + } + } + } + }, func(v *spec.Extensions, c fuzz.Continue) { numChildren := c.Intn(5) for i := 0; i < numChildren; i++ { diff --git a/pkg/spec3/header.go b/pkg/spec3/header.go index ee5a30f79..9ea30628c 100644 --- a/pkg/spec3/header.go +++ b/pkg/spec3/header.go @@ -36,6 +36,9 @@ type Header struct { // MarshalJSON is a custom marshal function that knows how to encode Header as JSON func (h *Header) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(h) + } b1, err := json.Marshal(h.Refable) if err != nil { return nil, err @@ -51,6 +54,18 @@ func (h *Header) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2, b3), nil } +func (h *Header) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + HeaderProps headerPropsOmitZero `json:",inline"` + spec.Extensions + } + x.Ref = h.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(h.Extensions) + x.HeaderProps = headerPropsOmitZero(h.HeaderProps) + return opts.MarshalNext(enc, x) +} + func (h *Header) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, h) @@ -109,3 +124,19 @@ type HeaderProps struct { // Examples of the header Examples map[string]*Example `json:"examples,omitempty"` } + +// Marshaling structure only, always edit along with corresponding +// struct (or compilation will fail). +type headerPropsOmitZero struct { + Description string `json:"description,omitempty"` + Required bool `json:"required,omitzero"` + Deprecated bool `json:"deprecated,omitzero"` + AllowEmptyValue bool `json:"allowEmptyValue,omitzero"` + Style string `json:"style,omitempty"` + Explode bool `json:"explode,omitzero"` + AllowReserved bool `json:"allowReserved,omitzero"` + Schema *spec.Schema `json:"schema,omitzero"` + Content map[string]*MediaType `json:"content,omitempty"` + Example interface{} `json:"example,omitempty"` + Examples map[string]*Example `json:"examples,omitempty"` +} diff --git a/pkg/spec3/media_type.go b/pkg/spec3/media_type.go index d390e69bc..47eef1edb 100644 --- a/pkg/spec3/media_type.go +++ b/pkg/spec3/media_type.go @@ -35,6 +35,9 @@ type MediaType struct { // MarshalJSON is a custom marshal function that knows how to encode MediaType as JSON func (m *MediaType) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(m) + } b1, err := json.Marshal(m.MediaTypeProps) if err != nil { return nil, err @@ -46,6 +49,16 @@ func (m *MediaType) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2), nil } +func (e *MediaType) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + MediaTypeProps mediaTypePropsOmitZero `json:",inline"` + spec.Extensions + } + x.Extensions = internal.SanitizeExtensions(e.Extensions) + x.MediaTypeProps = mediaTypePropsOmitZero(e.MediaTypeProps) + return opts.MarshalNext(enc, x) +} + func (m *MediaType) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, m) @@ -84,3 +97,10 @@ type MediaTypeProps struct { // A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded Encoding map[string]*Encoding `json:"encoding,omitempty"` } + +type mediaTypePropsOmitZero struct { + Schema *spec.Schema `json:"schema,omitzero"` + Example interface{} `json:"example,omitempty"` + Examples map[string]*Example `json:"examples,omitempty"` + Encoding map[string]*Encoding `json:"encoding,omitempty"` +} diff --git a/pkg/spec3/operation.go b/pkg/spec3/operation.go index 28230610b..f1e102547 100644 --- a/pkg/spec3/operation.go +++ b/pkg/spec3/operation.go @@ -35,6 +35,9 @@ type Operation struct { // MarshalJSON is a custom marshal function that knows how to encode Operation as JSON func (o *Operation) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(o) + } b1, err := json.Marshal(o.OperationProps) if err != nil { return nil, err @@ -46,6 +49,16 @@ func (o *Operation) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2), nil } +func (o *Operation) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + spec.Extensions + OperationProps operationPropsOmitZero `json:",inline"` + } + x.Extensions = internal.SanitizeExtensions(o.Extensions) + x.OperationProps = operationPropsOmitZero(o.OperationProps) + return opts.MarshalNext(enc, x) +} + // UnmarshalJSON hydrates this items instance with the data from JSON func (o *Operation) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { @@ -95,3 +108,17 @@ type OperationProps struct { // Servers contains an alternative server array to service this operation Servers []*Server `json:"servers,omitempty"` } + +type operationPropsOmitZero struct { + Tags []string `json:"tags,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + ExternalDocs *ExternalDocumentation `json:"externalDocs,omitzero"` + OperationId string `json:"operationId,omitempty"` + Parameters []*Parameter `json:"parameters,omitempty"` + RequestBody *RequestBody `json:"requestBody,omitzero"` + Responses *Responses `json:"responses,omitzero"` + Deprecated bool `json:"deprecated,omitzero"` + SecurityRequirement []map[string][]string `json:"security,omitempty"` + Servers []*Server `json:"servers,omitempty"` +} diff --git a/pkg/spec3/parameter.go b/pkg/spec3/parameter.go index 613da71a6..ada7edb63 100644 --- a/pkg/spec3/parameter.go +++ b/pkg/spec3/parameter.go @@ -36,6 +36,9 @@ type Parameter struct { // MarshalJSON is a custom marshal function that knows how to encode Parameter as JSON func (p *Parameter) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(p) + } b1, err := json.Marshal(p.Refable) if err != nil { return nil, err @@ -51,6 +54,18 @@ func (p *Parameter) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2, b3), nil } +func (p *Parameter) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + ParameterProps parameterPropsOmitZero `json:",inline"` + spec.Extensions + } + x.Ref = p.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(p.Extensions) + x.ParameterProps = parameterPropsOmitZero(p.ParameterProps) + return opts.MarshalNext(enc, x) +} + func (p *Parameter) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, p) @@ -114,3 +129,19 @@ type ParameterProps struct { // Examples of the parameter's potential value. Each example SHOULD contain a value in the correct format as specified in the parameter encoding Examples map[string]*Example `json:"examples,omitempty"` } + +type parameterPropsOmitZero struct { + Name string `json:"name,omitempty"` + In string `json:"in,omitempty"` + Description string `json:"description,omitempty"` + Required bool `json:"required,omitzero"` + Deprecated bool `json:"deprecated,omitzero"` + AllowEmptyValue bool `json:"allowEmptyValue,omitzero"` + Style string `json:"style,omitempty"` + Explode bool `json:"explode,omitzero"` + AllowReserved bool `json:"allowReserved,omitzero"` + Schema *spec.Schema `json:"schema,omitzero"` + Content map[string]*MediaType `json:"content,omitempty"` + Example interface{} `json:"example,omitempty"` + Examples map[string]*Example `json:"examples,omitempty"` +} diff --git a/pkg/spec3/path.go b/pkg/spec3/path.go index 40d9061ac..16fbbb4dd 100644 --- a/pkg/spec3/path.go +++ b/pkg/spec3/path.go @@ -35,15 +35,41 @@ type Paths struct { // MarshalJSON is a custom marshal function that knows how to encode Paths as JSON func (p *Paths) MarshalJSON() ([]byte, error) { - b1, err := json.Marshal(p.Paths) + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(p) + } + b1, err := json.Marshal(p.VendorExtensible) if err != nil { return nil, err } - b2, err := json.Marshal(p.VendorExtensible) + + pths := make(map[string]*Path) + for k, v := range p.Paths { + if strings.HasPrefix(k, "/") { + pths[k] = v + } + } + b2, err := json.Marshal(pths) if err != nil { return nil, err } - return swag.ConcatJSON(b1, b2), nil + concated := swag.ConcatJSON(b1, b2) + return concated, nil +} + +func (p *Paths) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + m := make(map[string]any, len(p.Extensions)+len(p.Paths)) + for k, v := range p.Extensions { + if internal.IsExtensionKey(k) { + m[k] = v + } + } + for k, v := range p.Paths { + if strings.HasPrefix(k, "/") { + m[k] = v + } + } + return opts.MarshalNext(enc, m) } // UnmarshalJSON hydrates this items instance with the data from JSON @@ -144,6 +170,9 @@ type Path struct { // MarshalJSON is a custom marshal function that knows how to encode Path as JSON func (p *Path) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(p) + } b1, err := json.Marshal(p.Refable) if err != nil { return nil, err @@ -159,6 +188,18 @@ func (p *Path) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2, b3), nil } +func (p *Path) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + spec.Extensions + PathProps + } + x.Ref = p.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(p.Extensions) + x.PathProps = p.PathProps + return opts.MarshalNext(enc, x) +} + func (p *Path) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, p) diff --git a/pkg/spec3/request_body.go b/pkg/spec3/request_body.go index 33267ce67..6f8607e40 100644 --- a/pkg/spec3/request_body.go +++ b/pkg/spec3/request_body.go @@ -36,6 +36,9 @@ type RequestBody struct { // MarshalJSON is a custom marshal function that knows how to encode RequestBody as JSON func (r *RequestBody) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(r) + } b1, err := json.Marshal(r.Refable) if err != nil { return nil, err @@ -51,6 +54,18 @@ func (r *RequestBody) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2, b3), nil } +func (r *RequestBody) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + RequestBodyProps requestBodyPropsOmitZero `json:",inline"` + spec.Extensions + } + x.Ref = r.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(r.Extensions) + x.RequestBodyProps = requestBodyPropsOmitZero(r.RequestBodyProps) + return opts.MarshalNext(enc, x) +} + func (r *RequestBody) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, r) @@ -77,6 +92,12 @@ type RequestBodyProps struct { Required bool `json:"required,omitempty"` } +type requestBodyPropsOmitZero struct { + Description string `json:"description,omitempty"` + Content map[string]*MediaType `json:"content,omitempty"` + Required bool `json:"required,omitzero"` +} + func (r *RequestBody) UnmarshalNextJSON(opts jsonv2.UnmarshalOptions, dec *jsonv2.Decoder) error { var x struct { spec.Extensions diff --git a/pkg/spec3/response.go b/pkg/spec3/response.go index 95b388e6c..73e241fdc 100644 --- a/pkg/spec3/response.go +++ b/pkg/spec3/response.go @@ -37,6 +37,9 @@ type Responses struct { // MarshalJSON is a custom marshal function that knows how to encode Responses as JSON func (r *Responses) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(r) + } b1, err := json.Marshal(r.ResponsesProps) if err != nil { return nil, err @@ -48,6 +51,25 @@ func (r *Responses) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2), nil } +func (r Responses) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + type ArbitraryKeys map[string]interface{} + var x struct { + ArbitraryKeys + Default *Response `json:"default,omitzero"` + } + x.ArbitraryKeys = make(map[string]any, len(r.Extensions)+len(r.StatusCodeResponses)) + for k, v := range r.Extensions { + if internal.IsExtensionKey(k) { + x.ArbitraryKeys[k] = v + } + } + for k, v := range r.StatusCodeResponses { + x.ArbitraryKeys[strconv.Itoa(k)] = v + } + x.Default = r.Default + return opts.MarshalNext(enc, x) +} + func (r *Responses) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, r) @@ -179,6 +201,9 @@ type Response struct { // MarshalJSON is a custom marshal function that knows how to encode Response as JSON func (r *Response) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(r) + } b1, err := json.Marshal(r.Refable) if err != nil { return nil, err @@ -194,6 +219,18 @@ func (r *Response) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2, b3), nil } +func (r Response) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + spec.Extensions + ResponseProps `json:",inline"` + } + x.Ref = r.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(r.Extensions) + x.ResponseProps = r.ResponseProps + return opts.MarshalNext(enc, x) +} + func (r *Response) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, r) @@ -247,6 +284,9 @@ type Link struct { // MarshalJSON is a custom marshal function that knows how to encode Link as JSON func (r *Link) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(r) + } b1, err := json.Marshal(r.Refable) if err != nil { return nil, err @@ -262,6 +302,18 @@ func (r *Link) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2, b3), nil } +func (r *Link) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + spec.Extensions + LinkProps `json:",inline"` + } + x.Ref = r.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(r.Extensions) + x.LinkProps = r.LinkProps + return opts.MarshalNext(enc, x) +} + func (r *Link) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, r) diff --git a/pkg/spec3/security_scheme.go b/pkg/spec3/security_scheme.go index edf7e6de3..dd1e98ed8 100644 --- a/pkg/spec3/security_scheme.go +++ b/pkg/spec3/security_scheme.go @@ -20,6 +20,8 @@ import ( "encoding/json" "github.com/go-openapi/swag" + "k8s.io/kube-openapi/pkg/internal" + jsonv2 "k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json" "k8s.io/kube-openapi/pkg/validation/spec" ) @@ -32,6 +34,9 @@ type SecurityScheme struct { // MarshalJSON is a custom marshal function that knows how to encode SecurityScheme as JSON func (s *SecurityScheme) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(s) + } b1, err := json.Marshal(s.SecuritySchemeProps) if err != nil { return nil, err @@ -47,6 +52,18 @@ func (s *SecurityScheme) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2, b3), nil } +func (s *SecurityScheme) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Ref string `json:"$ref,omitempty"` + SecuritySchemeProps `json:",inline"` + spec.Extensions + } + x.Ref = s.Refable.Ref.String() + x.Extensions = internal.SanitizeExtensions(s.Extensions) + x.SecuritySchemeProps = s.SecuritySchemeProps + return opts.MarshalNext(enc, x) +} + // UnmarshalJSON hydrates this items instance with the data from JSON func (s *SecurityScheme) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &s.SecuritySchemeProps); err != nil { diff --git a/pkg/spec3/server.go b/pkg/spec3/server.go index d5df0a781..654a42c06 100644 --- a/pkg/spec3/server.go +++ b/pkg/spec3/server.go @@ -41,6 +41,9 @@ type ServerProps struct { // MarshalJSON is a custom marshal function that knows how to encode Responses as JSON func (s *Server) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(s) + } b1, err := json.Marshal(s.ServerProps) if err != nil { return nil, err @@ -52,6 +55,16 @@ func (s *Server) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2), nil } +func (s *Server) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + ServerProps `json:",inline"` + spec.Extensions + } + x.Extensions = internal.SanitizeExtensions(s.Extensions) + x.ServerProps = s.ServerProps + return opts.MarshalNext(enc, x) +} + func (s *Server) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, s) @@ -96,6 +109,9 @@ type ServerVariableProps struct { // MarshalJSON is a custom marshal function that knows how to encode Responses as JSON func (s *ServerVariable) MarshalJSON() ([]byte, error) { + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(s) + } b1, err := json.Marshal(s.ServerVariableProps) if err != nil { return nil, err @@ -107,6 +123,16 @@ func (s *ServerVariable) MarshalJSON() ([]byte, error) { return swag.ConcatJSON(b1, b2), nil } +func (s *ServerVariable) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + ServerVariableProps `json:",inline"` + spec.Extensions + } + x.Extensions = internal.SanitizeExtensions(s.Extensions) + x.ServerVariableProps = s.ServerVariableProps + return opts.MarshalNext(enc, x) +} + func (s *ServerVariable) UnmarshalJSON(data []byte) error { if internal.UseOptimizedJSONUnmarshalingV3 { return jsonv2.Unmarshal(data, s) diff --git a/pkg/spec3/spec.go b/pkg/spec3/spec.go index bed096fb7..299d608a8 100644 --- a/pkg/spec3/spec.go +++ b/pkg/spec3/spec.go @@ -48,3 +48,12 @@ func (o *OpenAPI) UnmarshalJSON(data []byte) error { } return json.Unmarshal(data, &p) } + +func (o *OpenAPI) MarshalJSON() ([]byte, error) { + type OpenAPIWithNoFunctions OpenAPI + p := (*OpenAPIWithNoFunctions)(o) + if internal.UseOptimizedJSONMarshalingV3 { + return internal.DeterministicMarshal(&p) + } + return json.Marshal(&p) +} From 79e04fa1d96f33738330f2c686ba964b5cad2a4f Mon Sep 17 00:00:00 2001 From: Jefftree Date: Fri, 11 Aug 2023 18:10:48 +0000 Subject: [PATCH 2/3] fix paths --- pkg/handler3/handler_test.go | 2 +- pkg/spec3/spec.go | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/handler3/handler_test.go b/pkg/handler3/handler_test.go index 647c053df..e7db4703a 100644 --- a/pkg/handler3/handler_test.go +++ b/pkg/handler3/handler_test.go @@ -39,7 +39,7 @@ var returnedOpenAPI = []byte(`{ "title": "Kubernetes", "version": "v1.23.0" }, - "paths": {"x-kubernetes-extension": "val"}}`) + "paths": {}}`) func TestRegisterOpenAPIVersionedService(t *testing.T) { var s *spec3.OpenAPI diff --git a/pkg/spec3/spec.go b/pkg/spec3/spec.go index 299d608a8..8ab87b395 100644 --- a/pkg/spec3/spec.go +++ b/pkg/spec3/spec.go @@ -50,10 +50,28 @@ func (o *OpenAPI) UnmarshalJSON(data []byte) error { } func (o *OpenAPI) MarshalJSON() ([]byte, error) { - type OpenAPIWithNoFunctions OpenAPI - p := (*OpenAPIWithNoFunctions)(o) if internal.UseOptimizedJSONMarshalingV3 { - return internal.DeterministicMarshal(&p) + return internal.DeterministicMarshal(o) } + type OpenAPIWithNoFunctions OpenAPI + p := (*OpenAPIWithNoFunctions)(o) return json.Marshal(&p) } + +func (o *OpenAPI) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { + var x struct { + Version string `json:"openapi"` + Info *spec.Info `json:"info"` + Paths *Paths `json:"paths,omitzero"` + Servers []*Server `json:"servers,omitempty"` + Components *Components `json:"components,omitzero"` + ExternalDocs *ExternalDocumentation `json:"externalDocs,omitzero"` + } + x.Version = o.Version + x.Info = o.Info + x.Paths = o.Paths + x.Servers = o.Servers + x.Components = o.Components + x.ExternalDocs = o.ExternalDocs + return opts.MarshalNext(enc, x) +} From a7e6f26a6b9ad39f70cb9f290da15d018fe6c0d9 Mon Sep 17 00:00:00 2001 From: Jefftree Date: Fri, 18 Aug 2023 15:33:34 +0000 Subject: [PATCH 3/3] simplify openapi marshal --- pkg/spec3/spec.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/spec3/spec.go b/pkg/spec3/spec.go index 8ab87b395..43613519f 100644 --- a/pkg/spec3/spec.go +++ b/pkg/spec3/spec.go @@ -59,7 +59,7 @@ func (o *OpenAPI) MarshalJSON() ([]byte, error) { } func (o *OpenAPI) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encoder) error { - var x struct { + type OpenAPIOmitZero struct { Version string `json:"openapi"` Info *spec.Info `json:"info"` Paths *Paths `json:"paths,omitzero"` @@ -67,11 +67,6 @@ func (o *OpenAPI) MarshalNextJSON(opts jsonv2.MarshalOptions, enc *jsonv2.Encode Components *Components `json:"components,omitzero"` ExternalDocs *ExternalDocumentation `json:"externalDocs,omitzero"` } - x.Version = o.Version - x.Info = o.Info - x.Paths = o.Paths - x.Servers = o.Servers - x.Components = o.Components - x.ExternalDocs = o.ExternalDocs + x := (*OpenAPIOmitZero)(o) return opts.MarshalNext(enc, x) }