From 6e63ab7ab87a1642b4b48d3cfa84a0561f4cdf6e Mon Sep 17 00:00:00 2001 From: Nerzal Date: Sat, 8 Apr 2023 01:06:46 +0200 Subject: [PATCH] Support accept Header & Use RequestBody (#1541) --- operationv3.go | 186 +++++++++++++++++++++++++++++++------- operationv3_test.go | 214 ++++++++++++++++++++------------------------ 2 files changed, 250 insertions(+), 150 deletions(-) diff --git a/operationv3.go b/operationv3.go index e7575fba5..f4e80a497 100644 --- a/operationv3.go +++ b/operationv3.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/pkg/errors" "github.com/sv-tools/openapi/spec" "gopkg.in/yaml.v2" ) @@ -145,16 +146,49 @@ func (o *OperationV3) ParseTagsComment(commentLine string) { func (o *OperationV3) ParseAcceptComment(commentLine string) error { const errMessage = "could not parse accept comment" - // TODO this must be moved into another comment - // return parseMimeTypeList(commentLine, &o.RequestBody.Spec.Spec.Content, ) - // result, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") - // if err != nil { - // return errors.Wrap(err, errMessage) - // } + validTypes, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") + if err != nil { + return errors.Wrap(err, errMessage) + } - // for _, value := range result { - // o.RequestBody.Spec.Spec.Content[value] = spec.NewMediaType() - // } + if o.RequestBody == nil { + o.RequestBody = spec.NewRequestBodySpec() + } + + if o.RequestBody.Spec.Spec.Content == nil { + o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType], len(validTypes)) + } + + for _, value := range validTypes { + // skip correctly setup types like application/json + if o.RequestBody.Spec.Spec.Content[value] != nil { + continue + } + + mediaType := spec.NewMediaType() + schema := spec.NewSchemaSpec() + + switch value { + case "application/json", "multipart/form-data", "text/xml": + schema.Spec.Type = spec.NewSingleOrArray(OBJECT) + case "image/png", + "image/jpeg", + "image/gif", + "application/octet-stream", + "application/pdf", + "application/msexcel", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation": + schema.Spec.Type = spec.NewSingleOrArray(STRING) + schema.Spec.Format = "binary" + default: + schema.Spec.Type = spec.NewSingleOrArray(STRING) + } + + mediaType.Spec.Schema = schema + o.RequestBody.Spec.Spec.Content[value] = mediaType + } return nil } @@ -316,15 +350,33 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e } case "body": if objectType == PRIMITIVE { - param.Schema = PrimitiveSchemaV3(refType) - } else { - schema, err := o.parseAPIObjectSchema(commentLine, objectType, refType, astFile) + schema := PrimitiveSchemaV3(refType) + + err := o.parseParamAttributeForBody(commentLine, objectType, refType, schema.Spec) if err != nil { return err } - param.Schema = schema + o.fillRequestBody(schema, required, description, true) + + return nil + + } + + schema, err := o.parseAPIObjectSchema(commentLine, objectType, refType, astFile) + if err != nil { + return err } + + err = o.parseParamAttributeForBody(commentLine, objectType, refType, schema.Spec) + if err != nil { + return err + } + + o.fillRequestBody(schema, required, description, false) + + return nil + default: return fmt.Errorf("%s is not supported paramType", paramType) } @@ -343,7 +395,74 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e return nil } +func (o *OperationV3) fillRequestBody(schema *spec.RefOrSpec[spec.Schema], required bool, description string, primitive bool) { + if o.RequestBody == nil { + o.RequestBody = spec.NewRequestBodySpec() + o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) + + if primitive { + o.RequestBody.Spec.Spec.Content["text/plain"] = spec.NewMediaType() + } else { + o.RequestBody.Spec.Spec.Content["application/json"] = spec.NewMediaType() + } + } + + o.RequestBody.Spec.Spec.Description = description + o.RequestBody.Spec.Spec.Required = required + + for _, value := range o.RequestBody.Spec.Spec.Content { + value.Spec.Schema = schema + } +} + func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string, param *spec.Parameter) error { + if param == nil { + return fmt.Errorf("cannot parse empty parameter for comment: %s", comment) + } + + schemaType = TransToValidSchemeType(schemaType) + + for attrKey, re := range regexAttributes { + attr, err := findAttr(re, comment) + if err != nil { + continue + } + + switch attrKey { + case enumsTag: + err = setEnumParamV3(param.Schema.Spec, attr, objectType, schemaType) + case minimumTag, maximumTag: + err = setNumberParamV3(param.Schema.Spec, attrKey, schemaType, attr, comment) + case defaultTag: + err = setDefaultV3(param.Schema.Spec, schemaType, attr) + case minLengthTag, maxLengthTag: + err = setStringParamV3(param.Schema.Spec, attrKey, schemaType, attr, comment) + case formatTag: + param.Schema.Spec.Format = attr + case exampleTag: + val, err := defineType(schemaType, attr) + if err != nil { + continue // Don't set a example value if it's not valid + } + + param.Example = val + case schemaExampleTag: + err = setSchemaExampleV3(param.Schema.Spec, schemaType, attr) + case extensionsTag: + param.Schema.Spec.Extensions = setExtensionParam(attr) + case collectionFormatTag: + err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) + } + + if err != nil { + return err + } + } + + return nil +} + +func (o *OperationV3) parseParamAttributeForBody(comment, objectType, schemaType string, param *spec.Schema) error { schemaType = TransToValidSchemeType(schemaType) for attrKey, re := range regexAttributes { @@ -362,15 +481,13 @@ func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string case minLengthTag, maxLengthTag: err = setStringParamV3(param, attrKey, schemaType, attr, comment) case formatTag: - param.Schema.Spec.Format = attr + param.Format = attr case exampleTag: - err = setExampleV3(param, schemaType, attr) + err = setSchemaExampleV3(param, schemaType, attr) case schemaExampleTag: err = setSchemaExampleV3(param, schemaType, attr) case extensionsTag: - param.Schema.Spec.Extensions = setExtensionParam(attr) - case collectionFormatTag: - err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) + param.Extensions = setExtensionParam(attr) } if err != nil { @@ -390,28 +507,29 @@ func setCollectionFormatParamV3(param *spec.Parameter, name, schemaType, attr, c return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) } -func setSchemaExampleV3(param *spec.Parameter, schemaType string, value string) error { +func setSchemaExampleV3(param *spec.Schema, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a example value if it's not valid } + // skip schema - if param.Schema == nil { + if param == nil { return nil } switch v := val.(type) { case string: // replaces \r \n \t in example string values. - param.Schema.Spec.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) + param.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) default: - param.Schema.Spec.Example = val + param.Example = val } return nil } -func setExampleV3(param *spec.Parameter, schemaType string, value string) error { +func setExampleParameterV3(param *spec.Parameter, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a example value if it's not valid @@ -422,7 +540,7 @@ func setExampleV3(param *spec.Parameter, schemaType string, value string) error return nil } -func setStringParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { +func setStringParamV3(param *spec.Schema, name, schemaType, attr, commentLine string) error { if schemaType != STRING { return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } @@ -434,26 +552,26 @@ func setStringParamV3(param *spec.Parameter, name, schemaType, attr, commentLine switch name { case minLengthTag: - param.Schema.Spec.MinLength = &n + param.MinLength = &n case maxLengthTag: - param.Schema.Spec.MaxLength = &n + param.MaxLength = &n } return nil } -func setDefaultV3(param *spec.Parameter, schemaType string, value string) error { +func setDefaultV3(param *spec.Schema, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a default value if it's not valid } - param.Schema.Spec.Default = val + param.Default = val return nil } -func setEnumParamV3(param *spec.Parameter, attr, objectType, schemaType string) error { +func setEnumParamV3(param *spec.Schema, attr, objectType, schemaType string) error { for _, e := range strings.Split(attr, ",") { e = strings.TrimSpace(e) @@ -464,16 +582,16 @@ func setEnumParamV3(param *spec.Parameter, attr, objectType, schemaType string) switch objectType { case ARRAY: - param.Schema.Spec.Items.Schema.Spec.Enum = append(param.Schema.Spec.Items.Schema.Spec.Enum, value) + param.Items.Schema.Spec.Enum = append(param.Items.Schema.Spec.Enum, value) default: - param.Schema.Spec.Enum = append(param.Schema.Spec.Enum, value) + param.Enum = append(param.Enum, value) } } return nil } -func setNumberParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { +func setNumberParamV3(param *spec.Schema, name, schemaType, attr, commentLine string) error { switch schemaType { case INTEGER, NUMBER: n, err := strconv.Atoi(attr) @@ -483,9 +601,9 @@ func setNumberParamV3(param *spec.Parameter, name, schemaType, attr, commentLine switch name { case minimumTag: - param.Schema.Spec.Minimum = &n + param.Minimum = &n case maximumTag: - param.Schema.Spec.Maximum = &n + param.Maximum = &n } return nil diff --git a/operationv3_test.go b/operationv3_test.go index 5b96ab043..448fb169c 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -863,42 +863,10 @@ func TestParseParamCommentBodyArrayV3(t *testing.T) { err := o.ParseComment(comment, nil) assert.NoError(t, err) - expected := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ - Spec: &spec.Extendable[spec.Parameter]{ - Spec: &spec.Parameter{ - Name: "names", - Description: "Users List", - In: "body", - Required: true, - Schema: &spec.RefOrSpec[spec.Schema]{ - Spec: &spec.Schema{ - JsonSchema: spec.JsonSchema{ - JsonSchemaCore: spec.JsonSchemaCore{ - Type: typeArray, - }, - JsonSchemaTypeArray: spec.JsonSchemaTypeArray{ - Items: &spec.BoolOrSchema{ - Schema: &spec.RefOrSpec[spec.Schema]{ - Spec: &spec.Schema{ - JsonSchema: spec.JsonSchema{ - JsonSchemaCore: spec.JsonSchemaCore{ - Type: typeString, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - - expectedArray := []*spec.RefOrSpec[spec.Extendable[spec.Parameter]]{expected} - assert.Equal(t, o.Parameters, expectedArray) - + assert.NotNil(t, o.RequestBody) + assert.Equal(t, "Users List", o.RequestBody.Spec.Spec.Description) + assert.True(t, o.RequestBody.Spec.Spec.Required) + assert.Equal(t, typeArray, o.RequestBody.Spec.Spec.Content["application/json"].Spec.Schema.Spec.Type) } func TestParseParamCommentArrayV3(t *testing.T) { @@ -1024,16 +992,14 @@ func TestParseParamCommentByBodyTypeV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Some ID", parameterSpec.Description) - assert.Equal(t, "some_id", parameterSpec.Name) - assert.Equal(t, true, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, "#/components/schemas/model.OrderRow", parameterSpec.Schema.Ref.Ref) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Some ID", requestBodySpec.Description) + assert.Equal(t, true, requestBodySpec.Required) + assert.Equal(t, "#/components/schemas/model.OrderRow", requestBodySpec.Content["application/json"].Spec.Schema.Ref.Ref) } func TestParseParamCommentByBodyTextPlainV3(t *testing.T) { @@ -1045,16 +1011,14 @@ func TestParseParamCommentByBodyTextPlainV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Text to process", parameterSpec.Description) - assert.Equal(t, "text", parameterSpec.Name) - assert.Equal(t, true, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Text to process", requestBodySpec.Description) + assert.Equal(t, true, requestBodySpec.Required) + assert.Equal(t, typeString, requestBodySpec.Content["text/plain"].Spec.Schema.Spec.Type) } func TestParseParamCommentByBodyTypeWithDeepNestedFieldsV3(t *testing.T) { @@ -1068,19 +1032,17 @@ func TestParseParamCommentByBodyTypeWithDeepNestedFieldsV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - assert.Len(t, operation.Parameters, 1) + assert.Len(t, operation.Parameters, 0) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "test deep", parameterSpec.Description) - assert.Equal(t, "body", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "test deep", requestBodySpec.Description) + assert.True(t, requestBodySpec.Required) - assert.Equal(t, 2, len(parameterSpec.Schema.Spec.AllOf)) + assert.Equal(t, 2, len(requestBodySpec.Content["application/json"].Spec.Schema.Spec.AllOf)) assert.Equal(t, 3, len(operation.parser.openAPI.Components.Spec.Schemas)) } @@ -1093,17 +1055,15 @@ func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Some ID", parameterSpec.Description) - assert.Equal(t, "some_id", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) - assert.Equal(t, typeInteger, parameterSpec.Schema.Spec.Items.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Some ID", requestBodySpec.Description) + assert.True(t, requestBodySpec.Required) + assert.Equal(t, typeArray, requestBodySpec.Content["application/json"].Spec.Schema.Spec.Type) + assert.Equal(t, typeInteger, requestBodySpec.Content["application/json"].Spec.Schema.Spec.Items.Schema.Spec.Type) } func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFieldsV3(t *testing.T) { @@ -1116,18 +1076,15 @@ func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFieldsV3(t * err := operation.ParseComment(comment, nil) assert.NoError(t, err) - assert.Len(t, operation.Parameters, 1) + assert.Len(t, operation.Parameters, 0) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + assert.NotNil(t, operation.RequestBody) - parameterSpec := parameters[0].Spec.Spec + parameterSpec := operation.RequestBody.Spec.Spec.Content["application/json"].Spec assert.NotNil(t, parameterSpec) - assert.Equal(t, "test deep", parameterSpec.Description) - assert.Equal(t, "body", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) + assert.Equal(t, "test deep", operation.RequestBody.Spec.Spec.Description) assert.Equal(t, typeArray, parameterSpec.Schema.Spec.Type) + assert.True(t, operation.RequestBody.Spec.Spec.Required) assert.Equal(t, 2, len(parameterSpec.Schema.Spec.Items.Schema.Spec.AllOf)) } @@ -1504,23 +1461,6 @@ func TestParseParamCommentByExampleStringV3(t *testing.T) { assert.Equal(t, "True feelings", parameterSpec.Example) } -func TestParseParamCommentByExampleUnsupportedTypeV3(t *testing.T) { - t.Parallel() - var param spec.Parameter - - setExampleV3(¶m, "something", "random value") - assert.Equal(t, param.Example, nil) - - setExampleV3(¶m, STRING, "string value") - assert.Equal(t, param.Example, "string value") - - setExampleV3(¶m, INTEGER, "10") - assert.Equal(t, param.Example, 10) - - setExampleV3(¶m, NUMBER, "10") - assert.Equal(t, param.Example, float64(10)) -} - func TestParseParamCommentBySchemaExampleStringV3(t *testing.T) { t.Parallel() @@ -1530,40 +1470,38 @@ func TestParseParamCommentBySchemaExampleStringV3(t *testing.T) { err := operation.ParseComment(comment, nil) assert.NoError(t, err) - parameters := operation.Operation.Parameters - assert.NotNil(t, parameters) + requestBody := operation.RequestBody + assert.NotNil(t, requestBody) - parameterSpec := parameters[0].Spec.Spec - assert.NotNil(t, parameterSpec) - assert.Equal(t, "Some ID", parameterSpec.Description) - assert.Equal(t, "some_id", parameterSpec.Name) - assert.True(t, parameterSpec.Required) - assert.Equal(t, "body", parameterSpec.In) - assert.Equal(t, "True feelings", parameterSpec.Schema.Spec.Example) - assert.Equal(t, typeString, parameterSpec.Schema.Spec.Type) + requestBodySpec := requestBody.Spec.Spec + assert.NotNil(t, requestBodySpec) + assert.Equal(t, "Some ID", requestBodySpec.Description) + assert.True(t, requestBodySpec.Required) + assert.Equal(t, "True feelings", requestBodySpec.Content["text/plain"].Spec.Schema.Spec.Example) + assert.Equal(t, typeString, requestBodySpec.Content["text/plain"].Spec.Schema.Spec.Type) } func TestParseParamCommentBySchemaExampleUnsupportedTypeV3(t *testing.T) { t.Parallel() var param spec.Parameter - setSchemaExampleV3(¶m, "something", "random value") + setSchemaExampleV3(nil, "something", "random value") assert.Nil(t, param.Schema) - setSchemaExampleV3(¶m, STRING, "string value") + setSchemaExampleV3(nil, STRING, "string value") assert.Nil(t, param.Schema) param.Schema = spec.NewSchemaSpec() - setSchemaExampleV3(¶m, STRING, "string value") + setSchemaExampleV3(param.Schema.Spec, STRING, "string value") assert.Equal(t, "string value", param.Schema.Spec.Example) - setSchemaExampleV3(¶m, INTEGER, "10") + setSchemaExampleV3(param.Schema.Spec, INTEGER, "10") assert.Equal(t, 10, param.Schema.Spec.Example) - setSchemaExampleV3(¶m, NUMBER, "10") + setSchemaExampleV3(param.Schema.Spec, NUMBER, "10") assert.Equal(t, float64(10), param.Schema.Spec.Example) - setSchemaExampleV3(¶m, STRING, "string \\r\\nvalue") + setSchemaExampleV3(param.Schema.Spec, STRING, "string \\r\\nvalue") assert.Equal(t, "string \r\nvalue", param.Schema.Spec.Example) } @@ -1655,7 +1593,7 @@ func TestParseAndExtractionParamAttributeV3(t *testing.T) { assert.Error(t, err) err = op.parseParamAttribute(" default(0)", "", ARRAY, nil) - assert.NoError(t, err) + assert.Error(t, err) }) } @@ -1919,3 +1857,47 @@ func TestParseCodeSamplesV3(t *testing.T) { assert.Error(t, err, "no error should be thrown") }) } + +func TestParseAcceptCommentV3(t *testing.T) { + t.Parallel() + + comment := `/@Accept json,xml,plain,html,mpfd,x-www-form-urlencoded,json-api,json-stream,octet-stream,png,jpeg,gif,application/xhtml+xml,application/health+json` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + resultMapKeys := []string{ + "application/json", + "text/xml", + "text/plain", + "text/html", + "multipart/form-data", + "application/x-www-form-urlencoded", + "application/vnd.api+json", + "application/x-json-stream", + "application/octet-stream", + "image/png", + "image/jpeg", + "image/gif", + "application/xhtml+xml", + "application/health+json"} + + content := operation.RequestBody.Spec.Spec.Content + for _, key := range resultMapKeys { + assert.NotNil(t, content[key]) + } + + assert.Equal(t, typeObject, content["application/json"].Spec.Schema.Spec.Type) + assert.Equal(t, typeObject, content["text/xml"].Spec.Schema.Spec.Type) + assert.Equal(t, typeString, content["image/png"].Spec.Schema.Spec.Type) + assert.Equal(t, "binary", content["image/png"].Spec.Schema.Spec.Format) +} + +func TestParseAcceptCommentErrV3(t *testing.T) { + t.Parallel() + + comment := `/@Accept unknown` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) +}