Skip to content

Commit

Permalink
feat(net/goai): enhance openapi doc with responses and examples (#3859)
Browse files Browse the repository at this point in the history
  • Loading branch information
UncleChair authored Oct 21, 2024
1 parent e179e1d commit 555bb3f
Show file tree
Hide file tree
Showing 8 changed files with 354 additions and 80 deletions.
50 changes: 50 additions & 0 deletions net/goai/goai_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
package goai

import (
"fmt"

"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/internal/empty"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gres"
)

// Example is specified by OpenAPI/Swagger 3.0 standard.
Expand All @@ -25,6 +31,50 @@ type ExampleRef struct {
Value *Example
}

func (e *Examples) applyExamplesFile(path string) error {
if empty.IsNil(e) {
return nil
}
var json string
if resource := gres.Get(path); resource != nil {
json = string(resource.Content())
} else {
absolutePath := gfile.RealPath(path)
if absolutePath != "" {
json = gfile.GetContents(absolutePath)
}
}
if json == "" {
return nil
}
var data interface{}
err := gjson.Unmarshal([]byte(json), &data)
if err != nil {
return err
}

switch v := data.(type) {
case map[string]interface{}:
for key, value := range v {
(*e)[key] = &ExampleRef{
Value: &Example{
Value: value,
},
}
}
case []interface{}:
for i, value := range v {
(*e)[fmt.Sprintf("example %d", i+1)] = &ExampleRef{
Value: &Example{
Value: value,
},
}
}
default:
}
return nil
}

func (r ExampleRef) MarshalJSON() ([]byte, error) {
if r.Ref != "" {
return formatRefToBytes(r.Ref), nil
Expand Down
83 changes: 39 additions & 44 deletions net/goai/goai_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,13 @@ func (oai *OpenApiV3) addPath(in addPathInput) error {
}

var (
mime string
path = Path{XExtensions: make(XExtensions)}
inputMetaMap = gmeta.Data(inputObject.Interface())
outputMetaMap = gmeta.Data(outputObject.Interface())
isInputStructEmpty = oai.doesStructHasNoFields(inputObject.Interface())
inputStructTypeName = oai.golangTypeToSchemaName(inputObject.Type())
outputStructTypeName = oai.golangTypeToSchemaName(outputObject.Type())
operation = Operation{
mime string
path = Path{XExtensions: make(XExtensions)}
inputMetaMap = gmeta.Data(inputObject.Interface())
outputMetaMap = gmeta.Data(outputObject.Interface())
isInputStructEmpty = oai.doesStructHasNoFields(inputObject.Interface())
inputStructTypeName = oai.golangTypeToSchemaName(inputObject.Type())
operation = Operation{
Responses: map[string]ResponseRef{},
XExtensions: make(XExtensions),
}
Expand Down Expand Up @@ -129,7 +128,7 @@ func (oai *OpenApiV3) addPath(in addPathInput) error {
)
}

if err := oai.addSchema(inputObject.Interface(), outputObject.Interface()); err != nil {
if err := oai.addSchema(inputObject.Interface()); err != nil {
return err
}

Expand Down Expand Up @@ -235,48 +234,44 @@ func (oai *OpenApiV3) addPath(in addPathInput) error {
}

// =================================================================================================================
// Response.
// Default Response.
// =================================================================================================================
if _, ok := operation.Responses[responseOkKey]; !ok {
var (
response = Response{
Content: map[string]MediaType{},
XExtensions: make(XExtensions),
}
)
if len(outputMetaMap) > 0 {
if err := oai.tagMapToResponse(outputMetaMap, &response); err != nil {
return err
}
status := responseOkKey
if statusValue, ok := outputMetaMap[gtag.Status]; ok {
statusCode := gconv.Int(statusValue)
if statusCode < 100 || statusCode >= 600 {
return gerror.Newf("Invalid HTTP status code: %s", statusValue)
}
// Supported mime types of response.
var (
contentTypes = oai.Config.ReadContentTypes
tagMimeValue = gmeta.Get(outputObject.Interface(), gtag.Mime).String()
refInput = getResponseSchemaRefInput{
BusinessStructName: outputStructTypeName,
CommonResponseObject: oai.Config.CommonResponse,
CommonResponseDataField: oai.Config.CommonResponseDataField,
}
)
if tagMimeValue != "" {
contentTypes = gstr.SplitAndTrim(tagMimeValue, ",")
status = statusValue
}
if _, ok := operation.Responses[status]; !ok {
response, err := oai.getResponseFromObject(outputObject.Interface(), true)
if err != nil {
return err
}
for _, v := range contentTypes {
// If customized response mime type, it then ignores common response feature.
if tagMimeValue != "" {
refInput.CommonResponseObject = nil
refInput.CommonResponseDataField = ""
operation.Responses[status] = ResponseRef{Value: response}
}

// =================================================================================================================
// Other Responses.
// =================================================================================================================
if enhancedResponse, ok := outputObject.Interface().(ResponseStatusDef); ok {
for statusCode, data := range enhancedResponse.ResponseStatusMap() {
if statusCode < 100 || statusCode >= 600 {
return gerror.Newf("Invalid HTTP status code: %d", statusCode)
}
schemaRef, err := oai.getResponseSchemaRef(refInput)
if err != nil {
return err
if data == nil {
continue
}
response.Content[v] = MediaType{
Schema: schemaRef,
status := gconv.String(statusCode)
if _, ok := operation.Responses[status]; !ok {
response, err := oai.getResponseFromObject(data, false)
if err != nil {
return err
}
operation.Responses[status] = ResponseRef{Value: response}
}
}
operation.Responses[responseOkKey] = ResponseRef{Value: &response}
}

// Remove operation body duplicated properties.
Expand Down
9 changes: 9 additions & 0 deletions net/goai/goai_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)

// StatusCode is http status for response.
type StatusCode = int

// ResponseStatusDef is used to enhance the documentation of the response.
// Normal response structure could implement this interface to provide more information.
type ResponseStatusDef interface {
ResponseStatusMap() map[StatusCode]any
}

// Response is specified by OpenAPI/Swagger 3.0 standard.
type Response struct {
Description string `json:"description"`
Expand Down
79 changes: 79 additions & 0 deletions net/goai/goai_response_ref.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/os/gstructs"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gmeta"
"github.com/gogf/gf/v2/util/gtag"
)

type ResponseRef struct {
Expand All @@ -22,6 +24,83 @@ type ResponseRef struct {
// Responses is specified by OpenAPI/Swagger 3.0 standard.
type Responses map[string]ResponseRef

// object could be someObject.Interface()
// There may be some difference between someObject.Type() and reflect.TypeOf(object).
func (oai *OpenApiV3) getResponseFromObject(object interface{}, isDefault bool) (*Response, error) {
// Add object schema to oai
if err := oai.addSchema(object); err != nil {
return nil, err
}
var (
metaMap = gmeta.Data(object)
response = &Response{
Content: map[string]MediaType{},
XExtensions: make(XExtensions),
}
)
if len(metaMap) > 0 {
if err := oai.tagMapToResponse(metaMap, response); err != nil {
return nil, err
}
}
// Supported mime types of response.
var (
contentTypes = oai.Config.ReadContentTypes
tagMimeValue = gmeta.Get(object, gtag.Mime).String()
refInput = getResponseSchemaRefInput{
BusinessStructName: oai.golangTypeToSchemaName(reflect.TypeOf(object)),
CommonResponseObject: oai.Config.CommonResponse,
CommonResponseDataField: oai.Config.CommonResponseDataField,
}
)

// If customized response mime type, it then ignores common response feature.
if tagMimeValue != "" {
contentTypes = gstr.SplitAndTrim(tagMimeValue, ",")
refInput.CommonResponseObject = nil
refInput.CommonResponseDataField = ""
}

// If it is not default status, check if it has any fields.
// If so, it would override the common response.
if !isDefault {
fields, _ := gstructs.Fields(gstructs.FieldsInput{
Pointer: object,
RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag,
})
if len(fields) > 0 {
refInput.CommonResponseObject = nil
refInput.CommonResponseDataField = ""
}
}

// Generate response example from meta data.
responseExamplePath := metaMap[gtag.ResponseExampleShort]
if responseExamplePath == "" {
responseExamplePath = metaMap[gtag.ResponseExample]
}
examples := make(Examples)
if responseExamplePath != "" {
if err := examples.applyExamplesFile(responseExamplePath); err != nil {
return nil, err
}
}

// Generate response schema from input.
schemaRef, err := oai.getResponseSchemaRef(refInput)
if err != nil {
return nil, err
}

for _, contentType := range contentTypes {
response.Content[contentType] = MediaType{
Schema: schemaRef,
Examples: examples,
}
}
return response, nil
}

func (r ResponseRef) MarshalJSON() ([]byte, error) {
if r.Ref != "" {
return formatRefToBytes(r.Ref), nil
Expand Down
Loading

0 comments on commit 555bb3f

Please sign in to comment.