diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 32524fa..1204e86 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.19 - name: Login to Docker Hub if: startsWith(github.ref, 'refs/tags/v') uses: docker/login-action@v1 @@ -43,4 +43,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution - # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} \ No newline at end of file + # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9d2540d..ce8a5da 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,10 +17,10 @@ jobs: matrix: os: [ubuntu-latest] steps: - - name: Set up Go 1.16 + - name: Set up Go 1.19 uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.19 id: go - name: Check out code into the Go module directory uses: actions/checkout@master @@ -37,19 +37,19 @@ jobs: name: Lint checks runs-on: ubuntu-latest steps: - - name: Set up Go 1.16 + - name: Set up Go 1.19 uses: actions/setup-go@v2 with: - go-version: 1.16 + go-version: 1.19 id: go - name: Check out code into the Go module directory uses: actions/checkout@master - name: Download golangci-lint run: | - wget https://github.com/golangci/golangci-lint/releases/download/v1.41.0/golangci-lint-1.41.0-linux-amd64.tar.gz - tar -xvf ./golangci-lint-1.41.0-linux-amd64.tar.gz + wget https://github.com/golangci/golangci-lint/releases/download/v1.52.2/golangci-lint-1.52.2-linux-amd64.tar.gz + tar -xvf ./golangci-lint-1.52.2-linux-amd64.tar.gz - name: Running golangci-lint env: GO111MODULE: on GOPATH: /home/runner/work/ - run: GOLINTER=./golangci-lint-1.41.0-linux-amd64/golangci-lint make lint + run: GOLINTER=./golangci-lint-1.52.2-linux-amd64/golangci-lint make lint diff --git a/.gitignore b/.gitignore index 42a62fc..2a32ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ dist/ build/ +_build/ coverage.out -tmp/ \ No newline at end of file +tmp/ +*.log +log/ +__debug_bin +.vscode/ +.idea/ diff --git a/.golangci.yml b/.golangci.yml index cf721ae..fc8c33d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,11 +9,12 @@ run: linters: disable-all: true enable: # please keep this alphabetized - # Don't use soon to deprecated[1] linters that lead to false - # https://github.com/golangci/golangci-lint/issues/1841 - # - deadcode - # - structcheck - # - varcheck + # Don't use soon to deprecated[1] linters that lead to false + # https://github.com/golangci/golangci-lint/issues/1841 + # - deadcode + # - structcheck + # - varcheck + # - wsl - ineffassign - staticcheck - unused @@ -29,13 +30,13 @@ linters: - exhaustive - goconst - gocritic - - gomnd - - gosec + # - gomnd + # - gosec - misspell - nolintlint - prealloc - predeclared - - revive + # - revive - stylecheck - thelper - tparallel @@ -43,12 +44,11 @@ linters: - unconvert - unparam - whitespace - - wsl linters-settings: gofumpt: # Select the Go version to target. The default is `1.15`. - lang-version: "1.16" + lang-version: "1.19" # Choose whether or not to use the extra rules that are disabled # by default - extra-rules: false \ No newline at end of file + extra-rules: false diff --git a/.goreleaser.yml b/.goreleaser.yml index 99321aa..3b09f34 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -4,6 +4,7 @@ before: hooks: # You may remove this if you don't use go modules. - go mod tidy + - make gen-version builds: - env: - CGO_ENABLED=0 diff --git a/Makefile b/Makefile index ec8e550..bc39010 100644 --- a/Makefile +++ b/Makefile @@ -5,28 +5,44 @@ include go.mk .PHONY: clean clean: ## Clean build bundles - -rm -rf ./build + -rm -rf ./_build .PHONY: build-all build-all: build-darwin build-linux build-windows ## Build for all platforms .PHONY: build-darwin -build-darwin: ## Build for MacOS - -rm -rf ./build/darwin +build-darwin: gen-version ## Build for MacOS + -rm -rf ./_build/darwin GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \ - go build -o ./build/darwin/$(APPROOT) \ + go build -o ./_build/darwin/$(APPROOT) \ ./cmd/main.go .PHONY: build-linux -build-linux: ## Build for Linux - -rm -rf ./build/linux +build-linux: gen-version ## Build for Linux + -rm -rf ./_build/linux GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \ - go build -o ./build/linux/$(APPROOT) \ + go build -o ./_build/linux/$(APPROOT) \ ./cmd/main.go .PHONY: build-windows -build-windows: ## Build for Windows - -rm -rf ./build/windows +build-windows: gen-version ## Build for Windows + -rm -rf ./_build/windows GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \ - go build -o ./build/windows/$(APPROOT).exe \ + go build -o ./_build/windows/$(APPROOT).exe \ ./cmd/main.go + +.PHONY: gen-api-docs +gen-api-docs: ## Generate API documentation with OpenAPI format + @which swag > /dev/null || (echo "Installing swag@v1.7.8 ..."; go install github.com/swaggo/swag/cmd/swag@v1.7.8 && echo "Installation complete!\n") + # Generate API documentation with OpenAPI format + -swag init --parseDependency --parseDepth 1 -g cmd/main.go -o api/openapispec/ + # Format swagger comments + -swag fmt -g pkg/**/*.go + @echo "🎉 Done!" + +.PHONY: gen-version +gen-version: ## Generate version file + # Delete old version file + -rm -f ./pkg/version/z_update_version.go + # Update version + -cd pkg/version/scripts && go run gen/gen.go diff --git a/api/openapispec/docs.go b/api/openapispec/docs.go new file mode 100644 index 0000000..793bc6c --- /dev/null +++ b/api/openapispec/docs.go @@ -0,0 +1,510 @@ +// Package openapispec GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag +package openapispec + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" + + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/systemconfig": { + "put": { + "description": "Update the specified system config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Update system config", + "parameters": [ + { + "description": "Updated system config", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/systemconfig.UpdateSystemConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "description": "Create a new system config instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create system config", + "parameters": [ + { + "description": "Created system config", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/systemconfig.CreateSystemConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/systemconfig/count": { + "get": { + "description": "Count the total number of system configs", + "produces": [ + "application/json" + ], + "summary": "Count system configs", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/systemconfig/{id}": { + "get": { + "description": "Get system config information by system config ID", + "produces": [ + "application/json" + ], + "summary": "Get system config", + "parameters": [ + { + "type": "integer", + "description": "SystemConfig ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "delete": { + "description": "Delete specified system config by ID", + "produces": [ + "application/json" + ], + "summary": "Delete system config", + "parameters": [ + { + "type": "integer", + "description": "SystemConfig ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/systemconfigs": { + "get": { + "description": "Find system configs with query", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Find system configs", + "parameters": [ + { + "description": "query body", + "name": "query", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/systemconfig.QuerySystemConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + } + }, + "definitions": { + "entity.SystemConfig": { + "type": "object", + "properties": { + "config": { + "description": "Configuration data in JSON or YAML format", + "type": "string" + }, + "createdAt": { + "description": "Timestamp when the system was created", + "type": "string" + }, + "creator": { + "description": "Username or ID of the user who created the system", + "type": "string" + }, + "description": { + "description": "Description or purpose of the system", + "type": "string" + }, + "env": { + "description": "Environment where the system is deployed (e.g. prod, gray)", + "type": "string" + }, + "id": { + "description": "Unique ID of the system", + "type": "integer" + }, + "modifier": { + "description": "Username or ID of the user who last modified the system", + "type": "string" + }, + "tenant": { + "description": "Tenant or organization that the system belongs to", + "type": "string" + }, + "type": { + "description": "Type or category of the system (e.g. cache, message queue)", + "type": "string" + }, + "updatedAt": { + "description": "Timestamp when the system was last updated", + "type": "string" + } + } + }, + "systemconfig.CreateSystemConfigRequest": { + "type": "object", + "required": [ + "config", + "creator", + "env", + "tenant", + "type" + ], + "properties": { + "config": { + "description": "Configuration data in JSON or YAML format", + "type": "string" + }, + "creator": { + "description": "Username or ID of the user who created the system", + "type": "string" + }, + "description": { + "description": "Description or purpose of the system", + "type": "string" + }, + "env": { + "description": "Environment where the system is deployed (e.g. prod, gray)", + "type": "string" + }, + "modifier": { + "description": "Username or ID of the user who last modified the system", + "type": "string" + }, + "tenant": { + "description": "Tenant or organization that the system belongs to", + "type": "string" + }, + "type": { + "description": "Type or category of the system (e.g. cache, message queue)", + "type": "string" + } + } + }, + "systemconfig.QuerySystemConfigRequest": { + "type": "object", + "required": [ + "page", + "perPage" + ], + "properties": { + "keyword": { + "description": "Keyword is the keyword to search for.\nOptional: true", + "type": "string" + }, + "page": { + "description": "Page is the page number, starting from 1.\nRequired: true, Minimum value: 1", + "type": "integer", + "minimum": 1 + }, + "perPage": { + "description": "PerPage is the number of items per page.\nRequired: true, Minimum value: 1, Maximum value: 300", + "type": "integer", + "maximum": 300, + "minimum": 1 + } + } + }, + "systemconfig.UpdateSystemConfigRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "config": { + "description": "Configuration data in JSON or YAML format", + "type": "string" + }, + "creator": { + "description": "Username or ID of the user who created the system", + "type": "string" + }, + "description": { + "description": "Description or purpose of the system", + "type": "string" + }, + "env": { + "description": "Environment where the system is deployed (e.g. prod, gray)", + "type": "string" + }, + "id": { + "description": "Unique ID of the system", + "type": "integer" + }, + "modifier": { + "description": "Username or ID of the user who last modified the system", + "type": "string" + }, + "tenant": { + "description": "Tenant or organization that the system belongs to", + "type": "string" + }, + "type": { + "description": "Type or category of the system (e.g. cache, message queue)", + "type": "string" + } + } + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + "escape": func(v interface{}) string { + // escape tabs + str := strings.Replace(v.(string), "\t", "\\t", -1) + // replace " with \", and if that results in \\", replace that with \\\" + str = strings.Replace(str, "\"", "\\\"", -1) + return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register("swagger", &s{}) +} diff --git a/api/openapispec/swagger.json b/api/openapispec/swagger.json new file mode 100644 index 0000000..ff7f82d --- /dev/null +++ b/api/openapispec/swagger.json @@ -0,0 +1,437 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api/v1/systemconfig": { + "put": { + "description": "Update the specified system config", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Update system config", + "parameters": [ + { + "description": "Updated system config", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/systemconfig.UpdateSystemConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "description": "Create a new system config instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create system config", + "parameters": [ + { + "description": "Created system config", + "name": "config", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/systemconfig.CreateSystemConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/systemconfig/count": { + "get": { + "description": "Count the total number of system configs", + "produces": [ + "application/json" + ], + "summary": "Count system configs", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/systemconfig/{id}": { + "get": { + "description": "Get system config information by system config ID", + "produces": [ + "application/json" + ], + "summary": "Get system config", + "parameters": [ + { + "type": "integer", + "description": "SystemConfig ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "delete": { + "description": "Delete specified system config by ID", + "produces": [ + "application/json" + ], + "summary": "Delete system config", + "parameters": [ + { + "type": "integer", + "description": "SystemConfig ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/systemconfigs": { + "get": { + "description": "Find system configs with query", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Find system configs", + "parameters": [ + { + "description": "query body", + "name": "query", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/systemconfig.QuerySystemConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.SystemConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + } + }, + "definitions": { + "entity.SystemConfig": { + "type": "object", + "properties": { + "config": { + "description": "Configuration data in JSON or YAML format", + "type": "string" + }, + "createdAt": { + "description": "Timestamp when the system was created", + "type": "string" + }, + "creator": { + "description": "Username or ID of the user who created the system", + "type": "string" + }, + "description": { + "description": "Description or purpose of the system", + "type": "string" + }, + "env": { + "description": "Environment where the system is deployed (e.g. prod, gray)", + "type": "string" + }, + "id": { + "description": "Unique ID of the system", + "type": "integer" + }, + "modifier": { + "description": "Username or ID of the user who last modified the system", + "type": "string" + }, + "tenant": { + "description": "Tenant or organization that the system belongs to", + "type": "string" + }, + "type": { + "description": "Type or category of the system (e.g. cache, message queue)", + "type": "string" + }, + "updatedAt": { + "description": "Timestamp when the system was last updated", + "type": "string" + } + } + }, + "systemconfig.CreateSystemConfigRequest": { + "type": "object", + "required": [ + "config", + "creator", + "env", + "tenant", + "type" + ], + "properties": { + "config": { + "description": "Configuration data in JSON or YAML format", + "type": "string" + }, + "creator": { + "description": "Username or ID of the user who created the system", + "type": "string" + }, + "description": { + "description": "Description or purpose of the system", + "type": "string" + }, + "env": { + "description": "Environment where the system is deployed (e.g. prod, gray)", + "type": "string" + }, + "modifier": { + "description": "Username or ID of the user who last modified the system", + "type": "string" + }, + "tenant": { + "description": "Tenant or organization that the system belongs to", + "type": "string" + }, + "type": { + "description": "Type or category of the system (e.g. cache, message queue)", + "type": "string" + } + } + }, + "systemconfig.QuerySystemConfigRequest": { + "type": "object", + "required": [ + "page", + "perPage" + ], + "properties": { + "keyword": { + "description": "Keyword is the keyword to search for.\nOptional: true", + "type": "string" + }, + "page": { + "description": "Page is the page number, starting from 1.\nRequired: true, Minimum value: 1", + "type": "integer", + "minimum": 1 + }, + "perPage": { + "description": "PerPage is the number of items per page.\nRequired: true, Minimum value: 1, Maximum value: 300", + "type": "integer", + "maximum": 300, + "minimum": 1 + } + } + }, + "systemconfig.UpdateSystemConfigRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "config": { + "description": "Configuration data in JSON or YAML format", + "type": "string" + }, + "creator": { + "description": "Username or ID of the user who created the system", + "type": "string" + }, + "description": { + "description": "Description or purpose of the system", + "type": "string" + }, + "env": { + "description": "Environment where the system is deployed (e.g. prod, gray)", + "type": "string" + }, + "id": { + "description": "Unique ID of the system", + "type": "integer" + }, + "modifier": { + "description": "Username or ID of the user who last modified the system", + "type": "string" + }, + "tenant": { + "description": "Tenant or organization that the system belongs to", + "type": "string" + }, + "type": { + "description": "Type or category of the system (e.g. cache, message queue)", + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/api/openapispec/swagger.yaml b/api/openapispec/swagger.yaml new file mode 100644 index 0000000..3316392 --- /dev/null +++ b/api/openapispec/swagger.yaml @@ -0,0 +1,314 @@ +definitions: + entity.SystemConfig: + properties: + config: + description: Configuration data in JSON or YAML format + type: string + createdAt: + description: Timestamp when the system was created + type: string + creator: + description: Username or ID of the user who created the system + type: string + description: + description: Description or purpose of the system + type: string + env: + description: Environment where the system is deployed (e.g. prod, gray) + type: string + id: + description: Unique ID of the system + type: integer + modifier: + description: Username or ID of the user who last modified the system + type: string + tenant: + description: Tenant or organization that the system belongs to + type: string + type: + description: Type or category of the system (e.g. cache, message queue) + type: string + updatedAt: + description: Timestamp when the system was last updated + type: string + type: object + systemconfig.CreateSystemConfigRequest: + properties: + config: + description: Configuration data in JSON or YAML format + type: string + creator: + description: Username or ID of the user who created the system + type: string + description: + description: Description or purpose of the system + type: string + env: + description: Environment where the system is deployed (e.g. prod, gray) + type: string + modifier: + description: Username or ID of the user who last modified the system + type: string + tenant: + description: Tenant or organization that the system belongs to + type: string + type: + description: Type or category of the system (e.g. cache, message queue) + type: string + required: + - config + - creator + - env + - tenant + - type + type: object + systemconfig.QuerySystemConfigRequest: + properties: + keyword: + description: |- + Keyword is the keyword to search for. + Optional: true + type: string + page: + description: |- + Page is the page number, starting from 1. + Required: true, Minimum value: 1 + minimum: 1 + type: integer + perPage: + description: |- + PerPage is the number of items per page. + Required: true, Minimum value: 1, Maximum value: 300 + maximum: 300 + minimum: 1 + type: integer + required: + - page + - perPage + type: object + systemconfig.UpdateSystemConfigRequest: + properties: + config: + description: Configuration data in JSON or YAML format + type: string + creator: + description: Username or ID of the user who created the system + type: string + description: + description: Description or purpose of the system + type: string + env: + description: Environment where the system is deployed (e.g. prod, gray) + type: string + id: + description: Unique ID of the system + type: integer + modifier: + description: Username or ID of the user who last modified the system + type: string + tenant: + description: Tenant or organization that the system belongs to + type: string + type: + description: Type or category of the system (e.g. cache, message queue) + type: string + required: + - id + type: object +info: + contact: {} +paths: + /api/v1/systemconfig: + post: + consumes: + - application/json + description: Create a new system config instance + parameters: + - description: Created system config + in: body + name: config + required: true + schema: + $ref: '#/definitions/systemconfig.CreateSystemConfigRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.SystemConfig' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Create system config + put: + consumes: + - application/json + description: Update the specified system config + parameters: + - description: Updated system config + in: body + name: config + required: true + schema: + $ref: '#/definitions/systemconfig.UpdateSystemConfigRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.SystemConfig' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Update system config + /api/v1/systemconfig/{id}: + delete: + description: Delete specified system config by ID + parameters: + - description: SystemConfig ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.SystemConfig' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Delete system config + get: + description: Get system config information by system config ID + parameters: + - description: SystemConfig ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.SystemConfig' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Get system config + /api/v1/systemconfig/count: + get: + description: Count the total number of system configs + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.SystemConfig' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Count system configs + /api/v1/systemconfigs: + get: + consumes: + - application/json + description: Find system configs with query + parameters: + - description: query body + in: body + name: query + required: true + schema: + $ref: '#/definitions/systemconfig.QuerySystemConfigRequest' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.SystemConfig' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Find system configs +swagger: "2.0" diff --git a/assets/sql/app.sql b/assets/sql/app.sql new file mode 100644 index 0000000..13e28b2 --- /dev/null +++ b/assets/sql/app.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS `system_config` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', + `created_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + `updated_at` timestamp(3) NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间', + `tenant` varchar(32) NOT NULL DEFAULT 'MAIN_SITE' COMMENT '租户名称', + `env` varchar(50) NOT NULL COMMENT '环境', + `type` varchar(32) NOT NULL COMMENT '配置类型', + `config` mediumtext DEFAULT NULL COMMENT '配置内容', + `description` varchar(256) DEFAULT NULL COMMENT '描述', + `creator` varchar(32) DEFAULT NULL COMMENT '创建人', + `modifier` varchar(32) DEFAULT NULL COMMENT '修改人', + `deleted_at` timestamp(3) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`), + KEY `idx_system_config_deleted_at` (`deleted_at`) +) AUTO_INCREMENT = 1400002 DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC COMMENT = '系统配置表'; diff --git a/cmd/main.go b/cmd/main.go index 78b92d2..ae530a1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,76 +1,57 @@ package main import ( - "net/http" + "expvar" - "github.com/gin-gonic/gin" -) - -var db = make(map[string]string) - -func setupRouter() *gin.Engine { - // Disable Console Color - // gin.DisableConsoleColor() - r := gin.Default() - - // Ping test - r.GET("/ping", func(c *gin.Context) { - c.String(http.StatusOK, "pong") - }) + "github.com/spf13/cobra" + cliflag "k8s.io/component-base/cli/flag" + "k8s.io/component-base/term" - // Get user value - r.GET("/user/:name", func(c *gin.Context) { - user := c.Params.ByName("name") - value, ok := db[user] - if ok { - c.JSON(http.StatusOK, gin.H{"user": user, "value": value}) - } else { - c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"}) - } - }) + "github.com/elliotxx/go-web-template/cmd/options" + "github.com/elliotxx/go-web-template/pkg/util/cmdutil" +) - // Authorized group (uses gin.BasicAuth() middleware) - // Same than: - // authorized := r.Group("/") - // authorized.Use(gin.BasicAuth(gin.Credentials{ - // "foo": "bar", - // "manu": "123", - // })) - authorized := r.Group("/", gin.BasicAuth(gin.Accounts{ - "foo": "bar", // user:foo password:bar - "manu": "123", // user:manu password:123 +// NewAppCommand creates a *cobra.Command object with default parameters +func NewAppCommand() *cobra.Command { + o := options.NewAppOptions() + // Publish the app options to the public variables, + // You can visit http://localhost/debug/vars to view all public variables + expvar.Publish("appOptions", expvar.Func(func() any { + return o })) - /* example curl for /admin with basicauth header - Zm9vOmJhcg== is base64("foo:bar") - curl -X POST \ - http://localhost:8080/admin \ - -H 'authorization: Basic Zm9vOmJhcg==' \ - -H 'content-type: application/json' \ - -d '{"value":"bar"}' - */ - authorized.POST("admin", func(c *gin.Context) { - user := c.MustGet(gin.AuthUserKey).(string) + // Initialize root command + cmd := &cobra.Command{ + Use: "app", + Long: `App is a service that operates and maintains configuration code based on gitops technology`, + // stop printing usage when the command errors + SilenceUsage: true, + SilenceErrors: true, + Run: func(_ *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } - // Parse JSON - var json struct { - Value string `json:"value" binding:"required"` - } + // Add flags of each option to root command + fs := cmd.Flags() + namedFlagSets := o.Flags() + for _, f := range namedFlagSets.FlagSets { + fs.AddFlagSet(f) + } - if c.Bind(&json) == nil { - db[user] = json.Value - c.JSON(http.StatusOK, gin.H{"status": "ok"}) - } - }) + // Group options by flag set + cols, _, _ := term.TerminalSize(cmd.OutOrStdout()) + cliflag.SetUsageAndHelpFunc(cmd, namedFlagSets, cols) - return r + return cmd } func main() { - r := setupRouter() - // Listen and Server in 0.0.0.0:8080 - err := r.Run(":8080") - if err != nil { - return + cmd := NewAppCommand() + + if err := cmd.Execute(); err != nil { + cmdutil.CheckErr(err) } } diff --git a/cmd/main_test.go b/cmd/main_test.go deleted file mode 100644 index 4edcdad..0000000 --- a/cmd/main_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPingRoute(t *testing.T) { - router := setupRouter() - - w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodGet, "/ping", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "pong", w.Body.String()) -} - -func TestUserNameRoute(t *testing.T) { - router := setupRouter() - - w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodGet, "/user/admin", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - assert.Equal(t, "{\"status\":\"no value\",\"user\":\"admin\"}", w.Body.String()) -} - -func TestAdminRoute(t *testing.T) { - router := setupRouter() - - w := httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, "/admin", nil) - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusUnauthorized, w.Code) - assert.Equal(t, "", w.Body.String()) -} diff --git a/cmd/options/app.go b/cmd/options/app.go new file mode 100644 index 0000000..4ea13c4 --- /dev/null +++ b/cmd/options/app.go @@ -0,0 +1,167 @@ +package options + +import ( + "encoding/json" + "fmt" + + "github.com/elliotxx/go-web-template/cmd/options/types" + "github.com/elliotxx/go-web-template/pkg/server" + "github.com/elliotxx/go-web-template/pkg/util/cmdutil" + "github.com/elliotxx/go-web-template/pkg/util/configutil" + "github.com/elliotxx/go-web-template/pkg/version" + "github.com/hashicorp/go-multierror" + "github.com/koding/multiconfig" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/afero" + "k8s.io/component-base/cli/flag" +) + +// EnvironmentLoader satisifies the loader interface. +// It loads the configuration from the environment variables in the form of +// STRUCTNAME_FIELDNAME. +var envLoader = &multiconfig.EnvironmentLoader{ + // CamelCase adds a separator for field names in camelcase form. A + // fieldname of "AccessKey" would generate a environment name of + // "STRUCTNAME_ACCESSKEY". If CamelCase is enabled, the environment name + // will be generated in the form of "STRUCTNAME_ACCESS_KEY" + CamelCase: true, +} + +// AppOptions runs a App server. +type AppOptions struct { + Generic *GenericOptions `json:"generic,omitempty" yaml:"generic,omitempty"` + Logging *LoggingOptions `json:"logging,omitempty" yaml:"logging,omitempty"` + Network *NetworkOptions `json:"network,omitempty" yaml:"network,omitempty"` + Database *DatabaseOptions `json:"database,omitempty" yaml:"database,omitempty"` +} + +// NewAppOptions creates a new AppOptions object with default parameters +func NewAppOptions() *AppOptions { + return &AppOptions{ + Generic: NewGenericOptions(), + Logging: NewLoggingOptions(), + Network: NewNetworkOptions(), + Database: NewDatabaseOptions(), + } +} + +// Flags returns flags for a specific APIServer by section name +func (o *AppOptions) Flags() (fss flag.NamedFlagSets) { + // Add the generic flags + o.Logging.AddFlags(fss.FlagSet("logging")) + o.Network.AddFlags(fss.FlagSet("network")) + o.Generic.AddFlags(fss.FlagSet("generic")) + o.Database.AddFlags(fss.FlagSet("database")) + return fss +} + +func (o *AppOptions) Complete(args []string) error { + // NOTE: Priority of configuration effectiveness: + // Default value < Command Flags < ConfigFile < Environment + + // Load configuration from file + if o.Generic.ConfigFile != "" { + if err := o.LoadConfigFromFile(o.Generic.ConfigFile); err != nil { + return errors.Wrap(err, "failed to load config file") + } + } + + // Load configuration from environment + err := envLoader.Load(o) + if err != nil { + return errors.Wrap(err, "failed to load config file") + } + + return nil +} + +// Validate checks AppOptions and return a slice of found error(s) +func (o *AppOptions) Validate() (err error) { + if o == nil { + return errors.Errorf("options is nil") + } + + err = multierror.Append(err, multierror.Flatten(o.Generic.Validate())) + if !o.Generic.DumpVersion && !o.Generic.DumpEnvs { + err = multierror.Append(err, multierror.Flatten(o.Logging.Validate())) + err = multierror.Append(err, multierror.Flatten(o.Network.Validate())) + // err = multierror.Append(err, multierror.Flatten(o.Database.Validate())) + } + + return +} + +func (o *AppOptions) Config() *server.Config { + cfg := server.NewConfig() + o.Generic.ApplyTo(cfg) + o.Database.ApplyTo(cfg) + o.Logging.ApplyTo(cfg) + return cfg +} + +func (o *AppOptions) Run() (err error) { + if o.Generic.DumpVersion { + fmt.Println(version.YAML()) + return cmdutil.ErrExit + } + + if o.Generic.DumpEnvs { + envLoader.PrintEnvs(o) + return cmdutil.ErrExit + } + + // Init logrus configuration by options + if err := o.Logging.InitLogging(types.ProjectName); err != nil { + return err + } + + if o.Logging.DumpCurrentConfig { + logrus.Info("Dumping the currently used server configuration ...") + output, err := json.MarshalIndent(o, "", " ") + if err != nil { + logrus.Warn(err) + } + logrus.Info(string(output)) + } + + // Start app server + logrus.Info("Start instantiating App Server ...") + s, err := o.Config().New() + if err != nil { + return err + } + logrus.Info("Successfully complete instance!") + + logrus.Info("Start executing predecessor tasks ...") + if err = s.PreRun(); err != nil { + return err + } + logrus.Info("Successfully complete the predecessor task execution!") + + logrus.Info("Starting the server ...") + if err = s.Run(fmt.Sprintf(":%d", o.Network.Port)); err != nil { + return err + } + + return nil +} + +func (o *AppOptions) LoadConfigFromFile(configFile string) error { + // Validate + if configFile == "" { + return errors.Errorf("config file is not specified") + } + + if !configutil.IsValidConfigFilename(configFile) { + return errors.Errorf("invalid config file: %s", configFile) + } + + // Load configuration from file + err := configutil.FromFile(afero.NewOsFs(), configFile, o) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/options/dababase.go b/cmd/options/dababase.go new file mode 100644 index 0000000..2c2f4b7 --- /dev/null +++ b/cmd/options/dababase.go @@ -0,0 +1,150 @@ +package options + +import ( + "encoding/json" + "os" + "strconv" + "strings" + + "github.com/elliotxx/go-web-template/cmd/options/types" + "github.com/elliotxx/go-web-template/pkg/server" + gomysql "github.com/go-sql-driver/mysql" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var ( + ErrDBHostNotSpecified = errors.New("--db-host must be specified") + ErrDBNameNotSpecified = errors.New("--db-name must be specified") + ErrDBUserNotSpecified = errors.New("--db-user must be specified") + ErrDBPortNotSpecified = errors.New("--db-port must be specified") + _ types.Options = &DatabaseOptions{} +) + +// DatabaseOptions is a Database options struct +type DatabaseOptions struct { + DBName string `json:"dbName,omitempty" yaml:"dbName,omitempty"` + DBUser string `json:"dbUser,omitempty" yaml:"dbUser,omitempty"` + DBPassword string `json:"dbPassword,omitempty" yaml:"dbPassword,omitempty"` + DBHost string `json:"dbHost,omitempty" yaml:"dbHost,omitempty"` + DBPort int `json:"dbPort,omitempty" yaml:"dbPort,omitempty"` + // AutoMigrate will attempt to automatically migrate all tables + AutoMigrate bool `json:"autoMigrate,omitempty" yaml:"autoMigrate,omitempty"` + MigrateFile string `json:"migrateFile,omitempty" yaml:"migrateFile,omitempty"` +} + +// NewDatabaseOptions returns a DatabaseOptions instance with the default values +func NewDatabaseOptions() *DatabaseOptions { + return &DatabaseOptions{ + DBHost: "127.0.0.1", + DBPort: 3306, + AutoMigrate: false, + } +} + +// InstallDB uses the run options to generate and open a db session. +func (o *DatabaseOptions) InstallDB() (*gorm.DB, error) { + // Generate go-sql-driver.mysql config to format DSN + config := gomysql.NewConfig() + config.User = o.DBUser + config.Passwd = o.DBPassword + config.Addr = o.DBHost + ":" + strconv.Itoa(o.DBPort) + config.DBName = o.DBName + config.Net = "tcp" + config.ParseTime = true + config.InterpolateParams = true + config.Params = map[string]string{ + "charset": "utf8", + "loc": "Asia/Shanghai", + } + dsn := config.FormatDSN() + // silence log output + cfg := &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + } + return gorm.Open(mysql.Open(dsn), cfg) // todo: add db connection check to healthz check +} + +// Validate checks DatabaseOptions and return a slice of found error(s) +func (o *DatabaseOptions) Validate() error { + if o == nil { + return errors.Errorf("options is nil") + } + + if o.AutoMigrate && len(o.MigrateFile) == 0 { + return errors.Errorf("when --auto-migrate is true, --migrate-file must be specified") + } + + if len(o.DBHost) == 0 { + return ErrDBHostNotSpecified + } + if len(o.DBName) == 0 { + return ErrDBNameNotSpecified + } + if len(o.DBUser) == 0 { + return ErrDBUserNotSpecified + } + if o.DBPort == 0 { + return ErrDBPortNotSpecified + } + + return nil +} + +// ApplyTo apply database options to the server config +func (o *DatabaseOptions) ApplyTo(config *server.Config) { + d, err := o.InstallDB() + if err != nil { + logrus.Fatalf("Failed to apply database options to server.Config as: %+v", err) + } + config.DB = d + + // AutoMigrate will attempt to automatically migrate all tables + if o.AutoMigrate { + logrus.Debugf("AutoMigrate will attempt to automatically migrate all tables from [%s]", o.MigrateFile) + // Read all content by migrate file + migrateSQL, err := os.ReadFile(o.MigrateFile) + if err != nil { + logrus.Fatalf("Failed to read migrate file: %+v", err) + } + + // Split multiple SQL statements into individual statements + stmts := strings.Split(string(migrateSQL), ";") + + // Iterate over all statements and execute them + for _, stmt := range stmts { + // Ignore empty statements + if len(strings.TrimSpace(stmt)) == 0 { + continue + } + + // Use gorm.Exec() function to execute SQL statement + if err = config.DB.Exec(stmt).Error; err != nil { + logrus.Warnf("Failed to exec migrate sql: %+v", err) + } + } + } +} + +// AddFlags adds flags for a specific Option to the specified FlagSet +func (o *DatabaseOptions) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.DBName, "db-name", o.DBName, "the database name") + fs.StringVar(&o.DBUser, "db-user", o.DBUser, "the user name used to access database") + fs.StringVar(&o.DBPassword, "db-pwd", o.DBPassword, "the user password used to access database") + fs.StringVar(&o.DBHost, "db-host", o.DBHost, "database host") + fs.IntVar(&o.DBPort, "db-port", o.DBPort, "database port") + fs.BoolVar(&o.AutoMigrate, "auto-migrate", o.AutoMigrate, "Whether to enable automatic migration") + fs.StringVar(&o.MigrateFile, "migrate-file", o.MigrateFile, "The migrate sql file") +} + +// MarshalJSON is custom marshalling function for masking sensitive field values +func (o DatabaseOptions) MarshalJSON() ([]byte, error) { + type tempOptions DatabaseOptions + o2 := tempOptions(o) + o2.DBPassword = types.MaskString + return json.Marshal(&o2) +} diff --git a/cmd/options/generic.go b/cmd/options/generic.go new file mode 100644 index 0000000..aca8505 --- /dev/null +++ b/cmd/options/generic.go @@ -0,0 +1,60 @@ +package options + +import ( + "github.com/elliotxx/go-web-template/cmd/options/types" + "github.com/elliotxx/go-web-template/pkg/server" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" + "github.com/spf13/pflag" +) + +var _ types.Options = &GenericOptions{} + +// GenericOptions is a generic options struct +type GenericOptions struct { + ConfigFile string `json:"configFile" yaml:"configFile"` + DumpVersion bool `json:"dumpVersion" yaml:"dumpVersion"` + DumpEnvs bool `json:"dumpEnvs" yaml:"dumpEnvs"` +} + +// NewGenericOptions returns a GenericOptions instance with the default values +func NewGenericOptions() *GenericOptions { + return &GenericOptions{ + ConfigFile: "", + DumpVersion: false, + DumpEnvs: false, + } +} + +// Validate checks GenericOptions and return a slice of found error(s) +func (o *GenericOptions) Validate() error { + if o == nil { + return errors.Errorf("options is nil") + } + + var err *multierror.Error + if o.DumpVersion && o.ConfigFile != "" { + err = multierror.Append(err, errors.Errorf("--dump-version and --config-file/--cors-allowed-origins are mutually exclusive")) + } + + return err.ErrorOrNil() +} + +// ApplyTo apply generic options to the server config +func (o *GenericOptions) ApplyTo(config *server.Config) {} + +// AddFlags adds flags for a specific Option to the specified FlagSet +func (o *GenericOptions) AddFlags(fs *pflag.FlagSet) { + if o == nil { + return + } + + fs.StringVarP(&o.ConfigFile, "config-file", "f", "", + "The path to the configuration file. Valid extension: toml, yaml, yml, json") + + fs.BoolVarP(&o.DumpVersion, "version", "V", o.DumpVersion, + "Print the version information and exit") + + fs.BoolVarP(&o.DumpEnvs, "envs", "E", o.DumpEnvs, + "Output all the environment variable names that can be set up, and exit") +} diff --git a/cmd/options/logging.go b/cmd/options/logging.go new file mode 100644 index 0000000..345ba26 --- /dev/null +++ b/cmd/options/logging.go @@ -0,0 +1,191 @@ +package options + +import ( + "io" + "path/filepath" + "runtime" + "strconv" + + nested "github.com/antonfisher/nested-logrus-formatter" + "github.com/elliotxx/errors" + "github.com/elliotxx/go-web-template/cmd/options/types" + "github.com/elliotxx/go-web-template/pkg/server" + "github.com/hashicorp/go-multierror" + "github.com/rifflock/lfshook" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "gopkg.in/natefinch/lumberjack.v2" +) + +var _ types.Options = &LoggingOptions{} + +// LoggingOptions provides the logging configuration. +type LoggingOptions struct { + LogLevel string `json:"logLevel,omitempty" yaml:"logLevel,omitempty"` + DisableText bool `json:"disableText,omitempty" yaml:"disableText,omitempty"` + TextPretty bool `json:"textPretty,omitempty" yaml:"textPretty,omitempty"` + JSONPretty bool `json:"jsonPretty,omitempty" yaml:"jsonPretty,omitempty"` + ReportCaller bool `json:"reportCaller,omitempty" yaml:"reportCaller,omitempty"` + DumpCurrentConfig bool `json:"dumpCurrentConfig,omitempty" yaml:"dumpCurrentConfig,omitempty"` + EnableLoggingToFile bool `json:"enableLoggingToFile,omitempty" yaml:"enableLoggingToFile,omitempty"` + LoggingDirectory string `json:"loggingDirectory,omitempty" yaml:"loggingDirectory,omitempty"` +} + +// NewLoggingOptions returns a LoggingOptions instance with the default values +func NewLoggingOptions() *LoggingOptions { + return &LoggingOptions{ + DumpCurrentConfig: true, + TextPretty: true, + LogLevel: "info", + } +} + +// ApplyTo apply logging options to the server config +func (o *LoggingOptions) ApplyTo(config *server.Config) { + config.LoggingDirectory = o.LoggingDirectory +} + +// Validate checks LoggingOptions and return a slice of found error(s) +func (o *LoggingOptions) Validate() error { + if o == nil { + return errors.Errorf("options is nil") + } + + var err *multierror.Error + + if _, err2 := logrus.ParseLevel(o.LogLevel); err2 != nil { + err = multierror.Append(err, err2) + } + + if o.JSONPretty && !o.DisableText { + err = multierror.Append(err, errors.Errorf("--json-pretty cannot be enabled when --disable-text is disabled")) + } + + if o.EnableLoggingToFile && o.LoggingDirectory == "" { + err = multierror.Append(err, errors.Errorf("--logging-directory must be not empty when --enable-logging-to-file is enabled")) + } + + if o.EnableLoggingToFile && o.TextPretty { + err = multierror.Append(err, errors.Errorf("--text-pretty cannot be enabled when --enable-logging-to-file is enabled")) + } + + return err.ErrorOrNil() +} + +// AddFlags adds flags for a specific Option to the specified FlagSet +func (o *LoggingOptions) AddFlags(fs *pflag.FlagSet) { + if o == nil { + return + } + + fs.StringVarP(&o.LogLevel, "log-level", "L", o.LogLevel, "Log level. Valid values: [trace, debug, info, warn, warning, error, fatal, panic]") + fs.BoolVar(&o.JSONPretty, "json-pretty", o.JSONPretty, "JSONPretty will indent all json logs") + fs.BoolVar(&o.DisableText, "disable-text", o.DisableText, "Disable text mode") + fs.BoolVar(&o.TextPretty, "text-pretty", o.TextPretty, "TextPretty will colorize the logs") + fs.BoolVar(&o.ReportCaller, "report-caller", o.ReportCaller, "Flag for whether to log caller info") + fs.BoolVar(&o.DumpCurrentConfig, "dump-current-config", o.DumpCurrentConfig, "Dump current configuration") + fs.BoolVar(&o.EnableLoggingToFile, "enable-logging-to-file", o.EnableLoggingToFile, "Enable logging to file") + fs.StringVar(&o.LoggingDirectory, "logging-directory", ".", "Specify which directory to log to") +} + +// helper function configures the logging. +func (o *LoggingOptions) InitLogging(projectName string) error { + if o == nil { + return errors.Errorf("logging options is nil") + } + + if len(o.LogLevel) != 0 { + logLevel, err := logrus.ParseLevel(o.LogLevel) + if err != nil { + return errors.Wrap(err, "failed to init logging configuration") + } + + logrus.SetLevel(logLevel) + } + + if o.ReportCaller { + logrus.SetReportCaller(true) + } + + var formatter logrus.Formatter + if o.DisableText { + formatter = &logrus.JSONFormatter{ + TimestampFormat: "2006-01-02 15:04:05.000000", + PrettyPrint: o.JSONPretty, + CallerPrettyfier: func(frame *runtime.Frame) (function string, file string) { + return "", filepath.Base(frame.File) + ":" + strconv.Itoa(frame.Line) + }, + } + } else { + formatter = &nested.Formatter{ + TimestampFormat: "2006-01-02 15:04:05.000000", + NoColors: !o.TextPretty, + CallerFirst: true, + CustomCallerFormatter: func(frame *runtime.Frame) string { + return " " + filepath.Base(frame.File) + ":" + strconv.Itoa(frame.Line) + }, + } + } + logrus.SetFormatter(formatter) + + // Set log output to file + if o.EnableLoggingToFile { + // Set output logs to files (error, info, trace) + setLoggingToFile(o.LoggingDirectory, formatter, projectName) + } + + logrus.Info("Successfully initialized log configuration") + + return nil +} + +// setLoggingToFile sets the logging entry to the specified file(s) +func setLoggingToFile(loggingDirectory string, writerFormatter logrus.Formatter, projectName string) { + // Create lumberjack writers for three different log levels + errorLoggingFile := filepath.Join(loggingDirectory, projectName+".error.log") + errorRotateWriter := &lumberjack.Logger{ + Filename: errorLoggingFile, + MaxSize: 1, // MaxSize the max size in MB of the logfile before it's rolled + MaxBackups: 10, // MaxBackups the max number of rolled files to keep + MaxAge: 30, // MaxAge the max age in days to keep a logfile + } + infoLoggingFile := filepath.Join(loggingDirectory, projectName+".log") + infoRotateWriter := &lumberjack.Logger{ + Filename: infoLoggingFile, + MaxSize: 1, + MaxBackups: 10, + MaxAge: 30, + } + traceLoggingFile := filepath.Join(loggingDirectory, projectName+".trace.log") + traceRotateWriter := &lumberjack.Logger{ + Filename: traceLoggingFile, + MaxSize: 1, + MaxBackups: 10, + MaxAge: 30, + } + + // Set output logs to files (error, info, trace) + writerMap := lfshook.WriterMap{ + logrus.PanicLevel: io.MultiWriter(errorRotateWriter, infoRotateWriter, traceRotateWriter), + logrus.FatalLevel: io.MultiWriter(errorRotateWriter, infoRotateWriter, traceRotateWriter), + logrus.ErrorLevel: io.MultiWriter(errorRotateWriter, infoRotateWriter, traceRotateWriter), + logrus.WarnLevel: io.MultiWriter(infoRotateWriter, traceRotateWriter), + logrus.InfoLevel: io.MultiWriter(infoRotateWriter, traceRotateWriter), + logrus.DebugLevel: io.MultiWriter(traceRotateWriter), + logrus.TraceLevel: io.MultiWriter(traceRotateWriter), + } + + // Finally, log output to stderr, error file, info file, and trace file + logrus.AddHook(lfshook.NewHook( + writerMap, + writerFormatter, + )) + + logrus.WithFields(logrus.Fields{ + "logging-directory": loggingDirectory, + "project-name": projectName, + "error-logging-file": errorLoggingFile, + "info-logging-file": infoLoggingFile, + "trace-logging-file": traceLoggingFile, + }).Debug("Successfully set logging to file") +} diff --git a/cmd/options/network.go b/cmd/options/network.go new file mode 100644 index 0000000..d0fb14c --- /dev/null +++ b/cmd/options/network.go @@ -0,0 +1,62 @@ +package options + +import ( + "time" + + "github.com/elliotxx/errors" + "github.com/elliotxx/go-web-template/cmd/options/types" + "github.com/hashicorp/go-multierror" + "github.com/spf13/pflag" +) + +var _ types.Options = &NetworkOptions{} + +// NetworkOptions is a Network options struct +type NetworkOptions struct { + Port int `json:"port,omitempty" yaml:"port,omitempty"` + CorsAllowedOriginList []string `json:"corsAllowedOriginList,omitempty" yaml:"corsAllowedOriginList,omitempty"` + RequestTimeout time.Duration `json:"requestTimeout,omitempty" yaml:"requestTimeout,omitempty"` +} + +// NewNetworkOptions returns a NetworkOptions instance with the default values +func NewNetworkOptions() *NetworkOptions { + return &NetworkOptions{ + Port: 80, + CorsAllowedOriginList: []string{}, + RequestTimeout: 30 * time.Second, + } +} + +// Validate checks NetworkOptions and return a slice of found error(s) +func (o *NetworkOptions) Validate() error { + if o == nil { + return errors.Errorf("options is nil") + } + + var err *multierror.Error + + if o.RequestTimeout <= 0 { + err = multierror.Append(err, errors.Errorf("--request-timeout must be greater than 0")) + } + + if o.Port <= 0 { + err = multierror.Append(err, errors.Errorf("--port must be greater than 0")) + } + + return err.ErrorOrNil() +} + +// AddFlags adds flags for a specific Option to the specified FlagSet +func (o *NetworkOptions) AddFlags(fs *pflag.FlagSet) { + if o == nil { + return + } + + fs.StringSliceVar(&o.CorsAllowedOriginList, "cors-allowed-origins", o.CorsAllowedOriginList, + "List of allowed origins for CORS, comma separated") + + fs.DurationVar(&o.RequestTimeout, "request-timeout", o.RequestTimeout, + "An optional field indicating the duration a handler must keep a request open before timing it out") + + fs.IntVarP(&o.Port, "port", "p", o.Port, "Port") +} diff --git a/cmd/options/types/types.go b/cmd/options/types/types.go new file mode 100644 index 0000000..56b96e8 --- /dev/null +++ b/cmd/options/types/types.go @@ -0,0 +1,17 @@ +package types + +import ( + "github.com/spf13/pflag" +) + +type Options interface { + // Validate checks Options and return a slice of found error(s) + Validate() error + // AddFlags adds flags for a specific Option to the specified FlagSet + AddFlags(fs *pflag.FlagSet) +} + +const ( + ProjectName = "app" + MaskString = "******" +) diff --git a/config/local.yaml b/config/local.yaml new file mode 100644 index 0000000..2097f84 --- /dev/null +++ b/config/local.yaml @@ -0,0 +1,20 @@ +network: + requestTimeout: 30s +logging: + enableLoggingToFile: true + loggingDirectory: "logs" + logLevel: "debug" + dumpCurrentConfig: true + reportCaller: true + textPretty: false + jsonPretty: true + disableText: true +database: + dbPort: 3306 + dbPassword: app123 + dbName: "appdb" + dbUser: "app" + dbHost: "127.0.0.1" + autoMigrate: true + migrateFile: ./assets/sql/app.sql + diff --git a/go.mk b/go.mk index 76329f1..fd4a150 100644 --- a/go.mk +++ b/go.mk @@ -15,7 +15,7 @@ COVERAGETMP ?= coverage.tmp GOFORMATER ?= gofumpt GOFORMATER_VERSION ?= v0.2.0 GOLINTER ?= golangci-lint -GOLINTER_VERSION ?= v1.41.0 +GOLINTER_VERSION ?= v1.52.2 # To generate help information diff --git a/go.mod b/go.mod index 356eae6..66c8d38 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,108 @@ module github.com/elliotxx/go-web-template -go 1.16 +go 1.19 require ( - github.com/gin-gonic/gin v1.7.4 - github.com/stretchr/testify v1.7.0 + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/akkuman/gin-logrus-recovery v0.0.0-20201204100630-0da231b73e94 + github.com/antonfisher/nested-logrus-formatter v1.3.1 + github.com/arl/statsviz v0.5.2 + github.com/clbanning/mxj/v2 v2.7.0 + github.com/elliotxx/errors v1.0.0 + github.com/elliotxx/expvar v1.1.1 + github.com/elliotxx/healthcheck v0.2.1 + github.com/elliotxx/safe v1.0.0 + github.com/gin-contrib/cors v1.4.0 + github.com/gin-contrib/gzip v0.0.6 + github.com/gin-contrib/pprof v1.4.0 + github.com/gin-contrib/requestid v0.0.6 + github.com/gin-gonic/gin v1.9.1 + github.com/go-sql-driver/mysql v1.7.0 + github.com/gookit/goutil v0.6.12 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-version v1.6.0 + github.com/jinzhu/copier v0.3.5 + github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7 + github.com/niklasfasching/go-org v1.7.0 + github.com/pelletier/go-toml/v2 v2.0.9 + github.com/pkg/errors v0.9.1 + github.com/pterm/pterm v0.12.65 + github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/afero v1.9.5 + github.com/spf13/cast v1.5.1 + github.com/spf13/cobra v1.7.0 + github.com/spf13/jwalterweatherman v1.1.0 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.8.4 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.8.12 + golang.org/x/sync v0.3.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.5.1 + gorm.io/gorm v1.25.2 + k8s.io/component-base v0.27.4 +) + +require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/bytedance/sonic v1.10.0-rc2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/containerd/console v1.0.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/camelcase v1.0.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/spec v0.20.7 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/arch v0.4.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apimachinery v0.27.4 // indirect + k8s.io/klog/v2 v2.90.1 // indirect ) diff --git a/go.sum b/go.sum index bc23346..6407b27 100644 --- a/go.sum +++ b/go.sum @@ -1,57 +1,771 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.0.2 h1:2e/4KY6t3wokja01Cyty6qgkQM8MotJzjtqCH70oX2Q= +atomicgo.dev/schedule v0.0.2/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/akkuman/gin-logrus-recovery v0.0.0-20201204100630-0da231b73e94 h1:n/nUhk5JGyO48J4ePLJjlVj7vNxLnVSg4F0rVXoHILo= +github.com/akkuman/gin-logrus-recovery v0.0.0-20201204100630-0da231b73e94/go.mod h1:TVJVPrtnECQQu8iW0iiGg2Aeur/MYv4/gaDw5vUY8ww= +github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ= +github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA= +github.com/arl/statsviz v0.5.2 h1:0+F96LduGQx7HZlMTUL9PNHv7lixwWCdxJzWC+SGSkI= +github.com/arl/statsviz v0.5.2/go.mod h1:UomKe3l2yafXH6/LnOt8xGbiU3CEl70J1LJSW1fZO/E= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.0-rc2 h1:oDfRZ+4m6AYCOC0GFeOCeYqvBmucy1isvouS2K0cPzo= +github.com/bytedance/sonic v1.10.0-rc2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elliotxx/errors v1.0.0 h1:nVnsmBDrletkuv+hblHQtXA0wT17QZcug7TpkoeDdbU= +github.com/elliotxx/errors v1.0.0/go.mod h1:dJbmQ1+IP2Hr8Z0ryd/xS1XiBH0vXkBAkCaXfBz6K8I= +github.com/elliotxx/expvar v1.1.1 h1:5IEYB1bYvUl9wXv2nBdLM4XeYO5VA0YxqzdnNvBFOSw= +github.com/elliotxx/expvar v1.1.1/go.mod h1:qJqroAZZotHuFa8ZC1cyK67Vf5ccP6UnSW7zjTd7YXY= +github.com/elliotxx/healthcheck v0.2.1 h1:LwhK8Y0C5AGdrywIAG5+ivcNoUI5H4ry4j2ejzGBQaA= +github.com/elliotxx/healthcheck v0.2.1/go.mod h1:y6VW57YGHpqascrlQQBmLciXt66Cfx+l2bnv5fddbtg= +github.com/elliotxx/safe v1.0.0 h1:SOe7tVXDavTEDBPrFG/m/G++M2w9fU7idfwt7w5WkTQ= +github.com/elliotxx/safe v1.0.0/go.mod h1:iNcUMga5s3unppx7+pExhbGybGEuyGgN0F/srkHKlxw= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= +github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg= +github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90= +github.com/gin-contrib/requestid v0.0.6 h1:mGcxTnHQ45F6QU5HQRgQUDsAfHprD3P7g2uZ4cSZo9o= +github.com/gin-contrib/requestid v0.0.6/go.mod h1:9i4vKATX/CdggbkY252dPVasgVucy/ggBeELXuQztm4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= -github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= +github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= +github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/goutil v0.6.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc= +github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= +github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7 h1:SWlt7BoQNASbhTUD0Oy5yysI2seJ7vWuGUp///OM4TM= +github.com/koding/multiconfig v0.0.0-20171124222453-69c27309b2d7/go.mod h1:Y2SaZf2Rzd0pXkLVhLlCiAXFCLSXAIbTKDivVgff/AM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= +github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.65 h1:HNMNCh2Zi6Lk+g5b8pORrFM9Ygz10GZUUcCFUkGpK2Q= +github.com/pterm/pterm v0.12.65/go.mod h1:CpJq+fr0+xKGlPFDhKTkepte2fY3Ydr5bzSJ9di67uI= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc= +golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= +gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= +k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/component-base v0.27.4 h1:Wqc0jMKEDGjKXdae8hBXeskRP//vu1m6ypC+gwErj4c= +k8s.io/component-base v0.27.4/go.mod h1:hoiEETnLc0ioLv6WPeDt8vD34DDeB35MfQnxCARq3kY= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/.gitkeep b/pkg/.gitkeep deleted file mode 100644 index 45adbb2..0000000 --- a/pkg/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -.gitkeep \ No newline at end of file diff --git a/pkg/domain/entity/system_config.go b/pkg/domain/entity/system_config.go new file mode 100644 index 0000000..5374a34 --- /dev/null +++ b/pkg/domain/entity/system_config.go @@ -0,0 +1,96 @@ +package entity + +import ( + "fmt" + "time" +) + +// SystemConfig represents the configuration of a system. +type SystemConfig struct { + // Unique ID of the system + ID uint `yaml:"id" json:"id"` + // Tenant or organization that the system belongs to + Tenant string `yaml:"tenant" json:"tenant"` + // Environment where the system is deployed (e.g. prod, gray) + Env Env `yaml:"env" json:"env"` + // Type or category of the system (e.g. cache, message queue) + Type string `yaml:"type" json:"type"` + // Configuration data in JSON or YAML format + Config string `yaml:"config,omitempty" json:"config,omitempty"` + // Description or purpose of the system + Description string `yaml:"description,omitempty" json:"description,omitempty"` + // Username or ID of the user who created the system + Creator string `yaml:"creator,omitempty" json:"creator,omitempty"` + // Username or ID of the user who last modified the system + Modifier string `yaml:"modifier,omitempty" json:"modifier,omitempty"` + // Timestamp when the system was created + CreatedAt time.Time `yaml:"createdAt,omitempty" json:"createdAt,omitempty"` + // Timestamp when the system was last updated + UpdatedAt time.Time `yaml:"updatedAt,omitempty" json:"updatedAt,omitempty"` +} + +// Validate checks if the system config is valid. +// It returns an error if the system config is not valid. +func (s *SystemConfig) Validate() error { + if _, err := ParseEnv(string(s.Env)); err != nil { + return err + } + + return nil +} + +// Env represents the environment. +type Env string + +// These constants represent the possible environment. +const ( + // EnvPre represents the pre environment. + EnvPre Env = "pre" + + // EnvGray represents the gray environment. + EnvGray Env = "gray" + + // EnvProd represents the production environment. + EnvProd Env = "prod" + + // EnvDev represents the DEV environment. + EnvDev Env = "dev" + + // EnvTest represents the TEST environment. + EnvTest Env = "test" + + // EnvStable represents the STABLE environment. + EnvStable Env = "stable" +) + +// ParseEnv parses a string into a Env. +// If the string is not a valid Env, it returns an error. +func ParseEnv(str string) (Env, error) { + switch str { + case "pre": + return EnvPre, nil + case "gray": + return EnvGray, nil + case "prod": + return EnvProd, nil + case "dev": + return EnvDev, nil + case "test": + return EnvTest, nil + case "stable": + return EnvStable, nil + default: + return Env(""), fmt.Errorf("invalid environment: %q", str) + } +} + +// MustParseEnv parses a string into a Env. +// If the string is not a valid Env, it panics. +func MustParseEnv(str string) Env { + env, err := ParseEnv(str) + if err != nil { + panic(err) + } + + return env +} diff --git a/pkg/domain/repository/system_config_repository.go b/pkg/domain/repository/system_config_repository.go new file mode 100644 index 0000000..bded5d8 --- /dev/null +++ b/pkg/domain/repository/system_config_repository.go @@ -0,0 +1,25 @@ +package repository + +import ( + "context" + + "github.com/elliotxx/go-web-template/pkg/domain/entity" +) + +// SystemConfigRepository is an interface that defines the repository +// operations for system config. +// It follows the principles of domain-driven design (DDD). +type SystemConfigRepository interface { + // Create creates a new system config. + Create(ctx context.Context, systemConfig *entity.SystemConfig) error + // Delete deletes a system config by its ID. + Delete(ctx context.Context, id uint) error + // Update updates an existing system config. + Update(ctx context.Context, systemConfig *entity.SystemConfig) error + // Get retrieves a system config by its ID. + Get(ctx context.Context, id uint) (*entity.SystemConfig, error) + // Find returns a list of specified system config. + Find(ctx context.Context, query Query) ([]*entity.SystemConfig, error) + // Count returns the total of system configs. + Count(ctx context.Context) (int, error) +} diff --git a/pkg/domain/repository/types.go b/pkg/domain/repository/types.go new file mode 100644 index 0000000..b7d6f02 --- /dev/null +++ b/pkg/domain/repository/types.go @@ -0,0 +1,11 @@ +package repository + +// Query represents the query criteria for a database access. +type Query struct { + // Offset is the number of items to skip. + Offset int + // Limit is the maximum number of items to return. + Limit int + // Keyword is the keyword to search for. + Keyword string +} diff --git a/pkg/errcode/api.go b/pkg/errcode/api.go new file mode 100644 index 0000000..e2bbb49 --- /dev/null +++ b/pkg/errcode/api.go @@ -0,0 +1,52 @@ +package errcode + +import ( + "net/http" + "strings" + + "github.com/elliotxx/errors" +) + +func NewErrorCode(code string, message string) errors.ErrorCode { + mustSetCodeIfNotPresent(code) + return errors.NewErrorCode(code, message) +} + +// Scope returns the error's scope +func Scope(err error) string { + if err != nil { + if e, ok := err.(errors.DetailError); ok && len(e.GetCode()) == 5 { + return e.GetCode()[0:3] + } + if e, ok := err.(errors.ErrorCode); ok && len(e.GetCode()) == 5 { + return e.GetCode()[0:3] + } + return InvalidScope + } + return "" +} + +func InvalidErrorScope(code string) bool { + c := strings.TrimSpace(code) + return c == InvalidScope || c == "" +} + +// StatusCode returns the http status code of the error +func StatusCode(err error) int { + switch Scope(err) { + case Scope(Success): + return http.StatusOK + case Scope(NotFound): + return http.StatusNotFound + case Scope(ServerError), Scope(InternalError): + return http.StatusInternalServerError + case Scope(InvalidParams): + return http.StatusBadRequest + case Scope(AccessPermissionError): + return http.StatusUnauthorized + case Scope(TooManyRequests): + return http.StatusTooManyRequests + default: + return http.StatusInternalServerError + } +} diff --git a/pkg/errcode/common_errcode.go b/pkg/errcode/common_errcode.go new file mode 100644 index 0000000..8227683 --- /dev/null +++ b/pkg/errcode/common_errcode.go @@ -0,0 +1,41 @@ +package errcode + +var ( + Success = NewErrorCode("00000", "成功") + ClientError = NewErrorCode("A0001", "用户端错误") + NotFound = NewErrorCode("A0100", "不存在") + AccessPermissionError = NewErrorCode("A0200", "访问权限异常") + AbnormalUserOperation = NewErrorCode("A0300", "用户操作异常") + InvalidParams = NewErrorCode("A0400", "无效的用户输入") + BlankRequiredParams = NewErrorCode("A0401", "请求必填参数为空") + ExceedRangeParams = NewErrorCode("A0402", "请求参数值超出允许的范围") + MalformedParams = NewErrorCode("A0403", "参数格式不匹配") + ErrDeserializedParams = NewErrorCode("A0404", "请求参数反序列化失败") + SensitiveWordsParams = NewErrorCode("A0405", "请求参数包含违禁敏感词") + ServerError = NewErrorCode("A0500", "用户请求服务异常") + TooManyRequests = NewErrorCode("A0501", "请求次数超出限制") + ConcurrentExceedLimit = NewErrorCode("A0502", "请求并发数超出限制") + WaitUserOperation = NewErrorCode("A0503", "用户操作请等待") + RepeatedRequest = NewErrorCode("A0504", "用户重复请求") + AbnormalUserResources = NewErrorCode("A0600", "用户资源异常") + AbnormalUserVersion = NewErrorCode("A0700", "用户当前版本异常") + MismatchUserVersion = NewErrorCode("A0701", "用户安装版本与系统不匹配") + TooLowUserVersion = NewErrorCode("A0702", "用户安装版本过低") + TooHighUserVersion = NewErrorCode("A0703", "用户安装版本过高") + ExpiredUserVersion = NewErrorCode("A0704", "用户安装版本已过期") + MismatchAPIVersion = NewErrorCode("A0705", "用户API请求版本不匹配") + TooLowAPIVersion = NewErrorCode("A0706", "用户API请求版本过低") + TooHighAPIVersion = NewErrorCode("A0707", "用户API请求版本过高") + InternalError = NewErrorCode("B0001", "系统执行出错") + InvalidStartupParams = NewErrorCode("B0002", "系统启动参数错误") + SystemTimeout = NewErrorCode("B0100", "系统执行超时") + SystemResourceError = NewErrorCode("B0200", "系统资源异常") + ReadDiskFailed = NewErrorCode("B0201", "系统读取磁盘文件失败") + ThirdPartyServiceError = NewErrorCode("C0001", "调用第三方服务出错") + MiddlewareServiceError = NewErrorCode("C0100", "中间件服务出错") + MiddlewareServiceTimeout = NewErrorCode("C0101", "中间件执行超时") + DatabaseServiceError = NewErrorCode("C0200", "数据库服务出错") + DatabaseServiceTimeout = NewErrorCode("C0201", "数据库服务超时") + NotificationServiceError = NewErrorCode("C0300", "通知服务出错") + NotificationServiceTimeout = NewErrorCode("C0301", "通知服务超时") +) diff --git a/pkg/errcode/types.go b/pkg/errcode/types.go new file mode 100644 index 0000000..2d3e3ac --- /dev/null +++ b/pkg/errcode/types.go @@ -0,0 +1,14 @@ +package errcode + +import "fmt" + +const InvalidScope = "999" + +var codes = map[string]struct{}{} + +func mustSetCodeIfNotPresent(code string) { + if _, ok := codes[code]; ok { + panic(fmt.Sprintf("The error code %s already exists, please change one", code)) + } + codes[code] = struct{}{} +} diff --git a/pkg/handler/api/v1/systemconfig/handler.go b/pkg/handler/api/v1/systemconfig/handler.go new file mode 100644 index 0000000..320f44d --- /dev/null +++ b/pkg/handler/api/v1/systemconfig/handler.go @@ -0,0 +1,236 @@ +package systemconfig + +import ( + "context" + "strconv" + + "github.com/elliotxx/errors" + "github.com/elliotxx/go-web-template/pkg/domain/entity" + "github.com/elliotxx/go-web-template/pkg/domain/repository" + "github.com/elliotxx/go-web-template/pkg/errcode" + "github.com/elliotxx/go-web-template/pkg/util/kdump" + "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type Handler struct { + repo repository.SystemConfigRepository +} + +func NewHandler(repo repository.SystemConfigRepository) *Handler { + return &Handler{ + repo: repo, + } +} + +// @Summary Create system config +// @Description Create a new system config instance +// @Accept json +// @Produce json +// @Param system config body CreateSystemConfigRequest true "Created system config" +// @Success 200 {object} entity.SystemConfig "Success" +// @Failure 400 {object} errors.DetailError "Bad Request" +// @Failure 401 {object} errors.DetailError "Unauthorized" +// @Failure 429 {object} errors.DetailError "Too Many Requests" +// @Failure 404 {object} errors.DetailError "Not Found" +// @Failure 500 {object} errors.DetailError "Internal Server Error" +// @Router /api/v1/systemconfig [post] +func (h *Handler) CreateSystemConfig(c *gin.Context, log logrus.FieldLogger) (any, error) { + // Parse payload from requestPayload + var requestPayload CreateSystemConfigRequest + if err := c.ShouldBindJSON(&requestPayload); err != nil { + return nil, errcode.ErrDeserializedParams.Causewf(err, "failed to decode json") + } + log.Infof("Request payload: %v", kdump.FormatN(requestPayload)) + + // Convert request payload to domain model + var systemConfig entity.SystemConfig + if err := copier.Copy(&systemConfig, &requestPayload); err != nil { + return nil, errors.Wrap(err, "failed to convert request payload to domain model") + } + + // Create systemConfig with repository + err := h.repo.Create(context.TODO(), &systemConfig) + if err != nil { + return nil, errors.Wrap(err, "failed to creating systemConfig with repository") + } + + // Return created systemConfig + return systemConfig, nil +} + +// @Summary Delete system config +// @Description Delete specified system config by ID +// @Produce json +// @Param id path int true "SystemConfig ID" +// @Success 200 {object} entity.SystemConfig "Success" +// @Failure 400 {object} errors.DetailError "Bad Request" +// @Failure 401 {object} errors.DetailError "Unauthorized" +// @Failure 429 {object} errors.DetailError "Too Many Requests" +// @Failure 404 {object} errors.DetailError "Not Found" +// @Failure 500 {object} errors.DetailError "Internal Server Error" +// @Router /api/v1/systemconfig/{id} [delete] +func (h *Handler) DeleteSystemConfig(c *gin.Context, log logrus.FieldLogger) (any, error) { + // Parse payload from requestPayload + paramID := c.Param("id") + log.Infof("Request params id: %s", paramID) + + // Get systemConfig with repository + id, err := strconv.Atoi(paramID) + if err != nil { + return nil, err + } + + // Delete systemConfig with repository + err = h.repo.Delete(context.TODO(), uint(id)) + if err != nil { + return nil, errors.Wrap(err, "failed to deleting systemConfig with repository") + } + + // Return deleted systemConfig + return nil, nil +} + +// @Summary Update system config +// @Description Update the specified system config +// @Accept json +// @Produce json +// @Param system config body UpdateSystemConfigRequest true "Updated system config" +// @Success 200 {object} entity.SystemConfig "Success" +// @Failure 400 {object} errors.DetailError "Bad Request" +// @Failure 401 {object} errors.DetailError "Unauthorized" +// @Failure 429 {object} errors.DetailError "Too Many Requests" +// @Failure 404 {object} errors.DetailError "Not Found" +// @Failure 500 {object} errors.DetailError "Internal Server Error" +// @Router /api/v1/systemconfig [put] +func (h *Handler) UpdateSystemConfig(c *gin.Context, log logrus.FieldLogger) (any, error) { + // Parse payload from requestPayload + var requestPayload UpdateSystemConfigRequest + if err := c.ShouldBindJSON(&requestPayload); err != nil { + return nil, errcode.ErrDeserializedParams.Causewf(err, "failed to decode json") + } + log.Infof("Request payload: %v", kdump.FormatN(requestPayload)) + + // Convert request payload to domain model + var requestEntity entity.SystemConfig + if err := copier.Copy(&requestEntity, &requestPayload); err != nil { + return nil, errors.Wrap(err, "failed to convert request payload to domain model") + } + + // Get the existed systemConfig by id + updatedEntity, err := h.repo.Get(context.TODO(), requestEntity.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errcode.NotFound.Causewf(err, "failed to update system config") + } + return nil, errcode.InvalidParams.Cause(err) + } + + // Overwrite non-zero values in request entity to existed entity + copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) + + // Update systemConfig with repository + err = h.repo.Update(context.TODO(), updatedEntity) + if err != nil { + return nil, errors.Wrap(err, "failed to updating systemConfig with repository") + } + + // Return updated systemConfig + return updatedEntity, nil +} + +// @Summary Get system config +// @Description Get system config information by system config ID +// @Produce json +// @Param id path int true "SystemConfig ID" +// @Success 200 {object} entity.SystemConfig "Success" +// @Failure 400 {object} errors.DetailError "Bad Request" +// @Failure 401 {object} errors.DetailError "Unauthorized" +// @Failure 429 {object} errors.DetailError "Too Many Requests" +// @Failure 404 {object} errors.DetailError "Not Found" +// @Failure 500 {object} errors.DetailError "Internal Server Error" +// @Router /api/v1/systemconfig/{id} [get] +func (h *Handler) GetSystemConfig(c *gin.Context, log logrus.FieldLogger) (any, error) { + // Parse payload from request + paramID := c.Param("id") + log.Infof("Request params id: %s", paramID) + + // Get systemConfig with repository + id, err := strconv.Atoi(paramID) + if err != nil { + return nil, err + } + existedEntity, err := h.repo.Get(context.TODO(), uint(id)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errcode.NotFound.Causewf(err, "failed to get system config") + } + return nil, errors.Wrap(err, "failed to get systemConfig with repository") + } + + // Return systemConfig + return existedEntity, nil +} + +// @Summary Find system configs +// @Description Find system configs with query +// @Accept json +// @Produce json +// @Param query body QuerySystemConfigRequest true "query body" +// @Success 200 {object} entity.SystemConfig "Success" +// @Failure 400 {object} errors.DetailError "Bad Request" +// @Failure 401 {object} errors.DetailError "Unauthorized" +// @Failure 429 {object} errors.DetailError "Too Many Requests" +// @Failure 404 {object} errors.DetailError "Not Found" +// @Failure 500 {object} errors.DetailError "Internal Server Error" +// @Router /api/v1/systemconfigs [get] +func (h *Handler) FindSystemConfigs(c *gin.Context, log logrus.FieldLogger) (any, error) { + // Parse payload from request + var requestPayload QuerySystemConfigRequest + if err := c.ShouldBindJSON(&requestPayload); err != nil { + return nil, errcode.ErrDeserializedParams.Causewf(err, "failed to decode json") + } + log.Infof("Request payload: %v", kdump.FormatN(requestPayload)) + + // Calculate the limit and offset based on the pagination request + limit := requestPayload.PerPage + offset := (requestPayload.Page - 1) * requestPayload.PerPage + + // Find systemConfigs with repository + dataEntities, err := h.repo.Find(context.TODO(), repository.Query{ + Offset: offset, + Limit: limit, + Keyword: requestPayload.Keyword, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get all systemConfig with repository") + } + + // Return all systemConfig + return dataEntities, nil +} + +// @Summary Count system configs +// @Description Count the total number of system configs +// @Produce json +// @Success 200 {object} entity.SystemConfig "Success" +// @Failure 400 {object} errors.DetailError "Bad Request" +// @Failure 401 {object} errors.DetailError "Unauthorized" +// @Failure 429 {object} errors.DetailError "Too Many Requests" +// @Failure 404 {object} errors.DetailError "Not Found" +// @Failure 500 {object} errors.DetailError "Internal Server Error" +// @Router /api/v1/systemconfig/count [get] +func (h *Handler) CountSystemConfigs(c *gin.Context, log logrus.FieldLogger) (any, error) { + // Count systemConfigs with repository + total, err := h.repo.Count(context.TODO()) + if err != nil { + return nil, errors.Wrap(err, "failed to count systemConfig with repository") + } + + // Return total of all systemConfig + return CountSystemConfigResponse{ + Total: total, + }, nil +} diff --git a/pkg/handler/api/v1/systemconfig/request.go b/pkg/handler/api/v1/systemconfig/request.go new file mode 100644 index 0000000..f3e6d9e --- /dev/null +++ b/pkg/handler/api/v1/systemconfig/request.go @@ -0,0 +1,50 @@ +package systemconfig + +import "github.com/elliotxx/go-web-template/pkg/handler" + +// CreateSystemConfigRequest represents the create request structure for +// configuration of a system. +type CreateSystemConfigRequest struct { + // Tenant or organization that the system belongs to + Tenant string `json:"tenant" binding:"required"` + // Environment where the system is deployed (e.g. prod, gray) + Env string `json:"env" binding:"required"` + // Type or category of the system (e.g. cache, message queue) + Type string `json:"type" binding:"required"` + // Configuration data in JSON or YAML format + Config string `json:"config" binding:"required"` + // Description or purpose of the system + Description string `json:"description"` + // Username or ID of the user who created the system + Creator string `json:"creator" binding:"required"` + // Username or ID of the user who last modified the system + Modifier string `json:"modifier"` +} + +// UpdateSystemConfigRequest represents the update request structure for +// configuration of a system. +type UpdateSystemConfigRequest struct { + // Unique ID of the system + ID uint `json:"id" binding:"required"` + // Tenant or organization that the system belongs to + Tenant string `json:"tenant"` + // Environment where the system is deployed (e.g. prod, gray) + Env string `json:"env"` + // Type or category of the system (e.g. cache, message queue) + Type string `json:"type"` + // Configuration data in JSON or YAML format + Config string `json:"config"` + // Description or purpose of the system + Description string `json:"description"` + // Username or ID of the user who created the system + Creator string `json:"creator"` + // Username or ID of the user who last modified the system + Modifier string `json:"modifier"` +} + +// QuerySystemConfigRequest represents the query request structure for +// configuration of a system. +type QuerySystemConfigRequest struct { + handler.Pagination + handler.Search +} diff --git a/pkg/handler/api/v1/systemconfig/response.go b/pkg/handler/api/v1/systemconfig/response.go new file mode 100644 index 0000000..960e01c --- /dev/null +++ b/pkg/handler/api/v1/systemconfig/response.go @@ -0,0 +1,7 @@ +package systemconfig + +// CountSystemConfigResponse represents the count response structure for +// configuration of a system. +type CountSystemConfigResponse struct { + Total int `json:"total"` +} diff --git a/pkg/handler/debug/statsviz/statsviz_handler.go b/pkg/handler/debug/statsviz/statsviz_handler.go new file mode 100644 index 0000000..f85016d --- /dev/null +++ b/pkg/handler/debug/statsviz/statsviz_handler.go @@ -0,0 +1,14 @@ +package statsviz + +import ( + "github.com/arl/statsviz" + "github.com/gin-gonic/gin" +) + +func StatsvizHandler(context *gin.Context) { + if context.Param("filepath") == "/ws" { + statsviz.Ws(context.Writer, context.Request) + return + } + statsviz.IndexAtRoot("/debug/statsviz").ServeHTTP(context.Writer, context.Request) +} diff --git a/pkg/handler/endpoints/endpoints_handler.go b/pkg/handler/endpoints/endpoints_handler.go new file mode 100644 index 0000000..6928e21 --- /dev/null +++ b/pkg/handler/endpoints/endpoints_handler.go @@ -0,0 +1,59 @@ +package endpoints + +import ( + "fmt" + "net/http" + "sort" + "strings" + + "github.com/elliotxx/go-web-template/pkg/util/misc" + + "github.com/gin-gonic/gin" +) + +// NewEndpointsGETHandler returns a handler that lists all API +// endpoints for the GET method. +func NewEndpointsGETHandler(routes gin.RoutesInfo) gin.HandlerFunc { + return func(c *gin.Context) { + // endpointMethodPathPattern is the format string used to + // output each endpoint. + endpointMethodPathPattern := "%s\t%s" + + // Iterate over all the routes and add each endpoint to the + // endpoints slice. + endpoints := []string{} + for _, route := range routes { + endpoints = append( + endpoints, + fmt.Sprintf(endpointMethodPathPattern, route.Method, route.Path)) + } + + sort.Strings(endpoints) + c.String(http.StatusOK, strings.Join(endpoints, "\n")) + } +} + +// NewEndpointsOPTIONSHandler returns a handler that lists all API +// endpoints for the OPTIONS method. +func NewEndpointsOPTIONSHandler(routes gin.RoutesInfo) gin.HandlerFunc { + return func(c *gin.Context) { + // Iterate over all the routes and add each endpoint to the + // endpoints slice and add the HTTP method to the allow sets + // map. + allow := misc.NewSet() + endpoints := []string{} + for _, route := range routes { + allow.Set(route.Method) + endpoints = append(endpoints, route.Path) + } + + // Sort the endpoints in alphabetical orders. + sort.Strings(endpoints) + + // Set the response headers and status code. + c.Header("Allow", allow.String()) + c.Header("API-Endpoints", strings.Join(endpoints, ",")) + c.Header("Content-Length", "0") + c.Status(http.StatusOK) + } +} diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go new file mode 100644 index 0000000..8039d03 --- /dev/null +++ b/pkg/handler/handler.go @@ -0,0 +1,237 @@ +package handler + +import ( + "net/http" + "reflect" + "runtime" + "strings" + "time" + + "github.com/elliotxx/go-web-template/pkg/errcode" + "github.com/elliotxx/go-web-template/pkg/util/ctxutil" + "github.com/elliotxx/errors" + "github.com/gin-contrib/requestid" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// The Handler is an abstract of handle request +type Handler interface { + Validate(c *gin.Context) error + Handle(c *gin.Context, log logrus.FieldLogger) (any, error) +} + +// The HandlerFunc type is an adapter to allow the use of +// ordinary functions as HTTP handlers. If f is a function +// with the appropriate signature, HandlerFunc(f) is a +// Handler that calls f. +type HandlerFunc func(c *gin.Context, log logrus.FieldLogger) error + +// The HandlerDataFunc type is an adapter to allow the use of +// ordinary functions as HTTP handlers. If f is a function +// with the appropriate signature, HandlerDataFunc(f) is a +// Handler that calls f. +type HandlerDataFunc func(c *gin.Context, log logrus.FieldLogger) (any, error) + +// WrapF is a helper function for wrapping handler.HandlerFunc and returns a Gin middleware. +func WrapF(f HandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + // Create a logger for current request + log := getRequestLogger(c, f) + // Inject the logger to context + c.Request = c.Request.WithContext(ctxutil.WithLogger(log)) + + // Handle and calculate the cost time for request + handleRequest(c, log, f) + } +} + +// WrapFD is a helper function for wrapping handler.HandlerDataFunc and returns a Gin middleware. +func WrapFD(f HandlerDataFunc) gin.HandlerFunc { + return func(c *gin.Context) { + // Create a logger for current request + log := getRequestDataLogger(c, f) + // Inject the logger to context + c.Request = c.Request.WithContext(ctxutil.WithLogger(log)) + + // Handle and calculate the cost time for request + handleDataRequest(c, log, f) + } +} + +// WrapH is a helper function for wrapping handler.Handler and returns a Gin middleware. +func WrapH(h Handler) gin.HandlerFunc { + return func(c *gin.Context) { + // Create a logger for current request + log := getRequestDataLogger(c, h.Handle) + // Inject the logger to context + c.Request = c.Request.WithContext(ctxutil.WithLogger(log)) + + // Logging the request start message + loggingStartMsg(log) + + // Calculate the total cost time for request + requestStartTime := time.Now() + defer func() { + requestCostTime := time.Since(requestStartTime) + log.Debugf("Request total took [%v]", requestCostTime) + }() + + // Validate and calculate the cost time for request + if ok := validateRequest(c, log, h); ok { + // Handle and calculate the cost time for request + handleDataRequest(c, log, h.Handle) + } + } +} + +// Create a logger for current request +func getRequestDataLogger(c *gin.Context, f HandlerDataFunc) logrus.FieldLogger { + // Get handler name + fullName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() + fullNames := strings.Split(fullName, "/") + var name string + if len(fullNames) > 0 { + name = fullNames[len(fullNames)-1] + } else { + name = fullName + } + + // Create a logger with fields + return logrus.WithFields( + logrus.Fields{ + "traceID": requestid.Get(c), + "handler": name, + }, + ) +} + +// Create a logger for current request +func getRequestLogger(c *gin.Context, f HandlerFunc) logrus.FieldLogger { + // Get handler name + fullName := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() + fullNames := strings.Split(fullName, "/") + var name string + if len(fullNames) > 0 { + name = fullNames[len(fullNames)-1] + } else { + name = fullName + } + + // Create a logger with fields + return logrus.WithFields( + logrus.Fields{ + "traceID": requestid.Get(c), + "handler": name, + }, + ) +} + +func loggingStartMsg(log logrus.FieldLogger) { + log.Debug("Start processing request ...") +} + +// Validate and calculate the cost time for current request +func validateRequest(c *gin.Context, log logrus.FieldLogger, h Handler) bool { + log.Debug("Start validating request ...") + validStartTime := time.Now() + err := h.Validate(c) + validCostTime := time.Since(validStartTime) + log.Debugf("Validate request took [%v]", validCostTime) + + if err != nil { + log.Errorf("Failed to validate request: %v", err) + response := Response{ + Success: false, + TraceID: requestid.Get(c), + } + switch e := err.(type) { + case errors.DetailError: + response.Code = e.GetCode() + response.Message = errors.Wrap(e.GetCause(), e.GetMsg()).Error() + c.AbortWithStatusJSON(errcode.StatusCode(e), response) + case errors.ErrorCode: + response.Code = e.GetCode() + response.Message = e.GetMsg() + c.AbortWithStatusJSON(errcode.StatusCode(e), response) + default: + response.Code = errcode.InvalidParams.GetCode() + response.Message = e.Error() + c.AbortWithStatusJSON(http.StatusBadRequest, response) + } + return false + } + + return true +} + +// Handle and calculate the cost time for current request +func handleDataRequest(c *gin.Context, log logrus.FieldLogger, f HandlerDataFunc) { + log.Debug("Start handling request ...") + startTime := time.Now() + data, err := f(c, log) + endTime := time.Now() + costTime := endTime.Sub(startTime) + log.Debugf("Handle request took [%v]", costTime) + + if c.IsAborted() { + return + } + + response := Response{ + TraceID: requestid.Get(c), + StartTime: startTime, + EndTime: endTime, + CostTime: Duration(costTime), + } + + if err == nil { + response.Success = true + response.Message = http.StatusText(http.StatusOK) + response.Code = errcode.Success.GetCode() + response.Data = data + c.AbortWithStatusJSON(http.StatusOK, response) + } else { + log.Errorf("Failed to handle request: %+v", err) + + response.Success = false + switch e := err.(type) { + case errors.DetailError: + response.Code = e.GetCode() + response.Message = errors.Wrap(e.GetCause(), e.GetMsg()).Error() + c.AbortWithStatusJSON(errcode.StatusCode(e), response) + case errors.ErrorCode: + response.Code = e.GetCode() + response.Message = e.GetMsg() + c.AbortWithStatusJSON(errcode.StatusCode(e), response) + default: + response.Code = errcode.InternalError.GetCode() + response.Message = e.Error() + c.AbortWithStatusJSON(http.StatusInternalServerError, response) + } + } +} + +// Handle and calculate the cost time for current request +func handleRequest(c *gin.Context, log logrus.FieldLogger, f HandlerFunc) { + log.Debug("Start handling request ...") + handleStartTime := time.Now() + err := f(c, log) + handleCostTime := time.Since(handleStartTime) + log.Debugf("Handle request took [%v]", handleCostTime) + + if err != nil { + log.Errorf("Failed to handle request: %+v", err) + switch e := err.(type) { + case errors.DetailError: + c.JSON(errcode.StatusCode(e), gin.H{"code": e.GetCode(), "msg": e.GetMsg(), "cause": e.GetCause().Error()}) + case errors.ErrorCode: + c.JSON(errcode.StatusCode(e), gin.H{"code": e.GetCode(), "msg": e.GetMsg()}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"code": errcode.InternalError.GetCode(), "cause": e.Error()}) + } + return + } + + c.AbortWithStatus(http.StatusOK) +} diff --git a/pkg/handler/healthz/checks.go b/pkg/handler/healthz/checks.go new file mode 100644 index 0000000..e72c3a0 --- /dev/null +++ b/pkg/handler/healthz/checks.go @@ -0,0 +1,36 @@ +package healthz + +import ( + "github.com/elliotxx/healthcheck/checks" + "gorm.io/gorm" +) + +// gormDBCheck is a check that returns true if the database is +// available. +type gormDBCheck struct { + db *gorm.DB +} + +func NewGormDBCheck(db *gorm.DB) checks.Check { + return &gormDBCheck{ + db: db, + } +} + +func (c *gormDBCheck) Name() string { + return "Database" +} + +func (c *gormDBCheck) Pass() bool { + if c.db == nil { + return false + } + + sqldb, err := c.db.DB() + if err != nil { + return false + } + + sqlCheck := checks.NewSQLCheck(sqldb) + return sqlCheck.Pass() +} diff --git a/pkg/handler/healthz/healthz_handler.go b/pkg/handler/healthz/healthz_handler.go new file mode 100644 index 0000000..71de4ce --- /dev/null +++ b/pkg/handler/healthz/healthz_handler.go @@ -0,0 +1,46 @@ +package healthz + +import ( + "github.com/elliotxx/healthcheck" + "github.com/elliotxx/healthcheck/checks" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// Register registers the livez and readyz handlers to the specified +// router. +func Register(r *gin.RouterGroup, db *gorm.DB) { + r.GET("/livez", NewLivezHandler()) + r.GET("/readyz", NewReadyzHandler(db)) +} + +// NewLivezHandler creates a new liveness check handler that can be +// used to check if the application is running. +func NewLivezHandler() gin.HandlerFunc { + conf := healthcheck.HandlerConfig{ + Verbose: false, + // checkList is a list of healthcheck to run. + Checks: []checks.Check{ + checks.NewPingCheck(), + }, + FailureNotification: healthcheck.FailureNotification{Threshold: 1}, + } + + return healthcheck.NewHandler(conf) +} + +// NewReadyzHandler creates a new readiness check handler that can be +// used to check if the application is ready to serve traffic. +func NewReadyzHandler(db *gorm.DB) gin.HandlerFunc { + conf := healthcheck.HandlerConfig{ + Verbose: true, + // checkList is a list of healthcheck to run. + Checks: []checks.Check{ + checks.NewPingCheck(), + NewGormDBCheck(db), + }, + FailureNotification: healthcheck.FailureNotification{Threshold: 1}, + } + + return healthcheck.NewHandler(conf) +} diff --git a/pkg/handler/response.go b/pkg/handler/response.go new file mode 100644 index 0000000..36a8e09 --- /dev/null +++ b/pkg/handler/response.go @@ -0,0 +1,23 @@ +package handler + +import ( + "fmt" + "time" +) + +type Response struct { + Success bool `json:"success" yaml:"success"` + Code string `json:"code" yaml:"code"` + Message string `json:"message" yaml:"message"` + Data any `json:"data,omitempty" yaml:"data,omitempty"` + TraceID string `json:"traceID,omitempty" yaml:"traceID,omitempty"` + StartTime time.Time `json:"startTime,omitempty" yaml:"startTime,omitempty"` + EndTime time.Time `json:"endTime,omitempty" yaml:"endTime,omitempty"` + CostTime Duration `json:"costTime,omitempty" yaml:"costTime,omitempty"` +} + +type Duration time.Duration + +func (d Duration) MarshalJSON() (b []byte, err error) { + return []byte(fmt.Sprintf(`"%s"`, (time.Duration(d)).String())), nil +} diff --git a/pkg/handler/types.go b/pkg/handler/types.go new file mode 100644 index 0000000..e06a99c --- /dev/null +++ b/pkg/handler/types.go @@ -0,0 +1,18 @@ +package handler + +// Pagination represents the pagination parameters for a request. +type Pagination struct { + // Page is the page number, starting from 1. + // Required: true, Minimum value: 1 + Page int `json:"page" binding:"required,gte=1"` + // PerPage is the number of items per page. + // Required: true, Minimum value: 1, Maximum value: 300 + PerPage int `json:"perPage" binding:"required,gte=1,lte=300"` +} + +// Search represents the search criteria for a request. +type Search struct { + // Keyword is the keyword to search for. + // Optional: true + Keyword string `json:"keyword,omitempty"` +} diff --git a/pkg/infrastructure/persistence/db_util.go b/pkg/infrastructure/persistence/db_util.go new file mode 100644 index 0000000..c9fc397 --- /dev/null +++ b/pkg/infrastructure/persistence/db_util.go @@ -0,0 +1,88 @@ +package persistence + +import ( + "database/sql/driver" + "fmt" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +// MultiString is a custom type for handling arrays of strings with GORM. +type MultiString []string + +// Scan implements the Scanner interface for the MultiString type. +func (s *MultiString) Scan(src any) error { + switch src := src.(type) { + case []byte: + *s = strings.Split(string(src), ",") + case string: + *s = strings.Split(src, ",") + case nil: + *s = nil + default: + return fmt.Errorf("unsupported type %T", src) + } + return nil +} + +// Value implements the Valuer interface for the MultiString type. +func (s MultiString) Value() (driver.Value, error) { + if s == nil { + return nil, nil + } + return strings.Join(s, ","), nil +} + +// GormDataType gorm common data type +func (s MultiString) GormDataType() string { + return "text" +} + +// GormDBDataType gorm db data type +func (s MultiString) GormDBDataType(db *gorm.DB, field *schema.Field) string { + // returns different database type based on driver name + switch db.Dialector.Name() { + case "mysql", "sqlite": + return "text" + } + return "" +} + +// Create a mock database connection +func GetMockDB() (*gorm.DB, sqlmock.Sqlmock, error) { + // Create a sqlMock of sql.DB. + fakeDB, sqlMock, err := sqlmock.New() + if err != nil { + return nil, nil, err + } + + // common execution for orm + sqlMock.ExpectQuery("SELECT VERSION()").WillReturnRows(sqlmock.NewRows( + []string{"VERSION()"}).AddRow("5.7.35-log")) + + // Create the gorm database connection with fake db + fakeGDB, err := gorm.Open(mysql.New(mysql.Config{ + Conn: fakeDB, + SkipInitializeWithVersion: false, + }), &gorm.Config{ + SkipDefaultTransaction: true, + }) + if err != nil { + return nil, nil, err + } + + return fakeGDB, sqlMock, nil +} + +// Close the gorm database connection +func CloseDB(t *testing.T, gdb *gorm.DB) { + db, err := gdb.DB() + require.NoError(t, err) + require.NoError(t, db.Close()) +} diff --git a/pkg/infrastructure/persistence/system_config_model.go b/pkg/infrastructure/persistence/system_config_model.go new file mode 100644 index 0000000..38f6704 --- /dev/null +++ b/pkg/infrastructure/persistence/system_config_model.go @@ -0,0 +1,69 @@ +package persistence + +import ( + "github.com/elliotxx/errors" + "github.com/elliotxx/go-web-template/pkg/domain/entity" + "gorm.io/gorm" +) + +// SystemConfigModel is a DO used to map the entity to the database. +type SystemConfigModel struct { + gorm.Model + Tenant string + Env string + Type string + Config string + Description string + Creator string + Modifier string +} + +// The TableName method returns the name of the database table that the struct is mapped to. +func (m *SystemConfigModel) TableName() string { + return "system_config" +} + +// ToEntity converts the DO to an entity. +func (m *SystemConfigModel) ToEntity() (*entity.SystemConfig, error) { + if m == nil { + return nil, ErrSystemConfigModelNil + } + + env, err := entity.ParseEnv(m.Env) + if err != nil { + return nil, errors.Wrap(err, "failed to parse env") + } + + return &entity.SystemConfig{ + ID: m.ID, + Tenant: m.Tenant, + Env: env, + Type: m.Type, + Config: m.Config, + Description: m.Description, + Creator: m.Creator, + Modifier: m.Modifier, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + }, nil +} + +// FromEntity converts an entity to a DO. +func (m *SystemConfigModel) FromEntity(e *entity.SystemConfig) error { + if m == nil { + return ErrSystemConfigModelNil + } + + m.ID = e.ID + m.Tenant = e.Tenant + m.Env = string(e.Env) + m.Type = e.Type + m.Config = e.Config + m.Description = e.Description + m.Creator = e.Creator + m.Modifier = e.Modifier + m.CreatedAt = e.CreatedAt + m.UpdatedAt = e.UpdatedAt + + return nil +} diff --git a/pkg/infrastructure/persistence/system_config_repository.go b/pkg/infrastructure/persistence/system_config_repository.go new file mode 100644 index 0000000..e828e21 --- /dev/null +++ b/pkg/infrastructure/persistence/system_config_repository.go @@ -0,0 +1,128 @@ +package persistence + +import ( + "context" + + "github.com/elliotxx/go-web-template/pkg/domain/entity" + "github.com/elliotxx/go-web-template/pkg/domain/repository" + "github.com/elliotxx/errors" + "gorm.io/gorm" +) + +// The systemConfigRepository type implements the repository.SystemConfigRepository interface. +// If the systemConfigRepository type does not implement all the methods of the interface, +// the compiler will produce an error. +var _ repository.SystemConfigRepository = &systemConfigRepository{} + +// systemConfigRepository is a repository that stores systemConfigs in a gorm database. +type systemConfigRepository struct { + // db is the underlying gorm database where systemConfigs are stored. + db *gorm.DB +} + +// NewSystemConfigRepository creates a new systemConfig repository. +func NewSystemConfigRepository(db *gorm.DB) repository.SystemConfigRepository { + return &systemConfigRepository{db: db} +} + +// Create saves a system config to the repository. +func (r *systemConfigRepository) Create(ctx context.Context, dataEntity *entity.SystemConfig) error { + err := dataEntity.Validate() + if err != nil { + return err + } + + // Map the data from Entity to DO + var dataModel SystemConfigModel + err = dataModel.FromEntity(dataEntity) + if err != nil { + return err + } + + return r.db.Transaction(func(tx *gorm.DB) error { + // Create new record in the store + err = tx.WithContext(ctx).Create(&dataModel).Error + if err != nil { + return err + } + + // Map fresh record's data into Entity + newEntity, err := dataModel.ToEntity() + if err != nil { + return err + } + *dataEntity = *newEntity + + return nil + }) +} + +// Delete removes a system config from the repository. +func (r *systemConfigRepository) Delete(ctx context.Context, id uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + var dataModel SystemConfigModel + err := tx.WithContext(ctx).First(&dataModel, id).Error + if err != nil { + return err + } + + return tx.WithContext(ctx).Delete(&dataModel).Error + }) +} + +// Update updates an existing system config in the repository. +func (r *systemConfigRepository) Update(ctx context.Context, dataEntity *entity.SystemConfig) error { + // Map the data from Entity to DO + var dataModel SystemConfigModel + err := dataModel.FromEntity(dataEntity) + if err != nil { + return err + } + + return r.db.WithContext(ctx).Updates(&dataModel).Error +} + +// Find retrieves a system config by its ID. +func (r *systemConfigRepository) Get(ctx context.Context, id uint) (*entity.SystemConfig, error) { + var dataModel SystemConfigModel + err := r.db.WithContext(ctx).First(&dataModel, id).Error + if err != nil { + return nil, err + } + + return dataModel.ToEntity() +} + +// Find returns a list of specified system configs in the repository. +func (r *systemConfigRepository) Find(ctx context.Context, query repository.Query) ([]*entity.SystemConfig, error) { + var systemConfigModels []*SystemConfigModel + if err := r.db.WithContext(ctx). + Where("config LIKE ?", "%"+query.Keyword+"%"). + Limit(query.Limit). + Offset(query.Offset). + Find(&systemConfigModels).Error; err != nil { + return nil, err + } + + systemConfigEntities := make([]*entity.SystemConfig, 0, len(systemConfigModels)) + for _, model := range systemConfigModels { + newEntity, err := model.ToEntity() + if err != nil { + return nil, errors.Wrapf(err, "failed to convert db model (ID: %d) to entity", model.ID) + } + + systemConfigEntities = append(systemConfigEntities, newEntity) + } + return systemConfigEntities, nil +} + +// Count returns the total of system configs. +func (r *systemConfigRepository) Count(ctx context.Context) (int, error) { + var total int64 + err := r.db.WithContext(ctx).Model(&SystemConfigModel{}).Count(&total).Error + if err != nil { + return 0, err + } + + return int(total), nil +} diff --git a/pkg/infrastructure/persistence/system_config_repository_test.go b/pkg/infrastructure/persistence/system_config_repository_test.go new file mode 100644 index 0000000..383ee6b --- /dev/null +++ b/pkg/infrastructure/persistence/system_config_repository_test.go @@ -0,0 +1,136 @@ +package persistence + +import ( + "context" + "testing" + + "github.com/elliotxx/go-web-template/pkg/domain/entity" + "github.com/elliotxx/go-web-template/pkg/domain/repository" + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func TestSystemConfigRepository(t *testing.T) { + t.Run("Create", func(t *testing.T) { + fakeGDB, sqlMock, err := GetMockDB() + require.NoError(t, err) + repo := NewSystemConfigRepository(fakeGDB) + defer CloseDB(t, fakeGDB) + defer sqlMock.ExpectClose() + + var ( + expectedID, expectedRows uint = 1, 1 + actual = entity.SystemConfig{Env: entity.EnvProd} + ) + sqlMock.ExpectBegin() + sqlMock.ExpectExec("INSERT"). + WillReturnResult(sqlmock.NewResult(int64(expectedID), int64(expectedRows))) + sqlMock.ExpectCommit() + err = repo.Create(context.Background(), &actual) + require.NoError(t, err) + require.Equal(t, expectedID, actual.ID) + }) + + t.Run("Delete existed record", func(t *testing.T) { + fakeGDB, sqlMock, err := GetMockDB() + require.NoError(t, err) + repo := NewSystemConfigRepository(fakeGDB) + defer CloseDB(t, fakeGDB) + defer sqlMock.ExpectClose() + + var expectedID, expectedRows uint = 1, 1 + sqlMock.ExpectBegin() + sqlMock.ExpectQuery("SELECT"). + WillReturnRows(sqlmock.NewRows([]string{"id"}). + AddRow(1)) + sqlMock.ExpectExec("UPDATE"). + WillReturnResult(sqlmock.NewResult(int64(expectedID), int64(expectedRows))) + sqlMock.ExpectCommit() + err = repo.Delete(context.Background(), expectedID) + require.NoError(t, err) + }) + + t.Run("Delete not existing record", func(t *testing.T) { + fakeGDB, sqlMock, err := GetMockDB() + require.NoError(t, err) + repo := NewSystemConfigRepository(fakeGDB) + defer CloseDB(t, fakeGDB) + defer sqlMock.ExpectClose() + + sqlMock.ExpectBegin() + sqlMock.ExpectQuery("SELECT"). + WillReturnRows(sqlmock.NewRows([]string{"id"})) + err = repo.Delete(context.Background(), 1) + require.ErrorIs(t, err, gorm.ErrRecordNotFound) + }) + + t.Run("Update existed record", func(t *testing.T) { + fakeGDB, sqlMock, err := GetMockDB() + require.NoError(t, err) + repo := NewSystemConfigRepository(fakeGDB) + + var ( + expectedID, expectedRows uint = 1, 1 + actual = entity.SystemConfig{ + ID: 1, + Env: entity.EnvProd, + } + ) + sqlMock.ExpectExec("UPDATE"). + WillReturnResult(sqlmock.NewResult(int64(expectedID), int64(expectedRows))) + err = repo.Update(context.Background(), &actual) + require.NoError(t, err) + }) + + t.Run("Update not existing record", func(t *testing.T) { + fakeGDB, sqlMock, err := GetMockDB() + require.NoError(t, err) + repo := NewSystemConfigRepository(fakeGDB) + defer CloseDB(t, fakeGDB) + defer sqlMock.ExpectClose() + + actual := entity.SystemConfig{Env: entity.EnvProd} + err = repo.Update(context.Background(), &actual) + require.ErrorIs(t, err, gorm.ErrMissingWhereClause) + }) + + t.Run("Get", func(t *testing.T) { + fakeGDB, sqlMock, err := GetMockDB() + require.NoError(t, err) + repo := NewSystemConfigRepository(fakeGDB) + defer CloseDB(t, fakeGDB) + defer sqlMock.ExpectClose() + + var ( + expectedID uint = 1 + expectedEnv = entity.EnvProd + ) + sqlMock.ExpectQuery("SELECT"). + WillReturnRows(sqlmock.NewRows([]string{"id", "env"}). + AddRow(expectedID, string(expectedEnv))) + actual, err := repo.Get(context.Background(), expectedID) + require.NoError(t, err) + require.Equal(t, expectedID, actual.ID) + require.Equal(t, expectedEnv, actual.Env) + }) + + t.Run("Find", func(t *testing.T) { + fakeGDB, sqlMock, err := GetMockDB() + require.NoError(t, err) + repo := NewSystemConfigRepository(fakeGDB) + defer CloseDB(t, fakeGDB) + defer sqlMock.ExpectClose() + + sqlMock.ExpectQuery("SELECT"). + WillReturnRows(sqlmock.NewRows([]string{"id", "env"}). + AddRow(1, "prod"). + AddRow(2, "dev")) + actuals, err := repo.Find(context.Background(), repository.Query{ + Offset: 1, + Limit: 10, + }) + require.NoError(t, err) + require.Equal(t, 2, len(actuals)) + }) +} diff --git a/pkg/infrastructure/persistence/types.go b/pkg/infrastructure/persistence/types.go new file mode 100644 index 0000000..9417385 --- /dev/null +++ b/pkg/infrastructure/persistence/types.go @@ -0,0 +1,5 @@ +package persistence + +import "github.com/elliotxx/errors" + +var ErrSystemConfigModelNil = errors.New("system config model can't be nil") diff --git a/pkg/route/route.go b/pkg/route/route.go new file mode 100644 index 0000000..03976e7 --- /dev/null +++ b/pkg/route/route.go @@ -0,0 +1,63 @@ +package route + +import ( + "github.com/elliotxx/expvar" + docs "github.com/elliotxx/go-web-template/api/openapispec" + "github.com/elliotxx/go-web-template/pkg/handler" + "github.com/elliotxx/go-web-template/pkg/handler/api/v1/systemconfig" + "github.com/elliotxx/go-web-template/pkg/handler/debug/statsviz" + "github.com/elliotxx/go-web-template/pkg/handler/endpoints" + "github.com/elliotxx/go-web-template/pkg/handler/healthz" + "github.com/elliotxx/go-web-template/pkg/infrastructure/persistence" + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + "gorm.io/gorm" +) + +type Route struct { + DB *gorm.DB +} + +// Register registers some api to the route +func (r *Route) Register(engine *gin.Engine) error { + // Create the workspace domain service + systemConfigHandler := systemconfig.NewHandler(persistence.NewSystemConfigRepository(r.DB)) + + // Registers some api to the route + docs.SwaggerInfo.BasePath = "/" + root := engine.Group("/") + { + root.GET("/livez", healthz.NewLivezHandler()) + root.GET("/readyz", healthz.NewReadyzHandler(r.DB)) + root.GET("/docs/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + } + debug := engine.Group("/debug") + { + // Add a expvar handler for gin framework, expvar provides + // a standardized interface to public variables. + // You can visit http://localhost/debug/vars to view all + // public variables. + debug.GET("/vars", expvar.Handler(expvar.WithFilters("memstats"))) + // Register statsviz handler + debug.GET("/statsviz/*filepath", statsviz.StatsvizHandler) + // The default pprof router is /debug/pprof + pprof.RouteRegister(debug, "/pprof") + } + apiv1 := engine.Group("/api/v1") + { + // Register system config handler + apiv1.POST("/systemconfig", handler.WrapFD(systemConfigHandler.CreateSystemConfig)) + apiv1.DELETE("/systemconfig/:id", handler.WrapFD(systemConfigHandler.DeleteSystemConfig)) + apiv1.PUT("/systemconfig", handler.WrapFD(systemConfigHandler.UpdateSystemConfig)) + apiv1.GET("/systemconfig/:id", handler.WrapFD(systemConfigHandler.GetSystemConfig)) + apiv1.GET("/systemconfigs", handler.WrapFD(systemConfigHandler.FindSystemConfigs)) + apiv1.GET("/systemconfig/count", handler.WrapFD(systemConfigHandler.CountSystemConfigs)) + } + + engine.GET("/endpoints", endpoints.NewEndpointsGETHandler(engine.Routes())) + engine.OPTIONS("/endpoints", endpoints.NewEndpointsOPTIONSHandler(engine.Routes())) + + return nil +} diff --git a/pkg/server/server.go b/pkg/server/server.go new file mode 100644 index 0000000..a69f76c --- /dev/null +++ b/pkg/server/server.go @@ -0,0 +1,119 @@ +package server + +import ( + "fmt" + "path/filepath" + "time" + + recovery "github.com/akkuman/gin-logrus-recovery" + "github.com/elliotxx/go-web-template/pkg/route" + "github.com/gin-contrib/cors" + "github.com/gin-contrib/gzip" + "github.com/gin-contrib/requestid" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "gopkg.in/natefinch/lumberjack.v2" + "gorm.io/gorm" +) + +type Config struct { + LoggingDirectory string + DB *gorm.DB +} + +func NewConfig() *Config { + return &Config{} +} + +type AppServer struct { + ginEngine *gin.Engine + route *route.Route +} + +// New creates a new AppServer instance from Config +func (c *Config) New() (*AppServer, error) { + // Initialize the gin engine and route + engine := NewGinEngine(c) + router := &route.Route{ + DB: c.DB, + } + err := router.Register(engine) + if err != nil { + return nil, err + } + + return &AppServer{ + ginEngine: engine, + route: router, + }, nil +} + +// PreRun is a function that will be called before the server starts to run +func (s *AppServer) PreRun() error { + _ = logrus.WithFields(logrus.Fields{"func": "PreRun"}) + + return nil +} + +// Run is a function that will be called when the server starts to run +func (s *AppServer) Run(addr ...string) error { + return s.ginEngine.Run(addr...) +} + +// NewGinEngine creates a new GinEngine instance +func NewGinEngine(c *Config) *gin.Engine { + // Create the audit writer by lumberjack + auditLoggingFile := filepath.Join(c.LoggingDirectory, "audit.log") + auditRotateWriter := &lumberjack.Logger{ + Filename: auditLoggingFile, + MaxSize: 1, + MaxBackups: 10, + MaxAge: 30, + } + + // Set global config for gin + gin.DefaultWriter = auditRotateWriter + gin.DefaultErrorWriter = auditRotateWriter + + // Create a gin router + r := gin.New() + + // Use some middlewares + r.Use(requestid.New()) + r.Use(gzip.Gzip(gzip.DefaultCompression)) + // NOTE: cors.Default() allows all origins + r.Use(cors.Default()) + r.Use(gin.LoggerWithConfig(gin.LoggerConfig{ + Formatter: CustomLogFormatter, + Output: auditRotateWriter, + })) + r.Use(recovery.Recovery(logrus.StandardLogger())) + + return r +} + +// CustomLogFormatter is a custom formatter for logging messages +func CustomLogFormatter(param gin.LogFormatterParams) string { + // Custom format: + // [GIN] | 200 | 14.701µs | 127.0.0.1 | GET "/healthz" + var statusColor, methodColor, resetColor string + if param.IsOutputColor() { + statusColor = param.StatusCodeColor() + methodColor = param.MethodColor() + resetColor = param.ResetColor() + } + + if param.Latency > time.Minute { + // Truncate in a golang < 1.8 safe way + param.Latency -= param.Latency % time.Second + } + return fmt.Sprintf("[GIN] | %s | %s%3d%s | %13v | %15s | %s%-7s%s | %#v\n%s", + param.TimeStamp.Format("2006-1-2 15:04:05.000"), + statusColor, param.StatusCode, resetColor, + param.Latency, + param.ClientIP, + methodColor, param.Method, resetColor, + param.Path, + param.ErrorMessage, + ) +} diff --git a/pkg/server/types.go b/pkg/server/types.go new file mode 100644 index 0000000..7fd34d7 --- /dev/null +++ b/pkg/server/types.go @@ -0,0 +1,6 @@ +package server + +type Server interface { + PreRun() error + Run() error +} diff --git a/pkg/util/cmdutil/helpers.go b/pkg/util/cmdutil/helpers.go new file mode 100644 index 0000000..422bd1e --- /dev/null +++ b/pkg/util/cmdutil/helpers.go @@ -0,0 +1,61 @@ +package cmdutil + +import ( + "os" + "strings" + + "github.com/elliotxx/go-web-template/pkg/util/pretty" + "github.com/hashicorp/go-multierror" + "github.com/pkg/errors" +) + +const ( + DefaultErrorExitCode = 1 +) + +var fatalErrHandler = fatal + +// fatal prints the message (if provided) and then exits. +func fatal(msg string, code int) { + if len(msg) > 0 { + // add newline if needed + if !strings.HasSuffix(msg, "\n") { + msg += "\n" + } + pretty.ErrorT.WithWriter(os.Stderr).Print(msg) + } + os.Exit(code) +} + +// ErrExit may be passed to CheckError to instruct it to output nothing but exit with +// status code 1. +var ErrExit = errors.Errorf("exit") + +// CheckErr prints a user friendly error to STDERR and exits with a non-zero +// exit code. Unrecognized errors will be printed with an "error: " prefix. +// +// This method is generic to the command in use and may be used by non-Kubectl +// commands. +func CheckErr(err error) { + checkErr(err, fatalErrHandler) +} + +// checkErr formats a given error as a string and calls the passed handleErr +// func with that string and an kubectl exit code. +func checkErr(err error, handleErr func(string, int)) { + // flatten errors + if merr, ok := err.(*multierror.Error); ok { + err = multierror.Flatten(merr.ErrorOrNil()) + } + + if err == nil { + return + } + + switch { + case err == ErrExit: + handleErr("", DefaultErrorExitCode) + default: + handleErr(err.Error(), DefaultErrorExitCode) + } +} diff --git a/pkg/util/configutil/config_loader.go b/pkg/util/configutil/config_loader.go new file mode 100644 index 0000000..cda7a09 --- /dev/null +++ b/pkg/util/configutil/config_loader.go @@ -0,0 +1,51 @@ +package configutil + +import ( + "path/filepath" + "strings" + + "github.com/elliotxx/go-web-template/third_party/metadecoders" + "github.com/spf13/afero" +) + +var ( + ValidConfigFileExtensions = []string{"toml", "yaml", "yml", "json"} + validConfigFileExtensionsMap map[string]bool = make(map[string]bool) +) + +func init() { + for _, ext := range ValidConfigFileExtensions { + validConfigFileExtensionsMap[ext] = true + } +} + +// IsValidConfigFilename returns whether filename is one of the supported +// config formats in Hugo. +func IsValidConfigFilename(filename string) bool { + return validConfigFileExtensionsMap[GetFileExtension(filename)] +} + +// FromFile loads the configuration from the given filename. +func FromFile(fs afero.Fs, filename string, data any) error { + content, err := afero.ReadFile(fs, filename) + if err != nil { + return err + } + + err = FromConfigString(string(content), GetFileExtension(filename), data) + if err != nil { + return err + } + + return nil +} + +// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests. +func FromConfigString(config, configType string, data any) error { + return metadecoders.Default.UnmarshalTo([]byte(config), metadecoders.FormatFromString(configType), data) +} + +// GetFileExtension return the extension of specfied filename +func GetFileExtension(filename string) string { + return strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), ".")) +} diff --git a/pkg/util/ctxutil/ctxutil.go b/pkg/util/ctxutil/ctxutil.go new file mode 100644 index 0000000..d55a0eb --- /dev/null +++ b/pkg/util/ctxutil/ctxutil.go @@ -0,0 +1,44 @@ +package ctxutil + +import ( + "context" + + "github.com/sirupsen/logrus" +) + +type ContextKey string + +const ( + ContextKeyLogger ContextKey = "logger" +) + +// GetLogger returns the logger from the given context. +// +// Example: +// +// logger := ctxutil.GetLogger(ctx) +func GetLogger(ctx context.Context) logrus.FieldLogger { + if logger, ok := ctx.Value(ContextKeyLogger).(logrus.FieldLogger); ok { + return logger + } + + return logrus.New() +} + +// WithLogger returns a context by the TODO context and the given logger. +// +// Example: +// +// ctx = ctxutil.WithLogger(logger) +func WithLogger(logger logrus.FieldLogger) context.Context { + return context.WithValue(context.TODO(), ContextKeyLogger, logger) +} + +// CtxWithLogger returns a context by the parent context and the given logger. +// +// Example: +// +// ctx = ctxutil.CtxWithLogger(ctx, logger) +func CtxWithLogger(ctx context.Context, logger logrus.FieldLogger) context.Context { + return context.WithValue(ctx, ContextKeyLogger, logger) +} diff --git a/pkg/util/ctxutil/ctxutil_test.go b/pkg/util/ctxutil/ctxutil_test.go new file mode 100644 index 0000000..c11374c --- /dev/null +++ b/pkg/util/ctxutil/ctxutil_test.go @@ -0,0 +1,70 @@ +package ctxutil + +import ( + "context" + "fmt" + "testing" + + "github.com/sirupsen/logrus" +) + +func TestGetLogger(t *testing.T) { + type args struct { + ctx context.Context + } + isWantedLogger := func(got any) bool { + return got != nil + } + tests := []struct { + name string + args args + wantFunc func(want any) bool + }{ + { + name: "successful-get-logger-from-context", + args: args{ + ctx: context.WithValue(context.TODO(), ContextKeyLogger, logrus.WithError(fmt.Errorf("for test"))), + }, + wantFunc: isWantedLogger, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetLogger(tt.args.ctx); !isWantedLogger(got) { + t.Errorf("GetLogger() = %v, is not the desired value", got) + } + }) + } +} + +func TestWithLogger(t *testing.T) { + type args struct { + logger logrus.FieldLogger + } + isWantedContext := func(got context.Context) bool { + if _, ok := got.Value(ContextKeyLogger).(logrus.FieldLogger); ok { + return true + } + return false + } + tests := []struct { + name string + args args + wantFunc func(got context.Context) bool + }{ + { + name: "successful-create-new-context-using-logger", + args: args{ + logger: logrus.WithError(fmt.Errorf("for test")), + }, + wantFunc: isWantedContext, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := WithLogger(tt.args.logger); !isWantedContext(got) { + t.Errorf("GetLogger() = %v, is not the desired value", got) + } + }) + } +} diff --git a/pkg/util/gitutil/gitutil.go b/pkg/util/gitutil/gitutil.go new file mode 100644 index 0000000..781bb45 --- /dev/null +++ b/pkg/util/gitutil/gitutil.go @@ -0,0 +1,228 @@ +package gitutil + +import ( + "os/exec" + "strings" + + "github.com/pkg/errors" +) + +// https://git-scm.com/docs/git-tag +// https://github.com/vivin/better-setuptools-git-version/blob/master/better_setuptools_git_version.py + +var ErrEmptyGitTag = errors.New("empty tag") + +// get remote url +func GetRemoteURL() (string, error) { + stdout, err := exec.Command( + "git", "config", "--get", "remote.origin.url", + ).CombinedOutput() + if err != nil { + return "", err + } + return strings.TrimSpace(string(stdout)), nil +} + +func GetLatestTag() (string, error) { + tag, err := GetLatestTagFromLocal() + if tag == "" || err != nil { + return GetLatestTagFromRemote() + } + return tag, nil +} + +// get latest tag from remote, +// the fitting git clone depth is 1 +func GetLatestTagFromRemote() (tag string, err error) { + // get remote url + remoteURL, err := GetRemoteURL() + if err != nil { + return "", err + } + + // get latest tag from remote + stdout, err := exec.Command( + `bash`, `-c`, `git ls-remote --tags --sort=v:refname `+remoteURL+` | tail -n1 | sed 's/.*\///; s/\^{}//'`, + ).CombinedOutput() + if err != nil { + return "", err + } + return strings.TrimSpace(string(stdout)), nil +} + +func GetLatestTagFromLocal() (tag string, err error) { + tags, err := GetTagList() + if err != nil { + return "", err + } + if len(tags) > 0 { + tag = tags[len(tags)-1] + } + return strings.TrimSpace(tag), nil +} + +func GetTagList() (tags []string, err error) { + // git tag --merged + stdout, err := exec.Command( + `git`, `describe`, `--abbrev=0`, `--tags`, + ).CombinedOutput() + if err != nil { + return nil, err + } + + for _, s := range strings.Split(strings.TrimSpace(string(stdout)), "\n") { + if s := strings.TrimSpace(s); s != "" { + tags = append(tags, s) + } + } + return +} + +func GetTagListFromRemote(remoteURL string, reverse bool) (tags []string, err error) { + tmpTags := []string{} + // Get all tags from remote + stdout, err := exec.Command( + `bash`, `-c`, `git ls-remote --tags --sort=v:refname `+remoteURL+` | sed 's/.*\///; s/\^{}//'`, + ).CombinedOutput() + if err != nil { + return nil, err + } + + for _, s := range strings.Split(strings.TrimSpace(string(stdout)), "\n") { + if s := strings.TrimSpace(s); s != "" { + tmpTags = append(tmpTags, s) + } + } + + // Reverse slice + if reverse { + for i, j := 0, len(tmpTags)-1; i < j; i, j = i+1, j-1 { + tmpTags[i], tmpTags[j] = tmpTags[j], tmpTags[i] + } + } + + // Remove duplicates + tagSet := make(map[string]struct{}) + for _, tag := range tmpTags { + if _, ok := tagSet[tag]; !ok { + tags = append(tags, tag) + tagSet[tag] = struct{}{} + } + } + return +} + +func GetHeadHash() (sha string, err error) { + // git rev-parse HEAD + stdout, err := exec.Command( + `git`, `rev-parse`, `HEAD`, + ).CombinedOutput() + if err != nil { + return "", err + } + + sha = strings.TrimSpace(string(stdout)) + return +} + +func GetHeadHashShort() (sha string, err error) { + sha, err = GetHeadHash() + if err != nil { + return "", err + } + if len(sha) > 8 { + sha = sha[:8] + } + return +} + +func GetTagCommitSha(tag string) (sha string, err error) { + if tag == "" { + return "", ErrEmptyGitTag + } + + sha, err = GetTagCommitShaFromLocal(tag) + if sha == "" || err != nil { + return GetTagCommitShaFromRemote(tag) + } + return +} + +func GetTagCommitShaFromLocal(tag string) (sha string, err error) { + // git rev-list -n 1 {tag} + stdout, err := exec.Command( + `git`, `rev-list`, `-n`, `1`, tag, + ).CombinedOutput() + if err != nil { + return "", err + } + var lines []string + for _, s := range strings.Split(strings.TrimSpace(string(stdout)), "\n") { + if s := strings.TrimSpace(s); s != "" { + lines = append(lines, s) + } + } + if len(lines) > 0 { + sha = lines[len(lines)-1] + } + return strings.TrimSpace(sha), nil +} + +// get tag commit sha from remote, +// the fitting git clone depth is 1 +func GetTagCommitShaFromRemote(_ string) (string, error) { + // get remote url + remoteURL, err := GetRemoteURL() + if err != nil { + return "", err + } + + stdout, err := exec.Command( + `bash`, `-c`, `git ls-remote --tags --sort=v:refname `+remoteURL+` | tail -n1 | awk '{print $1}'`, + ).CombinedOutput() + if err != nil { + return "", err + } + + return strings.TrimSpace(string(stdout)), nil +} + +func IsHeadAtTag(tag string) (bool, error) { + if tag == "" { + return false, ErrEmptyGitTag + } + sha1, err1 := GetTagCommitSha(tag) + if err1 != nil { + return false, err1 + } + sha2, err2 := GetHeadHash() + if err2 != nil { + return false, err2 + } + return sha1 == sha2, nil +} + +func IsDirty() (dirty bool, err error) { + // git status -s + stdout, err := exec.Command( + `git`, `status`, `-s`, + ).CombinedOutput() + if err != nil { + return false, err + } + + dirty = strings.TrimSpace(string(stdout)) != "" + return +} + +func GetCurrentBranch() (string, error) { + // git status -s + stdout, err := exec.Command( + `git`, `symbolic-ref`, `--short`, `-q`, `HEAD`, + ).CombinedOutput() + if err != nil { + return "", err + } + + return strings.TrimSpace(string(stdout)), nil +} diff --git a/pkg/util/gitutil/types.go b/pkg/util/gitutil/types.go new file mode 100644 index 0000000..ff60c5a --- /dev/null +++ b/pkg/util/gitutil/types.go @@ -0,0 +1,62 @@ +package gitutil + +import "os" + +// GitInfo contains git information. +type GitInfo struct { + CurrentBranch string `json:"currentBranch,omitempty" yaml:"currentBranch,omitempty"` // Such as "master" + HeadCommit string `json:"headCommit,omitempty" yaml:"headCommit,omitempty"` // Such as "3836f8770ab8f488356b2129f42f2ae5c1134bb0" + TreeState string `json:"treeState,omitempty" yaml:"treeState,omitempty"` // Such as "clean", "dirty" + LatestTag string `json:"latestTag,omitempty" yaml:"latestTag,omitempty"` // Such as "v1.2.3" +} + +// NewGitInfoFrom returns git info from workDir, or nil if failed. +func NewGitInfoFrom(workDir string) (*GitInfo, error) { + // Cd to workDir + oldWd, err := os.Getwd() + if err != nil { + return nil, err + } + + _ = os.Chdir(workDir) + defer os.Chdir(oldWd) + + // Get git info + var ( + curCommit string + curBranch string + latestTag string + isDirty bool + gitTreeState string + ) + + if curCommit, err = GetHeadHash(); err != nil { + return nil, err + } + + if curBranch, err = GetCurrentBranch(); err != nil { + return nil, err + } + + if latestTag, err = GetLatestTag(); err != nil { + return nil, err + } + + if isDirty, err = IsDirty(); err != nil { + return nil, err + } + + // Get git tree state + if isDirty { + gitTreeState = "dirty" + } else { + gitTreeState = "clean" + } + + return &GitInfo{ + LatestTag: latestTag, + CurrentBranch: curBranch, + HeadCommit: curCommit, + TreeState: gitTreeState, + }, nil +} diff --git a/pkg/util/kdump/kdump.go b/pkg/util/kdump/kdump.go new file mode 100644 index 0000000..46f3f60 --- /dev/null +++ b/pkg/util/kdump/kdump.go @@ -0,0 +1,51 @@ +// Package kdump like fmt.Println but more pretty and beautiful print Go values. +package kdump + +import ( + "io" + "os" + + "github.com/gookit/goutil/dump" +) + +var std = (KDumper)(*dump.Std()) + +func New() KDumper { + return (KDumper)(*dump.NewDumper(os.Stdout, 3)) +} + +// V like fmt.Println, but the output is clearer and more beautiful +func V(vs ...any) { + std.Dump(vs...) +} + +// P like fmt.Println, but the output is clearer and more beautiful +func P(vs ...any) { + std.Print(vs...) +} + +// Print like fmt.Println, but the output is clearer and more beautiful +func Print(vs ...any) { + std.Print(vs...) +} + +// Println like fmt.Println, but the output is clearer and more beautiful +func Println(vs ...any) { + std.Println(vs...) +} + +// Fprint like fmt.Println, but the output is clearer and more beautiful +func Fprint(w io.Writer, vs ...any) { + std.Fprint(w, vs...) +} + +// Format like fmt.Println, but the output is clearer and more beautiful +func Format(vs ...any) string { + return std.Format(vs...) +} + +// Custom method outside the original dump package +// FormatN like fmt.Println, but the output is clearer and no color +func FormatN(vs ...any) string { + return std.FormatN(vs...) +} diff --git a/pkg/util/kdump/types.go b/pkg/util/kdump/types.go new file mode 100644 index 0000000..470495e --- /dev/null +++ b/pkg/util/kdump/types.go @@ -0,0 +1,91 @@ +// Package kdump like fmt.Println but more pretty and beautiful print Go values. +package kdump + +import ( + "bytes" + "io" + + "github.com/gookit/goutil/dump" +) + +type KDumper dump.Dumper + +func (d KDumper) WithOutput(output io.Writer) KDumper { + d.Output = output + return d +} + +func (d KDumper) WithNoType() KDumper { + d.NoType = true + return d +} + +func (d KDumper) WithNoColor() KDumper { + d.NoColor = true + return d +} + +func (d KDumper) WithIndentLen(indentLen int) KDumper { + d.IndentLen = indentLen + return d +} + +func (d KDumper) WithIndentChar(char byte) KDumper { + d.IndentChar = char + return d +} + +func (d KDumper) WithMaxDepth(depth int) KDumper { + d.MaxDepth = depth + return d +} + +func (d KDumper) WithShowFlag(flag int) KDumper { + d.ShowFlag = flag + return d +} + +func (d KDumper) WithCallerSkip(callerSkip int) KDumper { + d.CallerSkip = callerSkip + return d +} + +func (d KDumper) WithColorTheme(theme dump.Theme) KDumper { + d.ColorTheme = theme + return d +} + +// Dump vars +func (d KDumper) Dump(vs ...any) { + (*dump.Dumper)(&d).Dump(vs...) +} + +// Print vars. alias of Dump() +func (d KDumper) Print(vs ...any) { + (*dump.Dumper)(&d).Print(vs...) +} + +// Println vars. alias of Dump() +func (d KDumper) Println(vs ...any) { + (*dump.Dumper)(&d).Println(vs...) +} + +// Fprint print vars to io.Writer +func (d KDumper) Fprint(w io.Writer, vs ...any) { + (*dump.Dumper)(&d).Fprint(w, vs...) +} + +// Format like fmt.Println, but the output is clearer and more beautiful +func (d KDumper) Format(vs ...any) string { + w := &bytes.Buffer{} + (*dump.Dumper)(&d).Fprint(w, vs...) + return w.String() +} + +// Custom method outside the original dump package +// FormatN like fmt.Println, but the output is clearer and no color +func (d KDumper) FormatN(vs ...any) string { + w := &bytes.Buffer{} + New().WithNoColor().Fprint(w, vs...) + return w.String() +} diff --git a/pkg/util/misc/set.go b/pkg/util/misc/set.go new file mode 100644 index 0000000..5e5fba7 --- /dev/null +++ b/pkg/util/misc/set.go @@ -0,0 +1,43 @@ +package misc + +import ( + "sort" + "strings" + "sync" +) + +// Set is a helper type for storing unique values in a map. +type Set struct { + mu *sync.Mutex + items map[string]struct{} +} + +// NewSet returns an empty sets map. +func NewSet() *Set { + return &Set{ + mu: &sync.Mutex{}, + items: map[string]struct{}{}, + } +} + +// Set adds a value to the sets maps. +func (st *Set) Set(key string) { + // st[key] = struct{}{} + st.mu.Lock() + defer st.mu.Unlock() + st.items[key] = struct{}{} +} + +// String returns the sets map as a comma-separated string. +func (st *Set) String() string { + st.mu.Lock() + defer st.mu.Unlock() + + s := []string{} + for k := range st.items { + s = append(s, k) + } + sort.Strings(s) + + return strings.Join(s, ",") +} diff --git a/pkg/util/pretty/prefix_printer.go b/pkg/util/pretty/prefix_printer.go new file mode 100644 index 0000000..6d5c1aa --- /dev/null +++ b/pkg/util/pretty/prefix_printer.go @@ -0,0 +1,87 @@ +package pretty + +import "github.com/pterm/pterm" + +// Pretty prefix printer style. +// +// Usage: +// +// pretty.Info.Println("Hello") +// pretty.Warning.Println("Hello") +var ( + // Info returns a PrefixPrinter, which can be used to print text with an "info" Prefix. + Info = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.InfoMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.InfoMessageStyle, + Text: "ℹ️", + }, + } + + // Warning returns a PrefixPrinter, which can be used to print text with a "warning" Prefix. + Warning = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.WarningMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.WarningMessageStyle, + Text: "❗", + }, + } + + // Success returns a PrefixPrinter, which can be used to print text with a "success" Prefix. + Success = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.SuccessMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.SuccessMessageStyle, + Text: "✅", + }, + } + + // Error returns a PrefixPrinter, which can be used to print text with an "error" Prefix. + Error = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.ErrorMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.ErrorMessageStyle, + Text: "❌", + }, + } + + // Fatal returns a PrefixPrinter, which can be used to print text with an "fatal" Prefix. + // NOTICE: Fatal terminates the application immediately! + Fatal = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalMessageStyle, + Text: "💣", + }, + Fatal: true, + } + + // FatalN returns a PrefixPrinter, which can be used to print text with an "fatal" Prefix. + FatalN = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalMessageStyle, + Text: "💣", + }, + } + + // Debug Prints debug messages. By default, it will only print if PrintDebugMessages is true. + // You can change PrintDebugMessages with EnableDebugMessages and DisableDebugMessages, or by setting the variable itself. + Debug = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.DebugMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.DebugMessageStyle, + Text: "⭕", + }, + Debugger: true, + } + + // Check returns a PrefixPrinter, which can be used to print text with a "mark check" Prefix. + Check = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.SuccessMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.SuccessMessageStyle, + Text: "✅", + }, + } +) diff --git a/pkg/util/pretty/prefix_printer_test.go b/pkg/util/pretty/prefix_printer_test.go new file mode 100644 index 0000000..b9e8438 --- /dev/null +++ b/pkg/util/pretty/prefix_printer_test.go @@ -0,0 +1,18 @@ +package pretty + +import ( + "fmt" + "testing" +) + +func TestPrefixPrinter(t *testing.T) { + fmt.Println("PrefixPrinter:") + defer fmt.Println("") + + Info.Println("This is a Info message") + Warning.Println("This is a Warning message") + Success.Println("This is a Success message") + Error.Println("This is a Error message") + FatalN.Println("This is a FatalN message") + Debug.Println("This is a Debug message") +} diff --git a/pkg/util/pretty/prefix_text_printer.go b/pkg/util/pretty/prefix_text_printer.go new file mode 100644 index 0000000..c5d5bc7 --- /dev/null +++ b/pkg/util/pretty/prefix_text_printer.go @@ -0,0 +1,87 @@ +package pretty + +import "github.com/pterm/pterm" + +// Pretty prefix text printer style. +// +// Usage: +// +// pretty.InfoT.Println("Hello") +// pretty.WarningT.Println("Hello") +var ( + // DebugT Prints debug messages. By default it will only print if PrintDebugMessages is true. + // You can change PrintDebugMessages with EnableDebugMessages and DisableDebugMessages, or by setting the variable itself. + DebugT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.DebugMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.DebugMessageStyle, + Text: "#", + }, + Debugger: true, + } + + // InfoT returns a PrefixPrinter, which can be used to print text with an "info" Prefix. + InfoT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.SuccessMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.SuccessMessageStyle, + Text: "»", + }, + } + + // WarningT returns a PrefixPrinter, which can be used to print text with a "warning" Prefix. + WarningT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.WarningMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.WarningMessageStyle, + Text: "!", + }, + } + + // ErrorT returns a PrefixPrinter, which can be used to print text with an "error" Prefix. + ErrorT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.ErrorMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.ErrorMessageStyle, + Text: "✘", + }, + } + + // FatalT returns a PrefixPrinter, which can be used to print text with an "fatal" Prefix. + // NOTICE: Fatal terminates the application immediately! + FatalT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalMessageStyle, + Text: "☒", + }, + Fatal: true, + } + + // FatalN returns a PrefixPrinter, which can be used to print text with an "fatal" Prefix. + FatalNT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalMessageStyle, + Text: "☒", + }, + } + + // SuccessT returns a PrefixPrinter, which can be used to print text with a "success" Prefix. + SuccessT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.SuccessMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.SuccessMessageStyle, + Text: "✔︎", + }, + } + + // CheckT returns a PrefixPrinter, which can be used to print text with a "mark check" Prefix. + CheckT = pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.SuccessMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.SuccessMessageStyle, + Text: "☑", + }, + } +) diff --git a/pkg/util/pretty/prefix_text_printer_test.go b/pkg/util/pretty/prefix_text_printer_test.go new file mode 100644 index 0000000..9afbf44 --- /dev/null +++ b/pkg/util/pretty/prefix_text_printer_test.go @@ -0,0 +1,26 @@ +package pretty + +import ( + "fmt" + "testing" + + "github.com/pterm/pterm" +) + +func TestPrefixTextPrinter(t *testing.T) { + fmt.Println("PrefixTextPrinter:") + defer fmt.Println("") + + // Preset prefix printer style. + InfoT.Println("This is a InfoT message") + WarningT.Println("This is a WarningT message") + SuccessT.Println("This is a SuccessT message") + ErrorT.Println("This is a ErrorT message") + FatalNT.Println("This is a FatalNT message") + DebugT.Println("This is a DebugT message") + + // Custom prefix printer style. + InfoT.WithMessageStyle(pterm.GrayBoxStyle).Println("This is a InfoT message with gray box style") + InfoT.WithMessageStyle(pterm.NewStyle(pterm.BgLightWhite, pterm.FgBlue)).Println("This is a InfoT message with FgLightWhite and FgBlue") + InfoT.WithShowLineNumber(true).Println("This is a InfoT message with line number") +} diff --git a/pkg/util/pretty/spinner.go b/pkg/util/pretty/spinner.go new file mode 100644 index 0000000..850ffbc --- /dev/null +++ b/pkg/util/pretty/spinner.go @@ -0,0 +1,27 @@ +package pretty + +import ( + "time" + + "github.com/pterm/pterm" +) + +// Spinner style. +// +// Usage: +// +// sp, _ := pretty.Spinner.Start("Starting ...") +// time.Sleep(time.Second * 3) +// sp.Success("Done") +var Spinner = pterm.SpinnerPrinter{ + Sequence: []string{" ⣾ ", " ⣽ ", " ⣻ ", " ⢿ ", " ⡿ ", " ⣟ ", " ⣯ ", " ⣷ "}, + Style: &pterm.ThemeDefault.SpinnerStyle, + Delay: time.Millisecond * 100, + ShowTimer: true, + TimerRoundingFactor: time.Second, + TimerStyle: &pterm.ThemeDefault.TimerStyle, + MessageStyle: &pterm.ThemeDefault.InfoMessageStyle, + SuccessPrinter: &Success, + FailPrinter: &Error, + WarningPrinter: &Warning, +} diff --git a/pkg/util/pretty/spinner_test.go b/pkg/util/pretty/spinner_test.go new file mode 100644 index 0000000..6c0fc1c --- /dev/null +++ b/pkg/util/pretty/spinner_test.go @@ -0,0 +1,29 @@ +package pretty + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSpinner(t *testing.T) { + fmt.Println("Spinner:") + defer fmt.Println("") + + sp, err := Spinner.Start("Starting ...") + assert.Nil(t, err) + time.Sleep(time.Second * 1) + sp.Success("Success") + + sp, err = Spinner.Start("Starting ...") + assert.Nil(t, err) + time.Sleep(time.Second * 1) + sp.Fail("Fail") + + sp, err = Spinner.Start("Starting ...") + assert.Nil(t, err) + time.Sleep(time.Second * 1) + sp.Warning("Warning") +} diff --git a/pkg/util/pretty/spinner_text.go b/pkg/util/pretty/spinner_text.go new file mode 100644 index 0000000..3892819 --- /dev/null +++ b/pkg/util/pretty/spinner_text.go @@ -0,0 +1,27 @@ +package pretty + +import ( + "time" + + "github.com/pterm/pterm" +) + +// SpinnerT text style. +// +// Usage: +// +// sp, _ := pretty.SpinnerT.Start("Starting ...") +// time.Sleep(time.Second * 3) +// sp.Success("Done") +var SpinnerT = pterm.SpinnerPrinter{ + Sequence: []string{" ⣾ ", " ⣽ ", " ⣻ ", " ⢿ ", " ⡿ ", " ⣟ ", " ⣯ ", " ⣷ "}, + Style: &pterm.ThemeDefault.SpinnerStyle, + Delay: time.Millisecond * 100, + ShowTimer: true, + TimerRoundingFactor: time.Second, + TimerStyle: &pterm.ThemeDefault.TimerStyle, + MessageStyle: &pterm.ThemeDefault.InfoMessageStyle, + SuccessPrinter: &SuccessT, + FailPrinter: &ErrorT, + WarningPrinter: &WarningT, +} diff --git a/pkg/util/pretty/spinner_text_test.go b/pkg/util/pretty/spinner_text_test.go new file mode 100644 index 0000000..e34c00a --- /dev/null +++ b/pkg/util/pretty/spinner_text_test.go @@ -0,0 +1,29 @@ +package pretty + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSpinnerT(t *testing.T) { + fmt.Println("SpinnerT:") + defer fmt.Println("") + + sp, err := SpinnerT.Start("Starting ...") + assert.Nil(t, err) + time.Sleep(time.Second * 1) + sp.Success("Success") + + sp, err = SpinnerT.Start("Starting ...") + assert.Nil(t, err) + time.Sleep(time.Second * 1) + sp.Fail("Fail") + + sp, err = SpinnerT.Start("Starting ...") + assert.Nil(t, err) + time.Sleep(time.Second * 1) + sp.Warning("Warning") +} diff --git a/pkg/util/pretty/style.go b/pkg/util/pretty/style.go new file mode 100644 index 0000000..fa4f7da --- /dev/null +++ b/pkg/util/pretty/style.go @@ -0,0 +1,88 @@ +package pretty + +import "github.com/pterm/pterm" + +// Pretty style, contains the color and the style. +// +// Usage: +// +// var s1 string = pretty.GreenBold("Hello") +// var s2 string = pretty.NormalBold("Hello %s", "World") +// fmt.Println(s1, s2) +var ( + // Preset Color + // Red is an alias for FgRed.Sprintf. + Red = pterm.FgRed.Sprintf + // Cyan is an alias for FgCyan.Sprintf. + Cyan = pterm.FgCyan.Sprintf + // Gray is an alias for FgGray.Sprintf. + Gray = pterm.FgGray.Sprintf + // Blue is an alias for FgBlue.Sprintf. + Blue = pterm.FgBlue.Sprintf + // Black is an alias for FgBlack.Sprintf. + Black = pterm.FgBlack.Sprintf + // Green is an alias for FgGreen.Sprintf. + Green = pterm.FgGreen.Sprintf + // White is an alias for FgWhite.Sprintf. + White = pterm.FgWhite.Sprintf + // Yellow is an alias for FgYellow.Sprintf. + Yellow = pterm.FgYellow.Sprintf + // Magenta is an alias for FgMagenta.Sprintf. + Magenta = pterm.FgMagenta.Sprintf + + // Normal is an alias for FgDefault.Sprintf. + Normal = pterm.FgDefault.Sprintf + + // LightRed is a shortcut for FgLightRed.Sprintf. + LightRed = pterm.FgLightRed.Sprintf + // LightCyan is a shortcut for FgLightCyan.Sprintf. + LightCyan = pterm.FgLightCyan.Sprintf + // LightBlue is a shortcut for FgLightBlue.Sprintf. + LightBlue = pterm.FgLightBlue.Sprintf + // LightGreen is a shortcut for FgLightGreen.Sprintf. + LightGreen = pterm.FgLightGreen.Sprintf + // LightWhite is a shortcut for FgLightWhite.Sprintf. + LightWhite = pterm.FgLightWhite.Sprintf + // LightYellow is a shortcut for FgLightYellow.Sprintf. + LightYellow = pterm.FgLightYellow.Sprintf + // LightMagenta is a shortcut for FgLightMagenta.Sprintf. + LightMagenta = pterm.FgLightMagenta.Sprintf + + // Preset Style for Color and Bold + // RedBold is an shortcut for Sprintf of Style with Red and Bold. + RedBold = pterm.NewStyle(pterm.FgRed, pterm.Bold).Sprintf + // CyanBold is an shortcut for Sprintf of Style with FgCyan and Bold. + CyanBold = pterm.NewStyle(pterm.FgCyan, pterm.Bold).Sprintf + // GrayBold is an shortcut for Sprintf of Style with FgGray and Bold. + GrayBold = pterm.NewStyle(pterm.FgGray, pterm.Bold).Sprintf + // BlueBold is an shortcut for Sprintf of Style with FgBlue and Bold. + BlueBold = pterm.NewStyle(pterm.FgBlue, pterm.Bold).Sprintf + // BlackBold is an shortcut for Sprintf of Style with FgBlack and Bold. + BlackBold = pterm.NewStyle(pterm.FgBlack, pterm.Bold).Sprintf + // GreenBold is an shortcut for Sprintf of Style with FgGreen and Bold. + GreenBold = pterm.NewStyle(pterm.FgGreen, pterm.Bold).Sprintf + // WhiteBold is an shortcut for Sprintf of Style with FgWhite and Bold. + WhiteBold = pterm.NewStyle(pterm.FgWhite, pterm.Bold).Sprintf + // YellowBold is an shortcut for Sprintf of Style with FgYellow and Bold. + YellowBold = pterm.NewStyle(pterm.FgYellow, pterm.Bold).Sprintf + // MagentaBold is an shortcut for Sprintf of Style with FgMagenta and Bold. + MagentaBold = pterm.NewStyle(pterm.FgMagenta, pterm.Bold).Sprintf + + // NormalBold is an shortcut for Sprintf of Style with FgDefault and Bold. + NormalBold = pterm.NewStyle(pterm.FgDefault, pterm.Bold).Sprintf + + // LightRedBold is an shortcut for Sprintf of Style with FgLightRed and Bold. + LightRedBold = pterm.NewStyle(pterm.FgLightRed, pterm.Bold).Sprintf + // LightCyanBold is an shortcut for Sprintf of Style with FgLightCyan and Bold. + LightCyanBold = pterm.NewStyle(pterm.FgLightCyan, pterm.Bold).Sprintf + // LightBlueBold is an shortcut for Sprintf of Style with FgLightBlue and Bold. + LightBlueBold = pterm.NewStyle(pterm.FgLightBlue, pterm.Bold).Sprintf + // LightGreenBold is an shortcut for Sprintf of Style with FgLightGreen and Bold. + LightGreenBold = pterm.NewStyle(pterm.FgLightGreen, pterm.Bold).Sprintf + // LightWhiteBold is an shortcut for Sprintf of Style with FgLightWhite and Bold. + LightWhiteBold = pterm.NewStyle(pterm.FgLightWhite, pterm.Bold).Sprintf + // LightYellowBold is an shortcut for Sprintf of Style with FgLightYellow and Bold. + LightYellowBold = pterm.NewStyle(pterm.FgLightYellow, pterm.Bold).Sprintf + // LightMagentaBold is an shortcut for Sprintf of Style with FgLightMagenta and Bold. + LightMagentaBold = pterm.NewStyle(pterm.FgLightMagenta, pterm.Bold).Sprintf +) diff --git a/pkg/util/pretty/style_test.go b/pkg/util/pretty/style_test.go new file mode 100644 index 0000000..fddf64a --- /dev/null +++ b/pkg/util/pretty/style_test.go @@ -0,0 +1,25 @@ +package pretty + +import ( + "fmt" + "testing" +) + +func TestStyle(t *testing.T) { + fmt.Println("Style:") + defer fmt.Println("") + + fmt.Println(Cyan("This is a Cyan message")) + fmt.Println(Gray("This is a Gray message")) + fmt.Println(Blue("This is a Blue message")) + fmt.Println(Black("This is a Black message")) + fmt.Println(Green("This is a Green message")) + fmt.Println(White("This is a White message")) + fmt.Println(Yellow("This is a Yellow message")) + fmt.Println(Magenta("This is a Magenta message")) + fmt.Println(Normal("This is a Normal message")) + fmt.Println(Red("This is a Red message")) + fmt.Println(LightRed("This is a LightRed message")) + fmt.Println(RedBold("This is a RedBold message")) + fmt.Println(LightRedBold("This is a LightRedBold message")) +} diff --git a/pkg/util/safeutil/safeutil.go b/pkg/util/safeutil/safeutil.go new file mode 100644 index 0000000..04bb61e --- /dev/null +++ b/pkg/util/safeutil/safeutil.go @@ -0,0 +1,44 @@ +package safeutil + +import ( + "runtime/debug" + + "github.com/elliotxx/safe" + "github.com/sirupsen/logrus" +) + +// LoggerRecoverHandler returns a recover handler by the given logger. +// +// Example: +// +// func() { +// defer safe.HandleCrash(LoggerRecoverHandler(logrus.New())) +// ... +// } +func LoggerRecoverHandler(logger logrus.FieldLogger) safe.RecoverHandler { + return func(r any) { + msgFormat := "Recovered as [%v] from stack: %s" + + if logger != nil { + logger.Errorf(msgFormat, r, debug.Stack()) + } + } +} + +// Go starts a recoverable goroutine with a new logger (logrus.New()). +// +// Example: +// +// safeutil.Go(func(){...}) +func Go(do safe.DoFunc) { + safe.GoR(do, LoggerRecoverHandler(logrus.New())) +} + +// GoL starts a recoverable goroutine with a given logger. +// +// Example: +// +// safeutil.GoL(func(){...}, logger) +func GoL(do safe.DoFunc, logger logrus.FieldLogger) { + safe.GoR(do, LoggerRecoverHandler(logger)) +} diff --git a/pkg/util/safeutil/safeutil_test.go b/pkg/util/safeutil/safeutil_test.go new file mode 100644 index 0000000..bd8d046 --- /dev/null +++ b/pkg/util/safeutil/safeutil_test.go @@ -0,0 +1,75 @@ +package safeutil + +import ( + "os" + "testing" + "time" + + "github.com/elliotxx/safe" + "github.com/sirupsen/logrus" +) + +func TestGo(t *testing.T) { + type args struct { + do safe.DoFunc + } + + tests := []struct { + name string + args args + }{ + { + name: "successful-recover-crash-in-safe-Go", + args: args{ + do: func() { + panic("ah, I'm down") + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Go(tt.args.do) + time.Sleep(time.Second * 1) + }) + } +} + +func TestGoL(t *testing.T) { + type args struct { + do safe.DoFunc + logger logrus.FieldLogger + } + + getTestingLogger := func() logrus.FieldLogger { + logger := logrus.New() + logger.SetOutput(os.Stdout) + logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: "2006-01-02 15:04:05.000000", + PrettyPrint: true, + }) + + return logger + } + + tests := []struct { + name string + args args + }{ + { + name: "successful-recover-crash-in-safe-Go", + args: args{ + do: func() { + panic("ah, I'm down") + }, + logger: getTestingLogger(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + GoL(tt.args.do, tt.args.logger) + time.Sleep(time.Second * 1) + }) + } +} diff --git a/pkg/version/.gitignore b/pkg/version/.gitignore new file mode 100644 index 0000000..0c28e42 --- /dev/null +++ b/pkg/version/.gitignore @@ -0,0 +1 @@ +z_update_version.go diff --git a/pkg/version/api.go b/pkg/version/api.go new file mode 100644 index 0000000..5c780f5 --- /dev/null +++ b/pkg/version/api.go @@ -0,0 +1,29 @@ +package version + +func ReleaseVersion() string { + return info.ReleaseVersion +} + +func String() string { + return info.String() +} + +func ShortString() string { + return info.ShortString() +} + +func JSON() string { + return info.JSON() +} + +func YAML() string { + return info.YAML() +} + +func VersionInfo() *Info { + return info +} + +func VersionObject() any { + return info +} diff --git a/pkg/version/scripts/gen/gen.go b/pkg/version/scripts/gen/gen.go new file mode 100644 index 0000000..be758f6 --- /dev/null +++ b/pkg/version/scripts/gen/gen.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "os" + + "github.com/elliotxx/go-web-template/pkg/version" + "github.com/sirupsen/logrus" +) + +func main() { + versionInfo, err := version.NewInfo() + if err != nil { + logrus.Fatal(err) + } + + data := makeUpdateVersionGoFile(versionInfo) + + err = os.WriteFile("../z_update_version.go", []byte(data), 0o666) + if err != nil { + logrus.Fatalf("io.WriteFile: err = %v", err) + } + + fmt.Println(versionInfo.String()) +} + +func makeUpdateVersionGoFile(v *version.Info) string { + return fmt.Sprintf(`// Auto generated by 'go run gen.go', DO NOT EDIT. + +package version + +import "expvar" + +func init() { + info = &Info{ + ReleaseVersion: %q, + GitInfo: &GitInfo{ + LatestTag: %q, + Commit: %q, + TreeState: %q, + }, + BuildInfo: &BuildInfo{ + GoVersion: %q, + GOOS: %q, + GOARCH: %q, + NumCPU: %d, + Compiler: %q, + BuildTime: %q, + }, + } + expvar.Publish("version", expvar.Func(VersionObject)) +} +`, + v.ReleaseVersion, + v.GitInfo.LatestTag, + v.GitInfo.Commit, + v.GitInfo.TreeState, + v.BuildInfo.GoVersion, + v.BuildInfo.GOOS, + v.BuildInfo.GOARCH, + v.BuildInfo.NumCPU, + v.BuildInfo.Compiler, + v.BuildInfo.BuildTime, + ) +} diff --git a/pkg/version/types.go b/pkg/version/types.go new file mode 100644 index 0000000..6cb2351 --- /dev/null +++ b/pkg/version/types.go @@ -0,0 +1,191 @@ +//go:generate go run gen.go +//go:generate go fmt + +package version + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "runtime/debug" + "strings" + "time" + + git "github.com/elliotxx/go-web-template/pkg/util/gitutil" + goversion "github.com/hashicorp/go-version" + "gopkg.in/yaml.v3" +) + +var info = NewMainOrDefaultVersionInfo() + +const EnvSpecifiedVersion = "SPECIFIED_VERSION" + +func NewMainOrDefaultVersionInfo() *Info { + v := NewDefaultVersionInfo() + + if i, ok := debug.ReadBuildInfo(); ok { + mod := &i.Main + if mod.Replace != nil { + mod = mod.Replace + } + + if mod.Version != "(devel)" { + v.ReleaseVersion = mod.Version + } + } + + return v +} + +func NewDefaultVersionInfo() *Info { + return &Info{ + ReleaseVersion: "default-version", + GitInfo: &GitInfo{ + LatestTag: "", + Commit: "", + TreeState: "", + }, + BuildInfo: &BuildInfo{ + GoVersion: runtime.Version(), + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + NumCPU: runtime.NumCPU(), + Compiler: runtime.Compiler, + BuildTime: time.Now().Format("2006-01-02 15:04:05"), + }, + } +} + +// Info contains versioning information. +// following attributes: +// +// ReleaseVersion - "vX.Y.Z-00000000" used to indicate the last release version, +// containing GitVersion and GitCommitShort. +type Info struct { + ReleaseVersion string `json:"releaseVersion" yaml:"releaseVersion"` // Such as "v1.2.3-3836f877" + GitInfo *GitInfo `json:"gitInfo,omitempty" yaml:"gitInfo,omitempty"` + BuildInfo *BuildInfo `json:"buildInfo,omitempty" yaml:"buildInfo,omitempty"` +} + +// GitInfo contains git information. +// following attributes: +// +// LatestTag - "vX.Y.Z" used to indicate the last git tag. +// Commit - The git commit id corresponding to this source code. +// TreeState - "clean" indicates no changes since the git commit id +// "dirty" indicates source code changes after the git commit id +type GitInfo struct { + LatestTag string `json:"latestTag,omitempty" yaml:"latestTag,omitempty"` // Such as "v1.2.3" + Commit string `json:"commit,omitempty" yaml:"commit,omitempty"` // Such as "3836f8770ab8f488356b2129f42f2ae5c1134bb0" + TreeState string `json:"treeState,omitempty" yaml:"treeState,omitempty"` // Such as "clean", "dirty" +} + +type BuildInfo struct { + GoVersion string `json:"goVersion,omitempty" yaml:"goVersion,omitempty"` + GOOS string `json:"GOOS,omitempty" yaml:"GOOS,omitempty"` + GOARCH string `json:"GOARCH,omitempty" yaml:"GOARCH,omitempty"` + NumCPU int `json:"numCPU,omitempty" yaml:"numCPU,omitempty"` + Compiler string `json:"compiler,omitempty" yaml:"compiler,omitempty"` + BuildTime string `json:"buildTime,omitempty" yaml:"buildTime,omitempty"` // Such as "2021-10-20 18:24:03" +} + +func NewInfo() (*Info, error) { + var ( + isHeadAtTag bool + headHash string + headHashShort string + latestTag string + gitVersion *goversion.Version + releaseVersion string + isDirty bool + gitTreeState string + err error + ) + + // Get git info + if headHash, err = git.GetHeadHash(); err != nil { + return nil, err + } + + if headHashShort, err = git.GetHeadHashShort(); err != nil { + return nil, err + } + + if latestTag, err = git.GetLatestTag(); err != nil { + return nil, err + } + + if gitVersion, err = goversion.NewVersion(latestTag); err != nil { + return nil, err + } + + if isHeadAtTag, err = git.IsHeadAtTag(latestTag); err != nil { + return nil, err + } + + if isDirty, err = git.IsDirty(); err != nil { + return nil, err + } + + // Get git tree state + if isDirty { + gitTreeState = "dirty" + } else { + gitTreeState = "clean" + } + + // Get release version + if specifiedVersion := os.Getenv(EnvSpecifiedVersion); strings.TrimSpace(specifiedVersion) == "" { + if isHeadAtTag { + releaseVersion = gitVersion.Original() + } else { + releaseVersion = fmt.Sprintf("%s-%s", gitVersion.Original(), headHashShort) + } + } else { + releaseVersion = specifiedVersion + } + + return &Info{ + ReleaseVersion: releaseVersion, + GitInfo: &GitInfo{ + LatestTag: gitVersion.Original(), + Commit: headHash, + TreeState: gitTreeState, + }, + BuildInfo: &BuildInfo{ + GoVersion: runtime.Version(), + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + NumCPU: runtime.NumCPU(), + Compiler: runtime.Compiler, + BuildTime: time.Now().Format("2006-01-02 15:04:05"), + }, + }, nil +} + +func (v *Info) String() string { + return v.YAML() +} + +func (v *Info) ShortString() string { + return fmt.Sprintf("%s; git: %s; build time: %s", v.ReleaseVersion, v.GitInfo.Commit, v.BuildInfo.BuildTime) +} + +func (v *Info) JSON() string { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "" + } + + return string(data) +} + +func (v *Info) YAML() string { + data, err := yaml.Marshal(v) + if err != nil { + return "" + } + + return string(data) +} diff --git a/third_party/metadecoders/decoder.go b/third_party/metadecoders/decoder.go new file mode 100644 index 0000000..6ff0f05 --- /dev/null +++ b/third_party/metadecoders/decoder.go @@ -0,0 +1,307 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// 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 metadecoders + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "fmt" + "regexp" + "strings" + + "github.com/niklasfasching/go-org/org" + "github.com/pkg/errors" + + xml "github.com/clbanning/mxj/v2" + toml "github.com/pelletier/go-toml/v2" + "github.com/spf13/afero" + "github.com/spf13/cast" + jww "github.com/spf13/jwalterweatherman" + yaml "gopkg.in/yaml.v3" +) + +// Decoder provides some configuration options for the decoders. +type Decoder struct { + // Delimiter is the field delimiter used in the CSV decoder. It defaults to ','. + Delimiter rune + + // Comment, if not 0, is the comment character ued in the CSV decoder. Lines beginning with the + // Comment character without preceding whitespace are ignored. + Comment rune +} + +// OptionsKey is used in cache keys. +func (d Decoder) OptionsKey() string { + var sb strings.Builder + sb.WriteRune(d.Delimiter) + sb.WriteRune(d.Comment) + return sb.String() +} + +// Default is a Decoder in its default configuration. +var Default = Decoder{ + Delimiter: ',', +} + +// UnmarshalToMap will unmarshall data in format f into a new map. This is +// what's needed for Hugo's front matter decoding. +func (d Decoder) UnmarshalToMap(data []byte, f Format) (map[string]interface{}, error) { + m := make(map[string]interface{}) + if data == nil { + return m, nil + } + + err := d.UnmarshalTo(data, f, &m) + + return m, err +} + +// UnmarshalFileToMap is the same as UnmarshalToMap, but reads the data from +// the given filename. +func (d Decoder) UnmarshalFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) { + format := FormatFromString(filename) + if format == "" { + return nil, errors.Errorf("%q is not a valid configuration format", filename) + } + + data, err := afero.ReadFile(fs, filename) + if err != nil { + return nil, err + } + return d.UnmarshalToMap(data, format) +} + +// UnmarshalStringTo tries to unmarshal data to a new instance of type typ. +func (d Decoder) UnmarshalStringTo(data string, typ interface{}) (interface{}, error) { + data = strings.TrimSpace(data) + // We only check for the possible types in YAML, JSON and TOML. + switch typ.(type) { + case string: + return data, nil + case map[string]interface{}: + format := d.FormatFromContentString(data) + return d.UnmarshalToMap([]byte(data), format) + case []interface{}: + // A standalone slice. Let YAML handle it. + return d.Unmarshal([]byte(data), YAML) + case bool: + return cast.ToBoolE(data) + case int: + return cast.ToIntE(data) + case int64: + return cast.ToInt64E(data) + case float64: + return cast.ToFloat64E(data) + default: + return nil, errors.Errorf("unmarshal: %T not supported", typ) + } +} + +// Unmarshal will unmarshall data in format f into an interface{}. +// This is what's needed for Hugo's /data handling. +func (d Decoder) Unmarshal(data []byte, f Format) (interface{}, error) { + if data == nil { + switch f { + case CSV: + return make([][]string, 0), nil + default: + return make(map[string]interface{}), nil + } + } + var v interface{} + err := d.UnmarshalTo(data, f, &v) + + return v, err +} + +// UnmarshalTo unmarshals data in format f into v. +func (d Decoder) UnmarshalTo(data []byte, f Format, v interface{}) error { + var err error + + switch f { + case ORG: + err = d.unmarshalORG(data, v) + case JSON: + err = json.Unmarshal(data, v) + case XML: + var xmlRoot xml.Map + xmlRoot, err = xml.NewMapXml(data) + + var xmlValue map[string]interface{} + if err == nil { + xmlRootName, err := xmlRoot.Root() + if err != nil { + return errors.Wrap(err, "failed to unmarshal XML") + } + xmlValue = xmlRoot[xmlRootName].(map[string]interface{}) + } + + switch v := v.(type) { + case *map[string]interface{}: + *v = xmlValue + case *interface{}: + *v = xmlValue + } + case TOML: + err = toml.Unmarshal(data, v) + case YAML: + err = yaml.Unmarshal(data, v) + if err != nil { + return errors.Wrap(err, "failed to unmarshal YAML") + } + + // To support boolean keys, the YAML package unmarshals maps to + // map[interface{}]interface{}. Here we recurse through the result + // and change all maps to map[string]interface{} like we would've + // gotten from `json`. + var ptr interface{} + switch v.(type) { + case *map[string]interface{}: + ptr = *v.(*map[string]interface{}) + case *interface{}: + ptr = *v.(*interface{}) + default: + // Not a map. + } + + if ptr != nil { + if mm, changed := stringifyMapKeys(ptr); changed { + switch v.(type) { + case *map[string]interface{}: + *v.(*map[string]interface{}) = mm.(map[string]interface{}) + case *interface{}: + *v.(*interface{}) = mm + } + } + } + case CSV: + return d.unmarshalCSV(data, v) + + default: + return errors.Errorf("unmarshal of format %q is not supported", f) + } + + if err == nil { + return nil + } + + return errors.Errorf("unmarshal failed: %w", err) +} + +func (d Decoder) unmarshalCSV(data []byte, v interface{}) error { + r := csv.NewReader(bytes.NewReader(data)) + r.Comma = d.Delimiter + r.Comment = d.Comment + + records, err := r.ReadAll() + if err != nil { + return err + } + + switch v.(type) { + case *interface{}: + *v.(*interface{}) = records + default: + return errors.Errorf("CSV cannot be unmarshaled into %T", v) + + } + + return nil +} + +func parseORGDate(s string) string { + r := regexp.MustCompile(`[<\[](\d{4}-\d{2}-\d{2}) .*[>\]]`) + if m := r.FindStringSubmatch(s); m != nil { + return m[1] + } + return s +} + +func (d Decoder) unmarshalORG(data []byte, v interface{}) error { + config := org.New() + config.Log = jww.WARN + document := config.Parse(bytes.NewReader(data), "") + if document.Error != nil { + return document.Error + } + frontMatter := make(map[string]interface{}, len(document.BufferSettings)) + for k, v := range document.BufferSettings { + k = strings.ToLower(k) + if strings.HasSuffix(k, "[]") { + frontMatter[k[:len(k)-2]] = strings.Fields(v) + } else if k == "tags" || k == "categories" || k == "aliases" { + jww.WARN.Printf("Please use '#+%s[]:' notation, automatic conversion is deprecated.", k) + frontMatter[k] = strings.Fields(v) + } else if k == "date" { + frontMatter[k] = parseORGDate(v) + } else { + frontMatter[k] = v + } + } + switch v.(type) { + case *map[string]interface{}: + *v.(*map[string]interface{}) = frontMatter + default: + *v.(*interface{}) = frontMatter + } + return nil +} + +// stringifyMapKeys recurses into in and changes all instances of +// map[interface{}]interface{} to map[string]interface{}. This is useful to +// work around the impedance mismatch between JSON and YAML unmarshaling that's +// described here: https://github.com/go-yaml/yaml/issues/139 +// +// Inspired by https://github.com/stripe/stripe-mock, MIT licensed +func stringifyMapKeys(in interface{}) (interface{}, bool) { + switch in := in.(type) { + case []interface{}: + for i, v := range in { + if vv, replaced := stringifyMapKeys(v); replaced { + in[i] = vv + } + } + case map[string]interface{}: + for k, v := range in { + if vv, changed := stringifyMapKeys(v); changed { + in[k] = vv + } + } + case map[interface{}]interface{}: + res := make(map[string]interface{}) + var ( + ok bool + err error + ) + for k, v := range in { + var ks string + + if ks, ok = k.(string); !ok { + ks, err = cast.ToStringE(k) + if err != nil { + ks = fmt.Sprintf("%v", k) + } + } + if vv, replaced := stringifyMapKeys(v); replaced { + res[ks] = vv + } else { + res[ks] = v + } + } + return res, true + } + + return nil, false +} diff --git a/third_party/metadecoders/format.go b/third_party/metadecoders/format.go new file mode 100644 index 0000000..c28d54d --- /dev/null +++ b/third_party/metadecoders/format.go @@ -0,0 +1,104 @@ +// Copyright 2018 The Hugo Authors. All rights reserved. +// +// 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 metadecoders + +import ( + "path/filepath" + "strings" +) + +type Format string + +const ( + // These are the supported metdata formats in Hugo. Most of these are also + // supported as /data formats. + ORG Format = "org" + JSON Format = "json" + TOML Format = "toml" + YAML Format = "yaml" + CSV Format = "csv" + XML Format = "xml" +) + +// FormatFromString turns formatStr, typically a file extension without any ".", +// into a Format. It returns an empty string for unknown formats. +func FormatFromString(formatStr string) Format { + formatStr = strings.ToLower(formatStr) + if strings.Contains(formatStr, ".") { + // Assume a filename + formatStr = strings.TrimPrefix(filepath.Ext(formatStr), ".") + } + switch formatStr { + case "yaml", "yml": + return YAML + case "json": + return JSON + case "toml": + return TOML + case "org": + return ORG + case "csv": + return CSV + case "xml": + return XML + } + + return "" +} + +// FormatFromContentString tries to detect the format (JSON, YAML, TOML or XML) +// in the given string. +// It return an empty string if no format could be detected. +func (d Decoder) FormatFromContentString(data string) Format { + csvIdx := strings.IndexRune(data, d.Delimiter) + jsonIdx := strings.Index(data, "{") + yamlIdx := strings.Index(data, ":") + xmlIdx := strings.Index(data, "<") + tomlIdx := strings.Index(data, "=") + + if isLowerIndexThan(csvIdx, jsonIdx, yamlIdx, xmlIdx, tomlIdx) { + return CSV + } + + if isLowerIndexThan(jsonIdx, yamlIdx, xmlIdx, tomlIdx) { + return JSON + } + + if isLowerIndexThan(yamlIdx, xmlIdx, tomlIdx) { + return YAML + } + + if isLowerIndexThan(xmlIdx, tomlIdx) { + return XML + } + + if tomlIdx != -1 { + return TOML + } + + return "" +} + +func isLowerIndexThan(first int, others ...int) bool { + if first == -1 { + return false + } + for _, other := range others { + if other != -1 && other < first { + return false + } + } + + return true +}