Skip to content

Commit 59531e5

Browse files
committed
chore: change dir
1 parent ea5a17f commit 59531e5

File tree

7 files changed

+1543
-1
lines changed

7 files changed

+1543
-1
lines changed

_examples/echo/pkg/echo.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package pkg
2+
3+
import (
4+
"github.com/labstack/echo/v4"
5+
"github.com/rookie-luochao/go-openapi-ui/pkg/doc"
6+
)
7+
8+
func New(doc doc.Doc) echo.MiddlewareFunc {
9+
handle := doc.Handler()
10+
11+
return func(next echo.HandlerFunc) echo.HandlerFunc {
12+
return func(ctx echo.Context) error {
13+
handle(ctx.Response(), ctx.Request())
14+
15+
if ctx.Response().Committed {
16+
return nil
17+
}
18+
19+
return next(ctx)
20+
}
21+
}
22+
}

_examples/http/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import (
44
"net/http"
55

6-
"github.com/rookie-luochao/go-openapi-ui"
6+
"github.com/rookie-luochao/go-openapi-ui/pkg/doc"
77
)
88

99
func main() {

pkg/doc/assets/index.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>{{ .title }}</title>
6+
<meta name="description" content="{{ .description }}">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<style>
9+
body {
10+
margin: 0;
11+
padding: 0;
12+
}
13+
</style>
14+
</head>
15+
<body>
16+
<div id="openapi-ui-container" spec-url="{{ .url }}"></div>
17+
<script>{{ .body }}</script>
18+
</body>
19+
</html>

pkg/doc/assets/openapi-ui.umd.js

Lines changed: 1333 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/doc/doc.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package doc
2+
3+
import (
4+
"bytes"
5+
"embed"
6+
"errors"
7+
"net/http"
8+
"os"
9+
"strings"
10+
"text/template"
11+
)
12+
13+
// ErrSpecNotFound error for when spec file not found
14+
var ErrSpecNotFound = errors.New("spec not found")
15+
16+
// Doc configuration
17+
type Doc struct {
18+
DocsPath string
19+
SpecPath string
20+
SpecFile string
21+
SpecFS *embed.FS
22+
Title string
23+
Description string
24+
}
25+
26+
// HTML represents the openapi-ui index.html page
27+
//
28+
//go:embed assets/index.html
29+
var HTML string
30+
31+
// JavaScript represents the openapi-ui umd javascript
32+
//
33+
//go:embed assets/openapi-ui.umd.js
34+
var JavaScript string
35+
36+
// Body returns the final html with the js in the body
37+
func (r Doc) Body() ([]byte, error) {
38+
buf := bytes.NewBuffer(nil)
39+
tpl, err := template.New("doc").Parse(HTML)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
if err = tpl.Execute(buf, map[string]string{
45+
"body": JavaScript,
46+
"title": r.Title,
47+
"url": r.SpecPath,
48+
"description": r.Description,
49+
}); err != nil {
50+
return nil, err
51+
}
52+
53+
return buf.Bytes(), nil
54+
}
55+
56+
// Handler sets some defaults and returns a HandlerFunc
57+
func (r Doc) Handler() http.HandlerFunc {
58+
data, err := r.Body()
59+
if err != nil {
60+
panic(err)
61+
}
62+
63+
specFile := r.SpecFile
64+
if specFile == "" {
65+
panic(ErrSpecNotFound)
66+
}
67+
68+
if r.SpecPath == "" {
69+
r.SpecPath = "/openapi.json"
70+
}
71+
72+
var spec []byte
73+
if r.SpecFS == nil {
74+
spec, err = os.ReadFile(specFile)
75+
if err != nil {
76+
panic(err)
77+
}
78+
} else {
79+
spec, err = r.SpecFS.ReadFile(specFile)
80+
if err != nil {
81+
panic(err)
82+
}
83+
}
84+
85+
docsPath := r.DocsPath
86+
return func(w http.ResponseWriter, req *http.Request) {
87+
method := strings.ToLower(req.Method)
88+
if method != "get" && method != "head" {
89+
return
90+
}
91+
92+
header := w.Header()
93+
if strings.HasSuffix(req.URL.Path, r.SpecPath) {
94+
header.Set("Content-Type", "application/json")
95+
w.WriteHeader(http.StatusOK)
96+
_, _ = w.Write(spec)
97+
return
98+
}
99+
100+
if docsPath == "" || docsPath == req.URL.Path {
101+
header.Set("Content-Type", "text/html")
102+
w.WriteHeader(http.StatusOK)
103+
_, _ = w.Write(data)
104+
}
105+
}
106+
}

pkg/doc/doc_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package doc
2+
3+
import (
4+
"embed"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
//go:embed test-data/spec.json
14+
var spec embed.FS
15+
16+
func TestOpenapiUI(t *testing.T) {
17+
r := Doc{
18+
SpecFile: "test-data/spec.json",
19+
SpecFS: &spec,
20+
SpecPath: "/openapi.json", // "/openapi.yaml"
21+
Title: "Test API",
22+
}
23+
24+
t.Run("Body", func(t *testing.T) {
25+
body, err := r.Body()
26+
assert.NoError(t, err)
27+
assert.Contains(t, string(body), r.Title)
28+
})
29+
30+
t.Run("Handler", func(t *testing.T) {
31+
handler := r.Handler()
32+
33+
t.Run("Spec", func(t *testing.T) {
34+
req := httptest.NewRequest(http.MethodGet, "/openapi.json", nil)
35+
w := httptest.NewRecorder()
36+
handler(w, req)
37+
38+
resp := w.Result()
39+
assert.Equal(t, http.StatusOK, resp.StatusCode)
40+
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
41+
42+
body, err := io.ReadAll(resp.Body)
43+
assert.NoError(t, err)
44+
assert.Contains(t, string(body), `"swagger":"2.0"`)
45+
})
46+
47+
t.Run("Docs", func(t *testing.T) {
48+
req := httptest.NewRequest(http.MethodGet, "/", nil)
49+
w := httptest.NewRecorder()
50+
handler(w, req)
51+
52+
resp := w.Result()
53+
assert.Equal(t, http.StatusOK, resp.StatusCode)
54+
assert.Equal(t, "text/html", resp.Header.Get("Content-Type"))
55+
56+
body, err := io.ReadAll(resp.Body)
57+
assert.NoError(t, err)
58+
assert.Contains(t, string(body), r.Title)
59+
})
60+
})
61+
}

pkg/doc/test-data/spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"swagger":"2.0","info":{"description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","version":"1.0.6","title":"Swagger Petstore","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"}},"host":"petstore.swagger.io","basePath":"/v2","tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}],"schemes":["https","http"],"paths":{"/pet/{petId}/uploadImage":{"post":{"tags":["pet"],"summary":"uploads an image","description":"","operationId":"uploadFile","consumes":["multipart/form-data"],"produces":["application/json"],"parameters":[{"name":"petId","in":"path","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"name":"additionalMetadata","in":"formData","description":"Additional data to pass to server","required":false,"type":"string"},{"name":"file","in":"formData","description":"file to upload","required":false,"type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet":{"post":{"tags":["pet"],"summary":"Add a new pet to the store","description":"","operationId":"addPet","consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"tags":["pet"],"summary":"Update an existing pet","description":"","operationId":"updatePet","consumes":["application/json","application/xml"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"tags":["pet"],"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","operationId":"findPetsByStatus","produces":["application/json","application/xml"],"parameters":[{"name":"status","in":"query","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"type":"string","enum":["available","pending","sold"],"default":"available"},"collectionFormat":"multi"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}},"400":{"description":"Invalid status value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"tags":["pet"],"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","operationId":"findPetsByTags","produces":["application/json","application/xml"],"parameters":[{"name":"tags","in":"query","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"},"collectionFormat":"multi"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}},"400":{"description":"Invalid tag value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}],"deprecated":true}},"/pet/{petId}":{"get":{"tags":["pet"],"summary":"Find pet by ID","description":"Returns a single pet","operationId":"getPetById","produces":["application/json","application/xml"],"parameters":[{"name":"petId","in":"path","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"security":[{"api_key":[]}]},"post":{"tags":["pet"],"summary":"Updates a pet in the store with form data","description":"","operationId":"updatePetWithForm","consumes":["application/x-www-form-urlencoded"],"produces":["application/json","application/xml"],"parameters":[{"name":"petId","in":"path","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"name":"name","in":"formData","description":"Updated name of the pet","required":false,"type":"string"},{"name":"status","in":"formData","description":"Updated status of the pet","required":false,"type":"string"}],"responses":{"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"delete":{"tags":["pet"],"summary":"Deletes a pet","description":"","operationId":"deletePet","produces":["application/json","application/xml"],"parameters":[{"name":"api_key","in":"header","required":false,"type":"string"},{"name":"petId","in":"path","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/order":{"post":{"tags":["store"],"summary":"Place an order for a pet","description":"","operationId":"placeOrder","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}}}},"/store/order/{orderId}":{"get":{"tags":["store"],"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions","operationId":"getOrderById","produces":["application/json","application/xml"],"parameters":[{"name":"orderId","in":"path","description":"ID of pet that needs to be fetched","required":true,"type":"integer","maximum":10,"minimum":1,"format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}},"delete":{"tags":["store"],"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors","operationId":"deleteOrder","produces":["application/json","application/xml"],"parameters":[{"name":"orderId","in":"path","description":"ID of the order that needs to be deleted","required":true,"type":"integer","minimum":1,"format":"int64"}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}}},"/store/inventory":{"get":{"tags":["store"],"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","operationId":"getInventory","produces":["application/json"],"parameters":[],"responses":{"200":{"description":"successful operation","schema":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}}}},"security":[{"api_key":[]}]}},"/user/createWithArray":{"post":{"tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithArrayInput","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}],"responses":{"default":{"description":"successful operation"}}}},"/user/createWithList":{"post":{"tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithListInput","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}],"responses":{"default":{"description":"successful operation"}}}},"/user/{username}":{"get":{"tags":["user"],"summary":"Get user by user name","description":"","operationId":"getUserByName","produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"path","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}},"put":{"tags":["user"],"summary":"Updated user","description":"This can only be done by the logged in user.","operationId":"updateUser","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"path","description":"name that need to be updated","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}}},"delete":{"tags":["user"],"summary":"Delete user","description":"This can only be done by the logged in user.","operationId":"deleteUser","produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"path","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}}},"/user/login":{"get":{"tags":["user"],"summary":"Logs user into the system","description":"","operationId":"loginUser","produces":["application/json","application/xml"],"parameters":[{"name":"username","in":"query","description":"The user name for login","required":true,"type":"string"},{"name":"password","in":"query","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","headers":{"X-Expires-After":{"type":"string","format":"date-time","description":"date in UTC when token expires"},"X-Rate-Limit":{"type":"integer","format":"int32","description":"calls per hour allowed by the user"}},"schema":{"type":"string"}},"400":{"description":"Invalid username/password supplied"}}}},"/user/logout":{"get":{"tags":["user"],"summary":"Logs out current logged in user session","description":"","operationId":"logoutUser","produces":["application/json","application/xml"],"parameters":[],"responses":{"default":{"description":"successful operation"}}}},"/user":{"post":{"tags":["user"],"summary":"Create user","description":"This can only be done by the logged in user.","operationId":"createUser","consumes":["application/json"],"produces":["application/json","application/xml"],"parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}}}}},"securityDefinitions":{"api_key":{"type":"apiKey","name":"api_key","in":"header"},"petstore_auth":{"type":"oauth2","authorizationUrl":"https://petstore.swagger.io/oauth/authorize","flow":"implicit","scopes":{"read:pets":"read your pets","write:pets":"modify pets in your account"}}},"definitions":{"ApiResponse":{"type":"object","properties":{"code":{"type":"integer","format":"int32"},"type":{"type":"string"},"message":{"type":"string"}}},"Category":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Category"}},"Pet":{"type":"object","required":["name","photoUrls"],"properties":{"id":{"type":"integer","format":"int64"},"category":{"$ref":"#/definitions/Category"},"name":{"type":"string","example":"doggie"},"photoUrls":{"type":"array","xml":{"wrapped":true},"items":{"type":"string","xml":{"name":"photoUrl"}}},"tags":{"type":"array","xml":{"wrapped":true},"items":{"xml":{"name":"tag"},"$ref":"#/definitions/Tag"}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"xml":{"name":"Pet"}},"Tag":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Tag"}},"Order":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"petId":{"type":"integer","format":"int64"},"quantity":{"type":"integer","format":"int32"},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","enum":["placed","approved","delivered"]},"complete":{"type":"boolean"}},"xml":{"name":"Order"}},"User":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"integer","format":"int32","description":"User Status"}},"xml":{"name":"User"}}},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"}}

0 commit comments

Comments
 (0)