Repository: go-fuego/fuego

Issue:
Add option for setting request
**Is your feature request related to a problem? Please describe.**
When you are trying to use compat layer for gin for ex. you have no way to describe request body. 
There is already option.AddResponse that allows us to set responses for such cases but no option for request

**Describe the solution you'd like**
I think it's a good solution to add function option.AddRequest that accepts body model. Maybe we should add a constraint that this function must be called at most once for one route?

**Describe alternatives you've considered**
I don't think there is any alternative


Hints from issue comments:
@evilenzo: Solution proposed in PR:
- Name for function `option.RequestBody`
- Same logic as in `option.AddResponse`, described in doc comments

Solution patch:
diff --git a/openapi_operations.go b/openapi_operations.go
index 5a366db..43fa029 100644
--- a/openapi_operations.go
+++ b/openapi_operations.go
@@ -1,6 +1,7 @@
 package fuego
 
 import (
+	"reflect"
 	"strconv"
 
 	"github.com/getkin/kin-openapi/openapi3"
@@ -43,6 +44,23 @@ func (openAPI *OpenAPI) buildOpenapi3Response(description string, response Respo
 		WithContent(content)
 }
 
+func (openAPI *OpenAPI) buildOpenapi3RequestBody(requestBody RequestBody) *openapi3.RequestBody {
+	if requestBody.Type == nil {
+		panic("Type in RequestBody cannot be nil")
+	}
+
+	bodySchema := SchemaTagFromType(openAPI, requestBody.Type)
+	if len(requestBody.ContentTypes) == 0 {
+		requestBody.ContentTypes = []string{"application/json", "application/xml"}
+	}
+
+	content := openapi3.NewContentWithSchemaRef(&bodySchema.SchemaRef, requestBody.ContentTypes)
+	return openapi3.NewRequestBody().
+		WithRequired(true).
+		WithDescription("Request body for " + reflect.TypeOf(requestBody.Type).String()).
+		WithContent(content)
+}
+
 // openAPIResponse describes a response error in the OpenAPI spec.
 type openAPIResponse struct {
 	Description string
diff --git a/option.go b/option.go
index 0f448dc..e4a0e78 100644
--- a/option.go
+++ b/option.go
@@ -362,6 +362,27 @@ func OptionAddResponse(code int, description string, response Response) func(*Ba
 	}
 }
 
+// RequestBody represents a fuego.RequestBody that can be used
+// when setting custom request type on routes
+type RequestBody struct {
+	// user provided type
+	Type any
+	// content-type of the request i.e application/json
+	ContentTypes []string
+}
+
+// OptionRequestBody sets a request to a route
+// It replaces existing request body
+// Required: RequestBody.Type must be set
+// Optional: RequestBody.ContentTypes will default to `application/json` and `application/xml` if not set
+func OptionRequestBody(requestBody RequestBody) func(*BaseRoute) {
+	return func(r *BaseRoute) {
+		r.Operation.RequestBody = &openapi3.RequestBodyRef{
+			Value: r.OpenAPI.buildOpenapi3RequestBody(requestBody),
+		}
+	}
+}
+
 // OptionDefaultResponse adds a default response to a route
 // Required: Response.Type must be set
 // Optional: Response.ContentTypes will default to `application/json` and `application/xml` if not set
diff --git a/option/option.go b/option/option.go
index 8566cfb..5827f9c 100644
--- a/option/option.go
+++ b/option/option.go
@@ -163,6 +163,12 @@ var AddError = fuego.OptionAddError
 // Optional: fuego.Response.ContentTypes will default to `application/json` and `application/xml` if not set
 var AddResponse = fuego.OptionAddResponse
 
+// RequestBody sets a request to a route
+// It replaces existing request body
+// Required: fuego.RequestBody.Type must be set
+// Optional: fuego.RequestBody.ContentTypes will default to `application/json` and `application/xml` if not set
+var RequestBody = fuego.OptionRequestBody
+
 // DefaultResponse adds a default response to a route
 // Required: Response.Type must be set
 // Optional: Response.ContentTypes will default to `application/json` and `application/xml` if not set



Test patch:
diff --git a/option_test.go b/option_test.go
index c9a974f..4bf37f1 100644
--- a/option_test.go
+++ b/option_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/thejerf/slogassert"
 
@@ -486,6 +487,54 @@ func TestAddResponse(t *testing.T) {
 	})
 }
 
+func TestRequestBody(t *testing.T) {
+	type TestModel struct{}
+
+	t.Run("base", func(t *testing.T) {
+		s := fuego.NewServer()
+		route := fuego.Post(s, "/test", helloWorld,
+			fuego.OptionRequestBody(fuego.RequestBody{
+				ContentTypes: []string{"application/json"},
+				Type:         TestModel{},
+			}),
+		)
+
+		req := route.Operation.RequestBody
+		require.NotNil(t, req)
+		assert.Nil(t, req.Value.Content.Get("application/xml"))
+		require.NotNil(t, req.Value.Content.Get("application/json"))
+		assert.Equal(t, "#/components/schemas/TestModel", req.Value.Content.Get("application/json").Schema.Ref)
+		assert.NotNil(t, s.OpenAPI.Description().Components.Schemas["TestModel"])
+	})
+
+	t.Run("no content types provided", func(t *testing.T) {
+		s := fuego.NewServer()
+		route := fuego.Post(s, "/test", helloWorld,
+			fuego.OptionRequestBody(fuego.RequestBody{
+				Type: TestModel{},
+			}),
+		)
+		req := route.Operation.RequestBody
+		require.NotNil(t, req)
+		require.NotNil(t, req.Value.Content.Get("application/json"))
+		assert.NotNil(t, req.Value.Content.Get("application/xml"))
+		assert.Equal(t, "#/components/schemas/TestModel", req.Value.Content.Get("application/json").Schema.Ref)
+		assert.NotNil(t, s.OpenAPI.Description().Components.Schemas["TestModel"])
+	})
+
+	t.Run("should be fatal", func(t *testing.T) {
+		s := fuego.NewServer()
+
+		require.Panics(t, func() {
+			fuego.Get(s, "/test", helloWorld,
+				fuego.OptionRequestBody(
+					fuego.RequestBody{},
+				),
+			)
+		})
+	})
+}
+
 func TestHide(t *testing.T) {
 	s := fuego.NewServer()