-
Notifications
You must be signed in to change notification settings - Fork 0
/
parse_services.go
349 lines (303 loc) · 8.19 KB
/
parse_services.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package service
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/tvb-sz/serve-swagger-ui/client"
"github.com/tvb-sz/serve-swagger-ui/conf"
"github.com/tvb-sz/serve-swagger-ui/define"
"github.com/tvb-sz/serve-swagger-ui/utils/memory"
"io/fs"
"math/rand"
"os"
"path/filepath"
"sort"
"strings"
)
// watch path list
var watchPath = make([]string, 0)
// parseService swagger json parser service
type parseService struct{}
// Data swagger json file Data info
type Data struct {
Items map[string][]Swagger
Table map[string]string
}
// Swagger parsed swagger file item
type Swagger struct {
OpenVersion string // openapi version
Version string // swagger doc version
Title string // swagger title
Desc string // swagger desc
Name string // swagger concat name
Email string // swagger concat email
Icon string // swagger title first letter
Path string // json file path detail
Hash string // json file path hash
Key int // css icon bg num
}
// define swagger struct
type swagger struct {
Openapi string `json:"openapi"` // version for openapi up 3.0
Swagger string `json:"swagger"` // version for below 3.x
Info info `json:"info"`
}
type info struct {
Description string `json:"description"`
Version string `json:"version"`
Title string `json:"title"`
Contact contact `json:"contact"`
}
type contact struct {
Name string `json:"name"`
Email string `json:"email"`
}
// ParseWithCache with cache for parse
func (s *parseService) ParseWithCache() (Data, error) {
path := conf.Config.Swagger.Path
data, err := memory.GetWithSetter(define.SwaggerCacheKey, func() (interface{}, error) {
res, err := s.parse(path)
if err != nil {
return nil, err
}
return res, nil
}, 0)
if err != nil {
return Data{}, err
}
if res, ok := data.(Data); ok {
return res, nil
}
return Data{}, err
}
// CleanCache clean parsed cache for reload parse
func (s *parseService) CleanCache() {
memory.Del(define.SwaggerCacheKey)
}
// FirstDoc get the first doc hash string
func (s *parseService) FirstDoc() (string, error) {
data, err := s.ParseWithCache()
if err != nil {
return "", err
}
// get first swagger file
for hash := range data.Table {
return hash, nil
}
// do not happen
return "", nil
}
// StartFileWatcher use goroutine watch swagger file changed then clean parsed cache
func (s *parseService) StartFileWatcher() {
// collect init needed watch dir
watchPath = s.collectWatchDir(conf.Config.Swagger.Path)
// define watch func
var addWatch = func(pathItems []string, watcher *fsnotify.Watcher) {
for _, path := range pathItems {
_ = watcher.Add(path)
}
}
var removeWatch = func(pathItems []string, watcher *fsnotify.Watcher) {
for _, path := range pathItems {
_ = watcher.Remove(path)
}
}
watcher, err := fsnotify.NewWatcher()
defer watcher.Close()
if err != nil {
panic(err)
}
// init watch
addWatch(watchPath, watcher)
for {
select {
case event, ok := <-watcher.Events:
if ok {
client.Logger.Infof("recognize %s changed, auto reread", event.Name)
// check watch path is changed or not
newWatchPath := s.collectWatchDir(conf.Config.Swagger.Path)
if s.isWatchDirChanged(watchPath, newWatchPath) {
client.Logger.Info("subdirectory was changed, auto reinitialize watcher")
removeWatch(watchPath, watcher)
addWatch(newWatchPath, watcher)
watchPath = newWatchPath // assign new watch path to old
}
// when file change, just clear parsed cache, new cache will auto make when a new http request come
s.CleanCache()
}
case <-watcher.Errors:
// no code
}
}
}
// isWatchDirChanged check if watch path dir is change
func (s *parseService) isWatchDirChanged(prev, next []string) bool {
if (prev == nil) != (next == nil) {
return true
}
if len(prev) != len(next) {
return true
}
// this line can ensure the next[i] never out of index in for...range loop
next = next[:len(prev)]
for i, v := range prev {
if v != next[i] {
return true
}
}
return false
}
// collectWatchDir collect need watch path list
func (s *parseService) collectWatchDir(parent string) []string {
var patCollect = make([]string, 0)
_ = filepath.WalkDir(parent, func(path string, d fs.DirEntry, err error) error {
// collect Dir path
if err == nil && d.IsDir() {
patCollect = append(patCollect, path)
}
return nil
})
// sort string slice
sort.Strings(patCollect)
return patCollect
}
// parse all swagger json files
func (s *parseService) parse(path string) (Data, error) {
// res map[string][]Swagger
// no sub-path as default map key
// sub-path as group map key
var res = make(map[string][]Swagger, 0)
// table map[string]string
// key for json file dir md5 hash
// value for json file dir detail
var table = make(map[string]string, 0)
// ① read dir collect all json file
path = strings.TrimRight(path, "/")
dir, err := os.ReadDir(path)
if err != nil {
client.Logger.Errorf("open swagger json file path %s occur error: %s", path, err.Error())
return Data{}, err
}
// ② collect all swagger json file
for _, target := range dir {
if target.IsDir() {
// only collect json suffix
subSwg := s.collectSubDir(path + "/" + target.Name())
if len(subSwg) > 0 {
if _, ok := res[target.Name()]; !ok {
res[target.Name()] = make([]Swagger, 0)
}
res[target.Name()] = append(res[target.Name()], subSwg...)
}
} else {
// only collect json suffix
if strings.HasSuffix(target.Name(), ".json") {
if result, err1 := s.parseSwagger(path + "/" + target.Name()); err1 == nil {
if _, ok := res["default"]; !ok {
res["default"] = make([]Swagger, 0)
}
res["default"] = append(res["default"], result)
}
}
}
}
// check exist swagger file
if len(res) <= 0 {
client.Logger.Error("none swagger json file found")
return Data{}, fmt.Errorf("none swagger json file found")
}
// set hashTable map
for _, items := range res {
for _, item := range items {
table[item.Hash] = item.Path
}
}
return Data{Items: res, Table: table}, nil
}
// collectSubDir list toml sub dir
func (s *parseService) collectSubDir(subPath string) []Swagger {
res := make([]Swagger, 0)
_ = filepath.WalkDir(subPath, func(path string, d fs.DirEntry, err error) error {
// just collect .json suffix file
if err == nil && !d.IsDir() {
if strings.HasSuffix(path, ".json") {
if result, err1 := s.parseSwagger(path); err1 == nil {
res = append(res, result)
}
}
}
return nil
})
return res
}
// parseSwagger parse swagger json file info
func (s *parseService) parseSwagger(path string) (res Swagger, err error) {
var swg swagger
var stream []byte
stream, err = os.ReadFile(path)
if err != nil {
client.Logger.Errorf("open swagger json file %s occur error: %s", path, err.Error())
return res, err
}
err = json.Unmarshal(stream, &swg)
if err != nil {
client.Logger.Errorf("parse swagger json file %s occur error: %s", path, err.Error())
return res, err
}
// Openapi
if swg.Openapi != "" {
res.OpenVersion = swg.Openapi
}
if swg.Swagger != "" {
res.OpenVersion = swg.Swagger
}
// Version
res.Version = swg.Info.Version
if swg.Info.Version == "" {
res.Version = "None Version"
}
// Title
res.Title = swg.Info.Title
if swg.Info.Title == "" {
res.Title = "None Title"
}
// Desc
res.Desc = swg.Info.Description
if swg.Info.Description == "" {
res.Desc = "None description"
}
// Name
res.Name = swg.Info.Contact.Name
if swg.Info.Contact.Name == "" {
res.Name = "None Author"
}
// Email
res.Email = swg.Info.Contact.Email
if swg.Info.Contact.Email == "" {
res.Email = "None Email"
}
// Icon
res.Icon = s.upperIcon(res.Title)
// Hash && Path
res.Hash = s.md5(path)
res.Path = path
res.Key = rand.Intn(7) // random bg Key
return
}
// upperIcon set one string to upper if possible
func (s *parseService) upperIcon(str string) string {
res := strings.Split(str, "")
ss := []byte(res[0])
if len(ss) == 1 && ss[0] >= 'a' && ss[0] <= 'z' {
return strings.ToUpper(res[0])
}
return res[0]
}
// md5 generate md5
func (s *parseService) md5(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum([]byte("")))
}