diff --git a/.gitignore b/.gitignore index 2bfa5fdd..210a5eab 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ # IDEs .idea diff --git a/Makefile b/Makefile index 5df75b5f..22307d5d 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,19 @@ clean: rm -rf siren dist/ test: - go list ./... | grep -v extern | xargs go test -count 1 -cover -race -timeout 1m + go test ./... -coverprofile=coverage.out + +test-coverage: test + go tool cover -html=coverage.out dist: @bash ./scripts/build.sh + +check-swagger: + which swagger || (GO111MODULE=off go get -u github.com/go-swagger/go-swagger/cmd/swagger) + +swagger: check-swagger + GO111MODULE=on go mod vendor && GO111MODULE=off swagger generate spec -o ./swagger.yaml --scan-models + +swagger-serve: check-swagger + swagger serve -F=swagger swagger.yaml diff --git a/README.md b/README.md index 9bfcb7ac..b12914a2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,14 @@ Siren requires the following dependencies: * Golang (version 1.15 or above) * Git +Run the application dependecies using Docker: + +``` +$ docker-compose up +``` + +Update the configs(db credentials etc.) as per your dev machine and docker configs. + Run the following commands to compile from source ``` @@ -23,7 +31,13 @@ $ go build main.go To run tests locally ``` -$ go test +$ make test +``` + +To run tests locally with coverage + +``` +$ make test-coverage ``` To run server locally @@ -32,6 +46,9 @@ To run server locally $ go run main.go serve ``` +To view swagger docs of HTTP APIs visit `/docs` route on the server. +e.g. [http://localhost:3000/docs](http://localhost:3000/docs) + #### Config The config file used by application is `config.yaml` which should be present at the root of this directory. diff --git a/api/handlers/docs.go b/api/handlers/docs.go new file mode 100644 index 00000000..64e79f38 --- /dev/null +++ b/api/handlers/docs.go @@ -0,0 +1,12 @@ +package handlers + +import ( + "net/http" +) + +func SwaggerFile() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "./swagger.yaml") + return + } +} diff --git a/api/handlers/template.go b/api/handlers/template.go new file mode 100644 index 00000000..ab3cee3c --- /dev/null +++ b/api/handlers/template.go @@ -0,0 +1,109 @@ +package handlers + +import ( + "encoding/json" + "errors" + "github.com/gorilla/mux" + "github.com/odpf/siren/domain" + "net/http" +) + +// UpsertTemplates handler +func UpsertTemplates(service domain.TemplatesService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var template domain.Template + err := json.NewDecoder(r.Body).Decode(&template) + if err != nil { + badRequest(w, err) + return + } + createdTemplate, err := service.Upsert(&template) + if err != nil && err.Error() == ("name cannot be empty") { + badRequest(w, err) + return + } + if err != nil && err.Error() == ("body cannot be empty") { + badRequest(w, err) + return + } + if err != nil { + internalServerError(w, err) + return + } + returnJSON(w, createdTemplate) + return + } +} + +// IndexTemplates handler +func IndexTemplates(service domain.TemplatesService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + tag := r.URL.Query().Get("tag") + templates, err := service.Index(tag) + if err != nil { + internalServerError(w, err) + return + } + returnJSON(w, templates) + return + } +} + +// GetTemplates handler +func GetTemplates(service domain.TemplatesService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + name := params["name"] + templates, err := service.GetByName(name) + if err != nil { + internalServerError(w, err) + return + } + if templates == nil { + NotFound(w, errors.New(notFoundErrorMessage)) + return + } + returnJSON(w, templates) + return + } +} + +// DeleteTemplates handler +func DeleteTemplates(service domain.TemplatesService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + name := params["name"] + err := service.Delete(name) + if err != nil { + internalServerError(w, err) + return + } + returnJSON(w, nil) + return + } +} + +// RenderTemplates handler +func RenderTemplates(service domain.TemplatesService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + name := params["name"] + var body map[string]string + err := json.NewDecoder(r.Body).Decode(&body) + if err != nil { + badRequest(w, err) + return + } + renderedBody, err := service.Render(name, body) + if err != nil && err.Error() == "template not found" { + NotFound(w, err) + return + } + if err != nil { + internalServerError(w, err) + return + } + returnJSON(w, renderedBody) + return + } +} diff --git a/api/handlers/templates_test.go b/api/handlers/templates_test.go new file mode 100644 index 00000000..e2a44498 --- /dev/null +++ b/api/handlers/templates_test.go @@ -0,0 +1,447 @@ +package handlers_test + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/gorilla/mux" + "github.com/odpf/siren/api/handlers" + "github.com/odpf/siren/domain" + "github.com/odpf/siren/mocks" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestTemplates_UpsertTemplates(t *testing.T) { + t.Run("should return 200 OK on success", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + dummyTemplate := &domain.Template{ + Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }}, + } + + payload := []byte(`{"name":"foo", "body": "bar", "tags": ["test"], "variables": [{"name": "test-name", "default":"test-default", "description": "test-description", "type": "test-type" }]}`) + + mockedTemplatesService.On("Upsert", dummyTemplate).Return(dummyTemplate, nil).Once() + r, err := http.NewRequest(http.MethodPut, "/templates", bytes.NewBuffer(payload)) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.UpsertTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusOK + response, _ := json.Marshal(dummyTemplate) + expectedStringBody := string(response) + "\n" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Upsert", dummyTemplate) + }) + + t.Run("should return 400 Bad Request on failure", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + payload := []byte(`{"foo"}`) + r, err := http.NewRequest(http.MethodPut, "/templates", bytes.NewBuffer(payload)) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.UpsertTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusBadRequest + expectedStringBody := "{\"code\":400,\"message\":\"invalid character '}' after object key\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) + + t.Run("should return 400 Bad Request if name is empty in request body", func(t *testing.T) { + expectedError := errors.New("name cannot be empty") + mockedTemplatesService := &mocks.TemplatesService{} + dummyTemplate := &domain.Template{ + Name: "", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }}, + } + + payload := []byte(`{"name":"", "body": "bar", "tags": ["test"], "variables": [{"name": "test-name", "default":"test-default", "description": "test-description", "type": "test-type" }]}`) + + mockedTemplatesService.On("Upsert", dummyTemplate).Return(nil, expectedError).Once() + r, err := http.NewRequest(http.MethodPut, "/templates", bytes.NewBuffer(payload)) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.UpsertTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusBadRequest + expectedStringBody := "{\"code\":400,\"message\":\"name cannot be empty\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Upsert", dummyTemplate) + }) + + t.Run("should return 400 Bad Request if body is empty in request body", func(t *testing.T) { + expectedError := errors.New("body cannot be empty") + mockedTemplatesService := &mocks.TemplatesService{} + dummyTemplate := &domain.Template{ + Name: "foo", Body: "", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }}, + } + + payload := []byte(`{"name":"foo", "body": "", "tags": ["test"], "variables": [{"name": "test-name", "default":"test-default", "description": "test-description", "type": "test-type" }]}`) + + mockedTemplatesService.On("Upsert", dummyTemplate).Return(nil, expectedError).Once() + r, err := http.NewRequest(http.MethodPut, "/templates", bytes.NewBuffer(payload)) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.UpsertTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusBadRequest + expectedStringBody := "{\"code\":400,\"message\":\"body cannot be empty\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Upsert", dummyTemplate) + }) + + t.Run("should return 500 Error on failure", func(t *testing.T) { + expectedError := errors.New("random error") + mockedTemplatesService := &mocks.TemplatesService{} + dummyTemplate := &domain.Template{ + Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }}, + } + + payload := []byte(`{"name":"foo", "body": "bar", "tags": ["test"], "variables": [{"name": "test-name", "default":"test-default", "description": "test-description", "type": "test-type" }]}`) + + mockedTemplatesService.On("Upsert", dummyTemplate).Return(nil, expectedError).Once() + r, err := http.NewRequest(http.MethodPut, "/templates", bytes.NewBuffer(payload)) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.UpsertTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusInternalServerError + expectedStringBody := "{\"code\":500,\"message\":\"Internal server error\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Upsert", dummyTemplate) + }) +} + +func TestTemplates_GetTemplates(t *testing.T) { + t.Run("should return 200 OK on success if template exist", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + dummyTemplate := &domain.Template{ + ID: 1, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }}, + } + mockedTemplatesService.On("GetByName", "foo").Return(dummyTemplate, nil).Once() + r, err := http.NewRequest(http.MethodGet, "/templates", nil) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.GetTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusOK + response, _ := json.Marshal(dummyTemplate) + expectedStringBody := string(response) + "\n" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "GetByName", "foo") + }) + + t.Run("should return 404 Not found if template not exist", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + mockedTemplatesService.On("GetByName", "foo").Return(nil, nil).Once() + r, err := http.NewRequest(http.MethodGet, "/templates", nil) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.GetTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusNotFound + expectedStringBody := "{\"code\":404,\"message\":\"Not Found\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) + + t.Run("should return 500 Error on any failure", func(t *testing.T) { + expectedError := errors.New("random error") + mockedTemplatesService := &mocks.TemplatesService{} + mockedTemplatesService.On("GetByName", "foo").Return(nil, expectedError).Once() + r, err := http.NewRequest(http.MethodGet, "/templates", nil) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.GetTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusInternalServerError + expectedStringBody := "{\"code\":500,\"message\":\"Internal server error\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) +} + +func TestTemplates_IndexTemplates(t *testing.T) { + t.Run("should return 200 OK on success for non-empty tag", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + dummyTemplates := []domain.Template{{ + ID: 1, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }}, + }, + } + mockedTemplatesService.On("Index", "foo").Return(dummyTemplates, nil).Once() + r, err := http.NewRequest(http.MethodGet, "/templates", nil) + q := r.URL.Query() + q.Add("tag", "foo") + r.URL.RawQuery = q.Encode() + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.IndexTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusOK + response, _ := json.Marshal(dummyTemplates) + expectedStringBody := string(response) + "\n" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Index", "foo") + }) + + t.Run("should return 200 OK on success for empty tag", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + var dummyTemplates []domain.Template + mockedTemplatesService.On("Index", "").Return(dummyTemplates, nil).Once() + r, err := http.NewRequest(http.MethodGet, "/templates", nil) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.IndexTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusOK + response, _ := json.Marshal(dummyTemplates) + expectedStringBody := string(response) + "\n" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Index", "") + }) + + t.Run("should return 500 Error on any failure", func(t *testing.T) { + expectedError := errors.New("random error") + mockedTemplatesService := &mocks.TemplatesService{} + mockedTemplatesService.On("Index", "").Return(nil, expectedError).Once() + r, err := http.NewRequest(http.MethodGet, "/templates", nil) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.IndexTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusInternalServerError + expectedStringBody := "{\"code\":500,\"message\":\"Internal server error\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) +} + +func TestTemplates_RenderTemplates(t *testing.T) { + t.Run("should return 200 OK on success", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + inputBody := make(map[string]string) + inputBody["foo"] = "bar" + payload := []byte(`{"foo":"bar"}`) + mockedTemplatesService.On("Render", "foo", inputBody).Return("foo bar baz", nil).Once() + r, err := http.NewRequest(http.MethodPost, "/templates/{name}/render", bytes.NewBuffer(payload)) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.RenderTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusOK + expectedStringBody := "\"foo bar baz\"\n" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Render", "foo", inputBody) + }) + + t.Run("should return 400 Bad Request if bad input given", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + badPayload := []byte(`{"foo"}`) + r, err := http.NewRequest(http.MethodPost, "/templates/{name}/render", bytes.NewBuffer(badPayload)) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.RenderTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusBadRequest + expectedStringBody := "{\"code\":400,\"message\":\"invalid character '}' after object key\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) + + t.Run("should return 404 Not found if template not exist", func(t *testing.T) { + expectedError := errors.New("template not found") + mockedTemplatesService := &mocks.TemplatesService{} + inputBody := make(map[string]string) + inputBody["foo"] = "bar" + payload := []byte(`{"foo":"bar"}`) + mockedTemplatesService.On("Render", "foo", inputBody).Return("", expectedError).Once() + r, err := http.NewRequest(http.MethodPost, "/templates/{name}/render", bytes.NewBuffer(payload)) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.RenderTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusNotFound + expectedStringBody := "{\"code\":404,\"message\":\"template not found\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) + + t.Run("should return 500 Error on any failure", func(t *testing.T) { + expectedError := errors.New("random error") + mockedTemplatesService := &mocks.TemplatesService{} + inputBody := make(map[string]string) + inputBody["foo"] = "bar" + payload := []byte(`{"foo":"bar"}`) + mockedTemplatesService.On("Render", "foo", inputBody).Return("", expectedError).Once() + r, err := http.NewRequest(http.MethodPost, "/templates/{name}/render", bytes.NewBuffer(payload)) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.RenderTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusInternalServerError + expectedStringBody := "{\"code\":500,\"message\":\"Internal server error\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) +} + +func TestTemplates_DeleteTemplates(t *testing.T) { + t.Run("should return 200 OK on success", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + mockedTemplatesService.On("Delete", "foo").Return(nil).Once() + r, err := http.NewRequest(http.MethodDelete, "/templates", nil) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.DeleteTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusOK + expectedStringBody := "null\n" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + mockedTemplatesService.AssertCalled(t, "Delete", "foo") + }) + + t.Run("should return 500 Error on any failure", func(t *testing.T) { + mockedTemplatesService := &mocks.TemplatesService{} + expectedError := errors.New("random error") + mockedTemplatesService.On("Delete", "foo").Return(expectedError).Once() + r, err := http.NewRequest(http.MethodDelete, "/templates", nil) + r = mux.SetURLVars(r, map[string]string{"name": "foo"}) + if err != nil { + t.Fatal(err) + } + w := httptest.NewRecorder() + handler := handlers.DeleteTemplates(mockedTemplatesService) + expectedStatusCode := http.StatusInternalServerError + expectedStringBody := "{\"code\":500,\"message\":\"Internal server error\",\"data\":null}" + + handler.ServeHTTP(w, r) + + assert.Equal(t, expectedStatusCode, w.Code) + assert.Equal(t, expectedStringBody, w.Body.String()) + }) +} diff --git a/api/handlers/utils.go b/api/handlers/utils.go index 9851a59a..fda47fbd 100644 --- a/api/handlers/utils.go +++ b/api/handlers/utils.go @@ -5,7 +5,72 @@ import ( "net/http" ) +const ( + defaultErrorMessage = "Internal server error" + badRequestErrorMessage = "Bad Request" + notFoundErrorMessage = "Not Found" +) + func returnJSON(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) } + +type responseError struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +func internalServerError(w http.ResponseWriter, err error) (responseError, error) { + return sendError(w, defaultErrorMessage, http.StatusInternalServerError, nil) +} + +func badRequest(w http.ResponseWriter, err error) { + var errMessage string + if err != nil { + errMessage = err.Error() + } else { + errMessage = badRequestErrorMessage + } + + sendError(w, errMessage, http.StatusBadRequest, nil) +} + +func NotFound(w http.ResponseWriter, err error) { + var errMessage string + if err != nil { + errMessage = err.Error() + } else { + errMessage = notFoundErrorMessage + } + + sendError(w, errMessage, http.StatusNotFound, nil) +} + +func sendError(w http.ResponseWriter, errorMessage string, code int, data interface{}) (responseError, error) { + if code == 0 { + code = http.StatusInternalServerError + } + + response := responseError{ + Code: code, + Message: errorMessage, + Data: data, + } + + jsonBytes, err := json.Marshal(response) + if err != nil { + return response, err + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + + _, err = w.Write(jsonBytes) + if err != nil { + return response, err + } + + return response, nil +} diff --git a/api/middleware.go b/api/middleware.go index b1181214..bf7f133c 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -1,12 +1,19 @@ package api import ( + "github.com/go-openapi/runtime/middleware" + "github.com/gorilla/handlers" "net/http" "os" - - "github.com/gorilla/handlers" ) func logger(next http.Handler) http.Handler { return handlers.LoggingHandler(os.Stdout, next) } + +func SwaggerMiddleware(next http.Handler) http.Handler { + return middleware.SwaggerUI(middleware.SwaggerUIOpts{ + SpecURL: "/swagger.yaml", + Path: "docs", + }, next) +} diff --git a/api/router.go b/api/router.go index 88426d03..49527ed4 100644 --- a/api/router.go +++ b/api/router.go @@ -1,17 +1,32 @@ package api import ( + "github.com/go-openapi/runtime/middleware" "github.com/gorilla/mux" "github.com/odpf/siren/api/handlers" + "github.com/odpf/siren/service" ) // New initializes the service router -func New() *mux.Router { +func New(container *service.Container) *mux.Router { r := mux.NewRouter().StrictSlash(true) r.Use(logger) + // Route => handler r.Methods("GET").Path("/ping").Handler(handlers.Ping()) + r.Methods("GET").Path("/swagger.yaml").Handler(handlers.SwaggerFile()) + r.Methods("GET").Path("/documentation").Handler(middleware.SwaggerUI(middleware.SwaggerUIOpts{ + SpecURL: "/swagger.yaml", + Path: "documentation", + }, r.NotFoundHandler)) + + r.Methods("PUT").Path("/templates").Handler(handlers.UpsertTemplates(container.TemplatesService)) + r.Methods("GET").Path("/templates").Handler(handlers.IndexTemplates(container.TemplatesService)) + r.Methods("GET").Path("/templates/{name}").Handler(handlers.GetTemplates(container.TemplatesService)) + r.Methods("DELETE").Path("/templates/{name}").Handler(handlers.DeleteTemplates(container.TemplatesService)) + r.Methods("POST").Path("/templates/{name}/render").Handler(handlers.RenderTemplates(container.TemplatesService)) + return r } diff --git a/app/config.go b/app/config.go index be6ac8dd..e299000d 100644 --- a/app/config.go +++ b/app/config.go @@ -5,9 +5,9 @@ import ( "github.com/jeremywohl/flatten" "github.com/mcuadros/go-defaults" "github.com/mitchellh/mapstructure" + "github.com/odpf/siren/domain" "github.com/spf13/viper" "strings" - "github.com/odpf/siren/domain" ) // LoadConfig returns application configuration diff --git a/app/server.go b/app/server.go index 7e2dc830..362dfdd7 100644 --- a/app/server.go +++ b/app/server.go @@ -2,26 +2,33 @@ package app import ( "fmt" + "github.com/odpf/siren/api" "github.com/odpf/siren/domain" + "github.com/odpf/siren/service" + "github.com/odpf/siren/store" "log" "net/http" - - "github.com/odpf/siren/api" - "github.com/odpf/siren/store" ) // RunServer runs the application server func RunServer(c *domain.Config) error { - db, err := store.New(&c.DB) + store, err := store.New(&c.DB) if err != nil { return err } + services := service.Init(store) - models := []interface{}{} - store.Migrate(db, models...) - - r := api.New() + r := api.New(services) log.Printf("running server on port %d\n", c.Port) return http.ListenAndServe(fmt.Sprintf(":%d", c.Port), r) } + +func RunMigrations(c *domain.Config) error { + store, err := store.New(&c.DB) + if err != nil { + return err + } + service.MigrateAll(store) + return nil +} \ No newline at end of file diff --git a/cmd/migrate.go b/cmd/migrate.go new file mode 100644 index 00000000..52095b75 --- /dev/null +++ b/cmd/migrate.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/odpf/siren/app" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(&cobra.Command{ + Use: "migrate", + Short: "Run DB migrations", + RunE: migrate, + }) +} + +func migrate(cmd *cobra.Command, args []string) error { + c := app.LoadConfig() + return app.RunMigrations(c) +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..623eeeb5 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,14 @@ +version: "3" +services: + db: + image: "postgres:12" + container_name: "siren_postgres" + ports: + - "54320:5432" + volumes: + - siren_dbdata:/var/lib/postgresql/data + environment: + POSTGRES_DB: "db" + POSTGRES_HOST_AUTH_METHOD: "trust" +volumes: + siren_dbdata: diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 00000000..ede53aeb --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,25 @@ +// Package classification Siren. +// +// Documentation of our Siren API. +// +// Schemes: http +// BasePath: / +// Version: 1.0.0 +// Host: localhost:3000 +// +// Schemes: http, https +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Security: +// - basic +// +// SecurityDefinitions: +// basic: +// type: basic +// +// swagger:meta +package docs \ No newline at end of file diff --git a/docs/ping.go b/docs/ping.go new file mode 100644 index 00000000..27d78aa7 --- /dev/null +++ b/docs/ping.go @@ -0,0 +1,14 @@ +package docs + +//------------------------- +// swagger:route GET /ping ping ping +// Ping call +// responses: +// 200: pingResponse + +// Response body for Ping. +// swagger:response pingResponse +type pingResponse struct { + // in:body + Body string +} diff --git a/docs/templates.go b/docs/templates.go new file mode 100644 index 00000000..a7eb7687 --- /dev/null +++ b/docs/templates.go @@ -0,0 +1,92 @@ +package docs + +import "github.com/odpf/siren/domain" + +// swagger:response templatesResponse +type templatesResponse struct { + // in:body + Body domain.Template +} + +//------------------------- +//------------------------- +// swagger:route GET /templates templates listTemplatesRequest +// Templates list call +// responses: +// 200: listResponse + +// swagger:parameters listTemplatesRequest +type listTemplatesRequest struct { + // List Template Request + // in:query + Tag string `json:"tag"` +} + +// List templates response +// swagger:response listResponse +type listResponse struct { + // in:body + Body []domain.Template +} + +//------------------------- +// swagger:route PUT /templates templates createTemplateRequest +// Templates does some amazing stuff. +// responses: +// 200: templatesResponse + +// swagger:parameters createTemplateRequest +type createTemplateRequest struct { + // Create template request + // in:body + Body domain.Template +} + +//------------------------- + +// swagger:route GET /templates/{name} templates getTemplatesRequest +// Get template by name +// responses: +// 200: templatesResponse + +// swagger:parameters getTemplatesRequest +type getTemplatesRequest struct { + // Get Template Request + // in:path + Name string `json:"name"` +} + +//------------------------- + +// swagger:route DELETE /templates/{name} templates deleteTemplatesRequest +// Delete template by name +// responses: +// 200: templatesResponse + +// swagger:parameters deleteTemplatesRequest +type deleteTemplatesRequest struct { + // Delete Template Request + // in:path + Name string `json:"name"` +} + +//------------------------- + +// swagger:route POST /templates/{name}/render templates renderTemplatesRequest +// Render template by name +// responses: +// 200: renderTemplatesResponse + +// swagger:parameters renderTemplatesRequest +type renderTemplatesRequest struct { + // Render Template Request + // in:path + Name string `json:"name"` +} + +// swagger:response renderTemplatesResponse +type renderTemplatesResponse struct { + // Render Template Response + // in:body + Body string +} diff --git a/domain/provider.go b/domain/config.go similarity index 60% rename from domain/provider.go rename to domain/config.go index e71bce76..41ee1e5b 100644 --- a/domain/provider.go +++ b/domain/config.go @@ -2,12 +2,12 @@ package domain // DBConfig contains the database configuration type DBConfig struct { - Host string `mapstructure:"host"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` + Host string `mapstructure:"host" default:"localhost"` + User string `mapstructure:"user" default:"postgres"` + Password string `mapstructure:"password" default:""` Name string `mapstructure:"name" default:"postgres"` Port string `mapstructure:"port" default:"5432"` - SslMode string `mapstructure:"sslmode"` + SslMode string `mapstructure:"sslmode" default:"disable"` } // Config contains the application configuration diff --git a/domain/templates.go b/domain/templates.go new file mode 100644 index 00000000..16a54ff0 --- /dev/null +++ b/domain/templates.go @@ -0,0 +1,32 @@ +package domain + +import ( + "time" +) + +type Variable struct { + Name string `json:"name"` + Type string `json:"type"` + Default string `json:"default"` + Description string `json:"description"` +} + +type Template struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"CreatedAt"` + UpdatedAt time.Time `json:"UpdatedAt"` + Name string `json:"name"` + Body string `json:"body"` + Tags []string `json:"tags"` + Variables []Variable `json:"variables"` +} + +// TemplatesService interface +type TemplatesService interface { + Upsert(*Template) (*Template, error) + Index(string) ([]Template, error) + GetByName(string) (*Template, error) + Delete(string) error + Render(string, map[string]string) (string, error) + Migrate() error +} diff --git a/go.mod b/go.mod index 15dc1109..4d788f95 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,29 @@ module github.com/odpf/siren go 1.15 require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/go-openapi/loads v0.20.1 // indirect + github.com/go-openapi/runtime v0.19.26 + github.com/go-openapi/spec v0.20.2 // indirect github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/jeremywohl/flatten v1.0.1 + github.com/lib/pq v1.3.0 + github.com/magiconair/properties v1.8.4 // indirect github.com/mcuadros/go-defaults v1.2.0 - github.com/mitchellh/mapstructure v1.1.2 + github.com/mitchellh/mapstructure v1.4.1 + github.com/pelletier/go-toml v1.8.1 // indirect + github.com/spf13/afero v1.4.1 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v1.1.3 + github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.5.1 + github.com/stretchr/testify v1.6.1 + golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 // indirect + golang.org/x/sys v0.0.0-20201126233918-771906719818 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/ini.v1 v1.62.0 // indirect gorm.io/driver/postgres v1.0.8 gorm.io/gorm v1.20.12 ) diff --git a/go.sum b/go.sum index eff2524b..efc526d2 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,28 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= @@ -36,22 +52,142 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/analysis v0.19.16 h1:Ub9e++M8sDwtHD+S587TYi+6ANBG1NRYGZDihqk0SaY= +github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9 h1:9SnKdGhiPZHF3ttwFMiCBEb8jQ4IDdrK+5+a0oTygA4= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4= +github.com/go-openapi/loads v0.20.1 h1:LX55ObGRfG+53/1KRKTvCfqC1U2Htf7KgkPBpIVhuUM= +github.com/go-openapi/loads v0.20.1/go.mod h1:/6LfFL8fDvTSX8ypmYXIq3U9Q7nfniSOStW22m864WM= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= +github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.19.26 h1:K/6PoVNj5WJXUnMk+VEbELeXjtBkCS1UxTDa04tdXE0= +github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= +github.com/go-openapi/spec v0.20.2 h1:pFPUZsiIbZ20kLUcuCGeuQWG735fPMxW7wHF9BWlnQU= +github.com/go-openapi/spec v0.20.2/go.mod h1:RW6Xcbs6LOyWLU/mXGdzn2Qc+3aj+ASfI7rvSZh1Vls= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.0 h1:l2omNtmNbMc39IGptl9BuXBEKcZfS8zjrTsPKTiJiDM= +github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/go-openapi/swag v0.19.13 h1:233UVgMy1DlmCYYfOiFpta6e2urloh+sEs5id6lyzog= +github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= +github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= +github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI= +github.com/go-openapi/validate v0.20.1 h1:QGQ5CvK74E28t3DkegGweKR+auemUi5IdpMc4x3UW6s= +github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -64,14 +200,20 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU 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/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -165,23 +307,36 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -189,15 +344,28 @@ github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= +github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc= @@ -212,18 +380,33 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -237,6 +420,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= @@ -245,11 +430,13 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -260,12 +447,19 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ= +github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +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.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -281,12 +475,27 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.5 h1:TLtO+iD8krabXxvY1F1qpBOHgOxhLWR7XsT7kQeRmMY= +go.mongodb.org/mongo-driver v1.4.5/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -303,14 +512,21 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc= +golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 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= @@ -326,11 +542,13 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHl 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/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= 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/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-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -338,12 +556,24 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r 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-20190320064053-1272bf9dcd53/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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= @@ -351,7 +581,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ 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-20190412183630-56d357773e84/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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -361,37 +593,61 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/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-20201126233918-771906719818 h1:f1CIuDlJhwANEC2MM87MBEVMr3jl5bifgsfj90XAF9c= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 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.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/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= @@ -406,6 +662,8 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +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= @@ -437,13 +695,21 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/gorm v1.20.12 h1:ebZ5KrSHzet+sqOCVdH9mTjW91L298nX3v5lVxAzSUY= diff --git a/main.go b/main.go index 832954c5..6a64ab82 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,9 @@ package main -import "github.com/odpf/siren/cmd" +import ( + "github.com/odpf/siren/cmd" + _ "github.com/odpf/siren/docs" +) func main() { cmd.Execute() diff --git a/mocks/TemplatesService.go b/mocks/TemplatesService.go new file mode 100644 index 00000000..3878ec70 --- /dev/null +++ b/mocks/TemplatesService.go @@ -0,0 +1,131 @@ +// Code generated by mockery v2.6.0. DO NOT EDIT. + +package mocks + +import ( + domain "github.com/odpf/siren/domain" + mock "github.com/stretchr/testify/mock" +) + +// TemplatesService is an autogenerated mock type for the TemplatesService type +type TemplatesService struct { + mock.Mock +} + +// Delete provides a mock function with given fields: _a0 +func (_m *TemplatesService) Delete(_a0 string) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetByName provides a mock function with given fields: _a0 +func (_m *TemplatesService) GetByName(_a0 string) (*domain.Template, error) { + ret := _m.Called(_a0) + + var r0 *domain.Template + if rf, ok := ret.Get(0).(func(string) *domain.Template); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*domain.Template) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Index provides a mock function with given fields: _a0 +func (_m *TemplatesService) Index(_a0 string) ([]domain.Template, error) { + ret := _m.Called(_a0) + + var r0 []domain.Template + if rf, ok := ret.Get(0).(func(string) []domain.Template); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]domain.Template) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Migrate provides a mock function with given fields: +func (_m *TemplatesService) Migrate() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Render provides a mock function with given fields: _a0, _a1 +func (_m *TemplatesService) Render(_a0 string, _a1 map[string]string) (string, error) { + ret := _m.Called(_a0, _a1) + + var r0 string + if rf, ok := ret.Get(0).(func(string, map[string]string) string); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, map[string]string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Upsert provides a mock function with given fields: _a0 +func (_m *TemplatesService) Upsert(_a0 *domain.Template) (*domain.Template, error) { + ret := _m.Called(_a0) + + var r0 *domain.Template + if rf, ok := ret.Get(0).(func(*domain.Template) *domain.Template); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*domain.Template) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*domain.Template) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/mocks/store.go b/mocks/store.go new file mode 100644 index 00000000..292f4960 --- /dev/null +++ b/mocks/store.go @@ -0,0 +1,24 @@ +package mocks + +import ( + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +// NewStore returns store mock +func NewStore() (*gorm.DB, sqlmock.Sqlmock, error) { + sqldb, mock, err := sqlmock.New() + if err != nil { + return nil, nil, err + } + + db, err := gorm.Open(postgres.New(postgres.Config{ + Conn: sqldb, + }), &gorm.Config{}) + if err != nil { + return nil, nil, err + } + + return db, mock, nil +} diff --git a/service/container.go b/service/container.go new file mode 100644 index 00000000..9b05a9e7 --- /dev/null +++ b/service/container.go @@ -0,0 +1,27 @@ +package service + +import ( + "github.com/odpf/siren/domain" + "github.com/odpf/siren/templates" + "gorm.io/gorm" +) + +type Container struct { + TemplatesService domain.TemplatesService +} + +func Init(db *gorm.DB) *Container { + templatesService := templates.NewService(db) + return &Container{ + TemplatesService: templatesService, + } +} + +func MigrateAll(db *gorm.DB) error { + container := Init(db) + err := container.TemplatesService.Migrate() + if err != nil { + return err + } + return nil +} diff --git a/store/store.go b/store/store.go index 3329c36a..42e4a396 100644 --- a/store/store.go +++ b/store/store.go @@ -3,6 +3,7 @@ package store import ( "fmt" "github.com/odpf/siren/domain" + "gorm.io/gorm/logger" "log" "gorm.io/driver/postgres" @@ -12,24 +13,19 @@ import ( // New returns the database instance func New(c *domain.DBConfig) (*gorm.DB, error) { dsn := fmt.Sprintf( - "host=%s user=%s dbname=%s password=%s port=%s sslmode=%s", + "host=%s user=%s dbname=%s port=%s sslmode=%s password=%s ", c.Host, c.User, c.Name, - c.Password, c.Port, c.SslMode, + c.Password, ) - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Info)}) if err != nil { log.Panic(err) } return db, err } - -// Migrate auto migrate models -func Migrate(db *gorm.DB, models ...interface{}) error { - return db.AutoMigrate(models...) -} diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 00000000..84ae31b5 --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,164 @@ +basePath: / +consumes: +- application/json +definitions: + Template: + properties: + CreatedAt: + format: date-time + type: string + UpdatedAt: + format: date-time + type: string + body: + type: string + x-go-name: Body + id: + format: uint64 + type: integer + x-go-name: ID + name: + type: string + x-go-name: Name + tags: + items: + type: string + type: array + x-go-name: Tags + variables: + items: + $ref: '#/definitions/Variable' + type: array + x-go-name: Variables + type: object + x-go-package: github.com/odpf/siren/domain + Variable: + properties: + default: + type: string + x-go-name: Default + description: + type: string + x-go-name: Description + name: + type: string + x-go-name: Name + type: + type: string + x-go-name: Type + type: object + x-go-package: github.com/odpf/siren/domain +host: localhost:3000 +info: + description: Documentation of our Siren API. + title: Siren. + version: 1.0.0 +paths: + /ping: + get: + description: Ping call + operationId: ping + responses: + "200": + $ref: '#/responses/pingResponse' + tags: + - ping + /templates: + get: + description: Templates list call + operationId: listTemplatesRequest + parameters: + - description: List Template Request + in: query + name: tag + type: string + x-go-name: Tag + responses: + "200": + $ref: '#/responses/listResponse' + tags: + - templates + put: + operationId: createTemplateRequest + parameters: + - description: Create template request + in: body + name: Body + schema: + $ref: '#/definitions/Template' + responses: + "200": + $ref: '#/responses/templatesResponse' + summary: Templates does some amazing stuff. + tags: + - templates + /templates/{name}: + delete: + description: Delete template by name + operationId: deleteTemplatesRequest + parameters: + - description: Delete Template Request + in: path + name: name + required: true + type: string + x-go-name: Name + responses: + "200": + $ref: '#/responses/templatesResponse' + tags: + - templates + get: + description: Get template by name + operationId: getTemplatesRequest + parameters: + - description: Get Template Request + in: path + name: name + required: true + type: string + x-go-name: Name + responses: + "200": + $ref: '#/responses/templatesResponse' + tags: + - templates + /templates/{name}/render: + post: + description: Render template by name + operationId: renderTemplatesRequest + parameters: + - description: Render Template Request + in: path + name: name + required: true + type: string + x-go-name: Name + responses: + "200": + $ref: '#/responses/renderTemplatesResponse' + tags: + - templates +produces: +- application/json +responses: + listResponse: + description: List templates response + schema: + items: + $ref: '#/definitions/Template' + type: array + pingResponse: + description: Response body for Ping. + renderTemplatesResponse: + description: "" + templatesResponse: + description: "" + schema: + $ref: '#/definitions/Template' +schemes: +- http +securityDefinitions: + basic: + type: basic +swagger: "2.0" diff --git a/templates/model.go b/templates/model.go new file mode 100644 index 00000000..bd6d9139 --- /dev/null +++ b/templates/model.go @@ -0,0 +1,61 @@ +package templates + +import ( + "encoding/json" + "github.com/lib/pq" + "github.com/odpf/siren/domain" + "time" +) + +type Template struct { + ID uint `gorm:"primarykey"` + CreatedAt time.Time + UpdatedAt time.Time + Name string `gorm:"index:idx_name,unique"` + Body string + Tags pq.StringArray `gorm:"type:text[];index:idx_tags,type:gin"` + Variables string `gorm:"type:jsonb" sql:"type:jsonb" ` +} + +// TemplatesRepositoryMock interface +type TemplatesRepository interface { + Upsert(*Template) (*Template, error) + Index(string) ([]Template, error) + GetByName(string) (*Template, error) + Delete(string) error + Render(string, map[string]string) (string, error) + Migrate() error +} + +func (template *Template) fromDomain(t *domain.Template) (*Template, error) { + template.ID = t.ID + template.CreatedAt = t.CreatedAt + template.UpdatedAt = t.UpdatedAt + template.Name = t.Name + template.Tags = t.Tags + template.Body = t.Body + jsonString, err := json.Marshal(t.Variables) + if err != nil { + return nil, err + } + template.Variables = string(jsonString) + return template, nil +} + +func (template *Template) toDomain() (*domain.Template, error) { + var variables []domain.Variable + jsonBlob := []byte(template.Variables) + err := json.Unmarshal(jsonBlob, &variables) + if err != nil { + return nil, err + } + return &domain.Template{ + ID: template.ID, + Name: template.Name, + Body: template.Body, + Tags: template.Tags, + CreatedAt: template.CreatedAt, + UpdatedAt: template.UpdatedAt, + Variables: variables, + }, nil +} diff --git a/templates/repository.go b/templates/repository.go new file mode 100644 index 00000000..be231123 --- /dev/null +++ b/templates/repository.go @@ -0,0 +1,125 @@ +package templates + +import ( + "bytes" + "errors" + "fmt" + "github.com/odpf/siren/domain" + "gorm.io/gorm" + "text/template" +) + +const ( + leftDelim = "[[" + rightDelim = "]]" +) + +// Repository talks to the store to read or insert data +type Repository struct { + db *gorm.DB +} + +// NewRepository returns repository struct +func NewRepository(db *gorm.DB) *Repository { + return &Repository{db} +} + +func (r Repository) Migrate() error { + err := r.db.AutoMigrate(&Template{}) + if err != nil { + return err + } + return nil +} + +func (r Repository) Upsert(template *Template) (*Template, error) { + var newTemplate, existingTemplate Template + result := r.db.Where(fmt.Sprintf("name = '%s'", template.Name)).Find(&existingTemplate) + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + result = r.db.Create(template) + } else { + result = r.db.Where("id = ?", existingTemplate.ID).Updates(template) + } + if result.Error != nil { + return nil, result.Error + } + result = r.db.Where(fmt.Sprintf("name = '%s'", template.Name)).Find(&newTemplate) + if result.Error != nil { + return nil, result.Error + } + return &newTemplate, nil +} + +func (r Repository) Index(tag string) ([]Template, error) { + var templates []Template + var result *gorm.DB + if tag == "" { + result = r.db.Find(&templates) + } else { + result = r.db.Where("tags @>ARRAY[?]", tag).Find(&templates) + } + if result.Error != nil { + return nil, result.Error + } + return templates, nil +} + +func (r Repository) GetByName(name string) (*Template, error) { + var template Template + result := r.db.Where(fmt.Sprintf("name = '%s'", name)).Find(&template) + if result.Error != nil { + return nil, result.Error + } + if result.RowsAffected == 0 { + return nil, nil + } + return &template, nil +} + +func (r Repository) Delete(name string) error { + var template Template + result := r.db.Where("name = ?", name).Delete(&template) + return result.Error +} + +func enrichWithDefaults(variables []domain.Variable, requestVariables map[string]string) map[string]string { + result := make(map[string]string) + for i := 0; i < len(variables); i++ { + name := variables[i].Name + defaultValue := variables[i].Default + val, ok := requestVariables[name] + if ok { + result[name] = val + } else { + result[name] = defaultValue + } + } + return result +} + +var templateParser = template.New("parser").Delims(leftDelim, rightDelim).Parse + +func (r Repository) Render(name string, requestVariables map[string]string) (string, error) { + templateFromDB, err := r.GetByName(name) + if err != nil { + return "", err + } + if templateFromDB == nil { + return "", errors.New("template not found") + } + convertedTemplate, err := templateFromDB.toDomain() + enrichedVariables := enrichWithDefaults(convertedTemplate.Variables, requestVariables) + var tpl bytes.Buffer + tmpl, err := templateParser(convertedTemplate.Body) + if err != nil { + return "", err + } + err = tmpl.Execute(&tpl, enrichedVariables) + if err != nil { + return "", err + } + return tpl.String(), nil +} diff --git a/templates/repository_mock.go b/templates/repository_mock.go new file mode 100644 index 00000000..76919adf --- /dev/null +++ b/templates/repository_mock.go @@ -0,0 +1,128 @@ +package templates + +import ( + "github.com/stretchr/testify/mock" +) + +// TemplatesRepositoryMock is an autogenerated mock type for the TemplatesRepositoryMock type +type TemplatesRepositoryMock struct { + mock.Mock +} + +// Delete provides a mock function with given fields: _a0 +func (_m *TemplatesRepositoryMock) Delete(_a0 string) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetByName provides a mock function with given fields: _a0 +func (_m *TemplatesRepositoryMock) GetByName(_a0 string) (*Template, error) { + ret := _m.Called(_a0) + + var r0 *Template + if rf, ok := ret.Get(0).(func(string) *Template); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*Template) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Index provides a mock function with given fields: _a0 +func (_m *TemplatesRepositoryMock) Index(_a0 string) ([]Template, error) { + ret := _m.Called(_a0) + + var r0 []Template + if rf, ok := ret.Get(0).(func(string) []Template); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Template) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Migrate provides a mock function with given fields: +func (_m *TemplatesRepositoryMock) Migrate() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Render provides a mock function with given fields: _a0, _a1 +func (_m *TemplatesRepositoryMock) Render(_a0 string, _a1 map[string]string) (string, error) { + ret := _m.Called(_a0, _a1) + + var r0 string + if rf, ok := ret.Get(0).(func(string, map[string]string) string); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, map[string]string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Upsert provides a mock function with given fields: _a0 +func (_m *TemplatesRepositoryMock) Upsert(_a0 *Template) (*Template, error) { + ret := _m.Called(_a0) + + var r0 *Template + if rf, ok := ret.Get(0).(func(*Template) *Template); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*Template) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*Template) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/templates/repository_test.go b/templates/repository_test.go new file mode 100644 index 00000000..49974805 --- /dev/null +++ b/templates/repository_test.go @@ -0,0 +1,474 @@ +package templates + +import ( + "database/sql" + "database/sql/driver" + "errors" + "github.com/DATA-DOG/go-sqlmock" + "github.com/odpf/siren/mocks" + "github.com/stretchr/testify/suite" + "regexp" + "testing" + "text/template" + "time" +) + +// AnyTime is used to expect arbitrary time value +type AnyTime struct{} + +// Match satisfies sqlmock.Argument interface +func (a AnyTime) Match(v driver.Value) bool { + _, ok := v.(time.Time) + return ok +} + +type RepositoryTestSuite struct { + suite.Suite + sqldb *sql.DB + dbmock sqlmock.Sqlmock + repository TemplatesRepository +} + +func (s *RepositoryTestSuite) SetupTest() { + db, mock, _ := mocks.NewStore() + s.sqldb, _ = db.DB() + s.dbmock = mock + s.repository = NewRepository(db) +} + +func (s *RepositoryTestSuite) TearDownTest() { + s.sqldb.Close() +} + +func (s *RepositoryTestSuite) TestIndex() { + + s.Run("should get all templates if tag is not passed", func() { + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates"`) + template := Template{ + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + expectedTemplates := []Template{template} + expectedRows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(template.ID, template.CreatedAt, + template.UpdatedAt, template.Name, + template.Body, template.Tags, + template.Variables) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(expectedRows) + + actualTemplates, err := s.repository.Index("") + s.Equal(expectedTemplates, actualTemplates) + s.Nil(err) + }) + + s.Run("should get templates of matching tags", func() { + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE tags @>ARRAY[$1]`) + template := Template{ + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "foo", + Body: "bar", + Tags: []string{"foo"}, + Variables: `{"name":"test"}`, + } + expectedTemplates := []Template{template} + expectedRows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(template.ID, template.CreatedAt, template.UpdatedAt, template.Name, template.Body, template.Tags, template.Variables) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(expectedRows) + + actualTemplates, err := s.repository.Index("foo") + s.Equal(expectedTemplates, actualTemplates) + s.Nil(err) + }) + + s.Run("should return error if any", func() { + expectedErrorMessage := "random error" + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates"`) + s.dbmock.ExpectQuery(expectedQuery).WillReturnError(errors.New("random error")) + + actualTemplates, err := s.repository.Index("") + s.Equal(err.Error(), expectedErrorMessage) + s.Empty(actualTemplates) + }) +} + +func (s *RepositoryTestSuite) TestGetByName() { + + s.Run("should get template by name", func() { + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + expectedTemplate := &Template{ + ID: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + expectedRows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(expectedRows) + + actualTemplates, err := s.repository.GetByName("foo") + s.Equal(expectedTemplate, actualTemplates) + s.Nil(err) + }) + + s.Run("should return nil if template not found", func() { + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(sqlmock.NewRows(nil)) + + actualTemplate, err := s.repository.GetByName("foo") + s.Nil(actualTemplate) + s.Nil(err) + }) + + s.Run("should return error if any", func() { + expectedErrorMessage := "random error" + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + s.dbmock.ExpectQuery(expectedQuery).WillReturnError(errors.New("random error")) + + actualTemplates, err := s.repository.GetByName("foo") + s.Equal(err.Error(), expectedErrorMessage) + s.Empty(actualTemplates) + }) +} + +func (s *RepositoryTestSuite) TestDelete() { + + s.Run("should delete template by name", func() { + deleteQuery := regexp.QuoteMeta(`DELETE FROM "templates" WHERE name = $1`) + s.dbmock.ExpectExec(deleteQuery).WithArgs("foo").WillReturnResult(sqlmock.NewResult(0, 1)) + err := s.repository.Delete("foo") + s.Nil(err) + }) + + s.Run("should return error if any", func() { + expectedErrorMessage := "random error" + deleteQuery := regexp.QuoteMeta(`DELETE FROM "templates" WHERE name = $1`) + s.dbmock.ExpectExec(deleteQuery).WithArgs("foo").WillReturnError(errors.New(expectedErrorMessage)) + err := s.repository.Delete("foo") + s.Equal(err.Error(), expectedErrorMessage) + }) +} + +func (s *RepositoryTestSuite) TestUpsert() { + + s.Run("should insert template if not exist", func() { + firstSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + insertQuery := regexp.QuoteMeta(`INSERT INTO "templates" ("created_at","updated_at","name","body","tags","variables","id") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`) + secondSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + expectedTemplate := &Template{ + ID: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + expectedRows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + s.dbmock.ExpectQuery(firstSelectQuery).WillReturnRows(sqlmock.NewRows(nil)) + s.dbmock.ExpectQuery(insertQuery).WithArgs(expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables, expectedTemplate.ID). + WillReturnRows(sqlmock.NewRows(nil)) + s.dbmock.ExpectQuery(secondSelectQuery).WillReturnRows(expectedRows) + actualTemplate, err := s.repository.Upsert(expectedTemplate) + s.Equal(expectedTemplate, actualTemplate) + s.Nil(err) + }) + + s.Run("should update template if exist", func() { + firstSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + updateQuery := regexp.QuoteMeta(`UPDATE "templates" SET "created_at"=$1,"updated_at"=$2,"name"=$3,"body"=$4,"tags"=$5,"variables"=$6 WHERE id = $7`) + secondSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + timeNow := time.Now() + expectedTemplate := &Template{ + ID: 10, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + input := &Template{ + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + + expectedRows1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + + expectedRows2 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + + s.dbmock.ExpectQuery(firstSelectQuery).WillReturnRows(expectedRows1) + s.dbmock.ExpectExec(updateQuery).WithArgs(AnyTime{}, + AnyTime{}, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables, expectedTemplate.ID). + WillReturnResult(sqlmock.NewResult(10, 1)) + s.dbmock.ExpectQuery(secondSelectQuery).WillReturnRows(expectedRows2) + actualTemplate, err := s.repository.Upsert(input) + s.Equal(expectedTemplate, actualTemplate) + s.Nil(err) + }) + + s.Run("should return error if first select query fails", func() { + expectedErrorMessage := "random error" + firstSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + s.dbmock.ExpectQuery(firstSelectQuery).WillReturnError(errors.New("random error")) + timeNow := time.Now() + input := &Template{ + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + actualTemplate, err := s.repository.Upsert(input) + s.Equal(err.Error(), expectedErrorMessage) + s.Empty(actualTemplate) + }) + + s.Run("should return error if insert fails", func() { + expectedErrorMessage := "random error" + firstSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + insertQuery := regexp.QuoteMeta(`INSERT INTO "templates" ("created_at","updated_at","name","body","tags","variables","id") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`) + timeNow := time.Now() + expectedTemplate := &Template{ + ID: 10, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + input := &Template{ + ID: 10, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + s.dbmock.ExpectQuery(firstSelectQuery).WillReturnRows(sqlmock.NewRows(nil)) + s.dbmock.ExpectQuery(insertQuery).WithArgs(expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables, expectedTemplate.ID).WillReturnError(errors.New("random error")) + + actualTemplate, err := s.repository.Upsert(input) + s.Equal(err.Error(), expectedErrorMessage) + s.Empty(actualTemplate) + }) + + s.Run("should return error if update fails", func() { + expectedErrorMessage := "random error" + firstSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + updateQuery := regexp.QuoteMeta(`UPDATE "templates" SET "created_at"=$1,"updated_at"=$2,"name"=$3,"body"=$4,"tags"=$5,"variables"=$6 WHERE id = $7`) + timeNow := time.Now() + expectedTemplate := &Template{ + ID: 10, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + input := &Template{ + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + + expectedRows1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + + s.dbmock.ExpectQuery(firstSelectQuery).WillReturnRows(expectedRows1) + s.dbmock.ExpectExec(updateQuery).WithArgs(AnyTime{}, + AnyTime{}, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables, expectedTemplate.ID). + WillReturnError(errors.New(expectedErrorMessage)) + + actualTemplate, err := s.repository.Upsert(input) + s.Equal(err.Error(), expectedErrorMessage) + s.Empty(actualTemplate) + }) + + s.Run("should return error if second select query fails", func() { + expectedErrorMessage := "random error" + firstSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + updateQuery := regexp.QuoteMeta(`UPDATE "templates" SET "created_at"=$1,"updated_at"=$2,"name"=$3,"body"=$4,"tags"=$5,"variables"=$6 WHERE id = $7`) + secondSelectQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + timeNow := time.Now() + expectedTemplate := &Template{ + ID: 10, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + input := &Template{ + CreatedAt: timeNow, + UpdatedAt: timeNow, + Name: "foo", + Body: "bar", + Tags: []string{"baz"}, + Variables: `{"name":"foo"}`, + } + + expectedRows1 := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + + s.dbmock.ExpectQuery(firstSelectQuery).WillReturnRows(expectedRows1) + s.dbmock.ExpectExec(updateQuery).WithArgs(AnyTime{}, + AnyTime{}, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables, expectedTemplate.ID). + WillReturnResult(sqlmock.NewResult(10, 1)) + s.dbmock.ExpectQuery(secondSelectQuery).WillReturnError(errors.New(expectedErrorMessage)) + actualTemplate, err := s.repository.Upsert(input) + s.Equal(err.Error(), expectedErrorMessage) + s.Empty(actualTemplate) + }) +} + +func (s *RepositoryTestSuite) TestRender() { + + s.Run("should render template body from the input", func() { + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + expectedTemplate := &Template{ + ID: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "foo", + Body: "The quick [[.color]] fox jumped over the [[.adjective]] dog.", + Tags: []string{"baz"}, + Variables: `[{"name":"color","default":"brown","type":"string","description":"test"}, {"name":"adjective","default":"lazy","type":"string","description":"test"}]`, + } + expectedRows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(expectedRows) + expectedBody := "The quick red fox jumped over the dumb dog." + inputBody := make(map[string]string) + inputBody["color"] = "red" + inputBody["adjective"] = "dumb" + renderedBody, err := s.repository.Render("foo", inputBody) + s.Equal(expectedBody, renderedBody) + s.Nil(err) + }) + + s.Run("should render template body enriched with defaults", func() { + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + expectedTemplate := &Template{ + ID: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "foo", + Body: "The quick [[.color]] fox jumped over the [[.adjective]] dog.", + Tags: []string{"baz"}, + Variables: `[{"name":"color","default":"red","type":"string","description":"test"}, {"name":"adjective","default":"lazy","type":"string","description":"test"}]`, + } + expectedRows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(expectedRows) + expectedBody := "The quick brown fox jumped over the lazy dog." + inputBody := make(map[string]string) + inputBody["color"] = "brown" + renderedBody, err := s.repository.Render("foo", inputBody) + s.Equal(expectedBody, renderedBody) + s.Nil(err) + }) + + s.Run("should return error if template not found", func() { + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(sqlmock.NewRows(nil)) + inputBody := make(map[string]string) + inputBody["color"] = "brown" + renderedBody, err := s.repository.Render("foo", inputBody) + s.Equal(err.Error(), "template not found") + s.Equal("", renderedBody) + }) + + s.Run("should return error if any in template parse", func() { + expectedErrorMessage := "random error" + expectedQuery := regexp.QuoteMeta(`SELECT * FROM "templates" WHERE name = 'foo'`) + expectedTemplate := &Template{ + ID: 10, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "foo", + Body: "The quick [[.color]] fox jumped over the [[.adjective]] dog.", + Tags: []string{"baz"}, + Variables: `[{"name":"color","default":"red","type":"string","description":"test"}, {"name":"adjective","default":"lazy","type":"string","description":"test"}]`, + } + expectedRows := sqlmock.NewRows([]string{"id", "created_at", "updated_at", "name", "body", "tags", "variables"}). + AddRow(expectedTemplate.ID, expectedTemplate.CreatedAt, + expectedTemplate.UpdatedAt, expectedTemplate.Name, + expectedTemplate.Body, expectedTemplate.Tags, + expectedTemplate.Variables) + s.dbmock.ExpectQuery(expectedQuery).WillReturnRows(expectedRows) + inputBody := make(map[string]string) + oldTemplateParse := templateParser + defer func() { templateParser = oldTemplateParse }() + templateParser = func(_ string) (*template.Template, error) { + return nil, errors.New(expectedErrorMessage) + } + renderedBody, err := s.repository.Render("foo", inputBody) + s.Equal(err.Error(), expectedErrorMessage) + s.Equal("", renderedBody) + }) +} + +func TestRepository(t *testing.T) { + suite.Run(t, new(RepositoryTestSuite)) +} diff --git a/templates/service.go b/templates/service.go new file mode 100644 index 00000000..523b45d5 --- /dev/null +++ b/templates/service.go @@ -0,0 +1,82 @@ +package templates + +import ( + "errors" + "github.com/odpf/siren/domain" + "gorm.io/gorm" + "strings" +) + +// Service handles business logic +type Service struct { + repository TemplatesRepository +} + +// NewService returns repository struct +func NewService(db *gorm.DB) domain.TemplatesService { + return &Service{NewRepository(db)} +} + +func (service Service) Migrate() error { + return service.repository.Migrate() +} + +func (service Service) Upsert(template *domain.Template) (*domain.Template, error) { + t := &Template{} + t, err := t.fromDomain(template) + if err != nil { + return nil, err + } + err = isValid(template) + if err != nil { + return nil, err + } + upsertedTemplate, err := service.repository.Upsert(t) + if err != nil { + return nil, err + } + return upsertedTemplate.toDomain() +} + +func trimmer(x string) string { + return strings.Trim(x, " ") +} + +func isValid(template *domain.Template) error { + if trimmer(template.Name) == "" { + return errors.New("name cannot be empty") + } + if trimmer(template.Body) == "" { + return errors.New("body cannot be empty") + } + return nil +} + +func (service Service) Index(tag string) ([]domain.Template, error) { + templates, err := service.repository.Index(tag) + if err != nil { + return nil, err + } + domainTemplates := make([]domain.Template, 0, len(templates)) + for i := 0; i < len(templates); i++ { + t, _ := templates[i].toDomain() + domainTemplates = append(domainTemplates, *t) + } + return domainTemplates, nil +} + +func (service Service) GetByName(name string) (*domain.Template, error) { + template, err := service.repository.GetByName(name) + if err != nil || template == nil { + return nil, err + } + return template.toDomain() +} + +func (service Service) Delete(name string) error { + return service.repository.Delete(name) +} + +func (service Service) Render(name string, body map[string]string) (string, error) { + return service.repository.Render(name, body) +} diff --git a/templates/service_test.go b/templates/service_test.go new file mode 100644 index 00000000..8ba2d2a5 --- /dev/null +++ b/templates/service_test.go @@ -0,0 +1,242 @@ +package templates + +import ( + "errors" + "github.com/odpf/siren/domain" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestService_GetByName(t *testing.T) { + t.Run("should call repository GetByName method and return result in domain's type", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + dummyTemplate := &domain.Template{ + ID: 1, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }, + }, + } + modelTemplate := &Template{ + ID: 1, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: `[{"name":"test-name", "default": "test-default", "description": "test-description", "type": "test-type"}]`, + } + repositoryMock.On("GetByName", "foo").Return(modelTemplate, nil).Once() + result, err := dummyService.GetByName("foo") + assert.Nil(t, err) + assert.Equal(t, dummyTemplate, result) + repositoryMock.AssertCalled(t, "GetByName", "foo") + }) + + t.Run("should call repository GetByName method and return nil if template not found", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + repositoryMock.On("GetByName", "foo").Return(nil, nil).Once() + result, err := dummyService.GetByName("foo") + assert.Nil(t, err) + assert.Nil(t, result) + }) + + t.Run("should call repository GetByName method and return error if any", func(t *testing.T) { + expectedError := errors.New("unexpected error") + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + repositoryMock.On("GetByName", "foo").Return(nil, expectedError).Once() + result, err := dummyService.GetByName("foo") + assert.Nil(t, result) + assert.EqualError(t, err, expectedError.Error()) + }) +} + +func TestService_Delete(t *testing.T) { + t.Run("should call repository Delete method and return result", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + repositoryMock.On("Delete", "foo").Return(nil).Once() + result := dummyService.Delete("foo") + assert.Nil(t, result) + repositoryMock.AssertCalled(t, "Delete", "foo") + }) + + t.Run("should call repository Delete method and return error", func(t *testing.T) { + expectedError := errors.New("unexpected error") + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + repositoryMock.On("Delete", "foo").Return(expectedError).Once() + result := dummyService.Delete("foo") + assert.EqualError(t, result, expectedError.Error()) + }) +} + +func TestService_Index(t *testing.T) { + t.Run("should call repository Index method and return result in domain's type", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + dummyTemplate := []domain.Template{{ + ID: 1, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }, + }, + }} + modelTemplates := []Template{{ + ID: 1, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: `[{"name":"test-name", "default": "test-default", "description": "test-description", "type": "test-type"}]`, + }} + repositoryMock.On("Index", "foo").Return(modelTemplates, nil).Once() + result, err := dummyService.Index("foo") + assert.Nil(t, err) + assert.Equal(t, dummyTemplate, result) + repositoryMock.AssertCalled(t, "Index", "foo") + }) + + t.Run("should call repository Index method and return error if any", func(t *testing.T) { + expectedError := errors.New("unexpected error") + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + repositoryMock.On("Index", "foo").Return(nil, expectedError).Once() + result, err := dummyService.Index("foo") + assert.Nil(t, result) + assert.EqualError(t, err, expectedError.Error()) + }) +} + +func TestService_Upsert(t *testing.T) { + t.Run("should perform name validation", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + dummyTemplate := &domain.Template{ + Name: "", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }, + }, + } + result, err := dummyService.Upsert(dummyTemplate) + assert.EqualError(t, err, "name cannot be empty") + assert.Nil(t, result) + repositoryMock.AssertNotCalled(t, "Upsert") + }) + + t.Run("should perform body validation", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + dummyTemplate := &domain.Template{ + Name: "foo", Body: "", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }, + }, + } + result, err := dummyService.Upsert(dummyTemplate) + assert.EqualError(t, err, "body cannot be empty") + assert.Nil(t, result) + repositoryMock.AssertNotCalled(t, "Upsert") + }) + + t.Run("should call repository Upsert method and return result in domain's type", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + dummyTemplate := &domain.Template{ + Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }, + }, + } + modelTemplate := &Template{ + ID: 0, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: `[{"name":"test-name","type":"test-type","default":"test-default","description":"test-description"}]`, + } + repositoryMock.On("Upsert", modelTemplate).Return(modelTemplate, nil).Once() + result, err := dummyService.Upsert(dummyTemplate) + assert.Nil(t, err) + assert.Equal(t, dummyTemplate, result) + repositoryMock.AssertCalled(t, "Upsert", modelTemplate) + }) + + t.Run("should call repository Upsert method and return error if any", func(t *testing.T) { + expectedError := errors.New("unexpected error") + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + dummyTemplate := &domain.Template{ + Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: []domain.Variable{{ + Name: "test-name", + Default: "test-default", + Description: "test-description", + Type: "test-type", + }, + }, + } + modelTemplate := &Template{ + ID: 0, Name: "foo", Body: "bar", + Tags: []string{"test"}, + Variables: `[{"name":"test-name","type":"test-type","default":"test-default","description":"test-description"}]`, + } + repositoryMock.On("Upsert", modelTemplate).Return(nil, expectedError).Once() + result, err := dummyService.Upsert(dummyTemplate) + assert.Nil(t, result) + assert.EqualError(t, err, expectedError.Error()) + }) +} + +func TestService_Render(t *testing.T) { + t.Run("should call repository Render method and return result", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + input := make(map[string]string) + dummyService := Service{repository: repositoryMock} + repositoryMock.On("Render", "foo", input).Return("foo bar baz", nil).Once() + result, err := dummyService.Render("foo", input) + assert.Nil(t, err) + assert.Equal(t, "foo bar baz", result) + repositoryMock.AssertCalled(t, "Render", "foo", input) + }) + + t.Run("should call repository Render method and return error", func(t *testing.T) { + expectedError := errors.New("unexpected error") + repositoryMock := &TemplatesRepositoryMock{} + input := make(map[string]string) + dummyService := Service{repository: repositoryMock} + repositoryMock.On("Render", "foo", input).Return("", expectedError).Once() + result, err := dummyService.Render("foo", input) + assert.Empty(t, result) + assert.EqualError(t, err, expectedError.Error()) + }) +} + +func TestService_Migrate(t *testing.T) { + t.Run("should call repository Migrate method and return result", func(t *testing.T) { + repositoryMock := &TemplatesRepositoryMock{} + dummyService := Service{repository: repositoryMock} + repositoryMock.On("Migrate").Return(nil).Once() + err := dummyService.Migrate() + assert.Nil(t, err) + repositoryMock.AssertCalled(t, "Migrate") + }) +}