Skip to content

Commit

Permalink
Support application/x-www-form-urlencoded request body (#25)
Browse files Browse the repository at this point in the history
* Support application/x-www-form-urlencoded request body  
* No longer move the request body to parameters
  • Loading branch information
hgiasac committed Jun 8, 2024
1 parent b7fb2b4 commit 47b83ad
Show file tree
Hide file tree
Showing 6 changed files with 3,305 additions and 3,426 deletions.
49 changes: 32 additions & 17 deletions openapi/internal/oas2_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"slices"

rest "github.com/hasura/ndc-rest-schema/schema"
"github.com/hasura/ndc-rest-schema/utils"
"github.com/hasura/ndc-sdk-go/schema"
v2 "github.com/pb33f/libopenapi/datamodel/high/v2"
)
Expand Down Expand Up @@ -45,7 +46,7 @@ func (oc *oas2OperationBuilder) BuildFunction(pathKey string, operation *v2.Oper
if resultType == nil {
return nil, nil
}
reqBody, err := oc.convertParameters(operation.Parameters, pathKey, []string{funcName})
reqBody, err := oc.convertParameters(operation, pathKey, []string{funcName})
if err != nil {
return nil, fmt.Errorf("%s: %s", funcName, err)
}
Expand Down Expand Up @@ -99,19 +100,11 @@ func (oc *oas2OperationBuilder) BuildProcedure(pathKey string, method string, op
return nil, nil
}

reqBody, err := oc.convertParameters(operation.Parameters, pathKey, []string{procName})
reqBody, err := oc.convertParameters(operation, pathKey, []string{procName})
if err != nil {
return nil, fmt.Errorf("%s: %s", pathKey, err)
}

if reqBody != nil && len(operation.Consumes) > 0 {
contentType := rest.ContentTypeJSON
if !slices.Contains(operation.Consumes, rest.ContentTypeJSON) {
contentType = operation.Consumes[0]
}
reqBody.ContentType = contentType
}

procedure := rest.RESTProcedureInfo{
Request: &rest.Request{
URL: pathKey,
Expand All @@ -134,18 +127,26 @@ func (oc *oas2OperationBuilder) BuildProcedure(pathKey string, method string, op
return &procedure, nil
}

func (oc *oas2OperationBuilder) convertParameters(params []*v2.Parameter, apiPath string, fieldPaths []string) (*rest.RequestBody, error) {
func (oc *oas2OperationBuilder) convertParameters(operation *v2.Operation, apiPath string, fieldPaths []string) (*rest.RequestBody, error) {

if len(params) == 0 {
if operation == nil || len(operation.Parameters) == 0 {
return nil, nil
}

contentType := rest.ContentTypeJSON
if len(operation.Consumes) > 0 && !slices.Contains(operation.Consumes, rest.ContentTypeJSON) {
contentType = operation.Consumes[0]
}

var requestBody *rest.RequestBody
formData := rest.TypeSchema{
Type: "object",
Properties: make(map[string]rest.TypeSchema),
}
for _, param := range params {
formDataObject := schema.ObjectType{
Fields: schema.ObjectTypeFields{},
}
for _, param := range operation.Parameters {
if param == nil {
continue
}
Expand Down Expand Up @@ -203,8 +204,9 @@ func (oc *oas2OperationBuilder) convertParameters(params []*v2.Parameter, apiPat
}

oc.builder.typeUsageCounter.Increase(getNamedType(typeEncoder, true, ""))
schemaType := typeEncoder.Encode()
argument := schema.ArgumentInfo{
Type: typeEncoder.Encode(),
Type: schemaType,
}
if param.Description != "" {
argument.Description = &param.Description
Expand All @@ -214,11 +216,15 @@ func (oc *oas2OperationBuilder) convertParameters(params []*v2.Parameter, apiPat
case rest.InBody:
oc.Arguments["body"] = argument
requestBody = &rest.RequestBody{
Schema: typeSchema,
ContentType: contentType,
Schema: typeSchema,
}
case rest.InFormData:
oc.Arguments[paramName] = argument
if typeSchema != nil {
formDataObject.Fields[paramName] = schema.ObjectField{
Type: argument.Type,
Description: argument.Description,
}
formData.Properties[paramName] = *typeSchema
}
default:
Expand All @@ -232,8 +238,17 @@ func (oc *oas2OperationBuilder) convertParameters(params []*v2.Parameter, apiPat
}

if len(formData.Properties) > 0 {
bodyName := fmt.Sprintf("%sBody", utils.StringSliceToPascalCase(fieldPaths))
oc.builder.schema.ObjectTypes[bodyName] = formDataObject
oc.builder.typeUsageCounter.Increase(bodyName)

desc := fmt.Sprintf("Form data of %s", apiPath)
oc.Arguments["body"] = schema.ArgumentInfo{
Type: schema.NewNamedType(bodyName).Encode(),
Description: &desc,
}
requestBody = &rest.RequestBody{
ContentType: rest.ContentTypeMultipartFormData,
ContentType: contentType,
Schema: &formData,
}
}
Expand Down
102 changes: 8 additions & 94 deletions openapi/internal/oas3_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,81 +110,15 @@ func (oc *oas3OperationBuilder) BuildProcedure(operation *v3.Operation) (*rest.R
return nil, fmt.Errorf("%s: %s", oc.pathKey, err)
}
if reqBody != nil {
if reqBody.ContentType == rest.ContentTypeFormURLEncoded {
// convert URL encoded body to parameters
if reqBody.Schema != nil {
if reqBody.Schema.Type == "object" {
objectType, objectTypeName, err := oc.getObjectTypeFromSchemaType(schemaType.Encode())
if err != nil {
return nil, fmt.Errorf("%s: %s", oc.pathKey, err)
}
// remove unused request body type
delete(oc.builder.schema.ObjectTypes, objectTypeName)
for key, prop := range reqBody.Schema.Properties {
propType, ok := objectType.Fields[key]
if !ok {
continue
}
// renaming query parameter name `body` if exist to avoid conflicts
if paramData, ok := oc.Arguments[key]; ok {
oc.Arguments[fmt.Sprintf("param%s", key)] = paramData
}

desc := prop.Description
argument := schema.ArgumentInfo{
Type: propType.Type,
}
if desc != "" {
argument.Description = &desc
}
oc.Arguments[key] = argument
schemaProp := prop
oc.RequestParams = append(oc.RequestParams, rest.RequestParameter{
EncodingObject: reqBody.Encoding[key],
Name: key,
In: rest.InQuery,
Schema: &schemaProp,
})
}
} else {
description := fmt.Sprintf("Request body of %s %s", oc.method, oc.pathKey)
// renaming query parameter name `body` if exist to avoid conflicts
if paramData, ok := oc.Arguments["body"]; ok {
oc.Arguments["paramBody"] = paramData
for i, param := range oc.RequestParams {
if param.Name == "body" {
param.ArgumentName = "paramBody"
oc.RequestParams[i] = param
break
}
}
}

oc.Arguments["body"] = schema.ArgumentInfo{
Description: &description,
Type: schemaType.Encode(),
}
oc.RequestParams = append(oc.RequestParams, rest.RequestParameter{
Name: "body",
In: rest.InQuery,
Schema: reqBody.Schema,
})
}
}
reqBody = &rest.RequestBody{
ContentType: rest.ContentTypeFormURLEncoded,
}
} else {
description := fmt.Sprintf("Request body of %s %s", strings.ToUpper(oc.method), oc.pathKey)
// renaming query parameter name `body` if exist to avoid conflicts
if paramData, ok := oc.Arguments["body"]; ok {
oc.Arguments["paramBody"] = paramData
}
description := fmt.Sprintf("Request body of %s %s", strings.ToUpper(oc.method), oc.pathKey)
// renaming query parameter name `body` if exist to avoid conflicts
if paramData, ok := oc.Arguments["body"]; ok {
oc.Arguments["paramBody"] = paramData
}

oc.Arguments["body"] = schema.ArgumentInfo{
Description: &description,
Type: schemaType.Encode(),
}
oc.Arguments["body"] = schema.ArgumentInfo{
Description: &description,
Type: schemaType.Encode(),
}
}

Expand Down Expand Up @@ -419,23 +353,3 @@ func (oc *oas3OperationBuilder) convertResponse(responses *v3.Responses, apiPath
oc.builder.typeUsageCounter.Increase(getNamedType(schemaType, true, ""))
return schemaType, nil
}

func (oc *oas3OperationBuilder) getObjectTypeFromSchemaType(schemaType schema.Type) (*schema.ObjectType, string, error) {
iSchemaType, err := schemaType.InterfaceT()

switch st := iSchemaType.(type) {
case *schema.NullableType:
return oc.getObjectTypeFromSchemaType(st.UnderlyingType)
case *schema.NamedType:
objectType, ok := oc.builder.schema.ObjectTypes[st.Name]
if !ok {
return nil, "", fmt.Errorf("expect object type body, got %s", st.Name)
}

return &objectType, st.Name, nil
case *schema.ArrayType:
return nil, "", fmt.Errorf("expect named type body, got %s", schemaType)
default:
return nil, "", err
}
}
4 changes: 2 additions & 2 deletions openapi/oas3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ func TestOpenAPIv3ToRESTSchema(t *testing.T) {
EnvPrefix string
Expected string
}{
// go run . convert --log-level debug -f ./openapi/testdata/petstore3/source.json -o ./openapi/testdata/petstore3/expected.json --trim-prefix /v1 --spec openapi3 --env-prefix PET_STORE
// go run . convert -f ./openapi/testdata/petstore3/source.json -o ./openapi/testdata/petstore3/expected.json --trim-prefix /v1 --spec openapi3 --env-prefix PET_STORE
{
Name: "petstore3",
Source: "testdata/petstore3/source.json",
Expected: "testdata/petstore3/expected.json",
EnvPrefix: "PET_STORE",
},
// go run . convert --log-level debug -f ./openapi/testdata/onesignal/source.json -o ./openapi/testdata/onesignal/expected.json --spec openapi3
// go run . convert -f ./openapi/testdata/onesignal/source.json -o ./openapi/testdata/onesignal/expected.json --spec openapi3
{
Name: "onesignal",
Source: "testdata/onesignal/source.json",
Expand Down
2 changes: 2 additions & 0 deletions openapi/testdata/jsonplaceholder/expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,7 @@
}
],
"requestBody": {
"contentType": "application/json",
"schema": {
"type": "Post"
}
Expand Down Expand Up @@ -1078,6 +1079,7 @@
}
],
"requestBody": {
"contentType": "application/json",
"schema": {
"type": "Post"
}
Expand Down
90 changes: 56 additions & 34 deletions openapi/testdata/petstore2/expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,54 @@
}
}
},
"UpdatePetWithFormBody": {
"fields": {
"name": {
"description": "Updated name of the pet",
"type": {
"type": "nullable",
"underlying_type": {
"name": "String",
"type": "named"
}
}
},
"status": {
"description": "Updated status of the pet",
"type": {
"type": "nullable",
"underlying_type": {
"name": "String",
"type": "named"
}
}
}
}
},
"UploadFileBody": {
"fields": {
"additionalMetadata": {
"description": "Additional data to pass to server",
"type": {
"type": "nullable",
"underlying_type": {
"name": "String",
"type": "named"
}
}
},
"file": {
"description": "file to upload",
"type": {
"type": "nullable",
"underlying_type": {
"name": "Binary",
"type": "named"
}
}
}
}
},
"User": {
"fields": {
"email": {
Expand Down Expand Up @@ -781,24 +829,11 @@
}
},
"arguments": {
"additionalMetadata": {
"description": "Additional data to pass to server",
"type": {
"type": "nullable",
"underlying_type": {
"name": "String",
"type": "named"
}
}
},
"file": {
"description": "file to upload",
"body": {
"description": "Form data of /pet/{petId}/uploadImage",
"type": {
"type": "nullable",
"underlying_type": {
"name": "Binary",
"type": "named"
}
"name": "UploadFileBody",
"type": "named"
}
},
"petId": {
Expand Down Expand Up @@ -931,14 +966,11 @@
}
},
"arguments": {
"name": {
"description": "Updated name of the pet",
"body": {
"description": "Form data of /pet/{petId}",
"type": {
"type": "nullable",
"underlying_type": {
"name": "String",
"type": "named"
}
"name": "UpdatePetWithFormBody",
"type": "named"
}
},
"petId": {
Expand All @@ -947,16 +979,6 @@
"name": "Int64",
"type": "named"
}
},
"status": {
"description": "Updated status of the pet",
"type": {
"type": "nullable",
"underlying_type": {
"name": "String",
"type": "named"
}
}
}
},
"description": "Updates a pet in the store with form data",
Expand Down
Loading

0 comments on commit 47b83ad

Please sign in to comment.