├── .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 |
--------------------------------------------------------------------------------