Skip to content

Commit

Permalink
Fix for swagger docs serving (sei-protocol#1715)
Browse files Browse the repository at this point in the history
* working draft of swagger endpoint

* - delete docs package
- update README
- formatting

* - fix yml path in the docs

* - formatting

* - add tests for routes

* - Further clarify/streamline readme
  • Loading branch information
dssei authored Jun 6, 2024
1 parent 8884343 commit 4f2c02d
Show file tree
Hide file tree
Showing 27 changed files with 1,445 additions and 14 deletions.
26 changes: 25 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/gorilla/mux"
"github.com/rakyll/statik/fs"

"github.com/ethereum/go-ethereum/ethclient"
ethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/sei-protocol/sei-chain/app/antedecorators"
Expand Down Expand Up @@ -159,6 +163,9 @@ import (
"github.com/CosmWasm/wasmd/x/wasm"
wasmclient "github.com/CosmWasm/wasmd/x/wasm/client"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"

// unnamed import of statik for openapi/swagger UI support
_ "github.com/sei-protocol/sei-chain/docs/swagger"
)

// this line is used by starport scaffolding # stargate/wasm/app/enabledProposals
Expand Down Expand Up @@ -1725,7 +1732,7 @@ func (app *App) GetSubspace(moduleName string) paramstypes.Subspace {

// RegisterAPIRoutes registers all application module routes with the provided
// API server.
func (app *App) RegisterAPIRoutes(apiSvr *api.Server, _ config.APIConfig) {
func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig) {
clientCtx := apiSvr.ClientCtx
rpc.RegisterRoutes(clientCtx, apiSvr.Router)
// Register legacy tx routes.
Expand All @@ -1738,6 +1745,12 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, _ config.APIConfig) {
// Register legacy and grpc-gateway routes for all modules.
ModuleBasics.RegisterRESTRoutes(clientCtx, apiSvr.Router)
ModuleBasics.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter)

// register swagger API from root so that other applications can override easily
if apiConfig.Swagger {
RegisterSwaggerAPI(apiSvr.Router)
}

}

// RegisterTxService implements the Application.RegisterTxService method.
Expand Down Expand Up @@ -1781,6 +1794,17 @@ func (app *App) RegisterTendermintService(clientCtx client.Context) {
}
}

// RegisterSwaggerAPI registers swagger route with API Server
func RegisterSwaggerAPI(rtr *mux.Router) {
statikFS, err := fs.NewWithNamespace("swagger")
if err != nil {
panic(err)
}

staticServer := http.FileServer(statikFS)
rtr.PathPrefix("/swagger/").Handler(http.StripPrefix("/swagger/", staticServer))
}

func (app *App) checkTotalBlockGasWanted(ctx sdk.Context, txs [][]byte) bool {
totalGasWanted := uint64(0)
for _, tx := range txs {
Expand Down
73 changes: 73 additions & 0 deletions app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import (
"context"
"encoding/hex"
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/server/api"
cosmosConfig "github.com/cosmos/cosmos-sdk/server/config"
"github.com/gorilla/mux"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"math/big"
"reflect"
"testing"
"time"

Expand Down Expand Up @@ -405,3 +411,70 @@ func TestDecodeTransactionsConcurrently(t *testing.T) {
require.Nil(t, typedTxs[1])
require.NotNil(t, typedTxs[2])
}

func TestApp_RegisterAPIRoutes(t *testing.T) {
type args struct {
apiSvr *api.Server
apiConfig cosmosConfig.APIConfig
}
tests := []struct {
name string
args args
wantSwagger bool
}{
{
name: "swagger added to the router if configured",
args: args{
apiSvr: &api.Server{
ClientCtx: client.Context{},
Router: &mux.Router{},
GRPCGatewayRouter: runtime.NewServeMux(),
},
apiConfig: cosmosConfig.APIConfig{
Swagger: true,
},
},
wantSwagger: true,
},
{
name: "swagger not added to the router if not configured",
args: args{
apiSvr: &api.Server{
ClientCtx: client.Context{},
Router: &mux.Router{},
GRPCGatewayRouter: runtime.NewServeMux(),
},
apiConfig: cosmosConfig.APIConfig{},
},
wantSwagger: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
seiApp := &app.App{}
seiApp.RegisterAPIRoutes(tt.args.apiSvr, tt.args.apiConfig)
routes := tt.args.apiSvr.Router
gotSwagger := isSwaggerRouteAdded(routes)

if !reflect.DeepEqual(gotSwagger, tt.wantSwagger) {
t.Errorf("Run() gotSwagger = %v, want %v", gotSwagger, tt.wantSwagger)
}
})

}
}

func isSwaggerRouteAdded(router *mux.Router) bool {
var isAdded bool
err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
pathTemplate, err := route.GetPathTemplate()
if err == nil && pathTemplate == "/swagger/" {
isAdded = true
}
return nil
})
if err != nil {
return false
}
return isAdded
}
2 changes: 1 addition & 1 deletion config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ validator:
staked: '100000000stake'
client:
openapi:
path: 'docs/static/openapi.yml'
path: 'docs/swagger-ui/swagger.yml'
vuex:
path: 'vue/src/store'
faucet:
Expand Down
48 changes: 42 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# OpenAPI docs generation
# OpenAPI/Swagger docs generation

