├── .gitignore ├── static ├── 404.json ├── amis.tmpl └── index.json ├── go.mod ├── LICENSE.md ├── README.md ├── main.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | amis.db 3 | -------------------------------------------------------------------------------- /static/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "page", 3 | "title": "404", 4 | "body": [ 5 | { 6 | "type": "markdown", 7 | "value": "# 🚫 Oops, 找不到对应的页面配置\n[👉 点击我返回页面列表](/)" 8 | } 9 | ], 10 | "regions": [ 11 | "body" 12 | ] 13 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module goamis 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/boltdb/bolt v1.3.1 7 | github.com/gin-gonic/gin v1.7.4 8 | ) 9 | 10 | require ( 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/go-playground/locales v0.14.0 // indirect 13 | github.com/go-playground/universal-translator v0.18.0 // indirect 14 | github.com/go-playground/validator/v10 v10.9.0 // indirect 15 | github.com/golang/protobuf v1.5.2 // indirect 16 | github.com/json-iterator/go v1.1.12 // indirect 17 | github.com/leodido/go-urn v1.2.1 // indirect 18 | github.com/mattn/go-isatty v0.0.14 // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 20 | github.com/modern-go/reflect2 v1.0.2 // indirect 21 | github.com/ugorji/go/codec v1.2.6 // indirect 22 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 23 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 24 | golang.org/x/text v0.3.6 // indirect 25 | google.golang.org/protobuf v1.27.1 // indirect 26 | gopkg.in/yaml.v2 v2.4.0 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Avtion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goAmis 一种易用且友好的后台研发解决方案 2 | 3 | 在我们平常的开发过程中,时常会出现以下情况: 4 | 5 | 1. 需要临时编写脚本用于执行特定任务 6 | 2. 需要获取特定形式的数据并转换成 Excel 或进行可视化呈现 7 | 3. 需要根据业务快速搭建一套可用的操作后台,但苦于不熟悉前端 8 | 4. 需要提供脚本程序给 运营 / 产品 或其他产品研发人员用于导出数据 9 | 10 | 此类型的程序需要提供给非专业开发人员,因此我们必须考虑程序的**易用性**。 11 | 12 | > 在目前以及将来很长一段时间,程序易用性最好的体现就是以可视化界面的形式呈现,如现代流行编辑器 VSCode、API 调试软件 Postman 以及各类建立在前端 HTML 体系的软件。 13 | 14 | ## 简介 Introduction 15 | 16 | goAmis 是一种更适合 Golang 的可视化的开发解决方案,该方案的核心是 Amis 前端低代码框架,适用于中小型 Go 后端团队解决数据展示、批处理程序共享以及简易交互后台等业务需求。 17 | 18 | 值得注意的是,goAmis 并不是代码框架或脚手架,而是业务解决方案。 类似的项目有 gin-vue-admin、go-admin 以及各类与 Golang 相关的后台系统脚手架,但无一例外过于难以与公司已有研发框架结合。 19 | 20 | --- 21 | 22 | 在开始前,您可能需要先了解以下内容: 23 | 24 | 1. [什么是 amis - 一款百度开源的前端低代码框架](https://baidu.github.io/amis/zh-CN/docs/index#%E4%BB%80%E4%B9%88%E6%98%AF-amis) 25 | 2. [Gin - 一款 Golang 的 Web 开发框架](https://github.com/gin-gonic/gin) 26 | 27 | ## 快速上手 Getting Started 28 | 29 | 因为 Amis 是根据 JSON 对页面进行渲染的,所以我们只需要提供 API 接口用于 JSON 的存储、修改和读取即可。 30 | 31 | // TODO 32 | 33 | ## 入门 Basics 34 | 35 | 该部分的主要内容是让开发人员体验到 Amis 提供的强有力研发效能提升效果。 36 | 37 | 内容将会分成两部分,第一部分主要演示如何可视化共享批处理类型程序,第二部分的内容是将 JSON 类型数据进行可视化表格渲染并提供 Excel 下载。 38 | 39 | // TODO 40 | 41 | ## 进阶 Advanced 42 | 43 | 在进阶篇,我们关注的是如何将 Amis 与 Vue 结合起来,提供一种比常规前后端分离开发更敏捷的研发方案。 44 | 45 | // TODO 46 | 47 | ## 常见问题解答 FAQ 48 | 49 | 1. 使用飞书或钉钉机器人不比这个香? 50 | 51 | 我们可以把这种方案理解成是以命令行和指令的基础上的拓展,在一定可容忍的时间或开发成本内对已有或计划中的临时业务进行完善,提供更友好、有效和便利的可视化程序给非专业开发人员。 52 | 53 | 2. 从目前体验来看,这种方式有哪些缺点? 54 | 55 | 容易被卡脖子。因为 Amis 是百度开源项目,从目前国内开源的程度,很大可能是公司内部版阉割进行开源的,同时 Amis 存在商业趋向以及编辑器不开源的问题,所以这种解决方案更适合解决燃眉之急,而非天长地久。 56 | 57 | 3. 后端就一定是 Golang 吗?Java 或者 PHP 可行吗? 58 | 59 | 后端不一定需要是 Golang,从个人体验出发,Golang 研发领域目前没有比 Amis 结合起来更适合的低代码框架或者脚手架。Amis 应该同样适用于 Java 和 PHP,但本人不了解。 -------------------------------------------------------------------------------- /static/amis.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | goAmis 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | 28 |
29 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "html/template" 6 | "io/fs" 7 | "log" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/boltdb/bolt" 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | const ( 18 | defaultIndex = "index" 19 | defaultPort = "80" 20 | ) 21 | 22 | //go:embed static 23 | var systemStatic embed.FS 24 | 25 | // bolt 是一款适合读取密集型工作的内嵌式数据库 26 | var ( 27 | boltDB *bolt.DB 28 | defaultBucket = []byte("page") 29 | ) 30 | 31 | var page404Data = []byte(`{"type":"page","title":"404","body":[{"type":"markdown","value":"# 🚫 Oops, 找不到对应的页面配置\n[👉 点击我返回页面列表](/)"}],"regions":["body"]}`) 32 | 33 | type ( 34 | basicResp struct { 35 | Status int `json:"status"` 36 | Msg string `json:"msg"` 37 | Data map[string]interface{} `json:"data"` 38 | } 39 | pageItem struct { 40 | Name string `json:"name" validate:"required"` 41 | Config string `json:"config" validate:"required,json"` 42 | } 43 | ) 44 | 45 | // 初始化内嵌式数据库 46 | func initBoltDB() { 47 | var err error 48 | boltDB, err = bolt.Open("amis.db", 0600, nil) 49 | if err != nil { 50 | log.Fatalln(err) 51 | return 52 | } 53 | 54 | // 从 static 文件夹中获取全部的页面配置并写入数据库 55 | _ = boltDB.Batch(func(tx *bolt.Tx) error { 56 | bucket, err := tx.CreateBucketIfNotExists(defaultBucket) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | _ = fs.WalkDir(systemStatic, "static", func(path string, d fs.DirEntry, err error) error { 62 | // check if ext is json 63 | if d.IsDir() { 64 | return nil 65 | } 66 | _, filename := filepath.Split(path) 67 | fileExt := filepath.Ext(filename) 68 | if fileExt != ".json" { 69 | return nil 70 | } 71 | 72 | // write page data to db 73 | pageData, err := systemStatic.ReadFile(path) 74 | if err != nil { 75 | log.Printf("read page data failed, filename: %s, err: %v\n", filename, err) 76 | return nil 77 | } 78 | if err := bucket.Put([]byte(strings.TrimSuffix(filename, fileExt)), pageData); err != nil { 79 | log.Printf("failed to write page data to bolt db, err: %v\n", err) 80 | return nil 81 | } 82 | log.Printf("load page data to bolt db, file: %s\n", filename) 83 | return nil 84 | }) 85 | return nil 86 | }) 87 | } 88 | 89 | func main() { 90 | initBoltDB() 91 | 92 | tmplFs, err := fs.Sub(systemStatic, "static") 93 | if err != nil { 94 | log.Fatalln(err) 95 | return 96 | } 97 | tmpl, err := template.ParseFS(tmplFs, "*.tmpl") 98 | if err != nil { 99 | log.Fatalln(err) 100 | return 101 | } 102 | 103 | port := os.Getenv("PORT") 104 | if port == "" { 105 | port = defaultPort 106 | } 107 | 108 | engine := gin.Default() 109 | engine.SetHTMLTemplate(tmpl) 110 | 111 | // 首页直接跳转到默认页面 112 | engine.GET("/", func(c *gin.Context) { c.Redirect(http.StatusPermanentRedirect, "/page/"+defaultIndex) }) 113 | engine.GET("/page/:name", renderPage) 114 | 115 | // 页面配置 116 | engine.GET("/config/list", listConfig) 117 | engine.GET("/config/get/:name", getConfig) 118 | engine.GET("/config/delete/:name", deleteConfig) 119 | engine.POST("/config/save", saveConfig) 120 | if err := engine.Run(":" + port); err != nil { 121 | log.Fatalln(err) 122 | return 123 | } 124 | } 125 | 126 | func renderPage(c *gin.Context) { 127 | name := c.Param("name") 128 | if name == "" { 129 | name = "404" 130 | } 131 | c.HTML(http.StatusOK, "amis.tmpl", gin.H{ 132 | "pageTitle": name, 133 | "pageSchemaApi": "GET:/config/get/" + name, 134 | "getConfigAddr": "/config/get/" + name, 135 | }) 136 | } 137 | 138 | func listConfig(c *gin.Context) { 139 | var pages []*pageItem 140 | if err := boltDB.View(func(tx *bolt.Tx) error { 141 | bucket := tx.Bucket(defaultBucket) 142 | return bucket.ForEach(func(k, v []byte) error { 143 | pages = append(pages, &pageItem{Name: string(k), Config: string(v)}) 144 | return nil 145 | }) 146 | }); err != nil { 147 | c.AbortWithStatusJSON(http.StatusOK, &basicResp{Status: -1, Msg: err.Error()}) 148 | return 149 | } 150 | c.JSON(http.StatusOK, &basicResp{Status: 0, Data: map[string]interface{}{ 151 | "items": pages, 152 | "total": len(pages), 153 | }}) 154 | } 155 | 156 | func getConfig(c *gin.Context) { 157 | var name = c.Param("name") 158 | if name == "" { 159 | c.AbortWithStatusJSON(http.StatusOK, &basicResp{Status: -1, Msg: "name is empty"}) 160 | return 161 | } 162 | var pageData []byte 163 | _ = boltDB.View(func(tx *bolt.Tx) error { 164 | bucket := tx.Bucket(defaultBucket) 165 | pageData = bucket.Get([]byte(name)) 166 | return nil 167 | }) 168 | // 404 169 | if len(pageData) == 0 { 170 | pageData = page404Data 171 | } 172 | c.Data(http.StatusOK, "application/json", pageData) 173 | } 174 | 175 | func deleteConfig(c *gin.Context) { 176 | name := c.Param("name") 177 | if name == "" { 178 | c.AbortWithStatusJSON(http.StatusOK, &basicResp{Status: -1, Msg: "name is empty"}) 179 | return 180 | } 181 | if err := boltDB.Update(func(tx *bolt.Tx) error { 182 | bucket := tx.Bucket(defaultBucket) 183 | return bucket.Delete([]byte(name)) 184 | }); err != nil { 185 | c.AbortWithStatusJSON(http.StatusOK, &basicResp{Status: -1, Msg: err.Error()}) 186 | return 187 | } 188 | c.JSON(http.StatusOK, &basicResp{Status: 0, Msg: "delete page config successfully"}) 189 | } 190 | 191 | func saveConfig(c *gin.Context) { 192 | var req = new(pageItem) 193 | if err := c.ShouldBindJSON(req); err != nil { 194 | c.AbortWithStatusJSON(http.StatusOK, &basicResp{Status: -1, Msg: err.Error()}) 195 | return 196 | } 197 | if err := boltDB.Update(func(tx *bolt.Tx) error { 198 | bucket := tx.Bucket(defaultBucket) 199 | return bucket.Put([]byte(req.Name), []byte(req.Config)) 200 | }); err != nil { 201 | c.AbortWithStatusJSON(http.StatusOK, &basicResp{Status: -1, Msg: err.Error()}) 202 | return 203 | } 204 | c.JSON(http.StatusOK, &basicResp{Status: 0, Msg: "save page config successfully"}) 205 | } 206 | -------------------------------------------------------------------------------- /static/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "page", 3 | "body": [ 4 | { 5 | "type": "alert", 6 | "body": [ 7 | { 8 | "type": "tpl", 9 | "tpl": "🔥 Go-Amis 是由 @Avtion 基于 Amis + Golang 搭建的多功能服务平台", 10 | "inline": false, 11 | "wrapperComponent": "div" 12 | } 13 | ], 14 | "level": "success" 15 | }, 16 | { 17 | "type": "divider" 18 | }, 19 | { 20 | "title": "", 21 | "type": "crud", 22 | "api": "GET:/config/list", 23 | "bulkActions": [], 24 | "itemActions": [], 25 | "features": [ 26 | "create" 27 | ], 28 | "columnsTogglable": "auto", 29 | "perPageAvailable": [], 30 | "messages": {}, 31 | "footerToolbar": [], 32 | "headerToolbar": [ 33 | { 34 | "label": "新增", 35 | "type": "button", 36 | "actionType": "dialog", 37 | "dialog": { 38 | "title": "新增页面配置", 39 | "body": [ 40 | { 41 | "type": "form", 42 | "api": "POST:/config/save", 43 | "body": [ 44 | { 45 | "label": "页面名称", 46 | "type": "input-text", 47 | "name": "name", 48 | "required": true, 49 | "placeholder": "" 50 | }, 51 | { 52 | "type": "editor", 53 | "label": "页面配置", 54 | "name": "config", 55 | "placeholder": "", 56 | "language": "json", 57 | "required": true, 58 | "size": "xxl" 59 | } 60 | ], 61 | "rules": [] 62 | } 63 | ], 64 | "type": "dialog", 65 | "closeOnEsc": false, 66 | "closeOnOutside": false, 67 | "showCloseButton": true, 68 | "size": "lg" 69 | }, 70 | "level": "primary" 71 | }, 72 | { 73 | "type": "export-excel", 74 | "tpl": "内容", 75 | "align": "right" 76 | } 77 | ], 78 | "mode": "table", 79 | "columns": [ 80 | { 81 | "type": "tpl", 82 | "tpl": "${name}", 83 | "inline": false, 84 | "name": "name", 85 | "label": "页面名称", 86 | "placeholder": "-", 87 | "remark": "点击可直接跳转到目标页面" 88 | }, 89 | { 90 | "type": "operation", 91 | "label": "操作", 92 | "name": "operation", 93 | "buttons": [ 94 | { 95 | "type": "button", 96 | "label": "修改", 97 | "actionType": "dialog", 98 | "dialog": { 99 | "title": "修改页面配置", 100 | "body": [ 101 | { 102 | "type": "form", 103 | "api": "POST:/config/save", 104 | "body": [ 105 | { 106 | "type": "input-text", 107 | "label": "页面名称", 108 | "name": "name", 109 | "readOnly": true, 110 | "required": true, 111 | "placeholder": "", 112 | "description": "无法修改页面名称" 113 | }, 114 | { 115 | "label": "页面配置", 116 | "type": "editor", 117 | "name": "config", 118 | "required": true, 119 | "placeholder": "", 120 | "language": "json", 121 | "size": "xxl" 122 | } 123 | ], 124 | "rules": [], 125 | "initApi": "" 126 | } 127 | ], 128 | "type": "dialog", 129 | "closeOnEsc": false, 130 | "closeOnOutside": false, 131 | "showCloseButton": true, 132 | "size": "lg", 133 | "data": { 134 | "&": "$$", 135 | "unique": "${unique}", 136 | "id": "${id}", 137 | "desc": "${desc}" 138 | } 139 | }, 140 | "level": "primary", 141 | "disabledOn": "this.disableUpdate === 1" 142 | }, 143 | { 144 | "type": "button", 145 | "label": "删除", 146 | "actionType": "ajax", 147 | "dialog": { 148 | "title": "系统提示", 149 | "body": [ 150 | { 151 | "type": "form", 152 | "title": "表单", 153 | "body": [ 154 | { 155 | "label": "管理员密钥", 156 | "type": "input-password", 157 | "name": "token", 158 | "required": true 159 | } 160 | ], 161 | "api": "delete:/config/${id}?token=${token}", 162 | "autoFocus": true 163 | } 164 | ], 165 | "type": "dialog" 166 | }, 167 | "level": "default", 168 | "disabledOn": "this.disableDelete === 1", 169 | "size": "", 170 | "api": "GET:/config/delete/${name}" 171 | } 172 | ] 173 | } 174 | ], 175 | "showFooter": false, 176 | "initFetch": true 177 | } 178 | ], 179 | "regions": [ 180 | "body" 181 | ], 182 | "messages": {}, 183 | "className": "r b-a b-2x" 184 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 2 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 8 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 9 | github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= 10 | github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 11 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 12 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 13 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 14 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 15 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 16 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 17 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 18 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 19 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 20 | github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= 21 | github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 22 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 23 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 24 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 25 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 26 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 29 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 30 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 31 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 32 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 33 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 34 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 35 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 36 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 37 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 38 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 39 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 40 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 41 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 42 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 43 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 44 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 45 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 46 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 48 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 49 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 50 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 51 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 52 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 56 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 57 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 58 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 59 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 60 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 61 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 62 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 63 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 65 | github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E= 66 | github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= 67 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 68 | github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ= 69 | github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= 70 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 71 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 72 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= 73 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 74 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 75 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 76 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 77 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 83 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 85 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 86 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 87 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 88 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 89 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 90 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 91 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 92 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 94 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 95 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 96 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 97 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 99 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 100 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 101 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 102 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 103 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 104 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 105 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 106 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 107 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 108 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 109 | --------------------------------------------------------------------------------