├── .env.dev ├── test.env ├── .env.prod ├── .env.staging ├── .env.profile ├── swagger.png ├── .env.local ├── service ├── card.service.go ├── user.service.go ├── user.serviceimpl_test.go ├── card.serviceimpl.go └── user.serviceimpl.go ├── repository ├── card.repository.go ├── user.repository.go ├── card.repositoryimpl_test.go ├── card.repositoryimpl.go ├── user.repositoryimpl_test.go └── user.repositoryimpl.go ├── model ├── user.go └── card.go ├── go.mod ├── controller ├── dto │ └── dto.go ├── web │ └── web.controller.go └── api │ ├── response_data.go │ ├── card.controller.go │ ├── user.controller_test.go │ └── user.controller.go ├── main.go ├── .gitignore ├── view ├── static │ ├── css │ │ └── common.css │ └── js │ │ ├── user.js │ │ ├── list.js │ │ ├── card.js │ │ ├── detail.js │ │ └── delete.js └── templates │ ├── user.html │ ├── card.html │ ├── delete.html │ ├── list.html │ └── detail.html ├── infrastructure ├── database │ └── sqlstore.go └── server │ └── server.go ├── README2.md ├── common └── logger.go ├── mocks ├── service │ └── UserService.go └── repository │ └── UserRepository.go ├── docs ├── swagger.yaml ├── swagger.json └── docs.go ├── report.json ├── README.md └── go.sum /.env.dev: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.profile: -------------------------------------------------------------------------------- 1 | GO_PROFILE=local -------------------------------------------------------------------------------- /swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaeho310/golang-echo-sample/HEAD/swagger.png -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | DATASOURCE_DRIVER=sqlite3 2 | DATASOURCE_URL=./local.db 3 | DATASOURCE_USERNAME=_ 4 | DATASOURCE_PASSWORD=_ -------------------------------------------------------------------------------- /service/card.service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "platform-sample/controller/dto" 5 | "platform-sample/model" 6 | ) 7 | 8 | type CardService interface { 9 | CreateCard(*dto.CardDto) (*model.Card, error) 10 | DeleteCard(int, int) error 11 | } 12 | -------------------------------------------------------------------------------- /repository/card.repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "platform-sample/controller/dto" 5 | "platform-sample/model" 6 | ) 7 | 8 | type CardRepository interface { 9 | Save(*dto.CardDto) (*model.Card, error) 10 | DeleteById(int, int) error 11 | } 12 | -------------------------------------------------------------------------------- /service/user.service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "platform-sample/model" 4 | 5 | type UserService interface { 6 | GetUsers() ([]*model.User, error) 7 | CreateUser(*model.User) (*model.User, error) 8 | DeleteUser(int) error 9 | GetUser(int) (*model.User, error) 10 | UpdateUser(*model.User) (*model.User, error) 11 | } 12 | -------------------------------------------------------------------------------- /repository/user.repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "platform-sample/model" 5 | ) 6 | 7 | type UserRepository interface { 8 | FindAll() ([]*model.User, error) 9 | FindById(id int) (*model.User, error) 10 | DeleteById(id int) error 11 | Save(user *model.User) (*model.User, error) 12 | Update(user *model.User) (*model.User, error) 13 | } 14 | -------------------------------------------------------------------------------- /model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type User struct { 8 | ID uint `gorm:"primaryKey" json:"id"` 9 | CreatedAt time.Time `json:"createdAt"` 10 | UpdatedAt time.Time `json:"updatedAt"` 11 | DeletedAt *time.Time `json:"deletedAt"` 12 | Name string `json:"name,omitempty"` 13 | Cards []Card `gorm:"foreignKey:UserId"json:"cards"` 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module platform-sample 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 7 | github.com/jinzhu/gorm v1.9.16 8 | github.com/joho/godotenv v1.3.0 9 | github.com/labstack/echo/v4 v4.4.0 10 | github.com/mattn/go-sqlite3 v1.14.8 11 | github.com/stretchr/testify v1.6.1 12 | github.com/swaggo/echo-swagger v1.1.0 13 | github.com/swaggo/swag v1.7.0 14 | ) 15 | -------------------------------------------------------------------------------- /model/card.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type Card struct { 6 | ID uint `gorm:"primaryKey" json:"id"` 7 | CreatedAt time.Time `json:"createdAt"` 8 | UpdatedAt time.Time `json:"updatedAt"` 9 | DeletedAt *time.Time `json:"deletedAt"` 10 | Name string `json:"name,omitempty"` 11 | Limit int `json:"limit,omitempty"` 12 | UserId uint `json:"userId"` 13 | User User 14 | } 15 | -------------------------------------------------------------------------------- /controller/dto/dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import "platform-sample/model" 4 | 5 | type UserDto struct { 6 | Name string `json:"name"` 7 | } 8 | 9 | func (UserDto *UserDto) ToModel() *model.User { 10 | return &model.User{Name: UserDto.Name} 11 | } 12 | 13 | type CardDto struct { 14 | Name string `json:"name"` 15 | Limit int `json:"limit"` 16 | UserId uint `json:"userId"` 17 | } 18 | 19 | func (cardDto *CardDto) ToModel() *model.Card { 20 | return &model.Card{Name: cardDto.Name, Limit: cardDto.Limit, UserId: cardDto.UserId} 21 | } 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/joho/godotenv" 5 | "os" 6 | _ "platform-sample/docs" 7 | "platform-sample/infrastructure/database" 8 | "platform-sample/infrastructure/server" 9 | ) 10 | 11 | func init() { 12 | godotenv.Load(".env.profile") 13 | godotenv.Load(".env." + os.Getenv("GO_PROFILE")) 14 | } 15 | 16 | // @title Platform-sample Swagger API 17 | // @version 1.0.0 18 | // @host localhost:8395 19 | // @BasePath /api 20 | func main() { 21 | db := database.SqlStore{}.GetDb() 22 | defer db.Close() 23 | server.Server{MainDb: db}.Init() 24 | } 25 | -------------------------------------------------------------------------------- /repository/card.repositoryimpl_test.go: -------------------------------------------------------------------------------- 1 | package repository_test 2 | 3 | import ( 4 | "fmt" 5 | "platform-sample/infrastructure/database" 6 | "platform-sample/infrastructure/server" 7 | "platform-sample/repository" 8 | "testing" 9 | ) 10 | 11 | func initCardRepository() *repository.CardRepositoryImpl { 12 | mockDb := database.SqlStore{}.GetMockDb() 13 | mockServer := server.Server{MainDb: mockDb} 14 | return mockServer.InjectCardRepository() 15 | } 16 | 17 | func Test_GetCards(t *testing.T) { 18 | cardRepository := initCardRepository() 19 | cards, err := cardRepository.GetCards() 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | fmt.Println(cards) 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | *.db 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### Jetbrain IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /nbbuild/ 22 | /dist/ 23 | /nbdist/ 24 | /.nb-gradle/ 25 | build/ 26 | !**/src/main/**/build/ 27 | !**/src/test/**/build/ 28 | 29 | ### VS Code ### 30 | .vscode/ 31 | 32 | # Binaries for programs and plugins 33 | *.exe 34 | *.exe~ 35 | *.dll 36 | *.so 37 | *.dylib 38 | 39 | # Test binary, built with `go test -c` 40 | *.test 41 | 42 | # Output of the go coverage tool, specifically when used with LiteIDE 43 | *.out 44 | -------------------------------------------------------------------------------- /service/user.serviceimpl_test.go: -------------------------------------------------------------------------------- 1 | package service_test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | mocks "platform-sample/mocks/repository" 6 | "platform-sample/model" 7 | "platform-sample/service" 8 | "testing" 9 | ) 10 | 11 | func TestGetUser(t *testing.T) { 12 | mockRepository := &mocks.UserRepository{} 13 | userServiceImpl := service.UserServiceImpl{}.NewUserServiceImpl(mockRepository) 14 | 15 | // given 16 | mockUser := &model.User{Name: "Tom"} 17 | mockRepository.On("FindById", 1).Return(mockUser, nil) 18 | 19 | // when 20 | user, err := userServiceImpl.GetUser(1) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | 25 | // then 26 | assert.Equal(t, user, mockUser) 27 | } 28 | -------------------------------------------------------------------------------- /service/card.serviceimpl.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "platform-sample/controller/dto" 5 | "platform-sample/model" 6 | "platform-sample/repository" 7 | ) 8 | 9 | type CardServiceImpl struct { 10 | repository.CardRepository 11 | } 12 | 13 | func (CardServiceImpl) NewCardServiceImpl(repository repository.CardRepository) *CardServiceImpl { 14 | return &CardServiceImpl{repository} 15 | } 16 | 17 | func (cardServiceImpl *CardServiceImpl) CreateCard(cardDto *dto.CardDto) (*model.Card, error) { 18 | return cardServiceImpl.CardRepository.Save(cardDto) 19 | } 20 | 21 | func (cardServiceImpl *CardServiceImpl) DeleteCard(cardId int, userId int) error { 22 | return cardServiceImpl.CardRepository.DeleteById(cardId, userId) 23 | } 24 | -------------------------------------------------------------------------------- /view/static/css/common.css: -------------------------------------------------------------------------------- 1 | 2 | .my_container{ 3 | /* max-width: 50%; */ 4 | text-align: center; 5 | } 6 | 7 | /* 8 | * Custom translucent site header 9 | */ 10 | 11 | .site-header { 12 | background-color: rgba(0, 0, 0, .85); 13 | -webkit-backdrop-filter: saturate(180%) blur(20px); 14 | backdrop-filter: saturate(180%) blur(20px); 15 | } 16 | .site-header a { 17 | color: #8e8e8e; 18 | margin-right: 20px; 19 | transition: color .15s ease-in-out; 20 | } 21 | .site-header a:hover { 22 | color: #fff; 23 | text-decoration: none; 24 | } 25 | 26 | /* 27 | * Dummy devices (replace them with your own or something else entirely!) 28 | */ 29 | 30 | .flex-equal > * { 31 | flex: 1; 32 | } 33 | @media (min-width: 768px) { 34 | .flex-md-equal > * { 35 | flex: 1; 36 | } 37 | } -------------------------------------------------------------------------------- /infrastructure/database/sqlstore.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "os" 5 | "platform-sample/model" 6 | 7 | "github.com/jinzhu/gorm" 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | type SqlStore struct { 12 | } 13 | 14 | func (SqlStore) GetDb() *gorm.DB { 15 | db, err := gorm.Open(os.Getenv("DATASOURCE_DRIVER"), os.Getenv("DATASOURCE_URL")) 16 | 17 | db.LogMode(true) 18 | 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | migrate(db) 24 | return db 25 | } 26 | 27 | func (SqlStore) GetMockDb() *gorm.DB { 28 | db, err := gorm.Open("sqlite3", "./mock.db") 29 | 30 | db.LogMode(true) 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | migrate(db) 37 | 38 | return db 39 | } 40 | 41 | func migrate(db *gorm.DB) { 42 | db.AutoMigrate(&model.User{}) 43 | db.AutoMigrate(&model.Card{}) 44 | } 45 | -------------------------------------------------------------------------------- /controller/web/web.controller.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "net/http" 6 | ) 7 | 8 | type WebController struct { 9 | } 10 | 11 | func (webController WebController) Init(e *echo.Echo) { 12 | e.GET("/", func(c echo.Context) error { 13 | return c.Render(http.StatusOK, "list.html", nil) 14 | }) 15 | e.GET("/list", func(c echo.Context) error { 16 | return c.Render(http.StatusOK, "list.html", nil) 17 | }) 18 | e.GET("/detail/:id", func(c echo.Context) error { 19 | return c.Render(http.StatusOK, "detail.html", map[string]interface{}{"id": c.Param("id")}) 20 | }) 21 | e.GET("/card", func(c echo.Context) error { 22 | return c.Render(http.StatusOK, "card.html", nil) 23 | }) 24 | e.GET("/user", func(c echo.Context) error { 25 | return c.Render(http.StatusOK, "user.html", nil) 26 | }) 27 | 28 | e.GET("/delete", func(c echo.Context) error { 29 | return c.Render(http.StatusOK, "delete.html", nil) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /view/static/js/user.js: -------------------------------------------------------------------------------- 1 | // let token = $("meta[name='_csrf']").attr("content"); 2 | // let header = $("meta[name='_csrf_header']").attr("content"); 3 | let fileList = new Object(); 4 | 5 | $(document).ready(function(){ 6 | console.log("user ready"); 7 | 8 | }); 9 | 10 | $("#reg_btn").on("click", function() { 11 | // console.log("test"); 12 | let name = $("#userName").val(); 13 | console.log(name); 14 | var data = new Object(); 15 | data["name"] = name; 16 | $.ajax({ 17 | url: '/api/users', 18 | contentType: 'application/json', 19 | type: 'post', 20 | data: JSON.stringify(data), 21 | beforeSend: function(xhr) { 22 | // xhr.setRequestHeader(header, token); 23 | }, 24 | success: function(data) { 25 | alert("등록성공"); 26 | $("#userName").val(""); 27 | }, 28 | error: function(request,status,error){ 29 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 30 | } 31 | }) 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /repository/card.repositoryimpl.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "platform-sample/controller/dto" 6 | "platform-sample/model" 7 | ) 8 | 9 | type CardRepositoryImpl struct { 10 | db *gorm.DB 11 | } 12 | 13 | func (CardRepositoryImpl) NewCardRepositoryImpl(db *gorm.DB) *CardRepositoryImpl { 14 | return &CardRepositoryImpl{db} 15 | } 16 | 17 | func (cardRepositoryImpl *CardRepositoryImpl) Save(cardDto *dto.CardDto) (*model.Card, error) { 18 | card := cardDto.ToModel() 19 | err := cardRepositoryImpl.db.Save(card).Error 20 | if err != nil { 21 | return nil, err 22 | } 23 | return card, nil 24 | } 25 | 26 | func (cardRepositoryImpl *CardRepositoryImpl) GetCards() (*[]model.Card, error) { 27 | cards := &[]model.Card{} 28 | err := cardRepositoryImpl.db.Table("cards").Find(&cards).Error 29 | if err != nil { 30 | return nil, err 31 | } 32 | return cards, nil 33 | } 34 | 35 | func (cardRepositoryImpl *CardRepositoryImpl) DeleteById(cardId int, userId int) error { 36 | err := cardRepositoryImpl.db. 37 | Where("id = ?", cardId). 38 | Where("user_id = ?", userId). 39 | Delete(&model.Card{}).Error 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /controller/api/response_data.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "github.com/labstack/echo/v4" 6 | ) 7 | 8 | type ApiResult struct { 9 | Result interface{} `json:"result"` 10 | Success bool `json:"success"` 11 | Error ApiError `json:"error"` 12 | } 13 | 14 | type ApiError struct { 15 | Code int `json:"code,omitempty"` 16 | Message string `json:"message,omitempty"` 17 | Details interface{} `json:"details,omitempty"` 18 | } 19 | 20 | type ArrayResult struct { 21 | Items interface{} `json:"items"` 22 | TotalCount int64 `json:"totalCount"` 23 | } 24 | 25 | var ( 26 | ApiParameterError = ApiError{Code: 601, Message: "failed to parse filter parameters"} 27 | ApiQueryError = ApiError{Code: 602, Message: "failed to query"} 28 | ) 29 | 30 | func ReturnApiFail(c echo.Context, httpStatus int, apiError ApiError, err error, v ...interface{}) error { 31 | return c.JSON(httpStatus, ApiResult{ 32 | Success: false, 33 | Error: ApiError{ 34 | Code: apiError.Code, 35 | Message: fmt.Sprintf(apiError.Message, v...), 36 | Details: err.Error(), 37 | }, 38 | }) 39 | } 40 | 41 | func ReturnApiSuccess(c echo.Context, status int, result interface{}) error { 42 | return c.JSON(status, ApiResult{ 43 | Success: true, 44 | Result: result, 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /service/user.serviceimpl.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "platform-sample/model" 5 | "platform-sample/repository" 6 | ) 7 | 8 | type UserServiceImpl struct { 9 | repository.UserRepository 10 | } 11 | 12 | func (UserServiceImpl) NewUserServiceImpl(userRepository repository.UserRepository) *UserServiceImpl { 13 | return &UserServiceImpl{userRepository} 14 | } 15 | 16 | func (userServiceImpl *UserServiceImpl) GetUsers() ([]*model.User, error) { 17 | return userServiceImpl.UserRepository.FindAll() 18 | } 19 | 20 | func (userServiceImpl *UserServiceImpl) CreateUser(user *model.User) (*model.User, error) { 21 | return userServiceImpl.UserRepository.Save(user) 22 | } 23 | 24 | func (userServiceImpl *UserServiceImpl) DeleteUser(id int) error { 25 | 26 | // transaction 처리가 필요한 모든 메서드에, tx 객체를 전달 27 | // 메서드 아규먼트에 tx 추가 vs middleware(echo ctx 공유)를 쓴다 vs 레파지토리에 짠다. 28 | err := userServiceImpl.UserRepository.DeleteById(id) 29 | if err != nil { 30 | //tx.Rollback() 31 | return err 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func (userServiceImpl *UserServiceImpl) GetUser(id int) (*model.User, error) { 38 | return userServiceImpl.UserRepository.FindById(id) 39 | } 40 | 41 | func (userServiceImpl *UserServiceImpl) UpdateUser(user *model.User) (*model.User, error) { 42 | return userServiceImpl.UserRepository.Update(user) 43 | } 44 | -------------------------------------------------------------------------------- /repository/user.repositoryimpl_test.go: -------------------------------------------------------------------------------- 1 | package repository_test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "platform-sample/infrastructure/database" 6 | "platform-sample/infrastructure/server" 7 | "platform-sample/model" 8 | "platform-sample/repository" 9 | "testing" 10 | ) 11 | 12 | func initRepository() *repository.UserRepositoryImpl { 13 | mockDb := database.SqlStore{}.GetMockDb() 14 | mockServer := server.Server{MainDb: mockDb} 15 | return mockServer.InjectUserRepository() 16 | } 17 | 18 | func Test_CreateAndDeleteUser(t *testing.T) { 19 | // given 20 | newUser := &model.User{} 21 | newUser.Name = "Kitty" 22 | 23 | // when 24 | userRepositoryImpl := initRepository() 25 | user, err := userRepositoryImpl.Save(newUser) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | // then 30 | assert.Equal(t, user, newUser) 31 | 32 | // 실제 영속성을 건드므로 유저를 지우는걸 해야한다. 33 | err = userRepositoryImpl.DeleteById(int(user.ID)) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | } 38 | 39 | func Test_Duplicated_User(t *testing.T) { 40 | userRepositoryImpl := initRepository() 41 | newUser := &model.User{} 42 | newUser.Name = "Kitty" 43 | 44 | _, err1 := userRepositoryImpl.Save(newUser) 45 | if err1 != nil { 46 | t.Error(err1) 47 | } 48 | 49 | _, err2 := userRepositoryImpl.Save(newUser) 50 | assert.Error(t, err2) 51 | } 52 | -------------------------------------------------------------------------------- /controller/api/card.controller.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "net/http" 6 | "platform-sample/controller/dto" 7 | "platform-sample/service" 8 | "strconv" 9 | ) 10 | 11 | type CardController struct { 12 | service.CardService 13 | } 14 | 15 | func (CardController) NewCardController(service service.CardService) *CardController { 16 | return &CardController{service} 17 | } 18 | 19 | func (cardController *CardController) Init(e *echo.Group) { 20 | e.POST("", cardController.CreateCard) 21 | e.DELETE("", cardController.DeleteCard) 22 | } 23 | 24 | func (cardController *CardController) DeleteCard(c echo.Context) error { 25 | cardId, err := strconv.Atoi(c.QueryParam("cardId")) 26 | if err != nil { 27 | return ReturnApiFail(c, http.StatusBadRequest, ApiParameterError, err) 28 | } 29 | 30 | userId, err := strconv.Atoi(c.QueryParam("userId")) 31 | if err != nil { 32 | return ReturnApiFail(c, http.StatusBadRequest, ApiParameterError, err) 33 | } 34 | 35 | cardController.CardService.DeleteCard(cardId, userId) 36 | return ReturnApiSuccess(c, http.StatusNoContent, nil) 37 | } 38 | 39 | func (cardController *CardController) CreateCard(c echo.Context) error { 40 | cardDto := &dto.CardDto{} 41 | err := c.Bind(cardDto) 42 | if err != nil { 43 | return ReturnApiFail(c, http.StatusBadRequest, ApiParameterError, err) 44 | } 45 | card, err := cardController.CardService.CreateCard(cardDto) 46 | if err != nil { 47 | return ReturnApiFail(c, http.StatusInternalServerError, ApiQueryError, err) 48 | } 49 | return ReturnApiSuccess(c, http.StatusCreated, card) 50 | } 51 | -------------------------------------------------------------------------------- /README2.md: -------------------------------------------------------------------------------- 1 | # Swagger 2 | 3 | API 문서화 자동화 Tool
4 | 5 | ## 환경 6 | 7 | ```sh 8 | $ go get github.com/swaggo/swag/cmd/swag 9 | $ go get github.com/swaggo/echo-swagger 10 | 11 | $ swag init 12 | ``` 13 | 14 | - 참고
15 | orm library [gorm](https://gorm.io/index.html) 을 사용하게되면 gorm model을 찾지 못해서 ```$ swag init``` 을 사용시 16 | ```ParseComment error :cannot find type definition: gorm.Model```에러가 발생한다.
17 | 따라서 ```$ swag init``` 대신
18 | ```$ swag init --parseDependency --parseInternal ```을 사용해준다. 19 | 20 | 21 | ## Code 22 | 23 | main.go 24 | ```go 25 | // @title Platform-sample Swagger API 26 | // @version 1.0.0 27 | // @host localhost:8395 28 | // @BasePath /api 29 | func main(){ 30 | 31 | } 32 | ``` 33 | 34 | server.go 35 | ```go 36 | // swagger setting 37 | e.GET("/swagger/*", echoSwagger.WrapHandler) 38 | ``` 39 | 40 | handler 함수 위치에 작성 41 | ```go 42 | // @Summary Get user 43 | // @Description Get user's info 44 | // @Accept json 45 | // @Produce json 46 | // @Param id path string true "id of the user" 47 | // @Success 200 {object} model.User 48 | // @Router /users/{id} [get] 49 | func (userController *UserController) GetUser(c echo.Context) error { 50 | id, err := strconv.Atoi(c.Param("id")) 51 | if err != nil { 52 | return c.JSON(http.StatusBadRequest, err) 53 | } 54 | 55 | user, err := userController.UserService.GetUser(id) 56 | if err != nil { 57 | return c.JSON(http.StatusInternalServerError, err) 58 | } 59 | return c.JSON(http.StatusOK, user) 60 | } 61 | ``` 62 | 63 | 64 | ## 사용법 65 | 66 | ```sh 67 | http://localhost:8395/swagger/index.html 68 | ``` 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /view/static/js/list.js: -------------------------------------------------------------------------------- 1 | // let token = $("meta[name='_csrf']").attr("content"); 2 | // let header = $("meta[name='_csrf_header']").attr("content"); 3 | let userList = new Object(); 4 | 5 | $(document).ready(function(){ 6 | console.log("list ready"); 7 | getUser(); 8 | }); 9 | 10 | function getUser() { 11 | $.ajax({ 12 | url: '/api/users', 13 | contentType: 'application/json', 14 | type: 'get', 15 | beforeSend: function(xhr) { 16 | // xhr.setRequestHeader(header, token); 17 | }, 18 | success: function(data) { 19 | if (data.success) { 20 | let html = ""; 21 | for (let i = 0; i < data.result.length; i++) { 22 | html += ''; 23 | html += '' + data.result[i].id + ''; 24 | html += '' 25 | + data.result[i].name + ''; 26 | html += ''; 27 | userList[data.result[i].id] = data.result[i]; 28 | 29 | } 30 | $("#tableBody").empty(); 31 | $("#tableBody").append(html); 32 | } else { 33 | alert(" message = " + data.error) 34 | } 35 | }, 36 | error: function(request,status,error){ 37 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 38 | } 39 | }) 40 | } 41 | 42 | function goToDetail(n) { 43 | let url = "/detail/" + n 44 | window.location.href = url 45 | } 46 | 47 | $("#deleteBtn").on("click",function() { 48 | let result = confirm("삭제하시겠습니까?"); 49 | if (result == false) { 50 | return; 51 | } 52 | $.ajax({ 53 | url: '/api/users/' + $("#userBox").val(), 54 | contentType: 'application/json', 55 | type: 'delete', 56 | success: function(data) { 57 | alert("삭제되었습니다."); 58 | location.reload(); 59 | }, 60 | error: function(request,status,error){ 61 | alert(" message = " + request.responseText); 62 | } 63 | }) 64 | }) -------------------------------------------------------------------------------- /controller/api/user.controller_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/labstack/echo/v4" 8 | "github.com/stretchr/testify/assert" 9 | "log" 10 | "net/http" 11 | "net/http/httptest" 12 | "platform-sample/controller/api" 13 | "platform-sample/infrastructure/database" 14 | "platform-sample/infrastructure/server" 15 | mocks2 "platform-sample/mocks/service" 16 | "platform-sample/model" 17 | "platform-sample/service" 18 | "strings" 19 | "testing" 20 | ) 21 | 22 | func initIntegrateMockUserService() *service.UserServiceImpl { 23 | mockDb := database.SqlStore{}.GetMockDb() 24 | mockServer := server.Server{MainDb: mockDb} 25 | return mockServer.InjectUserService() 26 | } 27 | 28 | func Test_IntegrateCreateUser(t *testing.T) { 29 | mockUser := model.User{Name: "James"} 30 | byteData, err := json.Marshal(mockUser) 31 | if err != nil { 32 | log.Println(err) 33 | } 34 | 35 | buff := bytes.NewBuffer(byteData) 36 | fmt.Println(buff) 37 | e := echo.New() 38 | 39 | req := httptest.NewRequest(http.MethodPost, "/api/users", buff) 40 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 41 | rec := httptest.NewRecorder() 42 | 43 | ctx := e.NewContext(req, rec) 44 | 45 | userController := api.UserController{}.NewUserController(initIntegrateMockUserService()) 46 | if assert.NoError(t, userController.CreateUser(ctx)) { 47 | assert.Equal(t, http.StatusCreated, rec.Code) 48 | assert.Contains(t, rec.Body.String(), mockUser.Name) 49 | } 50 | } 51 | 52 | func Test_Unit_CreateUser(t *testing.T) { 53 | mockUser := &model.User{Name: "James"} 54 | byteData, err := json.Marshal(mockUser) 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | 59 | buff := bytes.NewBuffer(byteData) 60 | 61 | e := echo.New() 62 | 63 | req := httptest.NewRequest(http.MethodPost, "/api/users", buff) 64 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 65 | rec := httptest.NewRecorder() 66 | 67 | ctx := e.NewContext(req, rec) 68 | 69 | mockService := &mocks2.UserService{} 70 | mockService.On("CreateUser", mockUser).Return(mockUser, nil) 71 | userController := api.UserController{}.NewUserController(mockService) 72 | 73 | if assert.NoError(t, userController.CreateUser(ctx)) { 74 | assert.Equal(t, http.StatusCreated, rec.Code) 75 | //assert.Contains(t, rec.Body.String(), mockUser.Name) 76 | assert.Equal(t, string(byteData), strings.Trim(rec.Body.String(), "\n")) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /common/logger.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | //import ( 4 | // "os" 5 | // "strings" 6 | // 7 | // "github.com/jblim0125/golang-web-platform/model" 8 | // formatter "github.com/mobigen/gologger" 9 | // "github.com/sirupsen/logrus" 10 | //) 11 | // 12 | //// Logger empty struct 13 | //type Logger struct { 14 | // *logrus.Logger 15 | //} 16 | // 17 | //// Logger log variable 18 | //var l *Logger 19 | // 20 | //func init() { 21 | // l = &Logger{logrus.New()} 22 | // l.SetOutput(os.Stdout) 23 | // f := &formatter.Formatter{ 24 | // TimestampFormat: "2006-01-02 15:04:05.000", 25 | // ShowFields: true, 26 | // } 27 | // l.SetFormatter(f) 28 | // l.SetLevel(logrus.DebugLevel) 29 | // l.SetReportCaller(true) 30 | //} 31 | // 32 | //// GetInstance return logger instance 33 | //func (Logger) GetInstance() *Logger { 34 | // return l 35 | //} 36 | // 37 | //// SetLogLevel set log level 38 | //func (l *Logger) SetLogLevel(lv logrus.Level) { 39 | // switch lv { 40 | // case logrus.ErrorLevel: 41 | // l.SetLevel(lv) 42 | // case logrus.WarnLevel: 43 | // l.SetLevel(lv) 44 | // case logrus.InfoLevel: 45 | // l.SetLevel(lv) 46 | // case logrus.DebugLevel: 47 | // l.SetLevel(lv) 48 | // default: 49 | // l.Errorf("ERROR. Not Supported Log Level[ %d ]", lv) 50 | // } 51 | //} 52 | // 53 | //// GetLogLevel get log level 54 | //func (l *Logger) GetLogLevel() string { 55 | // text, _ := l.GetLevel().MarshalText() 56 | // return string(text) 57 | //} 58 | // 59 | //// Start Print Start Banner 60 | //func (l *Logger) Start() { 61 | // l.Errorf("%s", model.LINE90) 62 | // l.Errorf(" ") 63 | // l.Errorf(" START. %s:%s-%s", 64 | // strings.ToUpper(model.Name), model.Version, model.BuildHash) 65 | // l.Errorf(" ") 66 | // l.Errorf("%90s", "Copyright(C) 2021 Mobigen Corporation. ") 67 | // l.Errorf(" ") 68 | // l.Errorf("%s", model.LINE90) 69 | //} 70 | // 71 | //// Shutdown Print Shutdown 72 | //func (l *Logger) Shutdown() { 73 | // l.Errorf("%s", model.LINE90) 74 | // l.Errorf(" ") 75 | // l.Errorf(" %s Bye Bye.", strings.ToUpper(model.Name)) 76 | // l.Errorf(" ") 77 | // l.Errorf("%90s", "Copyright(C) 2021 Mobigen Corporation. ") 78 | // l.Errorf(" ") 79 | // l.Errorf("%s", model.LINE90) 80 | //} 81 | // 82 | //// GormPrint gorm log print function 83 | //func (l *Logger) GormPrint(v ...interface{}) { 84 | // if v[0] == "sql" { 85 | // l.Debugf("[ ORM_SQL ] %s", v[3]) 86 | // } 87 | // if v[0] == "log" { 88 | // l.Debugf("[ ORM_LOG ] %s", v[2]) 89 | // } 90 | //} 91 | -------------------------------------------------------------------------------- /view/static/js/card.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | console.log("card ready"); 3 | getUsers(); 4 | }); 5 | 6 | let userList = new Object() 7 | 8 | function getUsers() { 9 | $.ajax({ 10 | url: '/api/users', 11 | contentType: 'application/json', 12 | type: 'get', 13 | beforeSend: function(xhr) { 14 | // xhr.setRequestHeader(header, token); 15 | }, 16 | success: function(data) { 17 | if (data.success) { 18 | let userBox = $("#userBox") 19 | for (let i = 0; i < data.result.length; i++) { 20 | let option = document.createElement('option'); 21 | option.innerText = data.result[i].name; 22 | userBox.append(option); 23 | userList[data.result[i].name] = data.result[i]; 24 | } 25 | } else { 26 | alert(" message = " + data.error) 27 | } 28 | }, 29 | error: function(request,status,error){ 30 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 31 | } 32 | }) 33 | } 34 | 35 | $("#cancelBtn").on("click", function() { 36 | window.location.href="/list" 37 | }); 38 | 39 | $("#regBtn").on("click", function() { 40 | let cardName = $("#cardName").val(); 41 | let cardLimit = $("#cardLimit").val(); 42 | let userName = $("#userBox").val() 43 | isValidate = validationCheck(cardName, cardLimit) 44 | if (!isValidate) { 45 | return 46 | } 47 | let data = new Object() 48 | data["name"] = cardName 49 | data["limit"] = Number(cardLimit) 50 | data["userId"] = userList[userName].id 51 | $.ajax({ 52 | url: '/api/cards', 53 | contentType: 'application/json', 54 | type: 'post', 55 | data: JSON.stringify(data), 56 | beforeSend: function(xhr) { 57 | // xhr.setRequestHeader(header, token); 58 | }, 59 | success: function(data) { 60 | alert("등록성공") 61 | window.location.href="/list" 62 | }, 63 | error: function(request,status,error){ 64 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 65 | } 66 | }) 67 | }); 68 | 69 | function validationCheck(cardName, cardLimit) { 70 | if (!cardName || !cardLimit) { 71 | alert("빈칸을 모두 입력해주세요") 72 | return false 73 | } 74 | if (isNaN(cardLimit)) { 75 | alert("한도에는 숫자만 입력해주세요") 76 | return false 77 | } 78 | return true 79 | } -------------------------------------------------------------------------------- /view/static/js/detail.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | console.log("detail ready"); 3 | getUser($("#template_id").html()) 4 | }); 5 | 6 | let user = new Object() 7 | 8 | function getUser(id) { 9 | $.ajax({ 10 | url: '/api/users/' + id, 11 | contentType: 'application/json', 12 | type: 'get', 13 | beforeSend: function(xhr) { 14 | // xhr.setRequestHeader(header, token); 15 | }, 16 | success: function(data) { 17 | user = data.result 18 | $("#id").val(data.result.id); 19 | $("#name").val(data.result.name); 20 | $("#create").val(data.result.createdAt); 21 | $("#update").val(data.result.updatedAt); 22 | let cardList = [] 23 | for (let i = 0; i < data.result.cards.length; i++) { 24 | cardData = data.result.cards[i].name + "(" + data.result.cards[i].limit + ")" 25 | cardList.push(cardData) 26 | } 27 | $("#card").val(cardList.join(", ")) 28 | }, 29 | error: function(request,status,error){ 30 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 31 | } 32 | }) 33 | } 34 | 35 | $("#delete_btn").on("click", function() { 36 | let result = confirm("삭제하시겠습니까?"); 37 | if (result == false) { 38 | return; 39 | } 40 | let id = $("#id").val(); 41 | $.ajax({ 42 | url: '/api/users/'+ id, 43 | contentType: 'application/json', 44 | type: 'delete', 45 | beforeSend: function(xhr) { 46 | // xhr.setRequestHeader(header, token); 47 | }, 48 | success: function(data) { 49 | alert("삭제성공"); 50 | window.location.href="/list" 51 | }, 52 | error: function(request,status,error){ 53 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 54 | } 55 | }) 56 | }); 57 | 58 | $("#cancel_btn").on("click", function() { 59 | window.location.href="/list" 60 | }); 61 | 62 | $("#update_btn").on("click", function() { 63 | if (user["name"] == $("#name").val()) { 64 | alert("변경사항이 없습니다.") 65 | return; 66 | } 67 | let result = confirm("변경하시겠습니까?"); 68 | if (result == false) { 69 | return; 70 | } 71 | 72 | user["name"] = $("#name").val(); 73 | 74 | $.ajax({ 75 | url: '/api/users', 76 | contentType: 'application/json', 77 | type: 'patch', 78 | data: JSON.stringify(user), 79 | beforeSend: function(xhr) { 80 | // xhr.setRequestHeader(header, token); 81 | }, 82 | success: function(data) { 83 | alert("변경완료"); 84 | window.location.reload(true) 85 | }, 86 | error: function(request,status,error){ 87 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 88 | } 89 | }) 90 | }); -------------------------------------------------------------------------------- /mocks/service/UserService.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v0.0.0-dev. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | model "platform-sample/model" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // UserService is an autogenerated mock type for the UserService type 12 | type UserService struct { 13 | mock.Mock 14 | } 15 | 16 | // CreateUser provides a mock function with given fields: _a0 17 | func (_m *UserService) CreateUser(_a0 *model.User) (*model.User, error) { 18 | ret := _m.Called(_a0) 19 | 20 | var r0 *model.User 21 | if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok { 22 | r0 = rf(_a0) 23 | } else { 24 | if ret.Get(0) != nil { 25 | r0 = ret.Get(0).(*model.User) 26 | } 27 | } 28 | 29 | var r1 error 30 | if rf, ok := ret.Get(1).(func(*model.User) error); ok { 31 | r1 = rf(_a0) 32 | } else { 33 | r1 = ret.Error(1) 34 | } 35 | 36 | return r0, r1 37 | } 38 | 39 | // DeleteUser provides a mock function with given fields: _a0 40 | func (_m *UserService) DeleteUser(_a0 int) error { 41 | ret := _m.Called(_a0) 42 | 43 | var r0 error 44 | if rf, ok := ret.Get(0).(func(int) error); ok { 45 | r0 = rf(_a0) 46 | } else { 47 | r0 = ret.Error(0) 48 | } 49 | 50 | return r0 51 | } 52 | 53 | // GetUser provides a mock function with given fields: _a0 54 | func (_m *UserService) GetUser(_a0 int) (*model.User, error) { 55 | ret := _m.Called(_a0) 56 | 57 | var r0 *model.User 58 | if rf, ok := ret.Get(0).(func(int) *model.User); ok { 59 | r0 = rf(_a0) 60 | } else { 61 | if ret.Get(0) != nil { 62 | r0 = ret.Get(0).(*model.User) 63 | } 64 | } 65 | 66 | var r1 error 67 | if rf, ok := ret.Get(1).(func(int) error); ok { 68 | r1 = rf(_a0) 69 | } else { 70 | r1 = ret.Error(1) 71 | } 72 | 73 | return r0, r1 74 | } 75 | 76 | // GetUsers provides a mock function with given fields: 77 | func (_m *UserService) GetUsers() ([]*model.User, error) { 78 | ret := _m.Called() 79 | 80 | var r0 []*model.User 81 | if rf, ok := ret.Get(0).(func() []*model.User); ok { 82 | r0 = rf() 83 | } else { 84 | if ret.Get(0) != nil { 85 | r0 = ret.Get(0).([]*model.User) 86 | } 87 | } 88 | 89 | var r1 error 90 | if rf, ok := ret.Get(1).(func() error); ok { 91 | r1 = rf() 92 | } else { 93 | r1 = ret.Error(1) 94 | } 95 | 96 | return r0, r1 97 | } 98 | 99 | // UpdateUser provides a mock function with given fields: _a0 100 | func (_m *UserService) UpdateUser(_a0 *model.User) (*model.User, error) { 101 | ret := _m.Called(_a0) 102 | 103 | var r0 *model.User 104 | if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok { 105 | r0 = rf(_a0) 106 | } else { 107 | if ret.Get(0) != nil { 108 | r0 = ret.Get(0).(*model.User) 109 | } 110 | } 111 | 112 | var r1 error 113 | if rf, ok := ret.Get(1).(func(*model.User) error); ok { 114 | r1 = rf(_a0) 115 | } else { 116 | r1 = ret.Error(1) 117 | } 118 | 119 | return r0, r1 120 | } 121 | -------------------------------------------------------------------------------- /view/templates/user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Echo Study 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | 36 |
37 |
38 |
39 | 40 | 41 | 42 | 회원등록 43 |
44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /view/static/js/delete.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | console.log("card ready"); 3 | getUsers(); 4 | }); 5 | 6 | let userList = new Object() 7 | 8 | function getUsers() { 9 | $.ajax({ 10 | url: '/api/users', 11 | contentType: 'application/json', 12 | type: 'get', 13 | beforeSend: function(xhr) { 14 | // xhr.setRequestHeader(header, token); 15 | }, 16 | success: function(data) { 17 | if (data.success) { 18 | let userBox = $("#userBox") 19 | for (let i = 0; i < data.result.length; i++) { 20 | let option = document.createElement('option'); 21 | option.innerText = data.result[i].name; 22 | userBox.append(option); 23 | userList[data.result[i].name] = data.result[i]; 24 | } 25 | } else { 26 | alert(" message = " + data.error) 27 | } 28 | }, 29 | error: function(request,status,error){ 30 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 31 | } 32 | }) 33 | } 34 | 35 | function reloadCardBox() { 36 | let cardBox = $("#cardBox") 37 | cardBox.find("option").remove(); 38 | let option = document.createElement('option'); 39 | option.innerText = "삭제할 카드를 선택해주세요" 40 | cardBox.append(option); 41 | } 42 | 43 | $("#userBox").change(function (){ 44 | reloadCardBox(); 45 | let userName = $("#userBox").val(); 46 | if (userName == "사용자를 선택해주세요") { 47 | alert("사용자를 선택해주세요") 48 | return; 49 | } 50 | fillCardBox(userName); 51 | }) 52 | 53 | function fillCardBox(userName) { 54 | let cardBox = $("#cardBox") 55 | let cards = userList[userName].cards; 56 | for (let i = 0; i < cards.length; i++) { 57 | let option = document.createElement('option'); 58 | option.innerText = cards[i].name; 59 | cardBox.append(option); 60 | } 61 | } 62 | 63 | $("#cancelBtn").on("click", function() { 64 | window.location.href="/list" 65 | }); 66 | 67 | function getCardId(cardName) { 68 | let cards = userList[$("#userBox").val()].cards; 69 | for (let i = 0; i < cards.length; i++) { 70 | if (cardName == cards[i].name) { 71 | return cards[i].id 72 | } 73 | } 74 | 75 | } 76 | 77 | $("#deleteBtn").on("click", function() { 78 | let userId = userList[$("#userBox").val()].id; 79 | let cardName = $("#cardBox").val(); 80 | if (cardName == "삭제할 카드를 선택해주세요") { 81 | alert("삭제할 카드를 선택해주세요") 82 | return; 83 | } 84 | let cardId = getCardId(cardName); 85 | $.ajax({ 86 | url: '/api/cards?cardId=' + cardId + "&userId=" +userId, 87 | contentType: 'application/json', 88 | type: 'delete', 89 | beforeSend: function(xhr) { 90 | // xhr.setRequestHeader(header, token); 91 | }, 92 | success: function(data) { 93 | alert("삭제성공") 94 | window.location.href="/list" 95 | }, 96 | error: function(request,status,error){ 97 | alert("code:"+request.status+"\n"+"message:"+request.responseText+"\n"+"error:"+error); 98 | } 99 | }) 100 | }); -------------------------------------------------------------------------------- /view/templates/card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Echo Study 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 카드등록 42 |
43 |
44 | 45 | 48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 |
56 | 57 | 58 |
59 | 60 |
61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /repository/user.repositoryimpl.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "errors" 5 | "github.com/jinzhu/gorm" 6 | "platform-sample/model" 7 | ) 8 | 9 | type UserRepositoryImpl struct { 10 | db *gorm.DB 11 | } 12 | 13 | func (UserRepositoryImpl) NewUserRepositoryImpl(db *gorm.DB) *UserRepositoryImpl { 14 | return &UserRepositoryImpl{db} 15 | } 16 | 17 | func (userRepositoryImpl *UserRepositoryImpl) FindAll() ([]*model.User, error) { 18 | var users []*model.User 19 | // table.find는 할당이 안된 pointer를 넘겨도된다. 20 | //userRepositoryImpl.db.Table("users").Find(&users) 21 | err := userRepositoryImpl.db.Preload("Cards").Find(&users).Error 22 | if err != nil { 23 | return nil, err 24 | } 25 | return users, nil 26 | } 27 | func (userRepositoryImpl *UserRepositoryImpl) FindById(id int) (*model.User, error) { 28 | var user = new(model.User) 29 | 30 | // eager loading 이며 커넥션을 두번 맺는다.(쿼리가 두번 날라간다.) 31 | err := userRepositoryImpl.db.Preload("Cards").First(&user, id).Error 32 | 33 | // join을 사용해서 map을 넘겨줄껀지, eager loading에 커넥션을 두번 맺을지는 성능을 고려하여 선택.. 34 | //err := userRepositoryImpl.db. 35 | // Table("users"). 36 | // Select("users.*, cards.*"). 37 | // Where("users.id = ?",id). 38 | // Joins("join cards on users.id = cards.user_id"). 39 | // Scan(&user).Error 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | return user, nil 45 | 46 | } 47 | func (userRepositoryImpl *UserRepositoryImpl) DeleteById(id int) error { 48 | 49 | userRepositoryImpl.db.Transaction(func(tx *gorm.DB) error { 50 | err := tx.Delete(&model.User{}, id).Error 51 | if err != nil { 52 | return err 53 | } 54 | 55 | err = tx.Where("user_id = ?", id).Delete(&model.Card{}).Error 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | }) 62 | // 아래의 방법으로 참조를 끊어버릴수도 있다. 63 | //userRepositoryImpl.db.Model(&model.User{}).Association("Cards").Clear() 64 | // db.Model(&user).Association("Languages").Delete([]Language{languageZH, languageEN}) 65 | // db.Model(&user).Association("Languages").Clear() 66 | 67 | return nil 68 | } 69 | func (userRepositoryImpl *UserRepositoryImpl) Save(user *model.User) (*model.User, error) { 70 | var userCheck = new(model.User) 71 | err1 := userRepositoryImpl.db.Where("name = ?", user.Name).First(&userCheck).Error 72 | if err1 != nil { 73 | if errors.Is(err1, gorm.ErrRecordNotFound) { 74 | err2 := userRepositoryImpl.db.Create(user).Error 75 | if err2 != nil { 76 | return nil, err2 77 | } 78 | return user, nil 79 | } 80 | } 81 | // 조회가 된경우(중복된 유저이므로 회원가입 불가능) 82 | return nil, errors.New("중복된 유저입니다.") 83 | } 84 | 85 | func (userRepositoryImpl *UserRepositoryImpl) Update(user *model.User) (*model.User, error) { 86 | userRepositoryImpl.db.Save(&user) 87 | return user, nil 88 | } 89 | 90 | // deprecated 91 | //func (userRepositoryImpl *UserRepositoryImpl) DuplicatedCheck(name string) (bool, error) { 92 | // var user = new(model.User) 93 | // // 조회가 안된다는건 err가 있다는거 94 | // err := userRepositoryImpl.db.Where("name = ?", name).First(&user).Error 95 | // if err != nil { 96 | // // 동일한 회원이 조회가 안되는경우(회원가입 가능) 97 | // if errors.Is(err, gorm.ErrRecordNotFound) { 98 | // return false, nil 99 | // } 100 | // // 그 외의 에러 101 | // return false, err 102 | // } 103 | // // 동일한 회원이 조회가 되는경우 104 | // return true, errors.New("중복된 유저입니다.") 105 | //} 106 | -------------------------------------------------------------------------------- /view/templates/delete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Echo Study 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 카드삭제 42 |
43 |
44 | 45 | 48 |
49 | 50 |
51 | 52 | 55 |
56 | 57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /mocks/repository/UserRepository.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v0.0.0-dev. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | model "platform-sample/model" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // UserRepository is an autogenerated mock type for the UserRepository type 12 | type UserRepository struct { 13 | mock.Mock 14 | } 15 | 16 | // DeleteById provides a mock function with given fields: id 17 | func (_m *UserRepository) DeleteById(id int) error { 18 | ret := _m.Called(id) 19 | 20 | var r0 error 21 | if rf, ok := ret.Get(0).(func(int) error); ok { 22 | r0 = rf(id) 23 | } else { 24 | r0 = ret.Error(0) 25 | } 26 | 27 | return r0 28 | } 29 | 30 | // DuplicatedCheck provides a mock function with given fields: name 31 | func (_m *UserRepository) DuplicatedCheck(name string) (bool, error) { 32 | ret := _m.Called(name) 33 | 34 | var r0 bool 35 | if rf, ok := ret.Get(0).(func(string) bool); ok { 36 | r0 = rf(name) 37 | } else { 38 | r0 = ret.Get(0).(bool) 39 | } 40 | 41 | var r1 error 42 | if rf, ok := ret.Get(1).(func(string) error); ok { 43 | r1 = rf(name) 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | // FindAll provides a mock function with given fields: 52 | func (_m *UserRepository) FindAll() ([]*model.User, error) { 53 | ret := _m.Called() 54 | 55 | var r0 []*model.User 56 | if rf, ok := ret.Get(0).(func() []*model.User); ok { 57 | r0 = rf() 58 | } else { 59 | if ret.Get(0) != nil { 60 | r0 = ret.Get(0).([]*model.User) 61 | } 62 | } 63 | 64 | var r1 error 65 | if rf, ok := ret.Get(1).(func() error); ok { 66 | r1 = rf() 67 | } else { 68 | r1 = ret.Error(1) 69 | } 70 | 71 | return r0, r1 72 | } 73 | 74 | // FindById provides a mock function with given fields: id 75 | func (_m *UserRepository) FindById(id int) (*model.User, error) { 76 | ret := _m.Called(id) 77 | 78 | var r0 *model.User 79 | if rf, ok := ret.Get(0).(func(int) *model.User); ok { 80 | r0 = rf(id) 81 | } else { 82 | if ret.Get(0) != nil { 83 | r0 = ret.Get(0).(*model.User) 84 | } 85 | } 86 | 87 | var r1 error 88 | if rf, ok := ret.Get(1).(func(int) error); ok { 89 | r1 = rf(id) 90 | } else { 91 | r1 = ret.Error(1) 92 | } 93 | 94 | return r0, r1 95 | } 96 | 97 | // Save provides a mock function with given fields: _a0 98 | func (_m *UserRepository) Save(_a0 *model.User) (*model.User, error) { 99 | ret := _m.Called(_a0) 100 | 101 | var r0 *model.User 102 | if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok { 103 | r0 = rf(_a0) 104 | } else { 105 | if ret.Get(0) != nil { 106 | r0 = ret.Get(0).(*model.User) 107 | } 108 | } 109 | 110 | var r1 error 111 | if rf, ok := ret.Get(1).(func(*model.User) error); ok { 112 | r1 = rf(_a0) 113 | } else { 114 | r1 = ret.Error(1) 115 | } 116 | 117 | return r0, r1 118 | } 119 | 120 | // Update provides a mock function with given fields: _a0 121 | func (_m *UserRepository) Update(_a0 *model.User) (*model.User, error) { 122 | ret := _m.Called(_a0) 123 | 124 | var r0 *model.User 125 | if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok { 126 | r0 = rf(_a0) 127 | } else { 128 | if ret.Get(0) != nil { 129 | r0 = ret.Get(0).(*model.User) 130 | } 131 | } 132 | 133 | var r1 error 134 | if rf, ok := ret.Get(1).(func(*model.User) error); ok { 135 | r1 = rf(_a0) 136 | } else { 137 | r1 = ret.Error(1) 138 | } 139 | 140 | return r0, r1 141 | } 142 | -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | basePath: /api 2 | definitions: 3 | api.ApiError: 4 | properties: 5 | code: 6 | type: integer 7 | details: 8 | type: object 9 | message: 10 | type: string 11 | type: object 12 | api.ApiResult: 13 | properties: 14 | error: 15 | $ref: '#/definitions/api.ApiError' 16 | result: 17 | type: object 18 | success: 19 | type: boolean 20 | type: object 21 | api.UserDto: 22 | properties: 23 | name: 24 | type: string 25 | type: object 26 | model.User: 27 | properties: 28 | createdAt: 29 | type: string 30 | deletedAt: 31 | type: string 32 | id: 33 | type: integer 34 | name: 35 | type: string 36 | updatedAt: 37 | type: string 38 | type: object 39 | host: localhost:8395 40 | info: 41 | contact: {} 42 | title: Platform-sample Swagger API 43 | version: 1.0.0 44 | paths: 45 | /users: 46 | get: 47 | consumes: 48 | - application/json 49 | description: Get all user's info 50 | produces: 51 | - application/json 52 | responses: 53 | "200": 54 | description: OK 55 | schema: 56 | $ref: '#/definitions/model.User' 57 | summary: Get all users 58 | patch: 59 | consumes: 60 | - application/json 61 | description: Get user's info 62 | parameters: 63 | - description: body of the user 64 | in: body 65 | name: name 66 | required: true 67 | schema: 68 | $ref: '#/definitions/model.User' 69 | produces: 70 | - application/json 71 | responses: 72 | "201": 73 | description: Created 74 | schema: 75 | $ref: '#/definitions/model.User' 76 | summary: Update user 77 | post: 78 | consumes: 79 | - application/json 80 | description: Create new user 81 | parameters: 82 | - description: body of the user 83 | in: body 84 | name: user 85 | required: true 86 | schema: 87 | $ref: '#/definitions/api.UserDto' 88 | produces: 89 | - application/json 90 | responses: 91 | "201": 92 | description: Created 93 | schema: 94 | allOf: 95 | - $ref: '#/definitions/api.ApiResult' 96 | - properties: 97 | result: 98 | $ref: '#/definitions/model.User' 99 | type: object 100 | summary: Create user 101 | /users/{id}: 102 | delete: 103 | consumes: 104 | - application/json 105 | description: Delete user's info 106 | parameters: 107 | - description: id of the user 108 | in: path 109 | name: id 110 | required: true 111 | type: string 112 | produces: 113 | - application/json 114 | responses: 115 | "204": 116 | description: No Content 117 | schema: 118 | $ref: '#/definitions/model.User' 119 | summary: Delete user 120 | get: 121 | consumes: 122 | - application/json 123 | description: Get user's info 124 | parameters: 125 | - description: id of the user 126 | in: path 127 | name: id 128 | required: true 129 | type: string 130 | produces: 131 | - application/json 132 | responses: 133 | "200": 134 | description: OK 135 | schema: 136 | $ref: '#/definitions/model.User' 137 | summary: Get user 138 | swagger: "2.0" 139 | -------------------------------------------------------------------------------- /view/templates/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Echo Study 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | 회원목록 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
번호이름
63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /report.json: -------------------------------------------------------------------------------- 1 | {"Time":"2021-07-19T17:31:14.0798674+09:00","Action":"output","Package":"platform-sample","Output":"? \tplatform-sample\t[no test files]\n"} 2 | {"Time":"2021-07-19T17:31:14.2283627+09:00","Action":"skip","Package":"platform-sample","Elapsed":0.148} 3 | {"Time":"2021-07-19T17:31:14.7720616+09:00","Action":"run","Package":"platform-sample/service","Test":"TestGetUser"} 4 | {"Time":"2021-07-19T17:31:14.7720616+09:00","Action":"output","Package":"platform-sample/service","Test":"TestGetUser","Output":"=== RUN TestGetUser\n"} 5 | {"Time":"2021-07-19T17:31:14.7720616+09:00","Action":"output","Package":"platform-sample/service","Test":"TestGetUser","Output":"--- PASS: TestGetUser (0.00s)\n"} 6 | {"Time":"2021-07-19T17:31:14.772594+09:00","Action":"pass","Package":"platform-sample/service","Test":"TestGetUser","Elapsed":0} 7 | {"Time":"2021-07-19T17:31:14.772594+09:00","Action":"output","Package":"platform-sample/service","Output":"PASS\n"} 8 | {"Time":"2021-07-19T17:31:14.7944403+09:00","Action":"output","Package":"platform-sample/service","Output":"ok \tplatform-sample/service\t0.518s\n"} 9 | {"Time":"2021-07-19T17:31:14.8073762+09:00","Action":"pass","Package":"platform-sample/service","Elapsed":0.531} 10 | {"Time":"2021-07-19T17:31:16.11909+09:00","Action":"output","Package":"platform-sample/controller/api","Output":"fork/exec C:\\Users\\a\\AppData\\Local\\Temp\\go-build2124060754\\b151\\api.test.exe: Access is denied.\n"} 11 | {"Time":"2021-07-19T17:31:16.11909+09:00","Action":"output","Package":"platform-sample/controller/api","Output":"FAIL\tplatform-sample/controller/api\t0.453s\n"} 12 | {"Time":"2021-07-19T17:31:16.11909+09:00","Action":"fail","Package":"platform-sample/controller/api","Elapsed":0.454} 13 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"output","Package":"platform-sample/controller/web","Output":"? \tplatform-sample/controller/web\t[no test files]\n"} 14 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"skip","Package":"platform-sample/controller/web","Elapsed":0} 15 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"output","Package":"platform-sample/infrastructure/database","Output":"? \tplatform-sample/infrastructure/database\t[no test files]\n"} 16 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"skip","Package":"platform-sample/infrastructure/database","Elapsed":0} 17 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"output","Package":"platform-sample/infrastructure/server","Output":"? \tplatform-sample/infrastructure/server\t[no test files]\n"} 18 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"skip","Package":"platform-sample/infrastructure/server","Elapsed":0} 19 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"output","Package":"platform-sample/mocks/repository","Output":"? \tplatform-sample/mocks/repository\t[no test files]\n"} 20 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"skip","Package":"platform-sample/mocks/repository","Elapsed":0} 21 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"output","Package":"platform-sample/mocks/service","Output":"? \tplatform-sample/mocks/service\t[no test files]\n"} 22 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"skip","Package":"platform-sample/mocks/service","Elapsed":0} 23 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"output","Package":"platform-sample/model","Output":"? \tplatform-sample/model\t[no test files]\n"} 24 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"skip","Package":"platform-sample/model","Elapsed":0} 25 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"output","Package":"platform-sample/repository","Output":"? \tplatform-sample/repository\t[no test files]\n"} 26 | {"Time":"2021-07-19T17:31:16.1220859+09:00","Action":"skip","Package":"platform-sample/repository","Elapsed":0} 27 | -------------------------------------------------------------------------------- /view/templates/detail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Echo Study 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 상세정보 42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 |
50 | 51 | 52 |
53 | 54 |
55 | 56 | 57 |
58 | 59 |
60 | 61 | 62 |
63 | 64 |
65 | 66 | 67 |
68 | 69 |
70 | 71 | 72 | 73 |
74 | 75 |
76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /infrastructure/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | "github.com/labstack/echo/v4" 6 | echoSwagger "github.com/swaggo/echo-swagger" 7 | "html/template" 8 | "io" 9 | "platform-sample/controller/api" 10 | "platform-sample/controller/web" 11 | _ "platform-sample/docs" 12 | "platform-sample/repository" 13 | "platform-sample/service" 14 | ) 15 | 16 | type Server struct { 17 | MainDb *gorm.DB 18 | } 19 | 20 | type TemplateRenderer struct { 21 | templates *template.Template 22 | } 23 | 24 | func (server Server) Init() { 25 | e := echo.New() 26 | //e.Use(server.contextDB(server.MainDb)) 27 | //e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc { 28 | // return func(c echo.Context) error { 29 | // var tx *gorm.DB 30 | // //server.MainDb = db 31 | // fmt.Println("pre 시작") 32 | // if c.Request().Method != "GET" { 33 | // tx = server.MainDb.Begin() 34 | // c.Set("tx", tx) 35 | // //server.MainDb = tx 36 | // //EchoCtx = c 37 | // // 트랜잭션 시작 38 | // } 39 | // err := next(c) 40 | // if tx != nil{ 41 | // if err != nil { 42 | // tx.Rollback() 43 | // } 44 | // tx.Commit() 45 | // // 롤백 46 | // } 47 | // fmt.Println("Pre 종료") 48 | // return err 49 | // } 50 | //}) 51 | ////e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { 52 | // return func(c echo.Context) error { 53 | // fmt.Println("use 시작") 54 | // fmt.Println(c.Path()) 55 | // err := next(c) 56 | // fmt.Println("use 종료") 57 | // return err 58 | // } 59 | //}) 60 | renderer := &TemplateRenderer{ 61 | templates: template.Must(template.ParseGlob("view/templates/*.html")), 62 | } 63 | e.Renderer = renderer 64 | e.Static("/static", "view/static") 65 | web.WebController{}.Init(e) 66 | e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ 67 | AllowOrigins: []string{"*"}, 68 | AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, 69 | })) 70 | 71 | 72 | // api controller setting 73 | cardController := server.InjectCardController() 74 | cardController.Init(e.Group("/api/cards")) 75 | 76 | userController := server.InjectUserController() 77 | userController.Init(e.Group("/api/users")) 78 | 79 | // swagger setting 80 | e.GET("/swagger/*", echoSwagger.WrapHandler) 81 | 82 | e.Logger.Fatal(e.Start(":8395")) 83 | } 84 | 85 | func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 86 | 87 | if viewContext, isMap := data.(map[string]interface{}); isMap { 88 | viewContext["reverse"] = c.Echo().Reverse 89 | } 90 | 91 | return t.templates.ExecuteTemplate(w, name, data) 92 | } 93 | 94 | func (server Server) contextDB(db *gorm.DB) echo.MiddlewareFunc { 95 | return func(next echo.HandlerFunc) echo.HandlerFunc { 96 | return func(c echo.Context) error { 97 | c.Set("db", db) 98 | return next(c) 99 | } 100 | } 101 | } 102 | 103 | func (server Server) InjectDb() *gorm.DB { 104 | return server.MainDb 105 | } 106 | 107 | func (server Server) InjectUserRepository() *repository.UserRepositoryImpl { 108 | return repository.UserRepositoryImpl{}.NewUserRepositoryImpl(server.InjectDb()) 109 | //repository.CardRepositoryImpl{}.NewCardRepositoryImpl(server.InjectDb()) 110 | // TODO 이거 같은객체가 아니다. 동시성 및 멤버에 문제가 생길가능성 존재하여 위험하다. 111 | // 방법 1. db 쿼리문을 재사용하지않고 매번 사용한다. -> 위험하고 db단의 트랜잭션 처리를 애플리케이션단에서 잡아줘야한다. 112 | // 방법 2. 의존성 주입을 해줄 객체를 sington이나 static 객체로 한곳에서 관리한다. -> 주입부분을 갈아끼워야한다. 113 | // echo를 깊게 공부하지않아서 정확하지는 않지만, 하나의 transactin은 같은객체라면 echo가 잡아놔서 동시성을 처리해줄텐데 114 | // 어떤방법이 좋을지 모르겠다. 115 | } 116 | 117 | func (server Server) InjectUserService() *service.UserServiceImpl { 118 | return service.UserServiceImpl{}.NewUserServiceImpl(server.InjectUserRepository()) 119 | } 120 | 121 | func (server Server) InjectUserController() *api.UserController { 122 | return api.UserController{}.NewUserController(server.InjectUserService()) 123 | } 124 | 125 | func (server Server) InjectCardRepository() *repository.CardRepositoryImpl { 126 | return repository.CardRepositoryImpl{}.NewCardRepositoryImpl(server.InjectDb()) 127 | } 128 | 129 | func (server Server) InjectCardService() *service.CardServiceImpl { 130 | return service.CardServiceImpl{}.NewCardServiceImpl(server.InjectCardRepository()) 131 | } 132 | 133 | func (server Server) InjectCardController() *api.CardController { 134 | return api.CardController{}.NewCardController(server.InjectCardService()) 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # golang web sample 2 | ## 요약 3 | - golang echo framework를 사용한 api server sample 4 | - 동작확인을 위해 간단한 개발 5 | 6 | 7 | ## profile파일 추가 8 | - profile을 파일로 관리하여 원하는 자원을 편하게 쓸수 있게 한다. 9 | - 런타임시에만 일시적으로 환경변수를 적용시켜 관리 10 | 11 | ## Architecture 12 | - 3tier architecture(clean architecture)를 사용 13 | - layered architecture중 국내에서 가장 많이 사용하는 구조 14 | - presentation layer, business layer, data access layer 로 나뉜다. 15 | - presentation 계층(interfaces/controller) 16 | - 외부와 인터페이스 역할을 한다. 17 | - business 계층(service) 18 | - 비즈니스 로직을 작성한다. 19 | - data access 계층(repository) 20 | - 영속성 계층으로 쿼리문 등 영속성에 관련된 내용을 작성한다. 21 | - infrastructure계층 22 | - 애플리케이션의 기반이 되는 패키지들을 모아놓는다.(server, db...etc) 23 | 24 | ## Inversion of Control과 Dependency Injection 25 | - ioc와 di를 이용하여 계층구조의 의존성을 관리 26 | - 각 레이어는 내가 어떤 의존성을 갖는지 직접적으로 알 필요가 없으며(추상화된 주입이 이루어진다.) 27 | - 초기화시 ioc를 통해 계층관계의 의존성을 넣어준다. 28 | - ioc 29 | ``` go 30 | func (server Server) InjectDb() *gorm.DB { 31 | return server.MainDb 32 | } 33 | 34 | func (server Server) InjectUserRepository() *repository.UserRepositoryImpl { 35 | return repository.UserRepositoryImpl{}.NewUserRepositoryImpl(server.InjectDb()) 36 | } 37 | 38 | func (server Server) InjectUserService() *service.UserServiceImpl { 39 | return service.UserServiceImpl{}.NewUserServiceImpl(server.InjectUserRepository()) 40 | } 41 | 42 | func (server Server) InjectUserController() *api.UserController { 43 | return api.UserController{}.NewUserController(server.InjectUserService()) 44 | } 45 | 46 | ``` 47 | - di 48 | ```go 49 | type UserServiceImpl struct { 50 | repository.UserRepository 51 | } 52 | 53 | func (UserServiceImpl) NewUserServiceImpl(repository repository.UserRepository) *UserServiceImpl { 54 | return &UserServiceImpl{repository} 55 | } 56 | ``` 57 | controller, service, repository로 계층을 나눴지만, controller보다는 handler라는 이름을 사용할것을 추천한다. 58 | controller는 operator를 구축할때 종종 사용되는 네이밍으로 고랭 네이밍컨벤션에 위반될수 있으므로 애플리케이션 확장시 주의한다. 59 | 주입오류를 파일에서 확인하고 싶으면 New의 리턴값으로 인터페이스를 리턴하면 된다.
60 | golang에서 인터페이스를 리턴하면 마샬링등에서 에러가 나는 경우가 있지만 레이어를 구축할때는 고려하지 않아도 된다. 61 | 62 | ## testing 63 | - 사용 라이브러리 64 | ``` 65 | 통합테스트는 golang에 내장된 test 라이브러리를 사용한다. 66 | mockup의 경우 두 진영이 존재한다. 67 | 68 | github.com/golang/mock/gomock 69 | vs 70 | github.com/stretchr/testify/mock 71 | 72 | start 수는 testify가 golang 기본 네임스페이스보다 많다(13.8k, 5.9k) 73 | 74 | given when then을 사용하여 테스트 하는 방식은 거의 같으므로 75 | 76 | 사용하고 싶은걸 사용하면 된다. 77 | 78 | mock 구현체는 79 | golang/mock 진영은 mockgen을 사용 80 | stretchr/testify 진영은 vektra/mockery를 사용하여 구현한다. 81 | 82 | 양 진영에 장단점이 존재하지만, 83 | 편의성때문에 testify 를 더 많은 사람들이 사용하고 있다.(테스트의 기본적인 기능은 양진영 모두 포함하고 있어서 큰 차이가 없다.) 84 | 85 | ex) mock 구현명령어만 봐도 mockery가 쉽게 해놨다. 86 | mockgen -destination=$PWD/mocks -package mocks github.com/sgreben/gomock-vs-testify/comparison/gogenerate Interface1,Interface2 87 | mockery --all 88 | 89 | 이번 예시에서도 testfiy와 mockery 조합을 채택하였다. 90 | 91 | 두 진영에 대한 비교는 아래 블로그에 상세히 작성되어 있다. 92 | https://blog.codecentric.de/2019/07/gomock-vs-testify 93 | ``` 94 | 95 | - 명령어 96 | ```bash 97 | echo framework는 통합테스트만 지원, mockery 라이브러리를 사용한다. 98 | 홈 디렉터리에서 아래 명령어를 입력하면 mock 구현체가 모두 구현된다. 99 | $ go get github.com/stretchr/testify/mock 100 | $ go get github.com/vektra/mockery/v2/.../ 101 | $ mockery --all --keeptree 102 | ``` 103 | ## test코드 작성법 104 | - 통합테스트 105 | ``` 106 | 고랭 testing과 echo(controller 사용을 위해)를 사용하여 테스트하며 107 | 영속성 계층이 의존하는 db만(직접 mock을 생성해 변경), 주입하여 테스트한다. 108 | 109 | 모든계층을 테스트 할 수 있으며, db만 같다면 실제 운영환경과 같은 환경으로 테스트가 가능하다. 110 | (운영환경과 db까지 같은환경을 원한다면 db도 변경없이 테스트한다.) 111 | 112 | 원하는 계층의 mock을 직접 생성하면 단위테스트도 할 수있지만 113 | 114 | mock객체 생성은 mockery 라이브러리를 이용한다. 115 | ``` 116 | 117 | - 단위테스트 118 | ``` 119 | testify와 mockery를 사용한다. 120 | 각 계층간 interface역할을 하는 type ~~ interface의 mock을 mockery가 구현해주므로 121 | 나는 원하는 계층이 원하는 로직을 실행하는지 손쉽게 테스트 할 수 있다. 122 | ``` 123 | 124 | 125 | ## sonarqube 126 | - sonarqube config 파일 127 | ``` 128 | #Configure here general information about the environment, such as SonarQube server connection details for example 129 | #No information about specific project should appear here 130 | 131 | #----- Default SonarQube server 132 | sonar.host.url=url 입력 133 | sonar.login=token 입력 134 | 135 | # #----- Default source code encoding 136 | sonar.sourceEncoding=UTF-8 137 | 138 | sonar.projectKey=golang-echo-sample 139 | sonar.projectName=golang-echo-sample 140 | # sonar.projectVersion=1.0 141 | sonar.language=go 142 | sonar.sources=. 143 | sonar.exclusions=**/mock/**,**/secret/**,**/docs/**,**/data/**,.idea/**,**/vendor/** 144 | sonar.sourceEncoding=UTF-8 145 | sonar.tests=. 146 | sonar.test.inclusions=**/*_test.go 147 | sonar.test.exclusions=**/vendor/** 148 | sonar.go.coverage.reportPaths=**/coverage.out 149 | ``` 150 | - 명령어 151 | ```bash 152 | $ go test -v ./... -coverprofile=coverage.out 153 | $ go test -v ./... -json > report.json 154 | $ sonar-scanner 155 | ``` 156 | -------------------------------------------------------------------------------- /controller/api/user.controller.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "net/http" 6 | "platform-sample/controller/dto" 7 | _ "platform-sample/docs" 8 | "platform-sample/model" 9 | "platform-sample/service" 10 | "strconv" 11 | ) 12 | 13 | type UserController struct { 14 | service.UserService 15 | } 16 | 17 | func (UserController) NewUserController(service service.UserService) *UserController { 18 | return &UserController{service} 19 | } 20 | 21 | func (userController *UserController) Init(e *echo.Group) { 22 | e.POST("", userController.CreateUser) 23 | e.GET("", userController.GetUsers) 24 | e.DELETE("/:id", userController.DeleteUser) 25 | e.GET("/:id", userController.GetUser) 26 | e.PATCH("", userController.UpdateUser) 27 | } 28 | 29 | // CreateUser is create new user 30 | // @Summary Create user 31 | // @Description Create new user passing name parameter 32 | // @Accept json 33 | // @Produce json 34 | // @Param user body UserDto true "body of the user" 35 | // @Success 201 {object} ApiResult{result=model.User} 36 | // @Failure 500 {object} ApiResult{result=model.User} "Internal Server Error" 37 | // @Router /users [post] 38 | func (userController *UserController) CreateUser(c echo.Context) error { 39 | userDto := &dto.UserDto{} 40 | bindErr := c.Bind(userDto) 41 | user := userDto.ToModel() 42 | 43 | if bindErr != nil { 44 | 45 | c.Logger().Error(bindErr) 46 | return ReturnApiFail(c, http.StatusBadRequest, ApiParameterError, bindErr) 47 | } 48 | createUser, err := userController.UserService.CreateUser(user) 49 | if err != nil { 50 | c.Logger().Error(err) 51 | return ReturnApiFail(c, http.StatusInternalServerError, ApiQueryError, err) 52 | } 53 | c.Logger().Info(createUser) 54 | return ReturnApiSuccess(c, http.StatusCreated, createUser) 55 | } 56 | 57 | // GetUsers get all users' list 58 | // @Summary Get all users 59 | // @Description Get all user's info 60 | // @Accept json 61 | // @Produce json 62 | // @Success 200 {object} ApiResult{result=model.User} 63 | // @Router /users [get] 64 | func (userController *UserController) GetUsers(c echo.Context) error { 65 | users, err := userController.UserService.GetUsers() 66 | if err != nil { 67 | c.Logger().Error(err) 68 | return ReturnApiFail(c, http.StatusInternalServerError, ApiQueryError, err) 69 | } 70 | c.Logger().Info(users) 71 | return ReturnApiSuccess(c, http.StatusOK, users) 72 | } 73 | 74 | // DeleteUser delete specific user's info 75 | // @Summary Delete user 76 | // @Description Delete existing user's info passing id parameter 77 | // @Accept json 78 | // @Produce json 79 | // @Param id path string true "id of the user" 80 | // @Success 204 {object} ApiResult{result=model.User} 81 | // @Router /users/{id} [delete] 82 | func (userController *UserController) DeleteUser(c echo.Context) error { 83 | id, err := strconv.Atoi(c.Param("id")) 84 | if err != nil { 85 | c.Logger().Error(err) 86 | return ReturnApiFail(c, http.StatusBadRequest, ApiParameterError, err) 87 | } 88 | 89 | err = userController.UserService.DeleteUser(id) 90 | if err != nil { 91 | c.Logger().Error(err) 92 | return ReturnApiFail(c, http.StatusInternalServerError, ApiQueryError, err) 93 | } 94 | return ReturnApiSuccess(c, http.StatusNoContent, nil) 95 | } 96 | 97 | // GetUser get user's info using id 98 | // @Summary Get user 99 | // @Description Get user's info passing id parameter 100 | // @Accept json 101 | // @Produce json 102 | // @Param id path string true "id of the user" 103 | // @Success 200 {object} ApiResult{result=model.User} 104 | // @Failure 500 {object} ApiResult{result=model.User} "Internal Server Error" 105 | // @Router /users/{id} [get] 106 | func (userController *UserController) GetUser(c echo.Context) error { 107 | id, err := strconv.Atoi(c.Param("id")) 108 | if err != nil { 109 | c.Logger().Error(err) 110 | return ReturnApiFail(c, http.StatusBadRequest, ApiParameterError, err) 111 | } 112 | 113 | user, err := userController.UserService.GetUser(id) 114 | if err != nil { 115 | c.Logger().Error(err) 116 | return ReturnApiFail(c, http.StatusInternalServerError, ApiQueryError, err) 117 | } 118 | c.Logger().Info(user) 119 | return ReturnApiSuccess(c, http.StatusOK, user) 120 | } 121 | 122 | // UpdateUser updates existing user's info 123 | // @Summary Update user 124 | // @Description Update existing user's information 125 | // @Accept json 126 | // @Produce json 127 | // @Param name body model.User true "body of the user" 128 | // @Success 201 {object} ApiResult{result=model.User} 129 | // @Router /users [patch] 130 | func (userController *UserController) UpdateUser(c echo.Context) error { 131 | user := &model.User{} 132 | bindErr := c.Bind(user) 133 | if bindErr != nil { 134 | c.Logger().Error(bindErr) 135 | return ReturnApiFail(c, http.StatusBadRequest, ApiParameterError, bindErr) 136 | } 137 | 138 | createUser, err := userController.UserService.UpdateUser(user) 139 | if err != nil { 140 | c.Logger().Error(bindErr) 141 | return ReturnApiFail(c, http.StatusInternalServerError, ApiQueryError, err) 142 | } 143 | c.Logger().Info(createUser) 144 | return ReturnApiSuccess(c, http.StatusOK, user) 145 | } 146 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Platform-sample Swagger API", 5 | "contact": {}, 6 | "version": "1.0.0" 7 | }, 8 | "host": "localhost:8395", 9 | "basePath": "/api", 10 | "paths": { 11 | "/users": { 12 | "get": { 13 | "description": "Get all user's info", 14 | "consumes": [ 15 | "application/json" 16 | ], 17 | "produces": [ 18 | "application/json" 19 | ], 20 | "summary": "Get all users", 21 | "responses": { 22 | "200": { 23 | "description": "OK", 24 | "schema": { 25 | "$ref": "#/definitions/model.User" 26 | } 27 | } 28 | } 29 | }, 30 | "post": { 31 | "description": "Create new user", 32 | "consumes": [ 33 | "application/json" 34 | ], 35 | "produces": [ 36 | "application/json" 37 | ], 38 | "summary": "Create user", 39 | "parameters": [ 40 | { 41 | "description": "body of the user", 42 | "name": "user", 43 | "in": "body", 44 | "required": true, 45 | "schema": { 46 | "$ref": "#/definitions/api.UserDto" 47 | } 48 | } 49 | ], 50 | "responses": { 51 | "201": { 52 | "description": "Created", 53 | "schema": { 54 | "allOf": [ 55 | { 56 | "$ref": "#/definitions/api.ApiResult" 57 | }, 58 | { 59 | "type": "object", 60 | "properties": { 61 | "result": { 62 | "$ref": "#/definitions/model.User" 63 | } 64 | } 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | }, 71 | "patch": { 72 | "description": "Get user's info", 73 | "consumes": [ 74 | "application/json" 75 | ], 76 | "produces": [ 77 | "application/json" 78 | ], 79 | "summary": "Update user", 80 | "parameters": [ 81 | { 82 | "description": "body of the user", 83 | "name": "name", 84 | "in": "body", 85 | "required": true, 86 | "schema": { 87 | "$ref": "#/definitions/model.User" 88 | } 89 | } 90 | ], 91 | "responses": { 92 | "201": { 93 | "description": "Created", 94 | "schema": { 95 | "$ref": "#/definitions/model.User" 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "/users/{id}": { 102 | "get": { 103 | "description": "Get user's info", 104 | "consumes": [ 105 | "application/json" 106 | ], 107 | "produces": [ 108 | "application/json" 109 | ], 110 | "summary": "Get user", 111 | "parameters": [ 112 | { 113 | "type": "string", 114 | "description": "id of the user", 115 | "name": "id", 116 | "in": "path", 117 | "required": true 118 | } 119 | ], 120 | "responses": { 121 | "200": { 122 | "description": "OK", 123 | "schema": { 124 | "$ref": "#/definitions/model.User" 125 | } 126 | } 127 | } 128 | }, 129 | "delete": { 130 | "description": "Delete user's info", 131 | "consumes": [ 132 | "application/json" 133 | ], 134 | "produces": [ 135 | "application/json" 136 | ], 137 | "summary": "Delete user", 138 | "parameters": [ 139 | { 140 | "type": "string", 141 | "description": "id of the user", 142 | "name": "id", 143 | "in": "path", 144 | "required": true 145 | } 146 | ], 147 | "responses": { 148 | "204": { 149 | "description": "No Content", 150 | "schema": { 151 | "$ref": "#/definitions/model.User" 152 | } 153 | } 154 | } 155 | } 156 | } 157 | }, 158 | "definitions": { 159 | "api.ApiError": { 160 | "type": "object", 161 | "properties": { 162 | "code": { 163 | "type": "integer" 164 | }, 165 | "details": { 166 | "type": "object" 167 | }, 168 | "message": { 169 | "type": "string" 170 | } 171 | } 172 | }, 173 | "api.ApiResult": { 174 | "type": "object", 175 | "properties": { 176 | "error": { 177 | "$ref": "#/definitions/api.ApiError" 178 | }, 179 | "result": { 180 | "type": "object" 181 | }, 182 | "success": { 183 | "type": "boolean" 184 | } 185 | } 186 | }, 187 | "api.UserDto": { 188 | "type": "object", 189 | "properties": { 190 | "name": { 191 | "type": "string" 192 | } 193 | } 194 | }, 195 | "model.User": { 196 | "type": "object", 197 | "properties": { 198 | "createdAt": { 199 | "type": "string" 200 | }, 201 | "deletedAt": { 202 | "type": "string" 203 | }, 204 | "id": { 205 | "type": "integer" 206 | }, 207 | "name": { 208 | "type": "string" 209 | }, 210 | "updatedAt": { 211 | "type": "string" 212 | } 213 | } 214 | } 215 | } 216 | } -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | 4 | package docs 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "strings" 10 | 11 | "github.com/alecthomas/template" 12 | "github.com/swaggo/swag" 13 | ) 14 | 15 | var doc = `{ 16 | "schemes": {{ marshal .Schemes }}, 17 | "swagger": "2.0", 18 | "info": { 19 | "description": "{{.Description}}", 20 | "title": "{{.Title}}", 21 | "contact": {}, 22 | "version": "{{.Version}}" 23 | }, 24 | "host": "{{.Host}}", 25 | "basePath": "{{.BasePath}}", 26 | "paths": { 27 | "/users": { 28 | "get": { 29 | "description": "Get all user's info", 30 | "consumes": [ 31 | "application/json" 32 | ], 33 | "produces": [ 34 | "application/json" 35 | ], 36 | "summary": "Get all users", 37 | "responses": { 38 | "200": { 39 | "description": "OK", 40 | "schema": { 41 | "$ref": "#/definitions/model.User" 42 | } 43 | } 44 | } 45 | }, 46 | "post": { 47 | "description": "Create new user", 48 | "consumes": [ 49 | "application/json" 50 | ], 51 | "produces": [ 52 | "application/json" 53 | ], 54 | "summary": "Create user", 55 | "parameters": [ 56 | { 57 | "description": "body of the user", 58 | "name": "user", 59 | "in": "body", 60 | "required": true, 61 | "schema": { 62 | "$ref": "#/definitions/api.UserDto" 63 | } 64 | } 65 | ], 66 | "responses": { 67 | "201": { 68 | "description": "Created", 69 | "schema": { 70 | "allOf": [ 71 | { 72 | "$ref": "#/definitions/api.ApiResult" 73 | }, 74 | { 75 | "type": "object", 76 | "properties": { 77 | "result": { 78 | "$ref": "#/definitions/model.User" 79 | } 80 | } 81 | } 82 | ] 83 | } 84 | } 85 | } 86 | }, 87 | "patch": { 88 | "description": "Get user's info", 89 | "consumes": [ 90 | "application/json" 91 | ], 92 | "produces": [ 93 | "application/json" 94 | ], 95 | "summary": "Update user", 96 | "parameters": [ 97 | { 98 | "description": "body of the user", 99 | "name": "name", 100 | "in": "body", 101 | "required": true, 102 | "schema": { 103 | "$ref": "#/definitions/model.User" 104 | } 105 | } 106 | ], 107 | "responses": { 108 | "201": { 109 | "description": "Created", 110 | "schema": { 111 | "$ref": "#/definitions/model.User" 112 | } 113 | } 114 | } 115 | } 116 | }, 117 | "/users/{id}": { 118 | "get": { 119 | "description": "Get user's info", 120 | "consumes": [ 121 | "application/json" 122 | ], 123 | "produces": [ 124 | "application/json" 125 | ], 126 | "summary": "Get user", 127 | "parameters": [ 128 | { 129 | "type": "string", 130 | "description": "id of the user", 131 | "name": "id", 132 | "in": "path", 133 | "required": true 134 | } 135 | ], 136 | "responses": { 137 | "200": { 138 | "description": "OK", 139 | "schema": { 140 | "$ref": "#/definitions/model.User" 141 | } 142 | } 143 | } 144 | }, 145 | "delete": { 146 | "description": "Delete user's info", 147 | "consumes": [ 148 | "application/json" 149 | ], 150 | "produces": [ 151 | "application/json" 152 | ], 153 | "summary": "Delete user", 154 | "parameters": [ 155 | { 156 | "type": "string", 157 | "description": "id of the user", 158 | "name": "id", 159 | "in": "path", 160 | "required": true 161 | } 162 | ], 163 | "responses": { 164 | "204": { 165 | "description": "No Content", 166 | "schema": { 167 | "$ref": "#/definitions/model.User" 168 | } 169 | } 170 | } 171 | } 172 | } 173 | }, 174 | "definitions": { 175 | "api.ApiError": { 176 | "type": "object", 177 | "properties": { 178 | "code": { 179 | "type": "integer" 180 | }, 181 | "details": { 182 | "type": "object" 183 | }, 184 | "message": { 185 | "type": "string" 186 | } 187 | } 188 | }, 189 | "api.ApiResult": { 190 | "type": "object", 191 | "properties": { 192 | "error": { 193 | "$ref": "#/definitions/api.ApiError" 194 | }, 195 | "result": { 196 | "type": "object" 197 | }, 198 | "success": { 199 | "type": "boolean" 200 | } 201 | } 202 | }, 203 | "api.UserDto": { 204 | "type": "object", 205 | "properties": { 206 | "name": { 207 | "type": "string" 208 | } 209 | } 210 | }, 211 | "model.User": { 212 | "type": "object", 213 | "properties": { 214 | "createdAt": { 215 | "type": "string" 216 | }, 217 | "deletedAt": { 218 | "type": "string" 219 | }, 220 | "id": { 221 | "type": "integer" 222 | }, 223 | "name": { 224 | "type": "string" 225 | }, 226 | "updatedAt": { 227 | "type": "string" 228 | } 229 | } 230 | } 231 | } 232 | }` 233 | 234 | type swaggerInfo struct { 235 | Version string 236 | Host string 237 | BasePath string 238 | Schemes []string 239 | Title string 240 | Description string 241 | } 242 | 243 | // SwaggerInfo holds exported Swagger Info so clients can modify it 244 | var SwaggerInfo = swaggerInfo{ 245 | Version: "1.0.0", 246 | Host: "localhost:8395", 247 | BasePath: "/api", 248 | Schemes: []string{}, 249 | Title: "Platform-sample Swagger API", 250 | Description: "", 251 | } 252 | 253 | type s struct{} 254 | 255 | func (s *s) ReadDoc() string { 256 | sInfo := SwaggerInfo 257 | sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) 258 | 259 | t, err := template.New("swagger_info").Funcs(template.FuncMap{ 260 | "marshal": func(v interface{}) string { 261 | a, _ := json.Marshal(v) 262 | return string(a) 263 | }, 264 | }).Parse(doc) 265 | if err != nil { 266 | return doc 267 | } 268 | 269 | var tpl bytes.Buffer 270 | if err := t.Execute(&tpl, sInfo); err != nil { 271 | return doc 272 | } 273 | 274 | return tpl.String() 275 | } 276 | 277 | func init() { 278 | swag.Register(swag.Name, &s{}) 279 | } 280 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 3 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 4 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 5 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 6 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 7 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 8 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 9 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 10 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 14 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 19 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 20 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 21 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 22 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 23 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 24 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 25 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 26 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 27 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 28 | github.com/go-openapi/jsonreference v0.19.4/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 29 | github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= 30 | github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= 31 | github.com/go-openapi/spec v0.19.14/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA= 32 | github.com/go-openapi/spec v0.20.0 h1:HGLc8AJ7ynOxwv0Lq4TsnwLsWMawHAYiJIFzbcML86I= 33 | github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= 34 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 35 | github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= 36 | github.com/go-openapi/swag v0.19.12 h1:Bc0bnY2c3AoF7Gc+IMIAQQsD8fLHjHpc19wXvYuayQI= 37 | github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= 38 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 39 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 40 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 41 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 42 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 43 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 44 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 45 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 46 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 47 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 48 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 49 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 50 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 51 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 52 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 53 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 54 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 55 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 56 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 57 | github.com/labstack/echo/v4 v4.0.0/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno= 58 | github.com/labstack/echo/v4 v4.4.0 h1:rblX1cN6T4LvUW9ZKMPZ17uPl/Dc8igP7ZmjGHZoj4A= 59 | github.com/labstack/echo/v4 v4.4.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8= 60 | github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 61 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 62 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 63 | github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= 64 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 65 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 66 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 67 | github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= 68 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 69 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 70 | github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 71 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 72 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 73 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 74 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 75 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 76 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 77 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 78 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 79 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 80 | github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= 81 | github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 82 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 83 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 84 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 85 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 86 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 87 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 88 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 89 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 90 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 91 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 92 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 93 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 94 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 95 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 96 | github.com/swaggo/echo-swagger v1.1.0 h1:P46vSnGTjCo4PCDnztbyyiJ9csTt8/GvwL6UIhr4zEM= 97 | github.com/swaggo/echo-swagger v1.1.0/go.mod h1:JaipWDPqOBMwM40W6qz0o07lnPOxrhDkpjA2OaqfzL8= 98 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= 99 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= 100 | github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= 101 | github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= 102 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 103 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 104 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 105 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 106 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= 107 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 108 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= 109 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 110 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 111 | golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 112 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 113 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 114 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 115 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 116 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 117 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= 118 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 119 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 120 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 121 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 122 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 123 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 124 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 125 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 126 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 127 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 128 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 129 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 130 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 131 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= 132 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 133 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 137 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A= 147 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 149 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 150 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 151 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 152 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 153 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 154 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 155 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 156 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 157 | golang.org/x/tools v0.0.0-20201120155355-20be4ac4bd6e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 158 | golang.org/x/tools v0.0.0-20201207182000-5679438983bd h1:aZYo+3GGTb9Pya0Di6t7G0JOwKGb782xQAJlZyVcwII= 159 | golang.org/x/tools v0.0.0-20201207182000-5679438983bd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 160 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 161 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 162 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 163 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 165 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 166 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 167 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 168 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 169 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 170 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 171 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 172 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 173 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 174 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= 175 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 176 | --------------------------------------------------------------------------------