To generate the [OpenAPI](https://github.com/OAI/OpenAPI-Specification/) docs, first install the `ignite` tool.
> **Note:** Anytime we make changes to the APIs/proto files, we also need to update the Swagger/OpenAPI docs.
Both Swagger and OpenAPI terms are used interchangeably in this document.
- [OpenAPI](https://github.com/OAI/OpenAPI-Specification/) = specification
- Swagger = Tools for implementing the specification.

The process of Swagger docs generation involves running a script that does the following tasks:
1. Generating the swagger yml file using the `ignite` tool.
2. Updating the static assets for the Swagger UI.

## Prerequisites

The docs generation script uses the `ignite` tool to generate the OpenAPI docs.
So, first install the `ignite` tool, if not installed already.
We need version v0.23.0, which is outdated, but works with the current version of the codebase.
Pull binaries from the [releases page](https://github.com/ignite/cli/releases/tag/v0.23.0) or install from source code
following instructions.
Expand All @@ -20,11 +33,34 @@ Ignite CLI version: v0.23.0
....

```
Then, to generate the OpenAPI docs, run the following command:
## Running the script
Then run the following command to generate the OpenAPI docs and update static assets for Swagger UI:

```bash
ignite generate openapi
./scripts/update-swagger-ui-statik.sh
```
Updated docs will be available in the `docs/static/openapi.yml` directory.
The script generates a new swagger/openapi yml and saves it as `docs/swagger-ui/swagger.yml`.
It will then embed the yml file and static assets in `docs/swagger-ui/` directory into the `docs/swagger/statik.go` file.

If swagger endpoint is configured, the app on startup will "read" `statik.go` and make it's content available at the
`/swagger/` endpoint.

# Serving the OpenAPI docs on the node

To view the rendered OpenAPI docs, try the [Swagger UI](https://editor-next.swagger.io/)
To start serving the docs, enable the swagger docs serving.
To do that, update `api.swagger` value to `true` in the `app.toml` file.

```toml
###############################################################################
### API Configuration ###
###############################################################################

[api]

# Enable defines if the API server should be enabled.
enable = true

# Swagger defines if swagger documentation should automatically be registered.
swagger = true
```
Once node is restarted, swagger docs will be available at `http://<node-ip>:<port>/swagger/`
6 changes: 0 additions & 6 deletions docs/docs.go

This file was deleted.

Binary file added docs/swagger-ui/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/swagger-ui/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions docs/swagger-ui/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}

*,
*:before,
*:after {
box-sizing: inherit;
}

body {
margin: 0;
background: #fafafa;
}
19 changes: 19 additions & 0 deletions docs/swagger-ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
</head>

<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="./swagger-initializer.js" charset="UTF-8"> </script>
</body>
</html>
79 changes: 79 additions & 0 deletions docs/swagger-ui/oauth2-redirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;

if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}

arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};

isValid = qp.state === sentState;

if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}

if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}

oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}

if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>
20 changes: 20 additions & 0 deletions docs/swagger-ui/swagger-initializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">

// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "./swagger.yml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});

//</editor-fold>
};
2 changes: 2 additions & 0 deletions docs/swagger-ui/swagger-ui-bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/swagger-ui/swagger-ui-bundle.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/swagger-ui/swagger-ui-es-bundle-core.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/swagger-ui/swagger-ui-es-bundle-core.js.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/swagger-ui/swagger-ui-es-bundle.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/swagger-ui/swagger-ui-es-bundle.js.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/swagger-ui/swagger-ui-standalone-preset.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/swagger-ui/swagger-ui-standalone-preset.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/swagger-ui/swagger-ui.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/swagger-ui/swagger-ui.css.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/swagger-ui/swagger-ui.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/swagger-ui/swagger-ui.js.map

Large diffs are not rendered by default.

Loading

0 comments on commit 4f2c02d

Please sign in to comment.