From 4a3d6a886381e50e1b4210f1ac2bd17119ad0213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20W=C3=B3jcik?= Date: Wed, 23 Feb 2022 20:52:49 +0100 Subject: [PATCH 1/2] simpler initialization of router, with more but hidden options (#11) --- docs/docs.go | 119 +++++++++++++++++++++++++++++++ docs/handler.go | 33 +++++++-- docs/structures.go | 141 +++++++++++++++---------------------- docs/swagger_template.html | 4 +- examples/main.go | 21 ++---- go.mod | 2 +- go.sum | 16 +---- router.go | 44 ++---------- 8 files changed, 223 insertions(+), 157 deletions(-) create mode 100644 docs/docs.go diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..8080823 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,119 @@ +package docs + +import ( + "encoding/json" + "github.com/getkin/kin-openapi/openapi3" + "github.com/gin-gonic/gin" + "gopkg.in/yaml.v3" + "os" + "regexp" +) + +func New(options *Options) *Docs { + if options.Title == "" { + options.Title = defaultOptions.Title + } + if options.Version == "" { + options.Version = defaultOptions.Version + } + if options.InteractiveUrl == "" { + options.InteractiveUrl = defaultOptions.InteractiveUrl + } + if options.JsonUrl == "" { + options.JsonUrl = defaultOptions.JsonUrl + } + if options.YamlUrl == "" { + options.YamlUrl = defaultOptions.YamlUrl + } + if options.Servers == nil { + options.Servers = defaultOptions.Servers + } + + servers := openapi3.Servers{} + for _, url := range options.Servers { + servers = append(servers, &openapi3.Server{URL: url}) + } + + tags := openapi3.Tags{} + for _, tag := range options.Tags { + tags = append(tags, &openapi3.Tag{Name: tag}) + } + + d := Docs{ + OpenApi: &openapi3.T{ + OpenAPI: "3.0.0", + Info: &openapi3.Info{ + ExtensionProps: options.ExtensionProps, + Title: options.Title, + Description: options.Description, + TermsOfService: options.TermsOfService, + Contact: options.Contact, + License: options.License, + Version: options.Version, + }, + Paths: make(openapi3.Paths), + Servers: servers, + Tags: tags, + }, + InteractiveUrl: options.InteractiveUrl, + JsonUrl: options.JsonUrl, + YamlUrl: options.YamlUrl, + } + return &d +} + +type Docs struct { + OpenApi *openapi3.T + InteractiveUrl string + JsonUrl string + YamlUrl string +} + +func (d *Docs) SetPath(path string, method string, doc *Endpoint) { + existingPathItem := d.PathItem(d.NormalizePath(path)) + existingPathItem.SetOperation(method, (*openapi3.Operation)(doc)) + d.OpenApi.Paths[d.NormalizePath(path)] = existingPathItem +} + +func (d *Docs) SaveAsJson(path string) error { + data, err := json.Marshal(d.OpenApi) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +func (d *Docs) SaveAsYaml(path string) error { + data, err := yaml.Marshal(d.OpenApi) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) +} + +func (d *Docs) NormalizePath(path string) string { + reg := regexp.MustCompile("/:([0-9a-zA-Z]+)") + return reg.ReplaceAllString(path, "/{${1}}") +} + +func (d *Docs) PathItem(path string) *openapi3.PathItem { + pathItem := d.OpenApi.Paths.Find(path) + if pathItem == nil { + return &openapi3.PathItem{} + } + return pathItem +} + +func (d *Docs) RegisterRoutes(router gin.IRouter, port string) { + handler := NewHandler(d, port) + + if d.YamlUrl != NoUrl { + router.GET(d.YamlUrl, handler.YamlFile) + } + if d.JsonUrl != NoUrl { + router.GET(d.JsonUrl, handler.JsonFile) + if d.InteractiveUrl != NoUrl { + router.GET(d.InteractiveUrl, handler.Docs) + } + } +} diff --git a/docs/handler.go b/docs/handler.go index 53bd5de..4e7623b 100644 --- a/docs/handler.go +++ b/docs/handler.go @@ -2,7 +2,9 @@ package docs import ( _ "embed" + "fmt" "github.com/gin-gonic/gin" + "gopkg.in/yaml.v3" "html/template" "net/http" ) @@ -13,22 +15,43 @@ var docsTemplateStr string type Handler struct { docs *Docs template *template.Template + port string } -func NewHandler(docs *Docs) *Handler { +func NewHandler(docs *Docs, port string) *Handler { tmpl := template.Must(template.New("").Parse(docsTemplateStr)) - return &Handler{docs: docs, template: tmpl} + return &Handler{docs: docs, template: tmpl, port: port} } func (h *Handler) Docs(ctx *gin.Context) { ctx.Header("Content-Type", "text/html; charset=utf-8") ctx.Status(http.StatusOK) - err := h.template.Execute(ctx.Writer, h.docs) + + values := map[string]interface{}{ + "title": h.docs.OpenApi.Info.Title, + "jsonUrl": fmt.Sprintf("http://localhost:%s%s", h.port, h.docs.JsonUrl), + } + + err := h.template.Execute(ctx.Writer, values) if err != nil { panic(err.Error()) } } -func (h *Handler) File(ctx *gin.Context) { - ctx.JSON(http.StatusOK, h.docs.OpenAPIContent()) +func (h *Handler) JsonFile(ctx *gin.Context) { + ctx.Header("Content-Description", "File Transfer") + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.json", h.docs.OpenApi.Info.Title)) + ctx.JSON(http.StatusOK, h.docs.OpenApi) +} + +func (h *Handler) YamlFile(ctx *gin.Context) { + ctx.Header("Content-Description", "File Transfer") + ctx.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.yaml", h.docs.OpenApi.Info.Title)) + + bytes, err := yaml.Marshal(h.docs.OpenApi) + if err != nil { + panic(err) + } + + ctx.Data(http.StatusOK, "application/x-yaml; charset=utf-8", bytes) } diff --git a/docs/structures.go b/docs/structures.go index a6a9ab0..d3315ef 100644 --- a/docs/structures.go +++ b/docs/structures.go @@ -1,12 +1,7 @@ package docs import ( - "encoding/json" - "fmt" "github.com/getkin/kin-openapi/openapi3" - "github.com/gin-contrib/cors" - "os" - "regexp" ) const ( @@ -16,95 +11,75 @@ const ( JsonTag = "json" DefaultStatusTag = "default_status" StatusCodesTag = "status_codes" + NoUrl = "-" ) -type Docs struct { - OpenAPIPath string - OpenAPI *openapi3.T - OpenAPIUrl string - Title string - TermsOfService string - Description string - License *openapi3.License - Contact *openapi3.Contact - Version string - OpenAPIFilePath string - InMemory bool - CORS *cors.Config -} +type Options struct { + // ExtensionProps OpenAPI extensions. + // It reads/writes all properties with prefix "x-". + // It is empty as default. + ExtensionProps openapi3.ExtensionProps -func (d *Docs) NewOpenAPI() { - d.OpenAPI = &openapi3.T{ - OpenAPI: "3.0.0", - Info: &openapi3.Info{ - Title: d.Title, - Description: d.Description, - TermsOfService: d.TermsOfService, - Contact: d.Contact, - License: d.License, - Version: d.Version, - }, - } -} + // Title of the documentation. + // If not set, the default value is "Documentation". + Title string -func (d *Docs) Valid() error { - if !d.InMemory && d.OpenAPIFilePath == "" { - return fmt.Errorf("invalid docs configuration, OpenAPIFilePath field cannot to be empty without InMemory option") - } - return nil -} + // Description of the documentation. + // It is empty as default. + Description string -func (d *Docs) OpenAPIContent() *openapi3.T { - if !d.InMemory { - doc, err := openapi3.NewLoader().LoadFromFile(d.OpenAPIFilePath + "openapi.json") - if err != nil { - panic(fmt.Sprintf("unable to load openapi.json file (path: %s), err: %v", d.OpenAPIPath, err)) - } - return doc - } - return d.OpenAPI -} + // TermsOfService usually should contain the URL to terms of service. + // It is empty as default. + TermsOfService string -func (d *Docs) SetPath(path string, method string, doc *Endpoint) { - existingPathItem := d.PathItem(d.FixPath(path)) - existingPathItem.SetOperation(method, (*openapi3.Operation)(doc)) - if d.PathsIsEmpty() { - d.OpenAPI.Paths = make(openapi3.Paths) - } - d.OpenAPI.Paths[d.FixPath(path)] = existingPathItem -} + // Contact information must be in openAPI format. + // It is empty as default. + Contact *openapi3.Contact -func (d *Docs) SetPathItem(path string, pathItem *openapi3.PathItem) { - d.OpenAPI.Paths[d.FixPath(path)] = pathItem -} + // License information must be in openAPI format. + // It is empty as default. + License *openapi3.License -func (d *Docs) AddServer(addr string) { - d.OpenAPI.Servers = append(d.OpenAPI.Servers, &openapi3.Server{URL: addr}) -} + // Version of the documentation. + // If not set, the default value is "1.0.0". + Version string -func (d *Docs) Build() error { - data, err := json.Marshal(d.OpenAPI) - if err != nil { - return err - } - // I assume the folder will be created, if it doesn't mean that there is already such a folder. - _ = os.Mkdir(d.OpenAPIFilePath, 0755) - return os.WriteFile(d.OpenAPIFilePath+"openapi.json", data, 0755) -} + // InteractiveUrl is the path where your interactive documentation will be placed. It must be an absolute path, started with "/". + // If you run the server locally, then your interactive docs will be under "http://localhost:". + // If set to NoUrl, the interactive documentation will not be served. + // If not set, the default value is "/docs". + // + // Interactive documentation uses the JSON file, thus it requires the JsonUrl is set to a valid url. + // If the JsonUrl is set to NoUrl, the interactive documentation will be disabled. + InteractiveUrl string -func (d *Docs) PathsIsEmpty() bool { - return len(d.OpenAPI.Paths) == 0 -} + // JsonUrl is the path where your openAPI in JSON format will be placed. It must be an absolute path, started with "/". + // If you run the server locally, then the file will be under "http://localhost:". + // If set to NoUrl, the json file will not be served. + // If not set, the default value is "/docs.json". + JsonUrl string + + // YamlUrl is the path where your openAPI in YAML format will be placed. It must be an absolute path, started with "/". + // If you run the server locally, then the file will be under "http://localhost:". + // If set to NoUrl, the yaml file will not be served. + // If not set, the default value is "/docs.yaml". + YamlUrl string + + // Servers is the list of the API locations. In interactive documentation (see InteractiveUrl) you can try your API out using one of those servers. + // In case it is an empty list, it will be empty. + // In case it is nil, the default value is a list with one element "http://localhost:8080". + Servers []string -func (d *Docs) FixPath(path string) string { - reg := regexp.MustCompile("/:([0-9a-zA-Z]+)") - return reg.ReplaceAllString(path, "/{${1}}") + // Tags for the documentation. + // It is empty as default. + Tags []string } -func (d *Docs) PathItem(path string) *openapi3.PathItem { - pathItem := d.OpenAPI.Paths.Find(path) - if pathItem == nil { - return &openapi3.PathItem{} - } - return pathItem +var defaultOptions = &Options{ + Title: "Documentation", + Version: "1.0.0", + InteractiveUrl: "/docs", + JsonUrl: "/docs.json", + YamlUrl: "/docs.yaml", + Servers: []string{"http://localhost:8080"}, } diff --git a/docs/swagger_template.html b/docs/swagger_template.html index 476aa85..fd0d9ac 100644 --- a/docs/swagger_template.html +++ b/docs/swagger_template.html @@ -2,7 +2,7 @@ - {{ .Title }} - Documentation + {{ .title }} - Documentation @@ -10,7 +10,7 @@