diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index c6ccd9a6c..269b3c1e4 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -42,8 +42,23 @@ jobs: uses: dorny/paths-filter@v3 with: filters: ${{ steps.filter-setup.outputs.filters }} + + - name: Set packages + id: set-packages + run: | + # If this is a push to master or main, build a JSON array of all packages. + # Otherwise, use the filtered list of changed packages. + if [[ "${{ github.event_name }}" == "push" ]]; then + packages=$(find v3 -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | awk 'BEGIN{first=1; printf "["} {if(!first) printf ","; printf "\"%s\"", $0; first=0} END{printf "]"}') + else + packages='${{ steps.filter.outputs.changes }}' + fi + echo "packages<> $GITHUB_OUTPUT + echo "$packages" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + shell: bash outputs: - packages: ${{ steps.filter.outputs.changes || '[]' }} + packages: ${{ steps.set-packages.outputs.packages }} lint: needs: changes diff --git a/.github/workflows/test-swaggerui.yml b/.github/workflows/test-swaggerui.yml new file mode 100644 index 000000000..0dde89d20 --- /dev/null +++ b/.github/workflows/test-swaggerui.yml @@ -0,0 +1,34 @@ +name: "Test swaggerui" + +on: + push: + branches: + - master + - main + paths: + - 'v3/swaggerui/**/*.go' + - 'v3/swaggerui/go.mod' + - 'v3/swaggerui/go.sum' + pull_request: + paths: + - 'v3/swaggerui/**/*.go' + - 'v3/swaggerui/go.mod' + - 'v3/swaggerui/go.sum' + +jobs: + Tests: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: + - 1.25.x + steps: + - name: Fetch Repository + uses: actions/checkout@v5 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '${{ matrix.go-version }}' + - name: Run Test + working-directory: ./v3/swaggerui + run: go test -v -race ./... diff --git a/.github/workflows/test-swagger.yml b/.github/workflows/test-swaggo.yml similarity index 67% rename from .github/workflows/test-swagger.yml rename to .github/workflows/test-swaggo.yml index 72494c801..c8f1bad6f 100644 --- a/.github/workflows/test-swagger.yml +++ b/.github/workflows/test-swaggo.yml @@ -1,4 +1,4 @@ -name: "Test swagger" +name: "Test swaggo" on: push: @@ -6,12 +6,14 @@ on: - master - main paths: - - 'v3/swagger/**/*.go' - - 'v3/swagger/go.mod' + - 'v3/swaggo/**/*.go' + - 'v3/swaggo/go.mod' + - 'v3/swaggo/go.sum' pull_request: paths: - - 'v3/swagger/**/*.go' - - 'v3/swagger/go.mod' + - 'v3/swaggo/**/*.go' + - 'v3/swaggo/go.mod' + - 'v3/swaggo/go.sum' jobs: Tests: @@ -28,5 +30,5 @@ jobs: with: go-version: '${{ matrix.go-version }}' - name: Run Test - working-directory: ./v3/swagger + working-directory: ./v3/swaggo run: go test -v -race ./... diff --git a/README.md b/README.md index 0f0110fc2..a4019feee 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ Repository for third party middlewares and service implementations, with depende * [otel (opentelemetry)](./v3/otel/README.md) otel workflow status * [paseto](./v3/paseto/README.md) paseto workflow status * [socket.io](./v3/socketio/README.md) socket.io workflow status -* [swagger](./v3/swagger/README.md) swagger workflow status +* [swaggo](./v3/swaggo/README.md) _(formerly `swagger`)_ swaggo workflow status +* [swaggerui](./v3/swaggerui/README.md) swaggerui workflow status * [websocket](./v3/websocket/README.md) websocket workflow status ## 🥡 Service Implementations diff --git a/go.work b/go.work index 5c1858b5e..3cc2c6083 100644 --- a/go.work +++ b/go.work @@ -15,7 +15,8 @@ use ( ./v3/paseto ./v3/sentry ./v3/socketio -./v3/swagger + ./v3/swaggo +./v3/swaggerui ./v3/testcontainers ./v3/websocket ./v3/zap diff --git a/v3/README.md b/v3/README.md index 64a0f5b7c..606c27ef9 100644 --- a/v3/README.md +++ b/v3/README.md @@ -35,7 +35,8 @@ Repository for third party middlewares and service implementations, with depende * [otel (opentelemetry)](./otel/README.md) otel workflow status * [paseto](./paseto/README.md) paseto workflow status * [socket.io](./socketio/README.md) socket.io workflow status -* [swagger](./swagger/README.md) swagger workflow status +* [swaggo](./swaggo/README.md) _(formerly `swagger`)_ swaggo workflow status +* [swaggerui](./swaggerui/README.md) swaggerui workflow status * [websocket](./websocket/README.md) websocket workflow status ## 🥡 Service Implementations diff --git a/v3/swagger/README.md b/v3/swaggerui/README.md similarity index 82% rename from v3/swagger/README.md rename to v3/swaggerui/README.md index 7baaf6ed9..173f57ed1 100644 --- a/v3/swagger/README.md +++ b/v3/swaggerui/README.md @@ -1,15 +1,15 @@ --- -id: swagger -title: Swagger +id: swaggerui +title: Swagger UI Middleware --- -# Swagger +# Swagger UI Middleware -![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=swagger*) +![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=swaggerui*) [![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) -![Test](https://github.com/gofiber/contrib/workflows/Test%20swagger/badge.svg) +![Test](https://github.com/gofiber/contrib/workflows/Test%20swaggerui/badge.svg) -Swagger middleware for [Fiber](https://github.com/gofiber/fiber). The middleware handles Swagger UI. +Swagger UI middleware for [Fiber](https://github.com/gofiber/fiber). This handler serves pre-generated Swagger/OpenAPI specs via the swagger-ui package. **Compatible with Fiber v3.** @@ -27,7 +27,7 @@ We only support the latest two versions of Go. Visit [https://go.dev/doc/devel/r ### Signatures ```go -func New(config ...swagger.Config) fiber.Handler +func New(config ...swaggerui.Config) fiber.Handler ``` ### Installation @@ -35,9 +35,9 @@ Swagger is tested on the latests [Go versions](https://golang.org/dl/) with supp ```bash go mod init github.com// ``` -And then install the swagger middleware: +And then install the Swagger UI middleware: ```bash -go get github.com/gofiber/contrib/v3/swagger +go get github.com/gofiber/contrib/v3/swaggerui ``` ### Examples @@ -45,30 +45,30 @@ Import the middleware package ```go import ( "github.com/gofiber/fiber/v3" - "github.com/gofiber/contrib/v3/swagger" + "github.com/gofiber/contrib/v3/swaggerui" ) ``` Using the default config: ```go -app.Use(swagger.New()) +app.Use(swaggerui.New()) ``` Using a custom config: ```go -cfg := swagger.Config{ +cfg := swaggerui.Config{ BasePath: "/", FilePath: "./docs/swagger.json", Path: "swagger", Title: "Swagger API Docs", } -app.Use(swagger.New(cfg)) +app.Use(swaggerui.New(cfg)) ``` Use program data for Swagger content: ```go -cfg := swagger.Config{ +cfg := swaggerui.Config{ BasePath: "/", FilePath: "./docs/swagger.json", FileContent: mySwaggerByteSlice, @@ -76,7 +76,7 @@ cfg := swagger.Config{ Title: "Swagger API Docs", } -app.Use(swagger.New(cfg)) +app.Use(swaggerui.New(cfg)) ``` Using multiple instances of Swagger: @@ -84,7 +84,7 @@ Using multiple instances of Swagger: // Create Swagger middleware for v1 // // Swagger will be available at: /api/v1/docs -app.Use(swagger.New(swagger.Config{ +app.Use(swaggerui.New(swaggerui.Config{ BasePath: "/api/v1/", FilePath: "./docs/v1/swagger.json", Path: "docs", @@ -93,7 +93,7 @@ app.Use(swagger.New(swagger.Config{ // Create Swagger middleware for v2 // // Swagger will be available at: /api/v2/docs -app.Use(swagger.New(swagger.Config{ +app.Use(swaggerui.New(swaggerui.Config{ BasePath: "/api/v2/", FilePath: "./docs/v2/swagger.json", Path: "docs", diff --git a/v3/swagger/go.mod b/v3/swaggerui/go.mod similarity index 97% rename from v3/swagger/go.mod rename to v3/swaggerui/go.mod index 862de9704..03e51ce56 100644 --- a/v3/swagger/go.mod +++ b/v3/swaggerui/go.mod @@ -1,4 +1,4 @@ -module github.com/gofiber/contrib/v3/swagger +module github.com/gofiber/contrib/v3/swaggerui go 1.25.0 diff --git a/v3/swagger/go.sum b/v3/swaggerui/go.sum similarity index 100% rename from v3/swagger/go.sum rename to v3/swaggerui/go.sum diff --git a/v3/swagger/swagger.go b/v3/swaggerui/swagger.go similarity index 99% rename from v3/swagger/swagger.go rename to v3/swaggerui/swagger.go index 9bcb29522..605c1dec5 100644 --- a/v3/swagger/swagger.go +++ b/v3/swaggerui/swagger.go @@ -1,4 +1,4 @@ -package swagger +package swaggerui import ( "encoding/json" diff --git a/v3/swagger/swagger.json b/v3/swaggerui/swagger.json similarity index 100% rename from v3/swagger/swagger.json rename to v3/swaggerui/swagger.json diff --git a/v3/swagger/swagger.yaml b/v3/swaggerui/swagger.yaml similarity index 100% rename from v3/swagger/swagger.yaml rename to v3/swaggerui/swagger.yaml diff --git a/v3/swagger/swagger_missing.json b/v3/swaggerui/swagger_missing.json similarity index 100% rename from v3/swagger/swagger_missing.json rename to v3/swaggerui/swagger_missing.json diff --git a/v3/swagger/swagger_test.go b/v3/swaggerui/swagger_test.go similarity index 99% rename from v3/swagger/swagger_test.go rename to v3/swaggerui/swagger_test.go index af408ce09..246fa1ea8 100644 --- a/v3/swagger/swagger_test.go +++ b/v3/swaggerui/swagger_test.go @@ -1,4 +1,4 @@ -package swagger +package swaggerui import ( _ "embed" diff --git a/v3/swaggo/README.md b/v3/swaggo/README.md new file mode 100644 index 000000000..a12cea8ec --- /dev/null +++ b/v3/swaggo/README.md @@ -0,0 +1,144 @@ +--- +id: swaggo +title: Swaggo – Swagger Doc Generator Middleware (formerly "swagger") +--- + +# Swaggo – Swagger Doc Generator Middleware + +> ⚠️ This module was renamed from `swagger` to `swaggo` to clearly distinguish it from the ported `swaggerui` middleware. Update your imports accordingly. + +![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=swaggo*) +[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord) +![Test](https://github.com/gofiber/contrib/actions/workflows/test-swaggo.yml/badge.svg) + +Swaggo replaces the archived [github.com/gofiber/swagger](https://github.com/gofiber/swagger) module with an actively maintained drop-in generator for [Fiber](https://github.com/gofiber/fiber) v3. It mounts the official Swagger UI, serves the assets required by [swaggo/swag](https://github.com/swaggo/swag) generated documentation, and exposes helper utilities to wire the docs into any Fiber application. + +**Compatible with Fiber v3.** + +## Go version support + +We only support the latest two versions of Go. Visit [https://go.dev/doc/devel/release](https://go.dev/doc/devel/release) for more information. + +### Table of Contents +- [Signatures](#signatures) +- [Installation](#installation) +- [Usage](#usage) +- [Config](#config) +- [Default Config](#default-config) + +### Signatures +```go +var HandlerDefault = New() +func New(config ...Config) fiber.Handler +``` + +### Installation +Swagger Doc Generator is tested on the latest [Go versions](https://go.dev/dl/) with support for modules. Make sure to initialize one first if you have not done that yet: +```bash +go mod init github.com// +``` +Then install the middleware: +```bash +go get github.com/gofiber/contrib/v3/swaggo +``` + +### Usage +First, document your API using swaggo/swag comments and generate the documentation files (usually inside a `docs` package) by running `swag init`. + +```go +package main + +import ( + "github.com/gofiber/fiber/v3" + swaggo "github.com/gofiber/contrib/v3/swaggo" + + // docs are generated by Swag CLI, you have to import them. + // Replace with your own docs folder, usually "github.com/username/reponame/docs". + _ "github.com/username/reponame/docs" +) + +func main() { + app := fiber.New() + + // Mount the UI with the default configuration under /swagger + app.Get("/swagger/*", swaggo.HandlerDefault) + + // Customize the UI by passing a Config + app.Get("/docs/*", swaggo.New(swaggo.Config{ + URL: "http://example.com/doc.json", + DeepLinking: false, + DocExpansion: "none", + OAuth2RedirectUrl: "http://localhost:8080/swagger/oauth2-redirect.html", + })) + + app.Listen(":8080") +} +``` + +### Config +```go +type Config struct { + InstanceName string + Title string + ConfigURL string + URL string + QueryConfigEnabled bool + Layout string + Plugins []template.JS + Presets []template.JS + DeepLinking bool + DisplayOperationId bool + DefaultModelsExpandDepth int + DefaultModelExpandDepth int + DefaultModelRendering string + DisplayRequestDuration bool + DocExpansion string + Filter FilterConfig + MaxDisplayedTags int + ShowExtensions bool + ShowCommonExtensions bool + TagsSorter template.JS + OnComplete template.JS + SyntaxHighlight *SyntaxHighlightConfig + TryItOutEnabled bool + RequestSnippetsEnabled bool + OAuth2RedirectUrl string + RequestInterceptor template.JS + RequestCurlOptions []string + ResponseInterceptor template.JS + ShowMutatedRequest bool + SupportedSubmitMethods []string + ValidatorUrl string + WithCredentials bool + ModelPropertyMacro template.JS + ParameterMacro template.JS + PersistAuthorization bool + OAuth *OAuthConfig + PreauthorizeBasic template.JS + PreauthorizeApiKey template.JS + CustomStyle template.CSS + CustomScript template.JS +} +``` + +### Default Config +```go +var ConfigDefault = Config{ + Title: "Swagger UI", + Layout: "StandaloneLayout", + URL: "doc.json", + DeepLinking: true, + ShowMutatedRequest: true, + Plugins: []template.JS{ + template.JS("SwaggerUIBundle.plugins.DownloadUrl"), + }, + Presets: []template.JS{ + template.JS("SwaggerUIBundle.presets.apis"), + template.JS("SwaggerUIStandalonePreset"), + }, + SyntaxHighlight: &SyntaxHighlightConfig{Activate: true, Theme: "agate"}, +} +``` + +> Refer to [`config.go`](./config.go) for a complete list of options and documentation strings. + diff --git a/v3/swaggo/config.go b/v3/swaggo/config.go new file mode 100644 index 000000000..71c82d0b2 --- /dev/null +++ b/v3/swaggo/config.go @@ -0,0 +1,319 @@ +package swaggo + +import ( + "html/template" +) + +// Config stores SwaggerUI configuration variables +type Config struct { + // This parameter can be used to name different swagger document instances. + // default: "" + InstanceName string `json:"-"` + + // Title pointing to title of HTML page. + // default: "Swagger UI" + Title string `json:"-"` + + // URL to fetch external configuration document from. + // default: "" + ConfigURL string `json:"configUrl,omitempty"` + + // The URL pointing to API definition (normally swagger.json or swagger.yaml). + // default: "doc.json" + URL string `json:"url,omitempty"` + + // Enables overriding configuration parameters via URL search params. + // default: false + QueryConfigEnabled bool `json:"queryConfigEnabled,omitempty"` + + // The name of a component available via the plugin system to use as the top-level layout for Swagger UI. + // default: "StandaloneLayout" + Layout string `json:"layout,omitempty"` + + // An array of plugin functions to use in Swagger UI. + // default: [SwaggerUIBundle.plugins.DownloadUrl] + Plugins []template.JS `json:"-"` + + // An array of presets to use in Swagger UI. Usually, you'll want to include ApisPreset if you use this option. + // default: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset] + Presets []template.JS `json:"-"` + + // If set to true, enables deep linking for tags and operations. + // default: true + DeepLinking bool `json:"deepLinking"` + + // Controls the display of operationId in operations list. + // default: false + DisplayOperationId bool `json:"displayOperationId,omitempty"` + + // The default expansion depth for models (set to -1 completely hide the models). + // default: 1 + DefaultModelsExpandDepth int `json:"defaultModelsExpandDepth,omitempty"` + + // The default expansion depth for the model on the model-example section. + // default: 1 + DefaultModelExpandDepth int `json:"defaultModelExpandDepth,omitempty"` + + // Controls how the model is shown when the API is first rendered. + // The user can always switch the rendering for a given model by clicking the 'Model' and 'Example Value' links. + // default: "example" + DefaultModelRendering string `json:"defaultModelRendering,omitempty"` + + // Controls the display of the request duration (in milliseconds) for "Try it out" requests. + // default: false + DisplayRequestDuration bool `json:"displayRequestDuration,omitempty"` + + // Controls the default expansion setting for the operations and tags. + // 'list' (default, expands only the tags), + // 'full' (expands the tags and operations), + // 'none' (expands nothing) + DocExpansion string `json:"docExpansion,omitempty"` + + // If set, enables filtering. The top bar will show an edit box that you can use to filter the tagged operations that are shown. + // Can be Boolean to enable or disable, or a string, in which case filtering will be enabled using that string as the filter expression. + // Filtering is case sensitive matching the filter expression anywhere inside the tag. + // default: false + Filter FilterConfig `json:"-"` + + // If set, limits the number of tagged operations displayed to at most this many. The default is to show all operations. + // default: 0 + MaxDisplayedTags int `json:"maxDisplayedTags,omitempty"` + + // Controls the display of vendor extension (x-) fields and values for Operations, Parameters, Responses, and Schema. + // default: false + ShowExtensions bool `json:"showExtensions,omitempty"` + + // Controls the display of extensions (pattern, maxLength, minLength, maximum, minimum) fields and values for Parameters. + // default: false + ShowCommonExtensions bool `json:"showCommonExtensions,omitempty"` + + // Apply a sort to the tag list of each API. It can be 'alpha' (sort by paths alphanumerically) or a function (see Array.prototype.sort(). + // to learn how to write a sort function). Two tag name strings are passed to the sorter for each pass. + // default: "" -> Default is the order determined by Swagger UI. + TagsSorter template.JS `json:"-"` + + // Provides a mechanism to be notified when Swagger UI has finished rendering a newly provided definition. + // default: "" -> Function=NOOP + OnComplete template.JS `json:"-"` + + // An object with the activate and theme properties. + SyntaxHighlight *SyntaxHighlightConfig `json:"-"` + + // Controls whether the "Try it out" section should be enabled by default. + // default: false + TryItOutEnabled bool `json:"tryItOutEnabled,omitempty"` + + // Enables the request snippet section. When disabled, the legacy curl snippet will be used. + // default: false + RequestSnippetsEnabled bool `json:"requestSnippetsEnabled,omitempty"` + + // OAuth redirect URL. + // default: "" + OAuth2RedirectUrl string `json:"oauth2RedirectUrl,omitempty"` + + // MUST be a function. Function to intercept remote definition, "Try it out", and OAuth 2.0 requests. + // Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request. + // default: "" + RequestInterceptor template.JS `json:"-"` + + // If set, MUST be an array of command line options available to the curl command. This can be set on the mutated request in the requestInterceptor function. + // For example request.curlOptions = ["-g", "--limit-rate 20k"] + // default: nil + RequestCurlOptions []string `json:"request.curlOptions,omitempty"` + + // MUST be a function. Function to intercept remote definition, "Try it out", and OAuth 2.0 responses. + // Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response. + // default: "" + ResponseInterceptor template.JS `json:"-"` + + // If set to true, uses the mutated request returned from a requestInterceptor to produce the curl command in the UI, + // otherwise the request before the requestInterceptor was applied is used. + // default: true + ShowMutatedRequest bool `json:"showMutatedRequest"` + + // List of HTTP methods that have the "Try it out" feature enabled. An empty array disables "Try it out" for all operations. + // This does not filter the operations from the display. + // Possible values are ["get", "put", "post", "delete", "options", "head", "patch", "trace"] + // default: nil + SupportedSubmitMethods []string `json:"supportedSubmitMethods,omitempty"` + + // By default, Swagger UI attempts to validate specs against swagger.io's online validator. You can use this parameter to set a different validator URL. + // For example for locally deployed validators (https://github.com/swagger-api/validator-badge). + // Setting it to either none, 127.0.0.1 or localhost will disable validation. + // default: "" + ValidatorUrl string `json:"validatorUrl,omitempty"` + + // If set to true, enables passing credentials, as defined in the Fetch standard, in CORS requests that are sent by the browser. + // Note that Swagger UI cannot currently set cookies cross-domain (see https://github.com/swagger-api/swagger-js/issues/1163). + // as a result, you will have to rely on browser-supplied cookies (which this setting enables sending) that Swagger UI cannot control. + // default: false + WithCredentials bool `json:"withCredentials,omitempty"` + + // Function to set default values to each property in model. Accepts one argument modelPropertyMacro(property), property is immutable. + // default: "" + ModelPropertyMacro template.JS `json:"-"` + + // Function to set default value to parameters. Accepts two arguments parameterMacro(operation, parameter). + // Operation and parameter are objects passed for context, both remain immutable. + // default: "" + ParameterMacro template.JS `json:"-"` + + // If set to true, it persists authorization data and it would not be lost on browser close/refresh. + // default: false + PersistAuthorization bool `json:"persistAuthorization,omitempty"` + + // Configuration information for OAuth2, optional if using OAuth2 + OAuth *OAuthConfig `json:"-"` + + // (authDefinitionKey, username, password) => action + // Programmatically set values for a Basic authorization scheme. + // default: "" + PreauthorizeBasic template.JS `json:"-"` + + // (authDefinitionKey, apiKeyValue) => action + // Programmatically set values for an API key or Bearer authorization scheme. + // In case of OpenAPI 3.0 Bearer scheme, apiKeyValue must contain just the token itself without the Bearer prefix. + // default: "" + PreauthorizeApiKey template.JS `json:"-"` + + // Applies custom CSS styles. + // default: "" + CustomStyle template.CSS `json:"-"` + + // Applies custom JavaScript scripts. + // default "" + CustomScript template.JS `json:"-"` +} + +type FilterConfig struct { + Enabled bool + Expression string +} + +func (fc FilterConfig) Value() interface{} { + if fc.Expression != "" { + return fc.Expression + } + return fc.Enabled +} + +type SyntaxHighlightConfig struct { + // Whether syntax highlighting should be activated or not. + // default: true + Activate bool `json:"activate"` + // Highlight.js syntax coloring theme to use. + // Possible values are ["agate", "arta", "monokai", "nord", "obsidian", "tomorrow-night"] + // default: "agate" + Theme string `json:"theme,omitempty"` +} + +func (shc SyntaxHighlightConfig) Value() interface{} { + if shc.Activate { + return shc + } + return false +} + +type OAuthConfig struct { + // ID of the client sent to the OAuth2 provider. + // default: "" + ClientId string `json:"clientId,omitempty"` + + // Never use this parameter in your production environment. + // It exposes crucial security information. This feature is intended for dev/test environments only. + // Secret of the client sent to the OAuth2 provider. + // default: "" + ClientSecret string `json:"clientSecret,omitempty"` + + // Application name, displayed in authorization popup. + // default: "" + AppName string `json:"appName,omitempty"` + + // Realm query parameter (for oauth1) added to authorizationUrl and tokenUrl. + // default: "" + Realm string `json:"realm,omitempty"` + + // String array of initially selected oauth scopes + // default: nil + Scopes []string `json:"scopes,omitempty"` + + // Additional query parameters added to authorizationUrl and tokenUrl. + // default: nil + AdditionalQueryStringParams map[string]string `json:"additionalQueryStringParams,omitempty"` + + // Unavailable Only activated for the accessCode flow. + // During the authorization_code request to the tokenUrl, pass the Client Password using the HTTP Basic Authentication scheme + // (Authorization header with Basic base64encode(client_id + client_secret)). + // default: false + UseBasicAuthenticationWithAccessCodeGrant bool `json:"useBasicAuthenticationWithAccessCodeGrant,omitempty"` + + // Only applies to authorizationCode flows. + // Proof Key for Code Exchange brings enhanced security for OAuth public clients. + // default: false + UsePkceWithAuthorizationCodeGrant bool `json:"usePkceWithAuthorizationCodeGrant,omitempty"` +} + +var ( + ConfigDefault = Config{ + Title: "Swagger UI", + Layout: "StandaloneLayout", + Plugins: []template.JS{ + template.JS("SwaggerUIBundle.plugins.DownloadUrl"), + }, + Presets: []template.JS{ + template.JS("SwaggerUIBundle.presets.apis"), + template.JS("SwaggerUIStandalonePreset"), + }, + DeepLinking: true, + DefaultModelsExpandDepth: 1, + DefaultModelExpandDepth: 1, + DefaultModelRendering: "example", + DocExpansion: "list", + SyntaxHighlight: &SyntaxHighlightConfig{ + Activate: true, + Theme: "agate", + }, + ShowMutatedRequest: true, + } +) + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + if cfg.Title == "" { + cfg.Title = ConfigDefault.Title + } + + if cfg.Layout == "" { + cfg.Layout = ConfigDefault.Layout + } + + if cfg.DefaultModelRendering == "" { + cfg.DefaultModelRendering = ConfigDefault.DefaultModelRendering + } + + if cfg.DocExpansion == "" { + cfg.DocExpansion = ConfigDefault.DocExpansion + } + + if cfg.Plugins == nil { + cfg.Plugins = ConfigDefault.Plugins + } + + if cfg.Presets == nil { + cfg.Presets = ConfigDefault.Presets + } + + if cfg.SyntaxHighlight == nil { + cfg.SyntaxHighlight = ConfigDefault.SyntaxHighlight + } + + return cfg +} diff --git a/v3/swaggo/go.mod b/v3/swaggo/go.mod new file mode 100644 index 000000000..3d4dedb33 --- /dev/null +++ b/v3/swaggo/go.mod @@ -0,0 +1,38 @@ +module github.com/gofiber/contrib/v3/swaggo + +go 1.25.0 + +require ( + github.com/gofiber/fiber/v3 v3.0.0-rc.2 + github.com/gofiber/utils/v2 v2.0.0-rc.1 + github.com/swaggo/files/v2 v2.0.2 + github.com/swaggo/swag v1.16.4 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/gofiber/schema v1.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/tinylib/msgp v1.4.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.65.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/v3/swaggo/go.sum b/v3/swaggo/go.sum new file mode 100644 index 000000000..7af0270dc --- /dev/null +++ b/v3/swaggo/go.sum @@ -0,0 +1,109 @@ +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gofiber/fiber/v3 v3.0.0-rc.2 h1:5I3RQ7XygDBfWRlMhkATjyJKupMmfMAVmnsrgo6wmc0= +github.com/gofiber/fiber/v3 v3.0.0-rc.2/go.mod h1:EHKwhVCONMruJTOmvSPSy0CdACJ3uqCY8vGaBXft8yg= +github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY= +github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s= +github.com/gofiber/utils/v2 v2.0.0-rc.1 h1:b77K5Rk9+Pjdxz4HlwEBnS7u5nikhx7armQB8xPds4s= +github.com/gofiber/utils/v2 v2.0.0-rc.1/go.mod h1:Y1g08g7gvST49bbjHJ1AVqcsmg93912R/tbKWhn6V3E= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shamaton/msgpack/v2 v2.3.1 h1:R3QNLIGA/tbdczNMZ5PCRxrXvy+fnzsIaHG4kKMgWYo= +github.com/shamaton/msgpack/v2 v2.3.1/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= +github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= +github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8= +github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3/swaggo/index.go b/v3/swaggo/index.go new file mode 100644 index 000000000..760f5fae7 --- /dev/null +++ b/v3/swaggo/index.go @@ -0,0 +1,107 @@ +package swaggo + +const indexTmpl string = ` + + + + + + {{.Title}} + + + + + {{- if .CustomStyle}} + + {{- end}} + {{- if .CustomScript}} + + {{- end}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +` diff --git a/v3/swaggo/swagger.go b/v3/swaggo/swagger.go new file mode 100644 index 000000000..580bb4c4f --- /dev/null +++ b/v3/swaggo/swagger.go @@ -0,0 +1,135 @@ +package swaggo + +import ( + "errors" + "fmt" + "html/template" + "io" + "io/fs" + "path" + "strings" + "sync" + + "github.com/gofiber/fiber/v3" + "github.com/gofiber/utils/v2" + swaggerFiles "github.com/swaggo/files/v2" + "github.com/swaggo/swag" +) + +const ( + defaultDocURL = "doc.json" + defaultIndex = "index.html" +) + +var HandlerDefault = New() + +// New returns custom handler +func New(config ...Config) fiber.Handler { + cfg := configDefault(config...) + + index, err := template.New("swagger_index.html").Parse(indexTmpl) + if err != nil { + panic(fmt.Errorf("fiber: swagger middleware error -> %w", err)) + } + + var ( + basePrefix string + once sync.Once + ) + + return func(c fiber.Ctx) error { + once.Do(func() { + basePrefix = strings.ReplaceAll(c.Route().Path, "*", "") + }) + + prefix := basePrefix + if forwardedPrefix := getForwardedPrefix(c); forwardedPrefix != "" { + prefix = forwardedPrefix + prefix + } + + cfgCopy := cfg + if len(cfgCopy.URL) == 0 { + cfgCopy.URL = path.Join(prefix, defaultDocURL) + } + + p := utils.CopyString(c.Params("*")) + + switch p { + case defaultIndex: + c.Type("html") + return index.Execute(c, cfgCopy) + case defaultDocURL: + var doc string + if doc, err = swag.ReadDoc(cfgCopy.InstanceName); err != nil { + return err + } + return c.Type("json").SendString(doc) + case "", "/": + return c.Redirect().Status(fiber.StatusMovedPermanently).To(path.Join(prefix, defaultIndex)) + default: + filePath := path.Clean("/" + p) + filePath = strings.TrimPrefix(filePath, "/") + if filePath == "" { + return fiber.ErrNotFound + } + + file, err := swaggerFiles.FS.Open(filePath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fiber.ErrNotFound + } + return err + } + defer func() { + cerr := file.Close() + if cerr != nil && err == nil { + err = cerr + } + }() + + info, err := file.Stat() + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fiber.ErrNotFound + } + return err + } + + if info.IsDir() { + return fiber.ErrNotFound + } + + data, err := io.ReadAll(file) + if err != nil { + return err + } + + if ext := strings.TrimPrefix(path.Ext(filePath), "."); ext != "" { + c.Type(ext) + } + + return c.Send(data) + } + } +} + +func getForwardedPrefix(c fiber.Ctx) string { + header := c.GetReqHeaders()["X-Forwarded-Prefix"] + + if len(header) == 0 { + return "" + } + + prefix := "" + + for _, rawPrefix := range header { + endIndex := len(rawPrefix) + for endIndex > 1 && rawPrefix[endIndex-1] == '/' { + endIndex-- + } + + prefix += rawPrefix[:endIndex] + } + + return prefix +} diff --git a/v3/swaggo/swagger_test.go b/v3/swaggo/swagger_test.go new file mode 100644 index 000000000..ce683017f --- /dev/null +++ b/v3/swaggo/swagger_test.go @@ -0,0 +1,164 @@ +package swaggo + +import ( + "mime" + "net/http" + "sync" + "testing" + + "github.com/gofiber/fiber/v3" + "github.com/swaggo/swag" +) + +type mockedSwag struct{} + +func (s *mockedSwag) ReadDoc() string { + return `{ + "swagger": "2.0", + "info": { + "description": "This is a sample server.", + "title": "Swagger Example API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "paths": {} +}` +} + +var ( + registrationOnce sync.Once +) + +func Test_Swagger(t *testing.T) { + app := fiber.New() + + registrationOnce.Do(func() { + swag.Register(swag.Name, &mockedSwag{}) + }) + + app.Get("/swag/*", HandlerDefault) + + tests := []struct { + name string + url string + statusCode int + contentType string + location string + }{ + { + name: "Should be returns status 200 with 'text/html' content-type", + url: "/swag/index.html", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should be returns status 200 with 'application/json' content-type", + url: "/swag/doc.json", + statusCode: 200, + contentType: "application/json", + }, + { + name: "Should be returns status 200 with 'image/png' content-type", + url: "/swag/favicon-16x16.png", + statusCode: 200, + contentType: "image/png", + }, + { + name: "Should return status 301", + url: "/swag/", + statusCode: 301, + location: "/swag/index.html", + }, + { + name: "Should return status 404", + url: "/swag/notfound", + statusCode: 404, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, tt.url, nil) + if err != nil { + t.Fatal(err) + } + + resp, err := app.Test(req) + if err != nil { + t.Fatal(err) + } + + if resp.StatusCode != tt.statusCode { + t.Fatalf(`StatusCode: got %v - expected %v`, resp.StatusCode, tt.statusCode) + } + + if tt.contentType != "" { + ct := resp.Header.Get("Content-Type") + mediaType, _, err := mime.ParseMediaType(ct) + if err != nil { + t.Fatalf("invalid content type %q: %v", ct, err) + } + if mediaType != tt.contentType { + t.Fatalf(`Content-Type: got %s - expected %s`, mediaType, tt.contentType) + } + } + + if tt.location != "" { + location := resp.Header.Get("Location") + if location != tt.location { + t.Fatalf(`Location: got %s - expected %s`, location, tt.location) + } + } + }) + } +} + +func Test_Swagger_Proxy_Redirect(t *testing.T) { + app := fiber.New() + + registrationOnce.Do(func() { + swag.Register(swag.Name, &mockedSwag{}) + }) + + // Use new handler since the prefix is created only once per handler + app.Get("/swag/*", New()) + + statusCode := 301 + location := "/custom/path/swag/index.html" + + t.Run("Should return status 301 with proxy redirect", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/swag/", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Set("X-Forwarded-Prefix", "/custom/path/") + + resp, err := app.Test(req) + if err != nil { + t.Fatal(err) + } + + if resp.StatusCode != statusCode { + t.Fatalf(`StatusCode: got %v - expected %v`, resp.StatusCode, statusCode) + } + + if location != "" { + responseLocation := resp.Header.Get("Location") + if responseLocation != location { + t.Fatalf(`Location: got %s - expected %s`, responseLocation, location) + } + } + }) +}