diff --git a/protocol/goai/goai_config.go b/protocol/goai/goai_config.go index ac55edc7f..106b26328 100644 --- a/protocol/goai/goai_config.go +++ b/protocol/goai/goai_config.go @@ -8,8 +8,10 @@ package goai // Config provides extra configuration feature for OpenApiV3 implements. type Config struct { - ReadContentTypes []string // ReadContentTypes specifies the default MIME types for consuming if MIME types are not configured. - WriteContentTypes []string // WriteContentTypes specifies the default MIME types for producing if MIME types are not configured. + CommonResponse interface{} // Common response structure for all paths. + CommonResponseDataField string // Common response field name to be replaced with certain business response structure. + ReadContentTypes []string // ReadContentTypes specifies the default MIME types for consuming if MIME types are not configured. + WriteContentTypes []string // WriteContentTypes specifies the default MIME types for producing if MIME types are not configured. } // fillWithDefaultValue fills configuration object of `oai` with default values if these are not configured. diff --git a/protocol/goai/goai_path.go b/protocol/goai/goai_path.go index 45b005688..9553b8722 100644 --- a/protocol/goai/goai_path.go +++ b/protocol/goai/goai_path.go @@ -87,8 +87,8 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { path = Path{} inputMetaMap = gmeta.Data(inputObject.Interface()) outputMetaMap = gmeta.Data(outputObject.Interface()) - isInputStructEmpty = oai.structHasNoFields(inputObject.Interface()) - isOutputStructEmpty = oai.structHasNoFields(outputObject.Interface()) + isInputStructEmpty = oai.doesStructHasNoFields(inputObject.Interface()) + isOutputStructEmpty = oai.doesStructHasNoFields(outputObject.Interface()) inputStructTypeName = golangTypeToSchemaName(inputObject.Type()) outputStructTypeName = golangTypeToSchemaName(outputObject.Type()) operation = Operation{ @@ -214,10 +214,12 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { if isOutputStructEmpty { response.Content[v] = MediaType{} } else { + schemaRef, err := oai.getResponseSchemaRef(outputStructTypeName) + if err != nil { + return err + } response.Content[v] = MediaType{ - Schema: &SchemaRef{ - Ref: outputStructTypeName, - }, + Schema: schemaRef, } } } @@ -264,10 +266,58 @@ func (oai *OpenApiV3) addPath(in addPathInput) error { return nil } -func (oai *OpenApiV3) structHasNoFields(s interface{}) bool { +func (oai *OpenApiV3) doesStructHasNoFields(s interface{}) bool { structFields, _ := structs.Fields(structs.FieldsInput{ Pointer: s, RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, }) return len(structFields) == 0 } + +func (oai *OpenApiV3) getResponseSchemaRef(responseStructName string) (*SchemaRef, error) { + if oai.Config.CommonResponse == nil { + return &SchemaRef{ + Ref: responseStructName, + }, nil + } + + structFields, _ := structs.Fields(structs.FieldsInput{ + Pointer: oai.Config.CommonResponse, + RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, + }) + var ( + err error + bizResponseStructSchemaRef = oai.Components.Schemas[responseStructName] + schema = &Schema{ + Properties: map[string]SchemaRef{}, + } + ) + schema.Type = TypeObject + for _, structField := range structFields { + if !gstr.IsLetterUpper(structField.Name()[0]) { + continue + } + var ( + schemaRef *SchemaRef + fieldName = structField.Name() + ) + if jsonName := structField.TagJsonName(); jsonName != "" { + fieldName = jsonName + } + if structField.Name() == oai.Config.CommonResponseDataField { + schemaRef = &bizResponseStructSchemaRef + } else { + schemaRef, err = oai.newSchemaRefWithGolangType( + structField.Type().Type, + structField.TagMap(), + ) + } + if err != nil { + return nil, err + } + schema.Properties[fieldName] = *schemaRef + } + return &SchemaRef{ + Value: schema, + }, nil +} diff --git a/protocol/goai/goai_shema.go b/protocol/goai/goai_shema.go index 2e96314f2..bb36163f9 100644 --- a/protocol/goai/goai_shema.go +++ b/protocol/goai/goai_shema.go @@ -86,6 +86,20 @@ func (oai *OpenApiV3) doAddSchemaSingle(object interface{}) error { // Take the holder first. oai.Components.Schemas[structTypeName] = SchemaRef{} + schema, err := oai.structToSchema(object) + if err != nil { + return err + } + + oai.Components.Schemas[structTypeName] = SchemaRef{ + Ref: "", + Value: schema, + } + return nil +} + +// structToSchema converts and returns given struct object as Schema. +func (oai *OpenApiV3) structToSchema(object interface{}) (*Schema, error) { structFields, _ := structs.Fields(structs.FieldsInput{ Pointer: object, RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, @@ -111,13 +125,9 @@ func (oai *OpenApiV3) doAddSchemaSingle(object interface{}) error { structField.TagMap(), ) if err != nil { - return err + return nil, err } schema.Properties[fieldName] = *schemaRef } - oai.Components.Schemas[structTypeName] = SchemaRef{ - Ref: "", - Value: schema, - } - return nil + return schema, nil } diff --git a/protocol/goai/goai_test.go b/protocol/goai/goai_test.go index 5dd333630..0a81eef86 100644 --- a/protocol/goai/goai_test.go +++ b/protocol/goai/goai_test.go @@ -188,3 +188,48 @@ func TestOpenApiV3_Add_EmptyReqAndRes(t *testing.T) { fmt.Println(oai.String()) }) } + +func TestOpenApiV3_CommonResponse(t *testing.T) { + type CommonResponse struct { + Code int `json:"code" description:"Error code"` + Message string `json:"message" description:"Error message"` + Data interface{} `json:"data" description:"Result data for certain request according API definition"` + } + + type Req struct { + gmeta.Meta `method:"GET"` + Product string `json:"product" in:"query" v:"required" description:"Unique product key"` + Name string `json:"name" in:"query" v:"required" description:"Instance name"` + } + type Res struct { + Product string `json:"product" v:"required" description:"Unique product key"` + TemplateName string `json:"templateName" v:"required" description:"Workflow template name"` + Version string `json:"version" v:"required" description:"Workflow template version"` + TxID string `json:"txID" v:"required" description:"Transaction ID for creating instance"` + Globals string `json:"globals" description:"Globals"` + } + + f := func(ctx context.Context, req *Req) (res *Res, err error) { + return + } + + gtest.C(t, func(t *gtest.T) { + var ( + err error + oai = goai.New() + ) + + oai.Config.CommonResponse = CommonResponse{} + oai.Config.CommonResponseDataField = `Data` + + err = oai.Add(goai.AddInput{ + Path: "/index", + Object: f, + }) + t.AssertNil(err) + // Schema asserts. + t.Assert(len(oai.Components.Schemas), 2) + t.Assert(len(oai.Paths), 1) + t.Assert(len(oai.Paths["/index"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties), 3) + }) +}