├── .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 |
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 |
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 |
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 |
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 |
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 |
{{index . "id"}}
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 |
--------------------------------------------------------------------------------