From 0ad353bd5f6048974c894bd1ba3e9ee32af6c0ae Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Fri, 6 Jun 2025 12:29:28 +0200 Subject: [PATCH 1/8] feat(openapi): add auto-generate response errors --- README.md | 29 ++++++++------ examples/group/main.go | 21 ++++++++--- examples/middleware/main.go | 4 +- okapi.go | 75 +++++++++++++++++++------------------ openapi.go | 59 +++++++++++++++++++++++++---- 5 files changed, 125 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index dbfde7a..1109089 100644 --- a/README.md +++ b/README.md @@ -339,7 +339,10 @@ o.Get("/books", getBooksHandler, okapi.DocTags("Books"), okapi.DocQueryParam("author", "string", "Filter by author name", false), okapi.DocQueryParam("limit", "int", "Maximum results to return (default 20)", false), - okapi.DocResponse([]Book{}), + okapi.DocResponse([]Book{}), // Response for OpenAPI docs + okapi.DocErrorResponse(400, ErrorResponse{}),// Response error for OpenAPI docs + okapi.DocErrorResponse(401, ErrorResponse{}),// Response error for OpenAPI docs + ) ``` @@ -356,22 +359,26 @@ o.Post("/books", createBookHandler, BearerAuth(). RequestBody(BookRequest{}). Response(Book{}). + ErrorResponse(400,ErrorResponse{}). + ErrorResponse(401,ErrorResponse{}). Build(), ) ``` ### Available Documentation Options -| Method | Description | -|------------------------------------------|-------------------------------------| -| `DocSummary()`/`Doc().Summary()` | Short endpoint description | -| `DocTag()/DocTags()`/`Doc().Tags()` | Groups related endpoints | -| `DocBearerAuth()` | Enables Bearer token authentication | -| `DocRequestBody()`/`Doc().RequestBody()` | Documents request body structure | -| `DocResponse()`/`Doc().Response()` | Documents response structure | -| `DocPathParam()`/`Doc().PathParam()` | Documents path parameters | -| `DocQueryParam()`/`Doc().QueryParam()` | Documents query parameters | -| `DocHeader()`/ `Doc().Header()` | Documents header parameters | +| Method | Description | +|----------------------------------------------|-------------------------------------| +| `DocSummary()`/`Doc().Summary()` | Short endpoint description | +| `DocTag()/DocTags()`/`Doc().Tags()` | Groups related endpoints | +| `DocBearerAuth()` | Enables Bearer token authentication | +| `DocRequestBody()`/`Doc().RequestBody()` | Documents request body structure | +| `DocResponse()`/`Doc().Response()` | Documents response structure | +| `DocPathParam()`/`Doc().PathParam()` | Documents path parameters | +| `DocQueryParam()`/`Doc().QueryParam()` | Documents query parameters | +| `DocHeader()`/ `Doc().Header()` | Documents header parameters | +| `DocErrorResponse()`/`Doc().ErrorResponse()` | Documents response error | + ### Swagger UI Preview diff --git a/examples/group/main.go b/examples/group/main.go index acef830..2252040 100644 --- a/examples/group/main.go +++ b/examples/group/main.go @@ -35,6 +35,12 @@ type User struct { Name string `json:"name" form:"name" max:"15"` IsActive bool `json:"is_active" query:"is_active" yaml:"isActive"` } +type Users []User +type ErrorResponse struct { + Success bool `json:"success"` + Status int `json:"status"` + Message string `json:"message"` +} var ( users = []*User{ @@ -47,10 +53,10 @@ var ( func main() { // Example usage of Group handling in Okapi // Create a new Okapi instance - o := okapi.New(okapi.WithDebug()) + o := okapi.New(okapi.WithDebug()).WithOpenAPIDocs() o.Get("/", func(c okapi.Context) error { // Handler logic for the root route - return c.JSON(http.StatusOK, okapi.M{"message": "Welcome to Okapi!"}) + return c.OK(okapi.M{"message": "Welcome to Okapi!"}) }) // Create a new group with a base path api := o.Group("/api") @@ -60,10 +66,15 @@ func main() { // Define a route with a handler v1.Get("/users", func(c okapi.Context) error { // Handler logic for the route - return c.JSON(http.StatusOK, users) - }) + return c.OK(users) + }, + okapi.DocResponse(Users{}), + okapi.DocErrorResponse(400, ErrorResponse{}), + ) // Get user - v1.Get("/users/:id", show) + v1.Get("/users/:id", show, + okapi.DocResponse(User{}), + ) // Update user v1.Put("/users/:id", update) // Create user diff --git a/examples/middleware/main.go b/examples/middleware/main.go index bce29a6..9e28209 100644 --- a/examples/middleware/main.go +++ b/examples/middleware/main.go @@ -69,7 +69,7 @@ func main() { okapi.DocHeader("Key", "1234", "API Key", false), okapi.DocTag("bookController"), okapi.DocBearerAuth(), - okapi.DocRequest(Book{}), + okapi.DocRequestBody(Book{}), okapi.DocResponse(Book{})) // ******* Admin Routes | Restricted Area ******** basicAuth := okapi.BasicAuthMiddleware{ @@ -86,7 +86,7 @@ func main() { adminApi.Post("/books", adminStore, okapi.DocSummary("Store books"), okapi.DocResponse(Book{}), - okapi.DocRequest(Book{})) + okapi.DocRequestBody(Book{})) // ******* Public API Routes ******** v1 := api.Group("/v1") diff --git a/okapi.go b/okapi.go index d6008e3..74b1e5b 100644 --- a/okapi.go +++ b/okapi.go @@ -55,32 +55,35 @@ var ( ) type ( + // Okapi represents the core application structure of the framework, + // holding configuration, routers, middleware, server settings, and documentation components. Okapi struct { - context *Context - router *Router - middlewares []Middleware - Server *http.Server - TLSServer *http.Server - tlsConfig *tls.Config - tlsServerConfig *tls.Config - withTlsServer bool - tlsAddr string - routes []*Route - debug bool - accessLog bool - strictSlash bool - logger *slog.Logger - Renderer Renderer - corsEnabled bool - cors Cors - writeTimeout int - readTimeout int - idleTimeout int - optionsRegistered map[string]bool - openapiSpec *openapi3.T - openAPI *OpenAPI - openApiEnabled bool + context *Context // context manages request-scoped data and utilities. + router *Router // router handles route registration and HTTP method dispatching. + middlewares []Middleware // middlewares is the list of global middlewares applied to all routes. + Server *http.Server // Server is the primary HTTP server instance. + TLSServer *http.Server // TLSServer is the optional HTTPS server instance (if TLS is enabled). + tlsConfig *tls.Config // tlsConfig holds the TLS configuration for the main server. + tlsServerConfig *tls.Config // tlsServerConfig holds the TLS configuration for the optional TLS server. + withTlsServer bool // withTlsServer indicates whether the optional TLS server is enabled. + tlsAddr string // tlsAddr specifies the bind address for the TLS server. + routes []*Route // routes is a list of all registered routes in the application. + debug bool // debug enables verbose logging and debug features. + accessLog bool // accessLog enables logging of all incoming HTTP requests. + strictSlash bool // strictSlash enforces trailing slash consistency on routes. + logger *slog.Logger // logger is the structured logger used across the framework. + Renderer Renderer // Renderer defines how response data + corsEnabled bool // corsEnabled toggles automatic CORS handling. + cors Cors // cors contains the configuration for CORS handling. + writeTimeout int // writeTimeout sets the maximum duration before timing out writes (in seconds). + readTimeout int // readTimeout sets the maximum duration for reading the entire request (in seconds). + idleTimeout int // idleTimeout sets the maximum idle time before closing a keep-alive connection (in seconds). + optionsRegistered map[string]bool // optionsRegistered tracks which routes have automatically registered OPTIONS handlers. + openapiSpec *openapi3.T // openapiSpec holds the generated OpenAPI spec (v3) for documentation and tooling. + openAPI *OpenAPI // openAPI manages OpenAPI generation, UI handlers, and route metadata. + openApiEnabled bool // openApiEnabled toggles OpenAPI generation and exposure (e.g., `/docs`, `/openapi.json`). } + Router struct { mux *mux.Router } @@ -89,6 +92,8 @@ type ( // M is shortcut of map[string]any M map[string]any + // Route defines the structure of a registered HTTP route in the framework. + // It includes metadata used for routing, OpenAPI documentation, and middleware handling. Route struct { Name string Path string @@ -106,10 +111,11 @@ type ( RequiresAuth bool RequestExample map[string]interface{} ResponseExample map[string]interface{} - Responses map[int]any + ErrorResponses map[int]*openapi3.SchemaRef Description string disabled bool } + // Response interface defines the methods for writing HTTP responses. Response interface { http.ResponseWriter @@ -701,12 +707,13 @@ func (o *Okapi) addRoute(method, path, groupPath string, h HandleFunc, opts ...R } path = normalizeRoutePath(path) route := &Route{ - Name: handleName(h), - Path: path, - Method: method, - GroupPath: groupPath, - Handle: h, - chain: o, + Name: handleName(h), + Path: path, + Method: method, + GroupPath: groupPath, + Handle: h, + chain: o, + ErrorResponses: make(map[int]*openapi3.SchemaRef), } for _, opt := range opts { opt(route) @@ -1063,12 +1070,6 @@ func handleAccessLog(next HandleFunc) HandleFunc { func (o *Okapi) addDefaultErrorResponses(op *openapi3.Operation, r *Route) { // Add default error responses - op.Responses.Set("400", &openapi3.ResponseRef{ - Value: &openapi3.Response{ - Description: ptr("Bad Request"), - }, - }) - if r.RequiresAuth { op.Responses.Set("401", &openapi3.ResponseRef{ Value: &openapi3.Response{ diff --git a/openapi.go b/openapi.go index c33ad31..a83d015 100644 --- a/openapi.go +++ b/openapi.go @@ -28,9 +28,11 @@ import ( "crypto/sha256" "fmt" "github.com/getkin/kin-openapi/openapi3" + "net/http" "reflect" "regexp" "sort" + "strconv" "strings" "time" "unicode" @@ -173,7 +175,7 @@ type DocBuilder struct { // RequestBody adds a request body schema to the route documentation using the provided value. func (b *DocBuilder) RequestBody(v any) *DocBuilder { - b.options = append(b.options, DocRequest(v)) + b.options = append(b.options, DocRequestBody(v)) return b } @@ -183,6 +185,18 @@ func (b *DocBuilder) Response(v any) *DocBuilder { return b } +// ErrorResponse defines an error response schema for a specific HTTP status code +// in the route's OpenAPI documentation. +// +// Parameters: +// - status: the HTTP status code (e.g., 400, 404, 500). +// - v: a Go value (e.g., a struct instance) whose type will be used to generate +// the OpenAPI schema for the error response. +func (b *DocBuilder) ErrorResponse(status int, v any) *DocBuilder { + b.options = append(b.options, DocErrorResponse(status, v)) + return b +} + // Summary adds a short summary description to the route documentation. func (b *DocBuilder) Summary(summary string) *DocBuilder { b.options = append(b.options, DocSummary(summary)) @@ -372,6 +386,26 @@ func DocResponse(v any) RouteOption { } } +// DocErrorResponse defines an error response schema for a specific HTTP status code +// in the route's OpenAPI documentation. +// +// Parameters: +// - status: the HTTP status code (e.g., 400, 404, 500). +// - v: a Go value (e.g., a struct instance) whose type will be used to generate +// the OpenAPI schema for the error response. +// +// Returns: +// - A RouteOption function that adds the error schema to the route's documentation. +func DocErrorResponse(status int, v any) RouteOption { + return func(doc *Route) { + if v == nil { + return + } + // Generate a schema from the provided Go value and assign it to the error response + doc.ErrorResponses[status] = reflectToSchemaWithInfo(v).Schema + } +} + // DocRequestBody defines the request body schema for the route // v: a Go value whose type will be used to generate the request schema func DocRequestBody(v any) RouteOption { @@ -383,12 +417,6 @@ func DocRequestBody(v any) RouteOption { } } -// DocRequest defines the request schema for the route in the API documentation. -// This is an alias for RequestBody -func DocRequest(v any) RouteOption { - return DocRequestBody(v) -} - // DocBearerAuth marks the route as requiring Bearer token authentication func DocBearerAuth() RouteOption { return func(doc *Route) { @@ -497,7 +525,22 @@ func (o *Okapi) buildOpenAPISpec() { op.Responses.Set("200", &openapi3.ResponseRef{Value: apiResponse}) } - o.addDefaultErrorResponses(op, r) + + if r.ErrorResponses != nil && len(r.ErrorResponses) != 0 { + for key, resp := range r.ErrorResponses { + schemaRef := o.getOrCreateSchemaComponent(resp, schemaRegistry, spec.Components.Schemas) + apiResponse := &openapi3.Response{ + Description: ptr(http.StatusText(key)), + Content: openapi3.NewContentWithJSONSchemaRef(schemaRef), + } + op.Responses.Set(strconv.Itoa(key), &openapi3.ResponseRef{ + Value: apiResponse, + }) + } + } else { + // Add default responses + o.addDefaultErrorResponses(op, r) + } // Assign operation to correct HTTP verb switch r.Method { From ed4f93c44810ad37f31188583d0ecdecd664bb8e Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Fri, 6 Jun 2025 12:33:03 +0200 Subject: [PATCH 2/8] feat(openapi): add auto-generate response errors --- okapi.go | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/okapi.go b/okapi.go index 74b1e5b..fcde73d 100644 --- a/okapi.go +++ b/okapi.go @@ -58,30 +58,30 @@ type ( // Okapi represents the core application structure of the framework, // holding configuration, routers, middleware, server settings, and documentation components. Okapi struct { - context *Context // context manages request-scoped data and utilities. - router *Router // router handles route registration and HTTP method dispatching. - middlewares []Middleware // middlewares is the list of global middlewares applied to all routes. - Server *http.Server // Server is the primary HTTP server instance. - TLSServer *http.Server // TLSServer is the optional HTTPS server instance (if TLS is enabled). - tlsConfig *tls.Config // tlsConfig holds the TLS configuration for the main server. - tlsServerConfig *tls.Config // tlsServerConfig holds the TLS configuration for the optional TLS server. - withTlsServer bool // withTlsServer indicates whether the optional TLS server is enabled. - tlsAddr string // tlsAddr specifies the bind address for the TLS server. - routes []*Route // routes is a list of all registered routes in the application. - debug bool // debug enables verbose logging and debug features. - accessLog bool // accessLog enables logging of all incoming HTTP requests. - strictSlash bool // strictSlash enforces trailing slash consistency on routes. - logger *slog.Logger // logger is the structured logger used across the framework. - Renderer Renderer // Renderer defines how response data - corsEnabled bool // corsEnabled toggles automatic CORS handling. - cors Cors // cors contains the configuration for CORS handling. - writeTimeout int // writeTimeout sets the maximum duration before timing out writes (in seconds). - readTimeout int // readTimeout sets the maximum duration for reading the entire request (in seconds). - idleTimeout int // idleTimeout sets the maximum idle time before closing a keep-alive connection (in seconds). - optionsRegistered map[string]bool // optionsRegistered tracks which routes have automatically registered OPTIONS handlers. - openapiSpec *openapi3.T // openapiSpec holds the generated OpenAPI spec (v3) for documentation and tooling. - openAPI *OpenAPI // openAPI manages OpenAPI generation, UI handlers, and route metadata. - openApiEnabled bool // openApiEnabled toggles OpenAPI generation and exposure (e.g., `/docs`, `/openapi.json`). + context *Context + router *Router + middlewares []Middleware + Server *http.Server + TLSServer *http.Server + tlsConfig *tls.Config + tlsServerConfig *tls.Config + withTlsServer bool + tlsAddr string + routes []*Route + debug bool + accessLog bool + strictSlash bool + logger *slog.Logger + Renderer Renderer + corsEnabled bool + cors Cors + writeTimeout int + readTimeout int + idleTimeout int + optionsRegistered map[string]bool + openapiSpec *openapi3.T + openAPI *OpenAPI + openApiEnabled bool } Router struct { From 8687fcd6fa3e8754476358cbabdb550308b43e74 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Fri, 6 Jun 2025 17:36:20 +0200 Subject: [PATCH 3/8] add sample example --- .gitignore | 3 +- README.md | 97 ++++++++++++++++++++++++++++++++++------- examples/sample/main.go | 45 ++++++++++++++----- openapi.go | 2 +- 4 files changed, 120 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 5ee04d2..29aca0e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ Makefile uploads public .DS_Store -/.history \ No newline at end of file +/.history +coverage.txt \ No newline at end of file diff --git a/README.md b/README.md index 1109089..b327791 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ Whether you're building your next startup, internal tools, or side projects—** ```bash mkdir myapi && cd myapi go mod init myapi +``` + +```sh go get github.com/jkaninda/okapi ``` @@ -85,35 +88,69 @@ Create a file named `main.go`: package main import ( - "net/http" - "github.com/jkaninda/okapi" + "net/http" + "github.com/jkaninda/okapi" + "time" ) -func main() { - o := okapi.Default() - - o.Get("/", func(c okapi.Context) error { - return c.OK(okapi.M{"message": "Welcome to Okapi!"}) - }, - okapi.DocSummary("Welcome page"), - ) +type Response struct { + Success bool `json:"success"` + Message string `json:"message"` + Data any `json:"data"` +} +type ErrorResponse struct { + Success bool `json:"success"` + Status int `json:"status"` + Message interface{} `json:"message"` +} - if err := o.Start(); err != nil { - panic(err) - } +func main() { + // Create a new Okapi instance with default config + o := okapi.Default() + + o.Get("/", func(c okapi.Context) error { + resp := Response{ + Success: true, + Message: "Welcome to Okapi!", + Data: okapi.M{ + "name": "Okapi Web Framework", + "Licence": "MIT", + "date": time.Now(), + }, + } + return c.OK(resp) + }, + // OpenAPI Documentation + okapi.DocSummary("Welcome page"), + okapi.DocResponse(Response{}), // Success Response body + okapi.DocErrorResponse(http.StatusBadRequest, ErrorResponse{}), // Error response body + + ) + // Start the server + if err := o.Start(); err != nil { + panic(err) + } } ``` Run your server: ```bash -go run . +go run main.go ``` Visit [`http://localhost:8080`](http://localhost:8080) to see the response: ```json -{"message": "Welcome to Okapi!"} +{ + "success": true, + "message": "Welcome to Okapi!", + "data": { + "Licence": "MIT", + "date": "2025-06-06T16:58:45.44795+02:00", + "name": "Okapi Web Framework" + } +} ``` Visit [`http://localhost:8080/docs/`](http://localhost:8080/docs/) to see the documentation @@ -150,6 +187,20 @@ admin := api.Group("/admin", adminMiddleware) admin.Get("/dashboard", getDashboard) ``` +### Path Syntax Examples + +Okapi supports flexible and expressive route path patterns, including named parameters and wildcards: + +```go +o.Get("/books/{id}", getBook) // Named path parameter using curly braces +o.Get("/books/:id", getBook) // Named path parameter using colon prefix +o.Get("/*", getBook) // Catch-all wildcard (matches everything) +o.Get("/*any", getBook) // Catch-all with named parameter (name is ignored) +o.Get("/*path", getBook) // Catch-all with named parameter +``` + +Use whichever syntax feels most natural — Okapi normalizes both `{}` and `:` styles for named parameters and supports glob-style wildcards for flexible matching. + --- ## Request Handling @@ -556,6 +607,22 @@ o.Static("/static", "public/assets") } ``` +### Explore Another Project: Goma Gateway + +Are you building a microservices architecture? +Do you need a powerful yet lightweight API Gateway to secure and manage your services effortlessly? + +Check out my other project — **[Goma Gateway](https://github.com/jkaninda/goma-gateway)**. + +**Goma Gateway** is a high-performance, declarative API Gateway designed for modern microservices. It includes a rich set of built-in middleware for: + +* Security: ForwardAuth, Basic Auth, JWT, OAuth +* Caching and rate limiting +* Simple configuration, minimal overhead + +Whether you're managing internal APIs or exposing public endpoints, Goma Gateway helps you do it cleanly and securely. + + --- ## Contributing diff --git a/examples/sample/main.go b/examples/sample/main.go index 2c7512e..1b1daf8 100644 --- a/examples/sample/main.go +++ b/examples/sample/main.go @@ -27,32 +27,57 @@ package main import ( "github.com/jkaninda/okapi" "net/http" + "time" ) -type User struct { - Name string - Phone string +type Response struct { + Success bool `json:"success"` + Message string `json:"message"` + Data any `json:"data"` +} +type ErrorResponse struct { + Success bool `json:"success"` + Status int `json:"status"` + Message interface{} `json:"message"` } func main() { - // Example usage of the Okapi framework - // Create a new Okapi instance + // Create a new Okapi instance with default config o := okapi.Default() o.Get("/", func(c okapi.Context) error { - return c.JSON(http.StatusOK, okapi.M{"message": "Welcome to Okapi!"}) + resp := Response{ + Success: true, + Message: "Welcome to Okapi!", + Data: okapi.M{ + "name": "Okapi Web Framework", + "Licence": "MIT", + "date": time.Now(), + }, + } + return c.OK(resp) }, - okapi.Doc().Summary("Welcome page").Build(), + // OpenAPI Documentation + okapi.DocSummary("Welcome page"), + okapi.DocResponse(Response{}), // Success Response body + okapi.DocErrorResponse(http.StatusBadRequest, ErrorResponse{}), // Error response body ) o.Get("/greeting/:name", greetingHandler) // Start the server - err := o.Start() - if err != nil { + if err := o.Start(); err != nil { panic(err) } } func greetingHandler(c okapi.Context) error { name := c.Param("name") - return c.JSON(http.StatusOK, okapi.M{"message": "Hello " + name}) + if name == "" { + errorResponse := ErrorResponse{ + Success: false, + Status: http.StatusBadRequest, + Message: "name is empty", + } + return c.ErrorBadRequest(errorResponse) + } + return c.OK(okapi.M{"message": "Hello " + name}) } diff --git a/openapi.go b/openapi.go index a83d015..e9c1f95 100644 --- a/openapi.go +++ b/openapi.go @@ -526,7 +526,7 @@ func (o *Okapi) buildOpenAPISpec() { op.Responses.Set("200", &openapi3.ResponseRef{Value: apiResponse}) } - if r.ErrorResponses != nil && len(r.ErrorResponses) != 0 { + if len(r.ErrorResponses) != 0 { for key, resp := range r.ErrorResponses { schemaRef := o.getOrCreateSchemaComponent(resp, schemaRegistry, spec.Components.Schemas) apiResponse := &openapi3.Response{ From 758d2db117ed8a92588e493d2798d6b5e1fad21c Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Fri, 6 Jun 2025 18:20:33 +0200 Subject: [PATCH 4/8] ci: add coverage analytics --- .github/workflows/tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70b1ed6..5924616 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,9 +2,6 @@ name: Tests on: push: - branches: - - main - - develop pull_request: branches: - main @@ -20,7 +17,10 @@ jobs: go-version: '1.24.3' - name: Test - run: go test -v ./... + run: go test -coverprofile=coverage.txt - - name: Build - run: go build -v ./... \ No newline at end of file + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: jkaninda/okapi \ No newline at end of file From fe2999b84c58003a316de360f28fa65f576bdf48 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Fri, 6 Jun 2025 19:20:44 +0200 Subject: [PATCH 5/8] feat: add max multipart memory --- binder.go | 4 +- context.go | 31 ++++++++++---- examples/middleware/main.go | 1 - examples/multipart/main.go | 11 ++--- okapi.go | 83 +++++++++++++++++++++---------------- 5 files changed, 76 insertions(+), 54 deletions(-) diff --git a/binder.go b/binder.go index 781c6fd..5f932a6 100644 --- a/binder.go +++ b/binder.go @@ -95,7 +95,7 @@ func (c *Context) Bind(out any) error { } func (c *Context) BindMultipart(out any) error { - if err := c.Request.ParseMultipartForm(c.MaxMultipartMemory); err != nil { + if err := c.Request.ParseMultipartForm(c.okapi.maxMultipartMemory); err != nil { return fmt.Errorf("invalid multipart form: %w", err) } @@ -257,7 +257,7 @@ func (c *Context) bindFileFieldWithStatus(tag string, valField reflect.Value, fi func (c *Context) bindMultipleFilesWithStatus(tag string, valField reflect.Value) (bool, error) { // Get the multipart form if c.Request.MultipartForm == nil { - if err := c.Request.ParseMultipartForm(c.MaxMultipartMemory); err != nil { + if err := c.Request.ParseMultipartForm(c.okapi.maxMultipartMemory); err != nil { return false, fmt.Errorf("failed to parse multipart form: %w", err) } } diff --git a/context.go b/context.go index 794d745..612b0bc 100644 --- a/context.go +++ b/context.go @@ -48,9 +48,8 @@ type ( // Response http.ResponseWriter Response Response // CtxData is a key/value store for storing data in the context - CtxData map[string]any - MaxMultipartMemory int64 // Maximum memory for multipart forms - params *Params + CtxData map[string]any + params *Params } ) @@ -149,11 +148,10 @@ func (c *Context) GetInt64(key string) int64 { // Maintains thread safety during the copy operation. func (c *Context) Copy() *Context { newCtx := &Context{ - Request: c.Request, // Copy request reference - Response: c.Response, // Copy response reference - CtxData: make(map[string]any, len(c.CtxData)), // Initialize new data map - params: c.params, // Copy params - MaxMultipartMemory: c.MaxMultipartMemory, // Copy max memory for multipart forms + Request: c.Request, // Copy request reference + Response: c.Response, // Copy response reference + CtxData: make(map[string]any, len(c.CtxData)), // Initialize new data map + params: c.params, // Copy params } // Copy all key-value pairs to the new context for k, v := range c.CtxData { @@ -234,13 +232,14 @@ func (c *Context) Form(key string) string { // FormValue retrieves a form value, including multipart form data. func (c *Context) FormValue(key string) string { - _ = c.Request.ParseMultipartForm(defaultMaxMemory) // Parse multipart form + _ = c.Request.ParseMultipartForm(c.okapi.maxMultipartMemory) // Parse multipart form return c.Request.FormValue(key) } // FormFile retrieves a file from multipart form data. // Returns the file and any error encountered. func (c *Context) FormFile(key string) (*multipart.FileHeader, error) { + _ = c.Request.ParseMultipartForm(c.okapi.maxMultipartMemory) f, fh, err := c.Request.FormFile(key) if err != nil { return nil, err @@ -457,4 +456,18 @@ func (c *Context) ServeFileInline(path, filename string) { http.ServeFile(c.Response, c.Request, path) } +// *********** MultipartMemory ************** + +// MaxMultipartMemory returns the maximum memory for multipart form +func (c *Context) MaxMultipartMemory() int64 { + return c.okapi.maxMultipartMemory +} + +// SetMaxMultipartMemory sets the maximum memory for multipart form (default: 32 MB) +func (c *Context) SetMaxMultipartMemory(max int64) { + if max > 0 { + c.okapi.maxMultipartMemory = max + } +} + // ************ Errors in errors.go ***************** diff --git a/examples/middleware/main.go b/examples/middleware/main.go index 9e28209..45c9307 100644 --- a/examples/middleware/main.go +++ b/examples/middleware/main.go @@ -55,7 +55,6 @@ var ( func main() { // Example usage of middlewares handling in Okapi // Create a new Okapi instance - // Disable access log for cleaner output in this example o := okapi.New().WithOpenAPIDocs() o.Get("/", func(c okapi.Context) error { diff --git a/examples/multipart/main.go b/examples/multipart/main.go index c630686..8ae0c67 100644 --- a/examples/multipart/main.go +++ b/examples/multipart/main.go @@ -102,16 +102,11 @@ func main() { multipartBody := &MultipartBody{} // Parse the multipart form data if err := c.Bind(multipartBody); err != nil { - return c.JSON(400, okapi.M{ + return c.ErrorBadRequest(okapi.M{ "error": "Failed to parse form data", "details": err.Error(), }) } - - err := c.Request.ParseMultipartForm(32 << 20) - if err != nil { - return c.Error(400, err.Error()) - } // Access the uploaded file file := *multipartBody.Avatar @@ -154,7 +149,9 @@ func main() { _, err = f.Write(fileBytes) return c.HTMLView(200, successTemplate, okapi.M{}) - }) + }, + okapi.DocRequestBody(MultipartBody{}), + ) // Start the server err := o.Start() diff --git a/okapi.go b/okapi.go index fcde73d..401537b 100644 --- a/okapi.go +++ b/okapi.go @@ -58,30 +58,32 @@ type ( // Okapi represents the core application structure of the framework, // holding configuration, routers, middleware, server settings, and documentation components. Okapi struct { - context *Context - router *Router - middlewares []Middleware - Server *http.Server - TLSServer *http.Server - tlsConfig *tls.Config - tlsServerConfig *tls.Config - withTlsServer bool - tlsAddr string - routes []*Route - debug bool - accessLog bool - strictSlash bool - logger *slog.Logger - Renderer Renderer - corsEnabled bool - cors Cors - writeTimeout int - readTimeout int - idleTimeout int - optionsRegistered map[string]bool - openapiSpec *openapi3.T - openAPI *OpenAPI - openApiEnabled bool + context *Context + router *Router + middlewares []Middleware + Server *http.Server + TLSServer *http.Server + tlsConfig *tls.Config + tlsServerConfig *tls.Config + withTlsServer bool + tlsAddr string + routes []*Route + debug bool + accessLog bool + strictSlash bool + logger *slog.Logger + Renderer Renderer + corsEnabled bool + cors Cors + writeTimeout int + readTimeout int + idleTimeout int + optionsRegistered map[string]bool + openapiSpec *openapi3.T + openAPI *OpenAPI + openApiEnabled bool + maxMultipartMemory int64 // Maximum memory for multipart forms + } Router struct { @@ -316,6 +318,13 @@ func WithOpenAPIDisabled() OptionFunc { } } +// WithMaxMultipartMemory Maximum memory for multipart forms +func WithMaxMultipartMemory(max int64) OptionFunc { + return func(o *Okapi) { + o.maxMultipartMemory = max + } +} + // ************* Chaining methods ************* // These methods reuse the OptionFunc implementations @@ -358,6 +367,9 @@ func (o *Okapi) WithAddr(addr string) *Okapi { func (o *Okapi) DisableAccessLog() *Okapi { return o.apply(WithAccessLogDisabled()) } +func (o *Okapi) WithMaxMultipartMemory(max int64) *Okapi { + return o.apply(WithMaxMultipartMemory(max)) +} // WithOpenAPIDocs registers the OpenAPI JSON and Swagger UI handlers // at the configured PathPrefix (default: /docs). @@ -987,18 +999,19 @@ func initConfig(options ...OptionFunc) *Okapi { o := &Okapi{ context: &Context{ - Request: new(http.Request), - Response: &response{}, - MaxMultipartMemory: defaultMaxMemory, + Request: new(http.Request), + Response: &response{}, + CtxData: make(map[string]interface{}), }, - router: newRouter(), - Server: server, - TLSServer: &http.Server{}, - logger: slog.Default(), - accessLog: true, - middlewares: []Middleware{handleAccessLog}, - optionsRegistered: make(map[string]bool), - cors: Cors{}, + router: newRouter(), + Server: server, + TLSServer: &http.Server{}, + logger: slog.Default(), + accessLog: true, + middlewares: []Middleware{handleAccessLog}, + optionsRegistered: make(map[string]bool), + maxMultipartMemory: defaultMaxMemory, + cors: Cors{}, openAPI: &OpenAPI{ Title: FrameworkName, Version: "1.0.0", From be25a07de1d47ed6bef663545aed07c7a4226c3f Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Sun, 8 Jun 2025 06:58:18 +0200 Subject: [PATCH 6/8] feat: support stdlib handlers in Okapi routes, mark OpenAPI routes as deprecated --- README.md | 92 ++++++++++++++++++++++++++++++++++++++- okapi.go | 125 +++++++++++++++++++++++++++++++++++++++-------------- openapi.go | 14 ++++++ 3 files changed, 197 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index b327791..41ee1bb 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,7 @@ o := okapi.New(okapi.WithCors(cors)) ### Custom Middleware ```go -func logger(next okapi.HandlerFunc) okapi.HandlerFunc { +func customMiddleware(next okapi.HandlerFunc) okapi.HandlerFunc { return func(c okapi.Context) error { start := time.Now() err := next(c) @@ -340,7 +340,7 @@ func logger(next okapi.HandlerFunc) okapi.HandlerFunc { } } -o.Use(logger) +o.Use(customMiddleware) ``` --- @@ -429,6 +429,7 @@ o.Post("/books", createBookHandler, | `DocQueryParam()`/`Doc().QueryParam()` | Documents query parameters | | `DocHeader()`/ `Doc().Header()` | Documents header parameters | | `DocErrorResponse()`/`Doc().ErrorResponse()` | Documents response error | +| `DocDeprecated()`/`Doc().Deprecated()` | Mark route deprecated | ### Swagger UI Preview @@ -606,6 +607,93 @@ o.Static("/static", "public/assets") } } ``` +--- + +## Standard Library Compatibility + +**Okapi** integrates seamlessly with Go’s `net/http` standard library, enabling you to: + +1. Use existing `http.Handler` middleware +2. Register standard `http.HandlerFunc` handlers +3. Combine Okapi-style routes with standard library handlers + +This makes Okapi ideal for gradual adoption or hybrid use in existing Go projects. + + +### Middleware Compatibility + +Okapi’s `UseMiddleware` bridges standard `http.Handler` middleware into Okapi’s middleware system. This lets you reuse the wide ecosystem of community-built middleware—such as logging, metrics, tracing, compression, and more. + +#### Signature + +```go +func (o *Okapi) UseMiddleware(middleware func(http.Handler) http.Handler) +``` + +#### Example: Injecting a Custom Header + +```go +o := okapi.Default() + +// Add a custom version header to all responses +o.UseMiddleware(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Version", "v1.2.0") + next.ServeHTTP(w, r) + }) +}) +``` + +### Handler Compatibility + +You can register any `http.HandlerFunc` using `HandleStd`, or use full `http.Handler` instances via `HandleHTTP`. These retain Okapi’s routing and middleware features while supporting familiar handler signatures. + +#### HandleStd Signature + +```go +func (o *Okapi) HandleStd(method, path string, handler http.HandlerFunc, opts ...RouteOption) +``` + +#### Example: Basic Standard Library Handler + +```go +o := okapi.Default() + +o.HandleStd("GET", "/greeting", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello from Okapi!")) +}) +``` + +--- + +### Migration Tips + +Migrating an existing `net/http` application? Okapi makes it painless. + +#### Mixed Routing Support + +You can mix Okapi and standard handlers in the same application: + +```go +// Okapi-style route +o.Handle("GET", "/okapi", func(c okapi.Context) error { + return c.OK(okapi.M{"status": "ok"}) +}) + +// Standard library handler +o.HandleStd("GET", "/standard", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("standard response")) +}) +``` + + +#### Error Handling Differences +* `http.HandlerFunc`: must manually call `w.WriteHeader(...)` +* `okapi.Handle`: can return an error or use helpers like `c.JSON`, `c.Text`, `c.OK`, `c.ErrorNotFound()` or `c.AbortBadRequest()` + + +--- ### Explore Another Project: Goma Gateway diff --git a/okapi.go b/okapi.go index 401537b..2932f57 100644 --- a/okapi.go +++ b/okapi.go @@ -111,6 +111,7 @@ type ( QueryParams []*openapi3.ParameterRef Headers []*openapi3.ParameterRef RequiresAuth bool + deprecated bool RequestExample map[string]interface{} ResponseExample map[string]interface{} ErrorResponses map[int]*openapi3.SchemaRef @@ -553,6 +554,47 @@ func (o *Okapi) Use(middlewares ...Middleware) { o.middlewares = append(o.middlewares, middlewares...) } +// UseMiddleware registers a standard HTTP middleware function and integrates +// it into Okapi's middleware chain. +// +// This enables compatibility with existing middleware libraries that use the +// func(http.Handler) http.Handler pattern. +// +// Example: +// +// okapi.UseMiddleware(func(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// w.Header().Set("X-Powered-By", "Okapi") +// next.ServeHTTP(w, r) +// }) +// }) +// +// Internally, Okapi converts between http.Handler and HandleFunc to allow smooth interop. +func (o *Okapi) UseMiddleware(mw func(http.Handler) http.Handler) { + o.Use(func(next HandleFunc) HandleFunc { + // Convert HandleFunc to http.Handler + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := Context{ + Request: r, + Response: &response{writer: w}, + okapi: o, + } + if err := next(ctx); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) + + // Apply standard middleware + wrapped := mw(h) + + // Convert back to HandleFunc + return func(ctx Context) error { + wrapped.ServeHTTP(ctx.Response, ctx.Request) + return nil + } + }) +} + // StartServer starts the Okapi server with the specified HTTP server func (o *Okapi) StartServer(server *http.Server) error { if !ValidateAddr(server.Addr) { @@ -754,36 +796,39 @@ func (o *Okapi) addRoute(method, path, groupPath string, h HandleFunc, opts ...R return route } -// HandleFunc registers a new route with the specified HTTP method, path, and handler function. -// It performs the following operations: +// Handle registers a new route with the given HTTP method, path, and Okapi-style handler function. +// +// It performs the following steps: // 1. Normalizes the route path -// 2. Creates a new Route instance -// 3. Applies all registered middlewares to the handler -// 4. Registers the route with the underlying router -// 5. Sets up error handling for the handler +// 2. Creates and configures a new Route instance +// 3. Applies all registered middleware to the handler +// 4. Registers the route with the underlying router (with method filtering) +// 5. Adds centralized error handling for the route // // Parameters: -// - method: HTTP method (GET, POST, PUT, etc.) -// - path: URL path pattern (supports path parameters) -// - h: Handler function that processes the request +// - method: HTTP method (e.g., "GET", "POST", "PUT") +// - path: URL path pattern (supports path parameters, e.g., /users/:id) +// - h: A handler function using Okapi's Context abstraction +// - opts: Optional route metadata (e.g., OpenAPI summary, description, tags) // -// Middleware Processing: +// Middleware: // -// All middlewares registered via Use() will be applied in order before the handler. -// The middleware chain is built using the Next() method. +// All middleware registered via Use() or UseMiddleware() will be applied in order, +// wrapping around the handler. // // Error Handling: // -// Any error returned by the handler will be converted to a 500 Internal Server Error. +// Any non-nil error returned by the handler will automatically result in a +// 500 Internal Server Error response. // // Example: // -// okapi.HandleFunc("GET", "/users/:id", func(c Context) error { +// okapi.Handle("GET", "/users/:id", func(c Context) error { // id := c.Param("id") -// // ... handler logic +// // process request... // return nil // }) -func (o *Okapi) HandleFunc(method, path string, h HandleFunc, opts ...RouteOption) { +func (o *Okapi) Handle(method, path string, h HandleFunc, opts ...RouteOption) { path = normalizeRoutePath(path) route := &Route{ @@ -816,28 +861,26 @@ func (o *Okapi) HandleFunc(method, path string, h HandleFunc, opts ...RouteOptio } -// Handle registers a new route with the specified HTTP method, path, and http.Handler. -// It wraps the standard http.Handler into a HandleFunc signature and processes it similarly to HandleFunc. -// -// Parameters: -// - method: HTTP method (GET, POST, PUT, etc.) -// - path: URL path pattern (supports path parameters) -// - h: Standard http.Handler that processes the request +// HandleHTTP registers a new route using a standard http.Handler. // -// Middleware Processing: +// It wraps the provided http.Handler into Okapi's internal HandleFunc signature +// and processes it as if it were registered via Handle. // -// All middlewares registered via Use() will be applied in order before the handler. -// The middleware chain is built using the Next() method. +// Parameters: +// - method: HTTP method (e.g., "GET", "POST", "DELETE") +// - path: URL path pattern (supports dynamic segments) +// - h: A standard http.Handler (or http.HandlerFunc) +// - opts: Optional route metadata (e.g., OpenAPI summary, description, tags) // -// Differences from HandleFunc: -// - Accepts standard http.Handler instead of HandleFunc -// - Handler errors must be handled by the http.Handler itself -// - Returns nil error by default since http.Handler doesn't return errors +// Differences from Handle: +// - Uses the standard http.Handler interface +// - Middleware is still applied +// - Errors must be handled inside the handler itself (Okapi will not capture them) // // Example: // -// okapi.Handle("GET", "/static", http.FileServer(http.Dir("./public"))) -func (o *Okapi) Handle(method, path string, h http.Handler, opts ...RouteOption) { +// okapi.HandleHTTP("GET", "/static", http.FileServer(http.Dir("./public"))) +func (o *Okapi) HandleHTTP(method, path string, h http.Handler, opts ...RouteOption) { path = normalizeRoutePath(path) // Wrap http.Handler into HandleFunc signature @@ -880,6 +923,24 @@ func (o *Okapi) Handle(method, path string, h http.Handler, opts ...RouteOption) o.registerOptionsHandler(path) } +// HandleStd is a convenience method for registering handlers using the standard +// http.HandlerFunc signature (func(http.ResponseWriter, *http.Request)). +// +// Internally, it wraps the handler into http.Handler and delegates to HandleHTTP. +// +// Example: +// +// okapi.HandleStd("GET", "/greet", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("Hello from Okapi!")) +// }) +// +// This handler will still benefit from: +// - All registered middleware +// - Automatic route and CORS registration +func (o *Okapi) HandleStd(method, path string, h func(http.ResponseWriter, *http.Request), opts ...RouteOption) { + o.HandleHTTP(method, path, http.HandlerFunc(h), opts...) +} + // registerOptionsHandler registers OPTIONS handler func (o *Okapi) registerOptionsHandler(path string) { // Register OPTIONS handler only once per path if CORS is enabled diff --git a/openapi.go b/openapi.go index e9c1f95..6597441 100644 --- a/openapi.go +++ b/openapi.go @@ -215,6 +215,12 @@ func (b *DocBuilder) BearerAuth() *DocBuilder { return b } +// Deprecated marks the route as deprecated +func (b *DocBuilder) Deprecated() *DocBuilder { + b.options = append(b.options, DocDeprecated()) + return b +} + // PathParam adds a documented path parameter to the route. // name: parameter name // typ: parameter type (e.g., "string", "int") @@ -424,6 +430,13 @@ func DocBearerAuth() RouteOption { } } +// DocDeprecated marks the route as deprecated +func DocDeprecated() RouteOption { + return func(doc *Route) { + doc.deprecated = true + } +} + // buildOpenAPISpec constructs the complete OpenAPI specification document // by aggregating all the route documentation into a single OpenAPI 3.0 spec func (o *Okapi) buildOpenAPISpec() { @@ -482,6 +495,7 @@ func (o *Okapi) buildOpenAPISpec() { Tags: tags, Parameters: append(append(r.PathParams, r.QueryParams...), r.Headers...), Responses: &openapi3.Responses{}, + Deprecated: r.deprecated, } if r.RequiresAuth { From 737f2176c812f66611bf2489a0a6b7e21d349e5a Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Sun, 8 Jun 2025 07:35:42 +0200 Subject: [PATCH 7/8] docs: update example --- README.md | 67 ++++++++++++++++++++++++++---------- context.go | 5 +++ examples/sample/main.go | 75 +++++++++++++---------------------------- 3 files changed, 77 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 41ee1bb..e6693a2 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,9 @@ Built for **speed, simplicity, and real-world use**—whether you're prototyping * **Easy to Learn** – Familiar Go syntax and intuitive APIs mean you’re productive in minutes. * **Highly Flexible** – Designed to adapt to your architecture and workflow—not the other way around. * **Built for Production** – Lightweight, fast, and reliable under real-world load. - +* **Standard Library Compatibility** - Integrates seamlessly with Go’s net/http standard library, making it easy to combine Okapi with existing Go code and tools. +* **Automatic OpenAPI Documentation** - Generate comprehensive, first-class OpenAPI specs for every route—effortlessly keep your docs in sync with your code +* **Dynamic Route Management** - Instantly enable or disable routes and route groups at runtime, offering a clean, efficient alternative to commenting out code when managing your API endpoints. Ideal for: @@ -75,7 +77,7 @@ go mod init myapi ``` ```sh -go get github.com/jkaninda/okapi +go get github.com/jkaninda/okapi@latest ``` --- @@ -84,45 +86,74 @@ go get github.com/jkaninda/okapi Create a file named `main.go`: +### Example + +#### Hello + +```go +package main + +import ( + "github.com/jkaninda/okapi" +) +func main() { + + o := okapi.Default() + + o.Get("/", func(c okapi.Context) error { + return c.OK(okapi.M{"message": "Hello from Okapi Web Framework!","Licence":"MIT"}) + }) + // Start the server + if err := o.Start(); err != nil { + panic(err) + } +} +``` +#### Simple HTTP POST ```go package main import ( - "net/http" "github.com/jkaninda/okapi" - "time" + "net/http" ) type Response struct { Success bool `json:"success"` Message string `json:"message"` - Data any `json:"data"` + Data Book `json:"data"` +} +type Book struct { + Name string `json:"name" form:"name" max:"50" required:"true" description:"Book name"` + Price int `json:"price" form:"price" query:"price" yaml:"price" required:"true" description:"Book price"` } type ErrorResponse struct { Success bool `json:"success"` Status int `json:"status"` - Message interface{} `json:"message"` + Details any `json:"details"` } func main() { // Create a new Okapi instance with default config o := okapi.Default() - o.Get("/", func(c okapi.Context) error { - resp := Response{ + o.Post("/books", func(c okapi.Context) error { + book := Book{} + err := c.Bind(&book) + if err != nil { + return c.ErrorBadRequest(ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()}) + } + response := Response{ Success: true, - Message: "Welcome to Okapi!", - Data: okapi.M{ - "name": "Okapi Web Framework", - "Licence": "MIT", - "date": time.Now(), - }, + Message: "This is a simple HTTP POST", + Data: book, } - return c.OK(resp) + return c.OK(response) }, // OpenAPI Documentation - okapi.DocSummary("Welcome page"), - okapi.DocResponse(Response{}), // Success Response body + okapi.DocSummary("Create a new Book"), + okapi.DocRequestBody(Book{}), // Request body + okapi.DocResponse(Response{}), // Success Response body okapi.DocErrorResponse(http.StatusBadRequest, ErrorResponse{}), // Error response body ) @@ -434,7 +465,7 @@ o.Post("/books", createBookHandler, ### Swagger UI Preview -Okapi automatically generates Swagger UI for all documented routes: +Okapi automatically generates Swagger UI for all routes: ![Okapi Swagger Interface](https://raw.githubusercontent.com/jkaninda/okapi/main/swagger.png) diff --git a/context.go b/context.go index 612b0bc..567d6d2 100644 --- a/context.go +++ b/context.go @@ -336,6 +336,11 @@ func (c *Context) OK(v any) error { return c.JSON(http.StatusOK, v) } +// Created writes a JSON response with 201 status code. +func (c *Context) Created(v any) error { + return c.JSON(http.StatusCreated, v) +} + // XML writes an XML response with the given status code. func (c *Context) XML(code int, v any) error { return c.writeResponse(code, XML, func() error { diff --git a/examples/sample/main.go b/examples/sample/main.go index 1b1daf8..a915bf9 100644 --- a/examples/sample/main.go +++ b/examples/sample/main.go @@ -1,44 +1,23 @@ -/* - * MIT License - * - * Copyright (c) 2025 Jonas Kaninda - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - package main import ( "github.com/jkaninda/okapi" "net/http" - "time" ) type Response struct { Success bool `json:"success"` Message string `json:"message"` - Data any `json:"data"` + Data Book `json:"data"` +} +type Book struct { + Name string `json:"name" form:"name" max:"50" required:"true" description:"Book name"` + Price int `json:"price" form:"price" query:"price" yaml:"price" required:"true" description:"Book price"` } type ErrorResponse struct { - Success bool `json:"success"` - Status int `json:"status"` - Message interface{} `json:"message"` + Success bool `json:"success"` + Status int `json:"status"` + Details any `json:"details"` } func main() { @@ -46,38 +25,30 @@ func main() { o := okapi.Default() o.Get("/", func(c okapi.Context) error { - resp := Response{ + return c.OK(okapi.M{"message": "Hello from Okapi Web Framework!", "Licence": "MIT"}) + }) + o.Post("/books", func(c okapi.Context) error { + book := Book{} + err := c.Bind(&book) + if err != nil { + return c.ErrorBadRequest(ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()}) + } + response := Response{ Success: true, - Message: "Welcome to Okapi!", - Data: okapi.M{ - "name": "Okapi Web Framework", - "Licence": "MIT", - "date": time.Now(), - }, + Message: "This is a simple HTTP POST", + Data: book, } - return c.OK(resp) + return c.OK(response) }, // OpenAPI Documentation - okapi.DocSummary("Welcome page"), + okapi.DocSummary("create a new Book"), + okapi.DocRequestBody(Book{}), // Request body okapi.DocResponse(Response{}), // Success Response body okapi.DocErrorResponse(http.StatusBadRequest, ErrorResponse{}), // Error response body - ) - o.Get("/greeting/:name", greetingHandler) + ) // Start the server if err := o.Start(); err != nil { panic(err) } } -func greetingHandler(c okapi.Context) error { - name := c.Param("name") - if name == "" { - errorResponse := ErrorResponse{ - Success: false, - Status: http.StatusBadRequest, - Message: "name is empty", - } - return c.ErrorBadRequest(errorResponse) - } - return c.OK(okapi.M{"message": "Hello " + name}) -} From ab8a70a5044a13cbe4606d3b1e5ea5708d8271a5 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Sun, 8 Jun 2025 07:36:48 +0200 Subject: [PATCH 8/8] docs: update example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6693a2..5578e5d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Built for **speed, simplicity, and real-world use**—whether you're prototyping * **Highly Flexible** – Designed to adapt to your architecture and workflow—not the other way around. * **Built for Production** – Lightweight, fast, and reliable under real-world load. * **Standard Library Compatibility** - Integrates seamlessly with Go’s net/http standard library, making it easy to combine Okapi with existing Go code and tools. -* **Automatic OpenAPI Documentation** - Generate comprehensive, first-class OpenAPI specs for every route—effortlessly keep your docs in sync with your code +* **Automatic OpenAPI Documentation** - Generate comprehensive, first-class OpenAPI specs for every route—effortlessly keep your docs in sync with your code. * **Dynamic Route Management** - Instantly enable or disable routes and route groups at runtime, offering a clean, efficient alternative to commenting out code when managing your API endpoints. Ideal for: