Skip to content

Commit

Permalink
feat(*): support serving API documentation pages when the server star…
Browse files Browse the repository at this point in the history
…ts (#370)

* feat(*): support serving API documentation pages when the server starts

* chore(*): fix file header
  • Loading branch information
Xinzhao Xu committed Oct 25, 2020
1 parent ca969f3 commit 2fb93f5
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 167 deletions.
164 changes: 9 additions & 155 deletions cmd/nirvana/api/api.go
Expand Up @@ -17,14 +17,6 @@ limitations under the License.
package api

import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"

Expand All @@ -33,13 +25,13 @@ import (
"github.com/caicloud/nirvana/definition"
"github.com/caicloud/nirvana/log"
"github.com/caicloud/nirvana/service"
"github.com/caicloud/nirvana/utils/generators/swagger"
"github.com/caicloud/nirvana/utils/api"
generatorsutils "github.com/caicloud/nirvana/utils/generators/utils"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

const mimeHTML = "text/html"

func init() {
err := service.RegisterProducer(service.NewSimpleSerializer("text/html"))
if err != nil {
Expand Down Expand Up @@ -94,24 +86,13 @@ func (o *apiOptions) Run(cmd *cobra.Command, args []string) error {

log.Infof("Project root directory is %s", config.Root)

generator := swagger.NewDefaultGenerator(config, definitions)
swaggers, err := generator.Generate()
files, err := generatorsutils.GenSwaggerData(config, definitions)
if err != nil {
return err
}

files := map[string][]byte{}
for filename, s := range swaggers {
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}

files[filename] = data
}

if o.Output != "" {
if err = o.write(files); err != nil {
if err = api.WriteFiles(o.Output, files); err != nil {
return err
}
}
Expand All @@ -122,25 +103,6 @@ func (o *apiOptions) Run(cmd *cobra.Command, args []string) error {
return err
}

func (o *apiOptions) write(apis map[string][]byte) error {
dir := o.Output
dir, err := filepath.Abs(dir)
if err != nil {
return err
}
if err := os.MkdirAll(dir, 0775); err != nil {
return err
}
for version, data := range apis {
file := filepath.Join(dir, o.pathForVersion(version))
if err := ioutil.WriteFile(file, data, 0664); err != nil {
return err
}
}
log.Infof("Generated openapi schemes to %s", dir)
return nil
}

func (o *apiOptions) serve(apis map[string][]byte) error {
hosts := strings.Split(o.Serve, ":")
ip := strings.TrimSpace(hosts[0])
Expand All @@ -160,134 +122,26 @@ func (o *apiOptions) serve(apis map[string][]byte) error {
}
log.SetDefaultLogger(log.NewStdLogger(0))
cfg := nirvana.NewDefaultConfig()
versions := []string{}
versions := make([]string, 0, len(apis))
for v, data := range apis {
versions = append(versions, v)
cfg.Configure(nirvana.Descriptor(
o.descriptorForData(o.pathForVersion(v), data, definition.MIMEJSON),
api.DescriptorForData(api.PathForVersion("/", v), data, definition.MIMEJSON),
))
}
data, err := o.indexData(versions)
data, err := api.GenSwaggerPageData("/", versions)
if err != nil {
return err
}
cfg.Configure(
nirvana.Descriptor(o.descriptorForData("/", data, mimeHTML)),
nirvana.Descriptor(api.DescriptorForData("/", data, definition.MIMEHTML)),
nirvana.IP(ip),
nirvana.Port(port),
)
log.Infof("Listening on %s:%d. Please open your browser to view api docs", cfg.IP(), cfg.Port())
return nirvana.NewServer(cfg).Serve()
}

func (o *apiOptions) descriptorForData(path string, data []byte, ct string) definition.Descriptor {
return definition.Descriptor{
Path: path,
Definitions: []definition.Definition{
{
Method: definition.Get,
Consumes: []string{definition.MIMENone},
Produces: []string{ct},
Function: func(context.Context) ([]byte, error) {
return data, nil
},
Parameters: []definition.Parameter{},
Results: definition.DataErrorResults(""),
},
},
}
}

func (o *apiOptions) pathForVersion(version string) string {
return fmt.Sprintf("/api.%s.json", version)
}

func (o *apiOptions) indexData(versions []string) ([]byte, error) {
index := `
<!-- 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="https://unpkg.com/swagger-ui-dist@3.25.3/swagger-ui.css" >
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@3.25.3/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@3.25.3/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@3.25.3/swagger-ui-bundle.js"> </script>
<script src="https://unpkg.com/swagger-ui-dist@3.25.3/swagger-ui-standalone-preset.js"> </script>
<script>
// list of APIS
var apis = [
{{ range $i, $v := . }}
{
name: '{{ $v.Name }}',
url: '{{ $v.Path }}'
},
{{ end }}
];
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
urls: apis,
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>
`
tmpl, err := template.New("index.html").Parse(index)
if err != nil {
return nil, err
}
data := make([]struct {
Name string
Path string
}, len(versions))
for i, v := range versions {
data[i].Name = v
data[i].Path = o.pathForVersion(v)
}
buf := bytes.NewBuffer(nil)
if err := tmpl.Execute(buf, data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

func (o *apiOptions) Manuals() string {
return ""
}
3 changes: 2 additions & 1 deletion definition/helper.go
Expand Up @@ -24,7 +24,7 @@ import (

// MIME types
const (
// acceptTypeAll indicates a accept type from http request.
// MIMEAll indicates a accept type from http request.
// It means client can receive any content.
// Request content type in header "Content-Type" must not set to "*/*".
// It only can exist in request header "Accept".
Expand All @@ -33,6 +33,7 @@ const (
MIMEAll = "*/*"
MIMENone = ""
MIMEText = "text/plain"
MIMEHTML = "text/html"
MIMEJSON = "application/json"
MIMEXML = "application/xml"
MIMEOctetStream = "application/octet-stream"
Expand Down
12 changes: 9 additions & 3 deletions examples/api-basic/main.go
Expand Up @@ -17,15 +17,21 @@ limitations under the License.
package main

import (
"github.com/caicloud/nirvana"
"github.com/caicloud/nirvana/config"
"github.com/caicloud/nirvana/examples/api-basic/api/v1"
"github.com/caicloud/nirvana/examples/api-basic/api/v2"
v1 "github.com/caicloud/nirvana/examples/api-basic/api/v1"
v2 "github.com/caicloud/nirvana/examples/api-basic/api/v2"
"github.com/caicloud/nirvana/log"
)

func main() {
cmd := config.NewDefaultNirvanaCommand()
if err := cmd.Execute(v1.Descriptor(), v2.Descriptor()); err != nil {
cfg := nirvana.NewDefaultConfig()
cfg.Configure(
nirvana.Descriptor(v1.Descriptor(), v2.Descriptor()),
nirvana.APIDocs("github.com/caicloud/nirvana/examples/api-basic", "/docs"),
)
if err := cmd.ExecuteWithConfig(cfg); err != nil {
log.Fatal(err)
}
}
23 changes: 23 additions & 0 deletions examples/api-basic/nirvana.yaml
@@ -0,0 +1,23 @@
project: myproject
description: This project uses nirvana as API framework
schemes:
- http
hosts:
- localhost:8080
contacts:
- name: nobody
email: nobody@nobody.io
description: Maintain this project
versions:
- name: v1
description: The v1 version is the first version of this project
rules:
- prefix: /api/v1
regexp: ""
replacement: ""
- name: v2
description: The v2 version is the first version of this project
rules:
- prefix: /api/v2
regexp: ""
replacement: ""

0 comments on commit 2fb93f5

Please sign in to comment.