├── .envrc ├── scalar ├── scalar-version.txt ├── theme.go ├── go.mod ├── config.go ├── index.go ├── scalar.go ├── go.sum └── scalar_test.go ├── flake.lock ├── .gitignore ├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── release-drafter.yml │ ├── pr.yml │ ├── update-scalar-api-reference-v3.yml │ └── update-scalar-api-reference.yml ├── flake.nix ├── LICENSE └── README.md /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /scalar/scalar-version.txt: -------------------------------------------------------------------------------- 1 | 1.40.5 2 | -------------------------------------------------------------------------------- /scalar/theme.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | type Theme string 4 | 5 | const ( 6 | ThemeAlternate Theme = "alternate" 7 | ThemeDefault Theme = "default" 8 | ThemeMoon Theme = "moon" 9 | ThemePurple Theme = "purple" 10 | ThemeSolarized Theme = "solarized" 11 | ThemeBluePlanet Theme = "bluePlanet" 12 | ThemeSaturn Theme = "saturn" 13 | ThemeKepler Theme = "kepler" 14 | ThemeMars Theme = "mars" 15 | ThemeDeepSpace Theme = "deepSpace" 16 | ThemeLaserwave Theme = "laserwave" 17 | ThemeNone Theme = "none" 18 | ) 19 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1761597516, 6 | "narHash": "sha256-wxX7u6D2rpkJLWkZ2E932SIvDJW8+ON/0Yy8+a5vsDU=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "daf6dc47aa4b44791372d6139ab7b25269184d55", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-25.05", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | # Devenv 28 | .devenv* 29 | devenv.local.nix 30 | 31 | # direnv 32 | .direnv 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | open-pull-requests-limit: 15 10 | directories: 11 | - "**/*" 12 | labels: 13 | - "🤖 Dependencies" 14 | schedule: 15 | interval: "daily" 16 | 17 | - package-ecosystem: "github-actions" 18 | open-pull-requests-limit: 15 19 | directory: "/" 20 | labels: 21 | - "🤖 Dependencies" 22 | schedule: 23 | interval: "daily" 24 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION" 2 | tag-template: "scalar/v$RESOLVED_VERSION" 3 | categories: 4 | - title: "🚀 Features" 5 | labels: 6 | - "feature" 7 | - "enhancement" 8 | - title: "🐛 Bug Fixes" 9 | labels: 10 | - "fix" 11 | - "bugfix" 12 | - "bug" 13 | - title: "🧰 Maintenance" 14 | label: "chore" 15 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 16 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 17 | version-resolver: 18 | major: 19 | labels: 20 | - "major" 21 | minor: 22 | labels: 23 | - "minor" 24 | patch: 25 | labels: 26 | - "patch" 27 | default: patch 28 | 29 | template: | 30 | ## Changes 31 | 32 | $CHANGES 33 | 34 | autolabeler: 35 | - label: "chore" 36 | files: 37 | - "*.md" 38 | branch: 39 | - '/docs{0,1}\/.+/' 40 | - label: "bug" 41 | branch: 42 | - '/fix\/.+/' 43 | title: 44 | - "/fix/i" 45 | - label: "enhancement" 46 | branch: 47 | - '/feature\/.+/' 48 | body: 49 | - "/JIRA-[0-9]{1,4}/" 50 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A Nix-flake-based Go 1.25 development environment"; 3 | 4 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; 5 | 6 | outputs = inputs: let 7 | goVersion = 23; # Change this to update the whole stack 8 | 9 | supportedSystems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"]; 10 | forEachSupportedSystem = f: 11 | inputs.nixpkgs.lib.genAttrs supportedSystems (system: 12 | f { 13 | pkgs = import inputs.nixpkgs { 14 | inherit system; 15 | overlays = [inputs.self.overlays.default]; 16 | }; 17 | }); 18 | in { 19 | overlays.default = final: prev: { 20 | go = final."go_1_${toString goVersion}"; 21 | }; 22 | 23 | devShells = forEachSupportedSystem ({pkgs}: { 24 | default = pkgs.mkShell { 25 | packages = with pkgs; [ 26 | # go (version is specified by overlay) 27 | go_1_23 28 | 29 | # goimports, godoc, etc. 30 | gotools 31 | 32 | # https://github.com/golangci/golangci-lint 33 | golangci-lint 34 | ]; 35 | }; 36 | }); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, Thanapon Johdee 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /scalar/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yokeTH/gofiber-scalar/scalar/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.52.10 7 | github.com/stretchr/testify v1.11.1 8 | github.com/swaggo/swag/v2 v2.0.0-rc4 9 | ) 10 | 11 | require ( 12 | github.com/KyleBanks/depth v1.2.1 // indirect 13 | github.com/andybalholm/brotli v1.1.0 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 16 | github.com/go-openapi/jsonreference v0.20.2 // indirect 17 | github.com/go-openapi/spec v0.20.9 // indirect 18 | github.com/go-openapi/swag v0.22.3 // indirect 19 | github.com/google/uuid v1.6.0 // indirect 20 | github.com/josharian/intern v1.0.0 // indirect 21 | github.com/klauspost/compress v1.17.9 // indirect 22 | github.com/mailru/easyjson v0.7.7 // indirect 23 | github.com/mattn/go-colorable v0.1.13 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/mattn/go-runewidth v0.0.16 // indirect 26 | github.com/pkg/errors v0.9.1 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/rivo/uniseg v0.2.0 // indirect 29 | github.com/sv-tools/openapi v0.2.1 // indirect 30 | github.com/valyala/bytebufferpool v1.0.0 // indirect 31 | github.com/valyala/fasthttp v1.51.0 // indirect 32 | github.com/valyala/tcplisten v1.0.0 // indirect 33 | golang.org/x/sync v0.12.0 // indirect 34 | golang.org/x/sys v0.31.0 // indirect 35 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 36 | gopkg.in/yaml.v2 v2.4.0 // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - main 8 | - master 9 | - v3 10 | 11 | # pull_request event is required only for autolabeler 12 | pull_request: 13 | # Only following types are handled by the action, but one can default to all as well 14 | types: [opened, reopened, synchronize] 15 | # pull_request_target event is required for autolabeler to support PRs from forks 16 | # pull_request_target: 17 | # types: [opened, reopened, synchronize] 18 | 19 | permissions: 20 | contents: read 21 | 22 | jobs: 23 | update_release_draft: 24 | permissions: 25 | # write permission is required to create a github release 26 | contents: write 27 | # write permission is required for autolabeler 28 | # otherwise, read permission is required at least 29 | pull-requests: write 30 | runs-on: ubuntu-latest 31 | steps: 32 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 33 | #- name: Set GHE_HOST 34 | # run: | 35 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 36 | 37 | # Drafts your next Release notes as Pull Requests are merged into "master" 38 | - uses: release-drafter/release-drafter@v6 39 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 40 | # with: 41 | # config-name: my-config.yml 42 | # disable-autolabeler: true 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v6 16 | 17 | - name: Setup Go 18 | uses: actions/setup-go@v6 19 | with: 20 | go-version: "1.23.0" 21 | cache: true 22 | cache-dependency-path: scalar/go.sum 23 | 24 | - name: Run golangci-lint 25 | uses: golangci/golangci-lint-action@v9 26 | with: 27 | version: latest 28 | working-directory: scalar 29 | args: --timeout=10m 30 | 31 | test: 32 | runs-on: ubuntu-latest 33 | permissions: 34 | contents: write 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v6 38 | 39 | - name: Setup Go 40 | uses: actions/setup-go@v6 41 | with: 42 | go-version: "1.23.0" 43 | cache: true 44 | cache-dependency-path: scalar/go.sum 45 | 46 | - name: Run Tests with Coverage 47 | run: | 48 | cd scalar 49 | go test -v -coverprofile=coverage.out ./... 50 | 51 | - name: Save coverage report 52 | uses: actions/upload-artifact@v5 53 | with: 54 | name: scalar-coverage 55 | path: scalar/coverage.out 56 | 57 | build: 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v6 62 | 63 | - name: Setup Go 64 | uses: actions/setup-go@v6 65 | with: 66 | go-version: "1.23.0" 67 | cache: true 68 | cache-dependency-path: scalar/go.sum 69 | 70 | - name: Build 71 | run: | 72 | cd scalar 73 | go build ./... 74 | -------------------------------------------------------------------------------- /scalar/config.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | import ( 4 | "html/template" 5 | ) 6 | 7 | // Config defines the config for middleware. 8 | type Config struct { 9 | 10 | // BasePath for the UI path 11 | // 12 | // Optional. Default: / 13 | BasePath string 14 | 15 | // FileContent for the content of the swagger.json or swagger.yaml file. 16 | // 17 | // Optional. Default: nil 18 | FileContentString string 19 | 20 | // Path combines with BasePath for the full UI path 21 | // 22 | // Optional. Default: docs 23 | Path string 24 | 25 | // Title for the documentation site 26 | // 27 | // Optional. Default: Fiber API documentation 28 | Title string 29 | 30 | // CacheAge defines the max-age for the Cache-Control header in seconds. 31 | // 32 | // Optional. Default: 1 min 33 | CacheAge int 34 | 35 | // Scalar theme 36 | // 37 | // Optional. Default: ThemeNone 38 | Theme Theme 39 | 40 | // Custom Scalar Style 41 | // Ref: https://github.com/scalar/scalar/blob/main/packages/themes/src/variables.css 42 | // Optional. Default: "" 43 | CustomStyle template.CSS 44 | 45 | // Proxy to avoid CORS issues 46 | // Optional. 47 | ProxyUrl string 48 | 49 | // Raw Space Url 50 | // Optional. Default: doc.json 51 | RawSpecUrl string 52 | 53 | // ForceOffline 54 | // Optional: Default: true 55 | ForceOffline *bool 56 | 57 | // Fallback scalar cache 58 | // 59 | // Optional. Default: 86400 (1 Days) 60 | FallbackCacheAge int 61 | } 62 | 63 | var configDefault = Config{ 64 | BasePath: "/", 65 | Path: "/docs", 66 | Title: "Fiber API documentation", 67 | CacheAge: 60, 68 | Theme: ThemeNone, 69 | RawSpecUrl: "doc.json", 70 | ForceOffline: ForceOfflineTrue, 71 | FallbackCacheAge: 86400, 72 | } 73 | 74 | func ptr[T any](v T) *T { 75 | return &v 76 | } 77 | 78 | var ( 79 | ForceOfflineTrue = ptr(true) 80 | ForceOfflineFalse = ptr(false) 81 | ) 82 | -------------------------------------------------------------------------------- /scalar/index.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | const templateHTML = ` 4 | 5 | 6 | 7 | {{.Title}} 8 | 9 | 10 | 11 | {{- if .CustomStyle }} 12 | 17 | {{ end }} 18 | 19 | 20 | 21 |
22 | 62 | 63 | ` 64 | -------------------------------------------------------------------------------- /.github/workflows/update-scalar-api-reference-v3.yml: -------------------------------------------------------------------------------- 1 | name: Update Scalar API Reference v3 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 0" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | update: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Generate GitHub App Token 18 | uses: tibdex/github-app-token@v2 19 | id: generate-token 20 | with: 21 | app_id: ${{ secrets.APP_ID }} 22 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 23 | 24 | - name: Checkout repository 25 | uses: actions/checkout@v6 26 | with: 27 | lfs: true 28 | token: ${{ steps.generate-token.outputs.token }} 29 | ref: v3 30 | 31 | - name: Get latest version from npm 32 | id: get_version 33 | run: | 34 | latest=$(npm view @scalar/api-reference version) 35 | echo "Latest version: $latest" 36 | echo "version=$latest" >> $GITHUB_OUTPUT 37 | 38 | - name: Read current version 39 | id: read_current 40 | run: | 41 | version_file="scalar/scalar-version.txt" 42 | if [[ -f "$version_file" ]]; then 43 | current=$(cat "$version_file") 44 | else 45 | current="none" 46 | fi 47 | echo "Current version: $current" 48 | echo "current=$current" >> $GITHUB_OUTPUT 49 | 50 | - name: Skip if version is the same 51 | if: steps.get_version.outputs.version == steps.read_current.outputs.current 52 | run: | 53 | echo "Already up to date." 54 | exit 0 55 | 56 | - name: Download latest scalar.min.js 57 | run: | 58 | curl -L "https://cdn.jsdelivr.net/npm/@scalar/api-reference" -o scalar/scalar.min.js 59 | echo "${{ steps.get_version.outputs.version }}" > scalar/scalar-version.txt 60 | 61 | - name: Create Pull Request 62 | uses: peter-evans/create-pull-request@v8 63 | with: 64 | token: ${{ steps.generate-token.outputs.token }} 65 | commit-message: "Update Scalar API Reference to v${{ steps.get_version.outputs.version }}" 66 | committer: yoketh[bot] <1400162+yoketh[bot]@users.noreply.github.com> 67 | title: "Update Scalar API Reference to v${{ steps.get_version.outputs.version }}" 68 | body: "This PR updates `scalar.min.js` to the latest version of the Scalar API Reference." 69 | branch: update-scalar-${{ steps.get_version.outputs.version }}-v3 70 | delete-branch: true 71 | base: v3 72 | add-paths: | 73 | scalar/scalar.min.js 74 | scalar/scalar-version.txt 75 | labels: | 76 | 🤖 Dependencies 77 | reviewers: yokeTH 78 | -------------------------------------------------------------------------------- /.github/workflows/update-scalar-api-reference.yml: -------------------------------------------------------------------------------- 1 | name: Update Scalar API Reference v2 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 0" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | update: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Generate GitHub App Token 18 | uses: tibdex/github-app-token@v2 19 | id: generate-token 20 | with: 21 | app_id: ${{ secrets.APP_ID }} 22 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 23 | 24 | - name: Checkout repository 25 | uses: actions/checkout@v6 26 | with: 27 | lfs: true 28 | token: ${{ steps.generate-token.outputs.token }} 29 | ref: main 30 | 31 | - name: Get latest version from npm 32 | id: get_version 33 | run: | 34 | latest=$(npm view @scalar/api-reference version) 35 | echo "Latest version: $latest" 36 | echo "version=$latest" >> $GITHUB_OUTPUT 37 | 38 | - name: Read current version 39 | id: read_current 40 | run: | 41 | version_file="scalar/scalar-version.txt" 42 | if [[ -f "$version_file" ]]; then 43 | current=$(cat "$version_file") 44 | else 45 | current="none" 46 | fi 47 | echo "Current version: $current" 48 | echo "current=$current" >> $GITHUB_OUTPUT 49 | 50 | - name: Skip if version is the same 51 | if: steps.get_version.outputs.version == steps.read_current.outputs.current 52 | run: | 53 | echo "Already up to date." 54 | exit 0 55 | 56 | - name: Download latest scalar.min.js 57 | run: | 58 | curl -L "https://cdn.jsdelivr.net/npm/@scalar/api-reference" -o scalar/scalar.min.js 59 | echo "${{ steps.get_version.outputs.version }}" > scalar/scalar-version.txt 60 | 61 | - name: Create Pull Request 62 | uses: peter-evans/create-pull-request@v8 63 | with: 64 | token: ${{ steps.generate-token.outputs.token }} 65 | commit-message: "Update Scalar API Reference to v${{ steps.get_version.outputs.version }}" 66 | committer: yoketh[bot] <1400162+yoketh[bot]@users.noreply.github.com> 67 | title: "Update Scalar API Reference to v${{ steps.get_version.outputs.version }}" 68 | body: "This PR updates `scalar.min.js` to the latest version of the Scalar API Reference." 69 | branch: update-scalar-${{ steps.get_version.outputs.version }}-v2 70 | delete-branch: true 71 | base: main 72 | add-paths: | 73 | scalar/scalar.min.js 74 | scalar/scalar-version.txt 75 | labels: | 76 | 🤖 Dependencies 77 | reviewers: yokeTH 78 | -------------------------------------------------------------------------------- /scalar/scalar.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "path" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/gofiber/fiber/v2" 11 | "github.com/swaggo/swag/v2" 12 | ) 13 | 14 | //go:embed scalar.min.js 15 | var embeddedJS []byte 16 | 17 | func New(config ...Config) fiber.Handler { 18 | // Set default config 19 | cfg := configDefault 20 | 21 | // Override config if provided 22 | if len(config) > 0 { 23 | cfg = config[0] 24 | 25 | // Set default values 26 | if len(cfg.BasePath) == 0 { 27 | cfg.BasePath = configDefault.BasePath 28 | } 29 | if len(cfg.Path) == 0 { 30 | cfg.Path = configDefault.Path 31 | } 32 | if len(cfg.Title) == 0 { 33 | cfg.Title = configDefault.Title 34 | } 35 | if len(cfg.RawSpecUrl) == 0 { 36 | cfg.RawSpecUrl = configDefault.RawSpecUrl 37 | } 38 | if cfg.ForceOffline == nil { 39 | cfg.ForceOffline = configDefault.ForceOffline 40 | } 41 | if cfg.FallbackCacheAge == 0 { 42 | cfg.FallbackCacheAge = configDefault.FallbackCacheAge 43 | } 44 | if cfg.Theme == "" { 45 | cfg.Theme = ThemeNone 46 | } 47 | } 48 | 49 | rawSpec := cfg.FileContentString 50 | if len(rawSpec) == 0 { 51 | doc, err := swag.ReadDoc() 52 | if err != nil { 53 | panic(err) 54 | } 55 | rawSpec = doc 56 | } 57 | 58 | cfg.FileContentString = string(rawSpec) 59 | 60 | html, err := template.New("index.html").Parse(templateHTML) 61 | if err != nil { 62 | panic(fmt.Errorf("failed to parse html template:%v", err)) 63 | } 64 | 65 | var forceOfflineResolved bool 66 | if cfg.ForceOffline != nil { 67 | forceOfflineResolved = *cfg.ForceOffline 68 | } else if configDefault.ForceOffline != nil { 69 | forceOfflineResolved = *configDefault.ForceOffline 70 | } else { 71 | forceOfflineResolved = false 72 | } 73 | 74 | htmlData := struct { 75 | Config 76 | ForceOffline bool 77 | Extra map[string]any 78 | }{ 79 | Config: cfg, 80 | ForceOffline: forceOfflineResolved, 81 | Extra: map[string]any{}, 82 | } 83 | 84 | return func(ctx *fiber.Ctx) error { 85 | resolvedBasePath := cfg.BasePath 86 | if xf := ctx.Get("X-Forwarded-Prefix"); xf != "" { 87 | resolvedBasePath = xf 88 | } else if xf2 := ctx.Get("X-Forwarded-Path"); xf2 != "" { 89 | resolvedBasePath = xf2 90 | } 91 | scalarUIPath := cfg.Path 92 | specURL := path.Join("/", scalarUIPath, cfg.RawSpecUrl) 93 | jsFallbackPath := path.Join(resolvedBasePath, scalarUIPath, "/js/api-reference.min.js") 94 | 95 | // fallback js 96 | if strings.HasSuffix(jsFallbackPath, ctx.Path()) { 97 | ctx.Set("Cache-Control", fmt.Sprintf("public, max-age=%d", cfg.FallbackCacheAge)) 98 | ctx.Set("Content-Type", "application/javascript; charset=utf-8") 99 | return ctx.Send(embeddedJS) 100 | } 101 | 102 | if cfg.CacheAge > 0 { 103 | ctx.Set("Cache-Control", fmt.Sprintf("public, max-age=%d", cfg.CacheAge)) 104 | } else { 105 | ctx.Set("Cache-Control", "no-store") 106 | } 107 | 108 | if ctx.Path() == specURL { 109 | ctx.Set("Content-Type", "application/json") 110 | return ctx.SendString(rawSpec) 111 | } 112 | 113 | if !strings.HasPrefix(ctx.Path(), scalarUIPath) && ctx.Path() != specURL && strings.HasSuffix(jsFallbackPath, ctx.Path()) { 114 | return ctx.Next() 115 | } 116 | 117 | htmlData.Extra["FallbackUrl"] = jsFallbackPath 118 | ctx.Type("html") 119 | return html.Execute(ctx, htmlData) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > Fiber v3 is currently in development and in the release candidate (RC) stage. Since gofiber-scalar follows the same release cycle, it is also in RC. If you’re using Fiber v3, install it with: `go get -u github.com/yokeTH/gofiber-scalar/scalar/v3` 3 | 4 | # Gofiber Scalar 5 | 6 | Scalar middleware for [Fiber](https://github.com/gofiber/fiber). The middleware handles Scalar UI. 7 | 8 | **Note: Requires Go 1.23.0 and above** 9 | 10 | ### Table of Contents 11 | - [Signatures](#signatures) 12 | - [Installation](#installation) 13 | - [Examples](#examples) 14 | - [Config](#config) 15 | - [Default Config](#default-config) 16 | - [Constants](#Constants) 17 | 18 | ### Signatures 19 | ```go 20 | func New(config ...scalar.Config) fiber.Handler 21 | ``` 22 | 23 | ### Installation 24 | Scalar is tested on the latest [Go versions](https://golang.org/dl/) with support for modules. So make sure to initialize one first if you didn't do that yet: 25 | ```bash 26 | go mod init github.com// 27 | ``` 28 | And then install the Scalar middleware: 29 | ```bash 30 | go get -u github.com/yokeTH/gofiber-scalar/scalar/v2 31 | ``` 32 | 33 | ### Examples 34 | Using swaggo to generate documents default output path is `(root)/docs`: 35 | ```bash 36 | swag init 37 | # if you use swag-v2 38 | swag init -v3.1 39 | ``` 40 | 41 | Import the middleware package and generated docs 42 | ```go 43 | import ( 44 | _ "YOUR_MODULE/docs" 45 | 46 | "github.com/gofiber/fiber/v2" 47 | "github.com/yokeTH/gofiber-scalar/scalar/v2" 48 | ) 49 | ``` 50 | 51 | After Imported: 52 | 53 | > For v2, you do not need to register Swag docs manually. 54 | 55 | #### Using the default config: 56 | ```go 57 | app.Get("/docs/*", scalar.New()) 58 | ``` 59 | Now you can access scalar API documentation UI at `{HOSTNAME}/docs` and JSON documentation at `{HOSTNAME}/docs/doc.json`. Additionally, you can modify the path by configuring the middleware to suit your application's requirements. 60 | 61 | Using as the handler: for an example `localhost:8080/yourpath` 62 | 63 | ```go 64 | app.Get("/yourpath/*", scalar.New(scalar.Config{ 65 | Path: "/yourpath", 66 | })) 67 | ``` 68 | 69 | #### Use program data for Swagger content: 70 | ```go 71 | cfg := scalar.Config{ 72 | BasePath: "/", 73 | FileContentString: jsonString, 74 | Path: "/scalar", 75 | Title: "Scalar API Docs", 76 | } 77 | 78 | app.Get("/scalar/*",scalar.New(cfg)) 79 | ``` 80 | 81 | #### Use scalar prepared theme 82 | ```go 83 | cfg := scalar.Config{ 84 | Theme: scalar.ThemeMars, 85 | } 86 | 87 | app.Get("/docs/*",scalar.New(cfg)) 88 | ``` 89 | 90 | #### Path based reverse proxy 91 | 92 | Assuming `/api` is your reverse path, the configuration will use the following order to determine the path: 93 | 94 | 1. `X-Forwarded-Prefix` 95 | 2. `X-Forwarded-Path` 96 | 3. `BasePath` (fallback if the headers are not set) 97 | 98 | If you cannot configure the headers, you can use `BasePath` as a fallback. Note that this may break in a localhost environment. Example implementation: 99 | 100 | ```go 101 | cfg = scalar.Config{} 102 | if os.Getenv("APP_ENV") == "PROD" { 103 | cfg.BasePath = "/api" 104 | } 105 | ``` 106 | 107 | 108 | ### Config 109 | ```go 110 | type Config struct { 111 | // BasePath for the UI path 112 | // 113 | // Optional. Default: / 114 | BasePath string 115 | 116 | // FileContent for the content of the swagger.json or swagger.yaml file. 117 | // 118 | // Optional. Default: nil 119 | FileContentString string 120 | 121 | // Path combines with BasePath for the full UI path 122 | // 123 | // Optional. Default: docs 124 | Path string 125 | 126 | // Title for the documentation site 127 | // 128 | // Optional. Default: Fiber API documentation 129 | Title string 130 | 131 | // CacheAge defines the max-age for the Cache-Control header in seconds. 132 | // 133 | // Optional. Default: 1 min 134 | CacheAge int 135 | 136 | // Scalar theme 137 | // 138 | // Optional. Default: ThemeNone 139 | Theme Theme 140 | 141 | // Custom Scalar Style 142 | // Ref: https://github.com/scalar/scalar/blob/main/packages/themes/src/variables.css 143 | // Optional. Default: "" 144 | CustomStyle template.CSS 145 | 146 | // Proxy to avoid CORS issues 147 | // Optional. 148 | ProxyUrl string 149 | 150 | // Raw Space Url 151 | // Optional. Default: doc.json 152 | RawSpecUrl string 153 | 154 | // ForceOffline 155 | // Optional: Default: ForceOfflineTrue 156 | ForceOffline *bool 157 | 158 | // Fallback scalar cache 159 | // 160 | // Optional. Default: 86400 (1 Days) 161 | FallbackCacheAge int 162 | } 163 | ``` 164 | 165 | ### Default Config 166 | ```go 167 | var configDefault = Config{ 168 | BasePath: "/", 169 | Path: "docs", 170 | Title: "Fiber API documentation", 171 | CacheAge: 60, 172 | Theme: ThemeNone, 173 | RawSpecUrl: "doc.json", 174 | ForceOffline: ForceOfflineTrue, 175 | FallbackCacheAge: 86400, 176 | } 177 | ``` 178 | 179 | ### Constants 180 | Theme 181 | ```go 182 | const ( 183 | ThemeAlternate Theme = "alternate" 184 | ThemeDefault Theme = "default" 185 | ThemeMoon Theme = "moon" 186 | ThemePurple Theme = "purple" 187 | ThemeSolarized Theme = "solarized" 188 | ThemeBluePlanet Theme = "bluePlanet" 189 | ThemeSaturn Theme = "saturn" 190 | ThemeKepler Theme = "kepler" 191 | ThemeMars Theme = "mars" 192 | ThemeDeepSpace Theme = "deepSpace" 193 | ThemeLaserwave Theme = "laserwave" 194 | ThemeNone Theme = "none" 195 | ) 196 | 197 | var ( 198 | ForceOfflineTrue = ptr(true) 199 | ForceOfflineFalse = ptr(false) 200 | ) 201 | ``` 202 | -------------------------------------------------------------------------------- /scalar/go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 4 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 10 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 11 | github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 12 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 13 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 14 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 15 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 16 | github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= 17 | github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 18 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 19 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 20 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 21 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 22 | github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= 23 | github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= 24 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 25 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 26 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 27 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 28 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 29 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 30 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 31 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 32 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 33 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 34 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 35 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 36 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 37 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 38 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 39 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 40 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 41 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 42 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 43 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 44 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 45 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 46 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 47 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 48 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 49 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 50 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 51 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 55 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 57 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 58 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 61 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 63 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 64 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 65 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 66 | github.com/sv-tools/openapi v0.2.1 h1:ES1tMQMJFGibWndMagvdoo34T1Vllxr1Nlm5wz6b1aA= 67 | github.com/sv-tools/openapi v0.2.1/go.mod h1:k5VuZamTw1HuiS9p2Wl5YIDWzYnHG6/FgPOSFXLAhGg= 68 | github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY= 69 | github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE= 70 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 71 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 72 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 73 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 74 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 75 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 76 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 77 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 78 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 79 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 80 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 83 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 84 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 85 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 90 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 91 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 92 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 93 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 94 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 95 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 97 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | -------------------------------------------------------------------------------- /scalar/scalar_test.go: -------------------------------------------------------------------------------- 1 | package scalar 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/gofiber/fiber/v2" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/swaggo/swag/v2" 16 | ) 17 | 18 | type mock struct{} 19 | 20 | func (m *mock) ReadDoc() string { 21 | return ` 22 | { 23 | "openapi": "3.1.0", 24 | "info": { 25 | "title": "TestApi", 26 | "description": "Documentation for TestApi", 27 | "version": "1.0.0" 28 | }, 29 | "servers": [ 30 | { 31 | "url": "http://localhost/" 32 | } 33 | ], 34 | "paths": {}, 35 | "components": {} 36 | } 37 | ` 38 | } 39 | 40 | var ( 41 | registrationOnce sync.Once 42 | ) 43 | 44 | func setupApp() *fiber.App { 45 | app := fiber.New() 46 | 47 | registrationOnce.Do(func() { 48 | swag.Register(swag.Name, &mock{}) 49 | }) 50 | 51 | return app 52 | } 53 | 54 | func TestDefault(t *testing.T) { 55 | app := setupApp() 56 | app.Use(New()) 57 | 58 | tests := []struct { 59 | name string 60 | url string 61 | statusCode int 62 | contentType string 63 | location string 64 | }{ 65 | { 66 | name: "Should be returns status 200 with 'text/html' content-type", 67 | url: "/docs", 68 | statusCode: 200, 69 | contentType: "text/html", 70 | }, 71 | { 72 | name: "Should be returns status 200 with 'application/json' content-type", 73 | url: "/docs/doc.json", 74 | statusCode: 200, 75 | contentType: "application/json", 76 | }, 77 | } 78 | 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | req, err := http.NewRequest(http.MethodGet, tt.url, nil) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | resp, err := app.Test(req) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | 91 | if resp.StatusCode != tt.statusCode { 92 | t.Fatalf(`StatusCode: got %v - expected %v`, resp.StatusCode, tt.statusCode) 93 | } 94 | 95 | if tt.contentType != "" { 96 | ct := resp.Header.Get("Content-Type") 97 | if ct != tt.contentType { 98 | t.Fatalf(`Content-Type: got %s - expected %s`, ct, tt.contentType) 99 | } 100 | } 101 | 102 | if tt.location != "" { 103 | location := resp.Header.Get("Location") 104 | if location != tt.location { 105 | t.Fatalf(`Location: got %s - expected %s`, location, tt.location) 106 | } 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestCustomPath(t *testing.T) { 113 | app := setupApp() 114 | app.Use("/api-docs/*", New(Config{ 115 | Path: "api-docs", 116 | })) 117 | 118 | tests := []struct { 119 | name string 120 | url string 121 | statusCode int 122 | contentType string 123 | }{ 124 | { 125 | name: "Should be returns status 200 with custom path", 126 | url: "/api-docs", 127 | statusCode: 200, 128 | contentType: "text/html", 129 | }, 130 | { 131 | name: "Should be returns status 200 for spec with custom path", 132 | url: "/api-docs/doc.json", 133 | statusCode: 200, 134 | contentType: "application/json", 135 | }, 136 | { 137 | name: "Should return status 404 for original path", 138 | url: "/docs", 139 | statusCode: 404, 140 | }, 141 | } 142 | 143 | for _, tt := range tests { 144 | t.Run(tt.name, func(t *testing.T) { 145 | req := httptest.NewRequest(http.MethodGet, tt.url, nil) 146 | resp, err := app.Test(req) 147 | assert.NoError(t, err) 148 | assert.Equal(t, tt.statusCode, resp.StatusCode) 149 | 150 | if tt.contentType != "" { 151 | assert.Equal(t, tt.contentType, resp.Header.Get("Content-Type")) 152 | } 153 | }) 154 | } 155 | } 156 | 157 | func TestCustomTitle(t *testing.T) { 158 | app := setupApp() 159 | customTitle := "Custom API Documentation" 160 | app.Use(New(Config{ 161 | Title: customTitle, 162 | })) 163 | 164 | req := httptest.NewRequest(http.MethodGet, "/docs", nil) 165 | resp, err := app.Test(req) 166 | assert.NoError(t, err) 167 | assert.Equal(t, 200, resp.StatusCode) 168 | 169 | // Create a buffer to store the response body 170 | buf := new(strings.Builder) 171 | _, err = io.Copy(buf, resp.Body) 172 | assert.NoError(t, err) 173 | 174 | // Check if the custom title is in the HTML 175 | assert.Contains(t, buf.String(), fmt.Sprintf("%s", customTitle)) 176 | } 177 | 178 | func TestCustomSpecUrl(t *testing.T) { 179 | app := setupApp() 180 | app.Use(New(Config{ 181 | RawSpecUrl: "swagger.json", 182 | })) 183 | 184 | tests := []struct { 185 | name string 186 | url string 187 | statusCode int 188 | contentType string 189 | }{ 190 | { 191 | name: "Should be returns status 200 with custom spec URL", 192 | url: "/docs/swagger.json", 193 | statusCode: 200, 194 | contentType: "application/json", 195 | }, 196 | } 197 | 198 | for _, tt := range tests { 199 | t.Run(tt.name, func(t *testing.T) { 200 | req := httptest.NewRequest(http.MethodGet, tt.url, nil) 201 | resp, err := app.Test(req) 202 | assert.NoError(t, err) 203 | assert.Equal(t, tt.statusCode, resp.StatusCode) 204 | 205 | if tt.contentType != "" { 206 | assert.Equal(t, tt.contentType, resp.Header.Get("Content-Type")) 207 | } 208 | }) 209 | } 210 | } 211 | 212 | func TestCustomFileContent(t *testing.T) { 213 | app := setupApp() 214 | customSpec := `{"openapi":"3.0.0","info":{"title":"Custom API","version":"1.0.0"}}` 215 | 216 | app.Use(New(Config{ 217 | FileContentString: customSpec, 218 | })) 219 | 220 | req := httptest.NewRequest(http.MethodGet, "/docs/doc.json", nil) 221 | resp, err := app.Test(req) 222 | assert.NoError(t, err) 223 | assert.Equal(t, 200, resp.StatusCode) 224 | 225 | buf := new(strings.Builder) 226 | _, err = io.Copy(buf, resp.Body) 227 | assert.NoError(t, err) 228 | 229 | assert.Equal(t, customSpec, strings.TrimSpace(buf.String())) 230 | } 231 | 232 | func TestCacheControl(t *testing.T) { 233 | tests := []struct { 234 | name string 235 | cacheAge int 236 | expectedHeader string 237 | }{ 238 | { 239 | name: "Should set Cache-Control with custom max-age", 240 | cacheAge: 3600, 241 | expectedHeader: "public, max-age=3600", 242 | }, 243 | { 244 | name: "Should set Cache-Control to no-store when cache age is 0", 245 | cacheAge: 0, 246 | expectedHeader: "no-store", 247 | }, 248 | } 249 | 250 | for _, tt := range tests { 251 | t.Run(tt.name, func(t *testing.T) { 252 | app := setupApp() 253 | app.Use(New(Config{ 254 | CacheAge: tt.cacheAge, 255 | })) 256 | 257 | req := httptest.NewRequest(http.MethodGet, "/docs", nil) 258 | resp, err := app.Test(req) 259 | assert.NoError(t, err) 260 | assert.Equal(t, 200, resp.StatusCode) 261 | assert.Equal(t, tt.expectedHeader, resp.Header.Get("Cache-Control")) 262 | }) 263 | } 264 | } 265 | 266 | func TestCustomStyle(t *testing.T) { 267 | app := setupApp() 268 | customStyle := "--primary-color: #ff0000; --font-size: 16px;" 269 | 270 | app.Use(New(Config{ 271 | CustomStyle: template.CSS(customStyle), 272 | })) 273 | 274 | req := httptest.NewRequest(http.MethodGet, "/docs", nil) 275 | resp, err := app.Test(req) 276 | assert.NoError(t, err) 277 | assert.Equal(t, 200, resp.StatusCode) 278 | 279 | buf := new(strings.Builder) 280 | _, err = io.Copy(buf, resp.Body) 281 | assert.NoError(t, err) 282 | 283 | assert.Contains(t, buf.String(), customStyle) 284 | } 285 | 286 | func TestNextFunction(t *testing.T) { 287 | app := setupApp() 288 | 289 | app.Get("/docs", func(c *fiber.Ctx) error { 290 | return c.SendString("Next handler called") 291 | }) 292 | 293 | req := httptest.NewRequest(http.MethodGet, "/docs", nil) 294 | resp, err := app.Test(req) 295 | assert.NoError(t, err) 296 | assert.Equal(t, 200, resp.StatusCode) 297 | 298 | buf := new(strings.Builder) 299 | _, err = io.Copy(buf, resp.Body) 300 | assert.NoError(t, err) 301 | 302 | assert.Equal(t, "Next handler called", buf.String()) 303 | } 304 | 305 | func TestJSFallbackPath(t *testing.T) { 306 | app := setupApp() 307 | app.Use(New()) 308 | 309 | req := httptest.NewRequest(http.MethodGet, "/docs/js/api-reference.min.js", nil) 310 | resp, err := app.Test(req) 311 | assert.NoError(t, err) 312 | assert.Equal(t, 200, resp.StatusCode) 313 | 314 | buf := new(strings.Builder) 315 | _, err = io.Copy(buf, resp.Body) 316 | assert.NoError(t, err) 317 | assert.Greater(t, len(buf.String()), 0) 318 | } 319 | 320 | func TestCorrectHtmlRendering(t *testing.T) { 321 | app := setupApp() 322 | app.Use(New()) 323 | 324 | req := httptest.NewRequest(http.MethodGet, "/docs", nil) 325 | resp, err := app.Test(req) 326 | assert.NoError(t, err) 327 | assert.Equal(t, 200, resp.StatusCode) 328 | 329 | buf := new(strings.Builder) 330 | _, err = io.Copy(buf, resp.Body) 331 | assert.NoError(t, err) 332 | 333 | htmlContent := buf.String() 334 | 335 | assert.Contains(t, htmlContent, "") 336 | assert.Contains(t, htmlContent, "
") 337 | assert.Contains(t, htmlContent, "function initScalar()") 338 | assert.Contains(t, htmlContent, "createApiReference('#app'") 339 | } 340 | 341 | func TestFallbackCache(t *testing.T) { 342 | app := setupApp() 343 | app.Use(New()) 344 | 345 | req := httptest.NewRequest(http.MethodGet, "/docs/js/api-reference.min.js", nil) 346 | resp, err := app.Test(req) 347 | assert.NoError(t, err) 348 | assert.Equal(t, 200, resp.StatusCode) 349 | 350 | assert.Equal(t, "public, max-age=86400", resp.Header.Get("Cache-Control")) 351 | } 352 | --------------------------------------------------------------------------------