Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(net/goai): enhance openapi doc with responses and examples #3859

Merged
merged 32 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
502cb82
Feature: add change default response status code for goai
VoidGun Oct 13, 2024
a42f483
Feature: filter field with status tag in schema
VoidGun Oct 13, 2024
88056c7
Feature: add other response structure
UncleChair Oct 14, 2024
1c107dd
Feature: add examples for response goai doc
VoidGun Oct 14, 2024
af52524
Feature: add response override logic
UncleChair Oct 15, 2024
bb56a0b
Feature; add function for examples file apply
UncleChair Oct 15, 2024
4071e06
Feature: add examples for responses
UncleChair Oct 15, 2024
faa987f
Test: add unit test for goai enhancement for #3747
UncleChair Oct 15, 2024
ead35af
Pref: delete useless code
VoidGun Oct 15, 2024
020bf12
Feature: add interface for response status management
UncleChair Oct 16, 2024
84b68d6
Feature; add api doc generation for enhanced response interface
UncleChair Oct 16, 2024
98d0f69
Feature: add test cases
UncleChair Oct 16, 2024
da9066f
Pref: remove unused logic
UncleChair Oct 16, 2024
036c40e
Merge branch 'master' into goai/http_status_enhance
UncleChair Oct 16, 2024
3ad0769
Refactor: add response add function
VoidGun Oct 16, 2024
62c2594
Feature: add error check for AddStatus
VoidGun Oct 16, 2024
f46a693
Fix: unnecessary leading newline
VoidGun Oct 16, 2024
062595b
Fix: coding style
UncleChair Oct 17, 2024
b8c0b70
Fix: use g.IsNil to check nil
UncleChair Oct 17, 2024
8cfc44b
Fix: import cycle
UncleChair Oct 17, 2024
2d93bf5
Feat: change AddStatus to private function
UncleChair Oct 17, 2024
647f1bf
Test: add nil case for adding responses
UncleChair Oct 17, 2024
23e6a98
Refactor: change add status fucntion to get response and add into api…
VoidGun Oct 17, 2024
e34c469
Fix: add info for goai StatusCode
VoidGun Oct 19, 2024
f5cd443
Fix: extract other logic out of for loop
VoidGun Oct 19, 2024
d89c83b
Fix: change loop param name
VoidGun Oct 19, 2024
deb5f6d
Fix: change response interface name
VoidGun Oct 19, 2024
2697e8c
Fix: change function param usage
VoidGun Oct 19, 2024
62f1476
Fix: change related function usage and param name
VoidGun Oct 19, 2024
16ba360
Fix: change enhanced response interface and function name
VoidGun Oct 19, 2024
432e355
Merge branch 'master' into goai/http_status_enhance
UncleChair Oct 21, 2024
651248a
Fix: change response status interface name
UncleChair Oct 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
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().(EnhancedResponse); 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
8 changes: 8 additions & 0 deletions net/goai/goai_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)

type StatusCode = int
UncleChair marked this conversation as resolved.
Show resolved Hide resolved

// EnhancedResponse is used to enhance the documentation of the response.
// Normal response structure could implement this interface to provide more information.
type EnhancedResponse interface {
UncleChair marked this conversation as resolved.
Show resolved Hide resolved
ResponseStatusMap() map[StatusCode]any
}

// Response is specified by OpenAPI/Swagger 3.0 standard.
type Response struct {
Description string `json:"description"`
Expand Down
78 changes: 78 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,82 @@ 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) {
UncleChair marked this conversation as resolved.
Show resolved Hide resolved
// Add default status by default.
var isDefaultStatus = true
if len(isDefault) > 0 {
isDefaultStatus = isDefault[0]
}
// 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 tagMimeValue != "" {
contentTypes = gstr.SplitAndTrim(tagMimeValue, ",")
}
for _, v := range contentTypes {
UncleChair marked this conversation as resolved.
Show resolved Hide resolved
// If customized response mime type, it then ignores common response feature.
if tagMimeValue != "" {
refInput.CommonResponseObject = nil
refInput.CommonResponseDataField = ""
}
if !isDefaultStatus {
fields, _ := gstructs.Fields(gstructs.FieldsInput{
Pointer: object,
RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag,
})
if len(fields) > 0 {
refInput.CommonResponseObject = nil
refInput.CommonResponseDataField = ""
}
}
UncleChair marked this conversation as resolved.
Show resolved Hide resolved

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
}
}
schemaRef, err := oai.getResponseSchemaRef(refInput)
if err != nil {
return nil, err
}
response.Content[v] = 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
Loading