diff --git a/cmd/nirvana/api/api.go b/cmd/nirvana/api/api.go
index 6b46e4c4..1964e3ad 100644
--- a/cmd/nirvana/api/api.go
+++ b/cmd/nirvana/api/api.go
@@ -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"
@@ -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 {
@@ -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
}
}
@@ -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])
@@ -160,19 +122,19 @@ 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),
)
@@ -180,114 +142,6 @@ func (o *apiOptions) serve(apis map[string][]byte) error {
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 := `
-
-
-
-
-
- Swagger UI
-
-
-
-
-
-
-
-
-
-
-
-
-`
- 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 ""
}
diff --git a/definition/helper.go b/definition/helper.go
index cb2bd6b6..55a63db7 100644
--- a/definition/helper.go
+++ b/definition/helper.go
@@ -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".
@@ -33,6 +33,7 @@ const (
MIMEAll = "*/*"
MIMENone = ""
MIMEText = "text/plain"
+ MIMEHTML = "text/html"
MIMEJSON = "application/json"
MIMEXML = "application/xml"
MIMEOctetStream = "application/octet-stream"
diff --git a/examples/api-basic/main.go b/examples/api-basic/main.go
index 43be0093..356b4f72 100644
--- a/examples/api-basic/main.go
+++ b/examples/api-basic/main.go
@@ -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)
}
}
diff --git a/examples/api-basic/nirvana.yaml b/examples/api-basic/nirvana.yaml
new file mode 100644
index 00000000..f57d3f12
--- /dev/null
+++ b/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: ""
diff --git a/nirvana.go b/nirvana.go
index 42e24e30..fee03ca1 100644
--- a/nirvana.go
+++ b/nirvana.go
@@ -20,6 +20,8 @@ import (
"context"
"fmt"
"net/http"
+ "os"
+ "path"
"sync"
"sync/atomic"
@@ -27,10 +29,9 @@ import (
"github.com/caicloud/nirvana/errors"
"github.com/caicloud/nirvana/log"
"github.com/caicloud/nirvana/service"
-
- // This blank import will make it in the dependencies of projects using Nirvana
- // for API docs generation.
- _ "github.com/caicloud/nirvana/utils/api"
+ "github.com/caicloud/nirvana/utils/api"
+ generatorsutils "github.com/caicloud/nirvana/utils/generators/utils"
+ "github.com/caicloud/nirvana/utils/project"
)
// Server is a complete API server.
@@ -73,6 +74,13 @@ type Config struct {
// locked is for locking current config. If the field
// is not 0, any modification causes panic.
locked int32
+
+ // options for generating API docs
+
+ // root is the absolute path of the current project.
+ root string
+ // apiDocsPath is the path to the API documentation page, empty means do not serve the documentation page.
+ apiDocsPath string
}
// lock locks config. If succeed, it will return ture.
@@ -203,6 +211,76 @@ func NewServer(c *Config) Server {
var noConfigInstaller = errors.InternalServerError.Build("Nirvana:NoConfigInstaller", "no config installer for external config name ${name}")
+func (s *server) buildAPIDocs(definitions *api.Definitions) (map[string][]byte, error) {
+ config, _ := project.LoadDefaultProjectFile(s.config.root)
+ if config == nil {
+ config = &project.Config{
+ Root: s.config.root,
+ Project: "Unknown Project",
+ Description: "This project does not have a project config.",
+ Schemes: []string{"http"},
+ Hosts: []string{"localhost"},
+ Versions: []project.Version{
+ {
+ Name: "unversioned",
+ PathRules: []project.PathRule{
+ {
+ Prefix: "/",
+ },
+ },
+ },
+ },
+ }
+ log.Warning("can't find project file, instead by default config")
+ }
+ return generatorsutils.GenSwaggerData(config, definitions)
+}
+
+func (s *server) serveAPIDocs(builder service.Builder) error {
+ typeContainer := api.NewTypeContainer()
+ analyzer, err := api.NewAnalyzer(s.config.root, "./...")
+ if err != nil {
+ return err
+ }
+ result, err := api.NewPathDefinitions(typeContainer, builder.Definitions())
+ if err != nil {
+ return err
+ }
+ if err = typeContainer.Complete(analyzer); err != nil {
+ return err
+ }
+ definitions := &api.Definitions{
+ Definitions: result,
+ Types: typeContainer.Types(),
+ }
+ files, err := s.buildAPIDocs(definitions)
+ if err != nil {
+ return err
+ }
+
+ // serve the API docs page
+ if s.config.apiDocsPath != "" {
+ apiDescriptor := make([]definition.Descriptor, 0, len(files)+1)
+ versions := make([]string, 0, len(files))
+ for v, data := range files {
+ versions = append(versions, v)
+ apiDescriptor = append(apiDescriptor, api.DescriptorForData(api.PathForVersion(s.config.apiDocsPath, v), data, definition.MIMEJSON))
+ }
+ data, err := api.GenSwaggerPageData(s.config.apiDocsPath, versions)
+ if err != nil {
+ return err
+ }
+ apiDescriptor = append(
+ apiDescriptor,
+ api.DescriptorForData(s.config.apiDocsPath, data, definition.MIMEHTML),
+ )
+ if err = builder.AddDescriptor(apiDescriptor...); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
// Builder create a service builder for current server. Don't use this method directly except
// there is a special server to hold http services. After server shutdown, clean resources via
// returned cleaner.
@@ -221,6 +299,13 @@ func (s *server) Builder() (builder service.Builder, cleaner func() error, err e
if err := builder.AddDescriptor(s.config.descriptors...); err != nil {
return nil, nil, err
}
+
+ if s.config.root != "" {
+ if err := s.serveAPIDocs(builder); err != nil {
+ return nil, nil, err
+ }
+ }
+
if err := s.config.forEach(func(name string, config interface{}) error {
installer := ConfigInstallerFor(name)
if installer == nil {
@@ -388,3 +473,24 @@ func Modifier(modifiers ...service.DefinitionModifier) Configurer {
return nil
}
}
+
+// APIDocs returns a configurer to set API docs generation related options.
+func APIDocs(modulePath, apiDocsPath string) Configurer {
+ return func(c *Config) error {
+ if modulePath == "" {
+ return fmt.Errorf("the module path can't be empty")
+ }
+ var err error
+ root := path.Join(os.Getenv("GOPATH"), "src", modulePath)
+ s, _ := os.Stat(root)
+ if s == nil || !s.IsDir() {
+ root, err = os.Getwd()
+ if err != nil {
+ return err
+ }
+ }
+ c.root = root
+ c.apiDocsPath = apiDocsPath
+ return nil
+ }
+}
diff --git a/service/builder.go b/service/builder.go
index a799d7eb..42009620 100644
--- a/service/builder.go
+++ b/service/builder.go
@@ -38,9 +38,9 @@ type Builder interface {
SetModifier(m DefinitionModifier)
// Filters returns all request filters.
Filters() []Filter
- // AddFilters add filters to filter requests.
+ // AddFilter add filters to filter requests.
AddFilter(filters ...Filter)
- // AddDescriptors adds descriptors to router.
+ // AddDescriptor adds descriptors to router.
AddDescriptor(descriptors ...definition.Descriptor) error
// Middlewares returns all router middlewares.
Middlewares() map[string][]definition.Middleware
@@ -77,7 +77,7 @@ func (b *builder) Filters() []Filter {
return result
}
-// AddFilters add filters to filter requests.
+// AddFilter add filters to filter requests.
func (b *builder) AddFilter(filters ...Filter) {
b.filters = append(b.filters, filters...)
}
diff --git a/service/content.go b/service/content.go
index 5b473448..524dbfda 100644
--- a/service/content.go
+++ b/service/content.go
@@ -62,6 +62,7 @@ var producers = map[string]Producer{
definition.MIMEJSON: &JSONSerializer{},
definition.MIMEXML: &XMLSerializer{},
definition.MIMEOctetStream: NewSimpleSerializer(definition.MIMEOctetStream),
+ definition.MIMEHTML: NewSimpleSerializer(definition.MIMEHTML),
}
// AllConsumers returns all consumers.
diff --git a/utils/api/utils.go b/utils/api/utils.go
new file mode 100644
index 00000000..30ccb49a
--- /dev/null
+++ b/utils/api/utils.go
@@ -0,0 +1,161 @@
+/*
+Copyright 2020 Caicloud Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "os"
+ "path"
+ "path/filepath"
+
+ "github.com/caicloud/nirvana/definition"
+)
+
+// PathForVersion returns the path of API file.
+func PathForVersion(root, version string) string {
+ return path.Join(root, fmt.Sprintf("/api.%s.json", version))
+}
+
+// WriteFiles writes the API data into files.
+func WriteFiles(output string, apis map[string][]byte) error {
+ dir, err := filepath.Abs(output)
+ if err != nil {
+ return err
+ }
+ if err = os.MkdirAll(dir, 0775); err != nil {
+ return err
+ }
+ for version, data := range apis {
+ file := filepath.Join(dir, fmt.Sprintf("api.%s.json", version))
+ if err = ioutil.WriteFile(file, data, 0664); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// GenSwaggerPageData generates a Swagger UI page based on the given API files.
+func GenSwaggerPageData(root string, versions []string) ([]byte, error) {
+ index := `
+
+
+
+
+
+ Swagger UI
+
+
+
+
+
+
+
+
+
+
+
+
+`
+ tmpl, err := template.New("index.html").Parse(index)
+ if err != nil {
+ return nil, err
+ }
+ data := make([]struct {
+ Name string
+ Path string
+ }, 0, len(versions))
+ for _, v := range versions {
+ data = append(data, struct {
+ Name string
+ Path string
+ }{v, PathForVersion(root, v)})
+ }
+ buf := bytes.NewBuffer(nil)
+ if err = tmpl.Execute(buf, data); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// DescriptorForData generates a Descriptor for API docs page.
+func DescriptorForData(path string, data []byte, contentType string) definition.Descriptor {
+ return definition.Descriptor{
+ Path: path,
+ Definitions: []definition.Definition{
+ {
+ Method: definition.Get,
+ Consumes: []string{definition.MIMENone},
+ Produces: []string{contentType},
+ Function: func(context.Context) ([]byte, error) {
+ return data, nil
+ },
+ Parameters: []definition.Parameter{},
+ Results: definition.DataErrorResults(""),
+ },
+ },
+ }
+}
diff --git a/utils/generators/swagger/generator.go b/utils/generators/swagger/generator.go
index bdfdbdee..0bc2603d 100644
--- a/utils/generators/swagger/generator.go
+++ b/utils/generators/swagger/generator.go
@@ -464,7 +464,8 @@ func (g *Generator) generateParameter(param *api.Parameter) []spec.Parameter {
if len(param.Default) > 0 {
parameter.Required = false
}
- if parameter.In != "body" {
+ body := "body"
+ if parameter.In != body {
// Only body parameter can hold a schema. Other parameters uses type
// and format.
parameter.Type = schema.Type[0]
@@ -479,6 +480,10 @@ func (g *Generator) generateParameter(param *api.Parameter) []spec.Parameter {
parameter.Items.Format = schema.Items.Schema.Format
}
parameter.Schema = nil
+ } else {
+ // add parameter name for body, it required by swagger ui,
+ // cause api.Parameter.Name is always nil when In is body
+ parameter.Name = body
}
if len(param.Default) > 0 {
diff --git a/utils/generators/utils/utils.go b/utils/generators/utils/utils.go
new file mode 100644
index 00000000..d0f481a8
--- /dev/null
+++ b/utils/generators/utils/utils.go
@@ -0,0 +1,45 @@
+/*
+Copyright 2020 Caicloud Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+ "encoding/json"
+
+ "github.com/caicloud/nirvana/utils/api"
+ "github.com/caicloud/nirvana/utils/generators/swagger"
+ "github.com/caicloud/nirvana/utils/project"
+)
+
+// GenSwaggerData generates swagger data for definitions.
+func GenSwaggerData(config *project.Config, definitions *api.Definitions) (map[string][]byte, error) {
+ generator := swagger.NewDefaultGenerator(config, definitions)
+ swaggers, err := generator.Generate()
+ if err != nil {
+ return nil, err
+ }
+
+ files := make(map[string][]byte, len(swaggers))
+ for filename, s := range swaggers {
+ data, err := json.MarshalIndent(s, "", " ")
+ if err != nil {
+ return nil, err
+ }
+
+ files[filename] = data
+ }
+ return files, nil
+}