├── README.md ├── adapter ├── db │ ├── common.go │ ├── commont_test.go │ ├── memo_repository.go │ ├── memo_repository_test.go │ ├── multi_thread_test.go │ ├── tag_repository.go │ ├── tag_repository_test.go │ └── transaction_repository.go ├── error │ └── error.go ├── logger │ └── logger.go ├── memory │ ├── memo_repository.go │ ├── memo_repository_test.go │ ├── tag_repository.go │ ├── tag_repository_test.go │ └── transaction_repository.go └── view │ └── render │ └── json.go ├── di ├── injector.go └── wire_gen.go ├── domain ├── model │ ├── memo.go │ └── tag.go └── repository │ ├── memo_repository.go │ ├── tag_repository.go │ └── transaction_repository.go ├── go.mod ├── go.sum ├── infra ├── database │ ├── common.go │ └── mysql.go ├── error │ └── error.go └── logger │ └── logger.go ├── interface └── api │ ├── common.go │ ├── controller.go │ └── presenter.go ├── testutil └── testutil.go ├── usecase ├── common_test.go ├── input │ └── memo.go ├── interactor.go ├── memo.go ├── memo_test.go └── presenter.go ├── view ├── model │ └── json │ │ ├── error.go │ │ └── memo.go └── render │ └── json.go └── web.go /README.md: -------------------------------------------------------------------------------- 1 | # memo sample 2 | 3 | The sample code of "The Clean Architecture" 4 | 5 | 6 | overview 7 | 8 | [Description(Japanese)](https://qiita.com/muroon/items/8add8da911341312176d) 9 | 10 | [Cloud Spanner version of this project](https://github.com/muroon/memo_sample_spanner) 11 | 12 | -------------------------------------------------------------------------------- /adapter/db/common.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "memo_sample/adapter/error" 7 | "memo_sample/infra/database" 8 | "memo_sample/infra/error" 9 | ) 10 | 11 | var dbm *database.DBM 12 | 13 | var errm apperror.ErrorManager 14 | 15 | func init() { 16 | dbm = database.GetDBM() 17 | errm = apperrorsub.NewErrorManager() 18 | } 19 | 20 | // begin begin transaction 21 | func begin(ctx context.Context) (context.Context, error) { 22 | return (*dbm).Begin(ctx) 23 | } 24 | 25 | // rollback rollback transaction 26 | func rollback(ctx context.Context) (context.Context, error) { 27 | return (*dbm).Rollback(ctx) 28 | } 29 | 30 | // commit commit transaction 31 | func commit(ctx context.Context) (context.Context, error) { 32 | return (*dbm).Commit(ctx) 33 | } 34 | 35 | // prepare prepare statement 36 | func prepare(ctx context.Context, query string) (*sql.Stmt, error) { 37 | return (*dbm).Prepare(ctx, query) 38 | } 39 | -------------------------------------------------------------------------------- /adapter/db/commont_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "memo_sample/domain/repository" 5 | ) 6 | 7 | // connectTestDB DB接続 8 | func connectTestDB() { 9 | if err := (*dbm).ConnectTestDB(); err != nil { 10 | panic(err) 11 | } 12 | } 13 | 14 | // closeTestDB DB切断 15 | func closeTestDB() { 16 | _ = (*dbm).CloseDB() 17 | } 18 | 19 | // getTransactionRepositoryForTest get TransactionRepository 20 | func getTransactionRepositoryForTest() repository.TransactionRepository { 21 | return NewTransactionRepository() 22 | } 23 | -------------------------------------------------------------------------------- /adapter/db/memo_repository.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "memo_sample/domain/model" 7 | "memo_sample/domain/repository" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // NewMemoRepository get repository 14 | func NewMemoRepository() repository.MemoRepository { 15 | return memoRepository{} 16 | } 17 | 18 | // memoRepository Memo's Repository Sub 19 | type memoRepository struct{} 20 | 21 | // Save save Memo Data 22 | func (m memoRepository) Save(ctx context.Context, text string) (*model.Memo, error) { 23 | var err error 24 | var res sql.Result 25 | query := "insert into memo(text) values(?)" 26 | stmt, err := prepare(ctx, query) 27 | defer stmt.Close() 28 | if err != nil { 29 | return nil, errm.Wrap(err, http.StatusInternalServerError) 30 | } 31 | 32 | res, err = stmt.ExecContext(ctx, text) 33 | if err != nil { 34 | return nil, errm.Wrap(err, http.StatusInternalServerError) 35 | } 36 | 37 | id, err := res.LastInsertId() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return m.Get(ctx, int(id)) 43 | } 44 | 45 | // Get get Memo Data by ID 46 | func (m memoRepository) Get(ctx context.Context, id int) (*model.Memo, error) { 47 | mem := new(model.Memo) 48 | var err error 49 | query := "select * from memo where id = ?" 50 | stmt, err := prepare(ctx, query) 51 | defer stmt.Close() 52 | if err != nil { 53 | return nil, errm.Wrap(err, http.StatusInternalServerError) 54 | } 55 | 56 | err = stmt.QueryRowContext(ctx, id).Scan(&mem.ID, &mem.Text) 57 | if err != nil { 58 | return nil, errm.Wrap(err, http.StatusInternalServerError) 59 | } 60 | 61 | return mem, err 62 | } 63 | 64 | // GetAll get all Memo Data 65 | func (m memoRepository) GetAll(ctx context.Context) ([]*model.Memo, error) { 66 | var rows *sql.Rows 67 | var err error 68 | query := "select * from memo" 69 | stmt, err := prepare(ctx, query) 70 | defer stmt.Close() 71 | if err != nil { 72 | return nil, errm.Wrap(err, http.StatusInternalServerError) 73 | } 74 | 75 | rows, err = stmt.QueryContext(ctx) 76 | if err != nil { 77 | return nil, errm.Wrap(err, http.StatusInternalServerError) 78 | } 79 | 80 | return m.getModelList(rows) 81 | } 82 | 83 | // Search search memo by text 84 | func (m memoRepository) Search(ctx context.Context, text string) ([]*model.Memo, error) { 85 | var rows *sql.Rows 86 | var err error 87 | query := "select * from memo where text like '%" + text + "%'" 88 | stmt, err := prepare(ctx, query) 89 | defer stmt.Close() 90 | if err != nil { 91 | return nil, errm.Wrap(err, http.StatusInternalServerError) 92 | } 93 | 94 | rows, err = stmt.QueryContext(ctx) 95 | if err != nil { 96 | return nil, errm.Wrap(err, http.StatusInternalServerError) 97 | } 98 | 99 | return m.getModelList(rows) 100 | } 101 | 102 | // GetAllByIDs get all Memo Data by ID 103 | func (m memoRepository) GetAllByIDs(ctx context.Context, ids []int) ([]*model.Memo, error) { 104 | idvs := make([]string, 0) 105 | for _, id := range ids { 106 | idvs = append(idvs, strconv.Itoa(id)) 107 | } 108 | 109 | query := "select * from memo where id in (" + strings.Join(idvs, ",") + ")" 110 | 111 | var rows *sql.Rows 112 | var err error 113 | stmt, err := prepare(ctx, query) 114 | defer stmt.Close() 115 | if err != nil { 116 | return nil, errm.Wrap(err, http.StatusInternalServerError) 117 | } 118 | 119 | rows, err = stmt.QueryContext(ctx) 120 | if err != nil { 121 | return nil, errm.Wrap(err, http.StatusInternalServerError) 122 | } 123 | 124 | return m.getModelList(rows) 125 | } 126 | 127 | // getModelList get model list 128 | func (m memoRepository) getModelList(rows *sql.Rows) ([]*model.Memo, error) { 129 | list := make([]*model.Memo, 0) 130 | for rows.Next() { 131 | mem := new(model.Memo) 132 | err := rows.Scan(&mem.ID, &mem.Text) 133 | if err != nil { 134 | return list, errm.Wrap(err, http.StatusInternalServerError) 135 | } 136 | list = append(list, mem) 137 | } 138 | 139 | return list, nil 140 | } 141 | -------------------------------------------------------------------------------- /adapter/db/memo_repository_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "memo_sample/domain/repository" 6 | "testing" 7 | ) 8 | 9 | func getMemoRepositoryForTest() repository.MemoRepository { 10 | return NewMemoRepository() 11 | } 12 | 13 | func TestMemoSaveInDBSuccess(t *testing.T) { 14 | 15 | repo := getMemoRepositoryForTest() 16 | 17 | ctx := context.Background() 18 | 19 | connectTestDB() 20 | defer closeTestDB() 21 | 22 | // 1件名 23 | memo, err := repo.Save(ctx, "First") 24 | if err != nil { 25 | t.Error("failed TestMemoSaveInMemorySuccess Save", err) 26 | } 27 | 28 | memoGet, err := repo.Get(ctx, memo.ID) 29 | if err != nil || memoGet.ID != memo.ID { 30 | t.Error("failed TestMemoSaveInMemorySuccess Get", err, memoGet.ID) 31 | } 32 | 33 | // 2件名 34 | memo, err = repo.Save(ctx, "Second") 35 | if err != nil { 36 | t.Error("failed TestMemoSaveInMemorySuccess GenerateID", err) 37 | } 38 | 39 | memoGet, err = repo.Get(ctx, memo.ID) 40 | if err != nil || memoGet.ID != memo.ID { 41 | t.Error("failed TestMemoSaveInMemorySuccess Get", err, memoGet.ID) 42 | } 43 | 44 | // 全件取得 45 | list, err := repo.GetAll(ctx) 46 | if err != nil { 47 | t.Error("failed TestMemoSaveInMemorySuccess Get", err, len(list)) 48 | } 49 | 50 | for _, v := range list { 51 | t.Logf("TestMemoSaveInMemorySuccess GetAll MemoRepository id:%d, text:%s", v.ID, v.Text) 52 | } 53 | } 54 | 55 | func TestMemoTransactionCommitSuccess(t *testing.T) { 56 | 57 | repo := getMemoRepositoryForTest() 58 | repoTx := getTransactionRepositoryForTest() 59 | 60 | ctx := context.Background() 61 | 62 | connectTestDB() 63 | defer func() { 64 | if err := recover(); err != nil { 65 | _, _ = repoTx.Rollback(ctx) 66 | t.Error(err) 67 | } 68 | closeTestDB() 69 | }() 70 | 71 | ctx, err := repoTx.Begin(ctx) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | _, err = repo.Save(ctx, "Transaction Commit Test") 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | _, err = repoTx.Commit(ctx) 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | 87 | func TestMemoTransactionRollbackSuccess(t *testing.T) { 88 | 89 | repo := getMemoRepositoryForTest() 90 | repoTx := getTransactionRepositoryForTest() 91 | 92 | ctx := context.Background() 93 | 94 | connectTestDB() 95 | defer func() { 96 | if err := recover(); err != nil { 97 | _, _ = repoTx.Rollback(ctx) 98 | t.Error(err) 99 | } 100 | closeTestDB() 101 | }() 102 | 103 | ctx, err := repoTx.Begin(ctx) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | _, err = repo.Save(ctx, "Transaction Rollback Test") 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | _, _ = repoTx.Rollback(ctx) 114 | } 115 | 116 | func TestMemoSearchSuccess(t *testing.T) { 117 | 118 | repo := getMemoRepositoryForTest() 119 | 120 | ctx := context.Background() 121 | 122 | connectTestDB() 123 | defer closeTestDB() 124 | 125 | word := "Memo Search Test" 126 | _, err := repo.Save(ctx, word) 127 | if err != nil { 128 | t.Error(err) 129 | } 130 | 131 | word = "Memo" 132 | list, err := repo.Search(ctx, word) 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | 137 | for _, m := range list { 138 | t.Log(m) 139 | } 140 | } 141 | 142 | func TestMemoGetAllByIDsSuccess(t *testing.T) { 143 | 144 | repo := getMemoRepositoryForTest() 145 | 146 | ctx := context.Background() 147 | 148 | connectTestDB() 149 | defer closeTestDB() 150 | 151 | word := "Dummy First" 152 | memo1, err := repo.Save(ctx, word) 153 | if err != nil { 154 | t.Error(err) 155 | } 156 | 157 | word = "Dummy Second" 158 | memo2, err := repo.Save(ctx, word) 159 | if err != nil { 160 | t.Error(err) 161 | } 162 | 163 | ids := []int{memo1.ID, memo2.ID} 164 | list, err := repo.GetAllByIDs(ctx, ids) 165 | if err != nil { 166 | t.Error(err) 167 | } 168 | 169 | for _, m := range list { 170 | t.Log(m) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /adapter/db/multi_thread_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | type Result string 10 | 11 | var errChan chan error 12 | var resChan chan Result 13 | 14 | func TestMutiThreadSuccess(t *testing.T) { 15 | 16 | connectTestDB() 17 | defer closeTestDB() 18 | 19 | threadCount := 2 20 | errChan = make(chan error, threadCount) 21 | resChan = make(chan Result, threadCount) 22 | 23 | for i := 0; i < threadCount; i++ { 24 | name := fmt.Sprintf("Thread%d", i) 25 | go executeTestOnThread(name) 26 | } 27 | 28 | for i := 0; i < threadCount; i++ { 29 | select { 30 | case err := <-errChan: 31 | t.Error(err) 32 | case name := <-resChan: 33 | t.Log("Finished:", name) 34 | } 35 | } 36 | } 37 | 38 | func executeTestOnThread(word string) { 39 | ctx := context.Background() 40 | 41 | err := executeTestRepositoryOnThread(ctx, word) 42 | if err != nil { 43 | errChan <- err 44 | return 45 | } 46 | resChan <- Result(word) 47 | } 48 | 49 | func executeTestRepositoryOnThread(ctx context.Context, word string) error { 50 | 51 | baseWord := word + ":%d" 52 | 53 | loopCount := 2 54 | for i := 0; i < loopCount; i++ { 55 | var err error 56 | memoText, tagTitle := fmt.Sprintf(baseWord, i), fmt.Sprintf(baseWord, i) 57 | _, _, err = savaContents(ctx, memoText, tagTitle) 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func savaContents(ctx context.Context, memoText, tagTitle string) (string, string, error) { 67 | repoT := getTagRepositoryForTest() 68 | repoM := getMemoRepositoryForTest() 69 | repoTx := getTransactionRepositoryForTest() 70 | 71 | ctx, err := repoTx.Begin(ctx) 72 | if err != nil { 73 | _, _ = repoTx.Rollback(ctx) 74 | return memoText, tagTitle, err 75 | } 76 | 77 | memo, err := repoM.Save(ctx, memoText) 78 | if err != nil { 79 | _, _ = repoTx.Rollback(ctx) 80 | return memoText, tagTitle, err 81 | } 82 | 83 | tag, err := repoT.Save(ctx, tagTitle) 84 | if err != nil { 85 | _, _ = repoTx.Rollback(ctx) 86 | return memoText, tagTitle, err 87 | } 88 | 89 | err = repoT.SaveTagAndMemo(ctx, tag.ID, memo.ID) 90 | if err != nil { 91 | _, _ = repoTx.Rollback(ctx) 92 | return memoText, tagTitle, err 93 | } 94 | 95 | ctx, err = repoTx.Commit(ctx) 96 | if err != nil { 97 | _, _ = repoTx.Rollback(ctx) 98 | return memoText, tagTitle, err 99 | } 100 | 101 | m, err := repoM.Get(ctx, memo.ID) 102 | if err != nil { 103 | return memoText, tagTitle, err 104 | } 105 | 106 | t, err := repoT.Get(ctx, tag.ID) 107 | if err != nil { 108 | return memoText, tagTitle, err 109 | } 110 | 111 | return m.Text, t.Title, err 112 | } 113 | -------------------------------------------------------------------------------- /adapter/db/tag_repository.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "memo_sample/domain/model" 7 | "memo_sample/domain/repository" 8 | 9 | "net/http" 10 | ) 11 | 12 | // NewTagRepository get repository 13 | func NewTagRepository() repository.TagRepository { 14 | return tagRepository{} 15 | } 16 | 17 | // tagRepository Tag's Repository Sub 18 | type tagRepository struct{} 19 | 20 | // Save save Tag Data 21 | func (m tagRepository) Save(ctx context.Context, title string) (*model.Tag, error) { 22 | var err error 23 | var res sql.Result 24 | query := "insert into tag(title) values(?)" 25 | stmt, err := prepare(ctx, query) 26 | defer stmt.Close() 27 | if err != nil { 28 | return nil, errm.Wrap(err, http.StatusInternalServerError) 29 | } 30 | 31 | res, err = stmt.ExecContext(ctx, title) 32 | if err != nil { 33 | return nil, errm.Wrap(err, http.StatusInternalServerError) 34 | } 35 | 36 | id, err := res.LastInsertId() 37 | if err != nil { 38 | return nil, errm.Wrap(err, http.StatusInternalServerError) 39 | } 40 | 41 | return m.Get(ctx, int(id)) 42 | } 43 | 44 | // Get get Tag Data by ID 45 | func (m tagRepository) Get(ctx context.Context, id int) (*model.Tag, error) { 46 | tag := new(model.Tag) 47 | var err error 48 | query := "select * from tag where id = ?" 49 | stmt, err := prepare(ctx, query) 50 | defer stmt.Close() 51 | if err != nil { 52 | return nil, errm.Wrap(err, http.StatusInternalServerError) 53 | } 54 | 55 | err = stmt.QueryRowContext(ctx, id).Scan(&tag.ID, &tag.Title) 56 | if err != nil { 57 | return nil, errm.Wrap(err, http.StatusInternalServerError) 58 | } 59 | 60 | return tag, err 61 | } 62 | 63 | // GetAll get all Tag Data 64 | func (m tagRepository) GetAll(ctx context.Context) ([]*model.Tag, error) { 65 | var rows *sql.Rows 66 | var err error 67 | query := "select * from tag" 68 | stmt, err := prepare(ctx, query) 69 | defer stmt.Close() 70 | if err != nil { 71 | return nil, errm.Wrap(err, http.StatusInternalServerError) 72 | } 73 | 74 | rows, err = stmt.QueryContext(ctx) 75 | if err != nil { 76 | return nil, errm.Wrap(err, http.StatusInternalServerError) 77 | } 78 | 79 | return m.getModelList(rows) 80 | } 81 | 82 | // Search search list by title 83 | func (m tagRepository) Search(ctx context.Context, title string) ([]*model.Tag, error) { 84 | var rows *sql.Rows 85 | var err error 86 | query := "select * from tag where title like '%" + title + "%'" 87 | stmt, err := prepare(ctx, query) 88 | defer stmt.Close() 89 | if err != nil { 90 | return nil, errm.Wrap(err, http.StatusInternalServerError) 91 | } 92 | 93 | rows, err = stmt.QueryContext(ctx) 94 | if err != nil { 95 | return nil, errm.Wrap(err, http.StatusInternalServerError) 96 | } 97 | 98 | return m.getModelList(rows) 99 | } 100 | 101 | // SaveTagAndMemo save tag and memo link 102 | func (m tagRepository) SaveTagAndMemo(ctx context.Context, tagID int, memoID int) error { 103 | var err error 104 | query := "insert into tag_memo(tag_id, memo_id) values(?, ?)" 105 | stmt, err := prepare(ctx, query) 106 | defer stmt.Close() 107 | if err != nil { 108 | return errm.Wrap(err, http.StatusInternalServerError) 109 | } 110 | 111 | _, err = stmt.ExecContext(ctx, tagID, memoID) 112 | return err 113 | } 114 | 115 | // GetAllByMemoID get all Tag Data By MemoID 116 | func (m tagRepository) GetAllByMemoID(ctx context.Context, id int) ([]*model.Tag, error) { 117 | var rows *sql.Rows 118 | var err error 119 | query := "select tag.* from tag, tag_memo where tag_memo.memo_id = ? and tag.id = tag_memo.tag_id" 120 | stmt, err := prepare(ctx, query) 121 | defer stmt.Close() 122 | if err != nil { 123 | return nil, errm.Wrap(err, http.StatusInternalServerError) 124 | } 125 | 126 | rows, err = stmt.QueryContext(ctx, id) 127 | if err != nil { 128 | return nil, errm.Wrap(err, http.StatusInternalServerError) 129 | } 130 | 131 | return m.getModelList(rows) 132 | } 133 | 134 | // SearchMemoIDsByTitle search memo ids by tag's title 135 | func (m tagRepository) SearchMemoIDsByTitle(ctx context.Context, title string) ([]int, error) { 136 | var rows *sql.Rows 137 | var err error 138 | query := "select tag_memo.memo_id as mid from tag, tag_memo where tag.id = tag_memo.tag_id and tag.title like '%" + title + "%'" 139 | stmt, err := prepare(ctx, query) 140 | defer stmt.Close() 141 | if err != nil { 142 | return nil, errm.Wrap(err, http.StatusInternalServerError) 143 | } 144 | 145 | rows, err = stmt.QueryContext(ctx) 146 | if err != nil { 147 | return nil, errm.Wrap(err, http.StatusInternalServerError) 148 | } 149 | 150 | list := []int{} 151 | for rows.Next() { 152 | var mid int64 153 | err := rows.Scan(&mid) 154 | if err != nil { 155 | return list, errm.Wrap(err, http.StatusInternalServerError) 156 | } 157 | list = append(list, int(mid)) 158 | } 159 | 160 | return list, nil 161 | } 162 | 163 | // getModelList get model list 164 | func (m tagRepository) getModelList(rows *sql.Rows) ([]*model.Tag, error) { 165 | list := make([]*model.Tag, 0) 166 | for rows.Next() { 167 | tag := &model.Tag{} 168 | err := rows.Scan(&tag.ID, &tag.Title) 169 | if err != nil { 170 | return list, errm.Wrap(err, http.StatusInternalServerError) 171 | } 172 | list = append(list, tag) 173 | } 174 | 175 | return list, nil 176 | } 177 | -------------------------------------------------------------------------------- /adapter/db/tag_repository_test.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "memo_sample/domain/repository" 7 | "testing" 8 | ) 9 | 10 | func getTagRepositoryForTest() repository.TagRepository { 11 | return NewTagRepository() 12 | } 13 | 14 | func TestTagSaveInDBSuccess(t *testing.T) { 15 | 16 | repo := getTagRepositoryForTest() 17 | 18 | ctx := context.Background() 19 | 20 | connectTestDB() 21 | defer closeTestDB() 22 | 23 | tag, err := repo.Save(ctx, "Tag First") 24 | if err != nil { 25 | t.Error("failed TestTagSaveInTagrySuccess Save", err) 26 | } 27 | 28 | tagGet, err := repo.Get(ctx, tag.ID) 29 | if err != nil || tagGet.ID != tag.ID { 30 | t.Error("failed TestTagSaveInDBSuccess Get", err, tag.ID) 31 | } 32 | } 33 | 34 | func TestTagTransactionCommitSuccess(t *testing.T) { 35 | repo := getTagRepositoryForTest() 36 | repoTx := getTransactionRepositoryForTest() 37 | 38 | ctx := context.Background() 39 | 40 | connectTestDB() 41 | defer func() { 42 | if err := recover(); err != nil { 43 | _, _ = repoTx.Rollback(ctx) 44 | t.Error(err) 45 | } 46 | closeTestDB() 47 | }() 48 | 49 | ctx, err := repoTx.Begin(ctx) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | _, err = repo.Save(ctx, "Transaction Commit Test") 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | _, err = repoTx.Commit(ctx) 60 | if err != nil { 61 | panic(err) 62 | } 63 | } 64 | 65 | func TestTagAndMemoTransactionCommitSuccess(t *testing.T) { 66 | 67 | repoT := getTagRepositoryForTest() 68 | repoM := getMemoRepositoryForTest() 69 | repoTx := getTransactionRepositoryForTest() 70 | 71 | ctx := context.Background() 72 | 73 | connectTestDB() 74 | defer func() { 75 | if err := recover(); err != nil { 76 | _, _ = repoTx.Rollback(ctx) 77 | t.Error(err) 78 | } 79 | closeTestDB() 80 | }() 81 | 82 | ctx, err := repoTx.Begin(ctx) 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | memo, err := repoM.Save(ctx, "Transaction Commit Memo") 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | tag, err := repoT.Save(ctx, "Transaction Commit Tag") 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | err = repoT.SaveTagAndMemo(ctx, tag.ID, memo.ID) 98 | if err != nil { 99 | panic(err) 100 | } 101 | 102 | _, err = repoTx.Commit(ctx) 103 | if err != nil { 104 | panic(err) 105 | } 106 | } 107 | 108 | func TestTagAndMemoTransactionRollbackSuccess(t *testing.T) { 109 | 110 | repoT := getTagRepositoryForTest() 111 | repoM := getMemoRepositoryForTest() 112 | repoTx := getTransactionRepositoryForTest() 113 | 114 | ctx := context.Background() 115 | 116 | connectTestDB() 117 | defer func() { 118 | if err := recover(); err != nil { 119 | _, _ = repoTx.Rollback(ctx) 120 | t.Error(err) 121 | } 122 | closeTestDB() 123 | }() 124 | 125 | ctx, err := repoTx.Begin(ctx) 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | memo, err := repoM.Save(ctx, "Transaction Rollback Memo") 131 | if err != nil { 132 | panic(err) 133 | } 134 | 135 | tag, err := repoT.Save(ctx, "Transaction Rollback Tag") 136 | if err != nil { 137 | panic(err) 138 | } 139 | 140 | err = repoT.SaveTagAndMemo(ctx, tag.ID, memo.ID) 141 | if err != nil { 142 | panic(err) 143 | } 144 | 145 | // 強制的にロールバック 146 | _, _ = repoTx.Rollback(ctx) 147 | } 148 | 149 | func TestTagAndMemoGetAllByMemoIDSuccess(t *testing.T) { 150 | repoT := getTagRepositoryForTest() 151 | repoM := getMemoRepositoryForTest() 152 | repoTx := getTransactionRepositoryForTest() 153 | 154 | ctx := context.Background() 155 | 156 | connectTestDB() 157 | defer func() { 158 | if err := recover(); err != nil { 159 | _, _ = repoTx.Rollback(ctx) 160 | t.Error(err) 161 | } 162 | closeTestDB() 163 | }() 164 | 165 | ctx, err := repoTx.Begin(ctx) 166 | if err != nil { 167 | panic(err) 168 | } 169 | 170 | memo, err := repoM.Save(ctx, "GetAllByMemoID Test Memo") 171 | if err != nil { 172 | panic(err) 173 | } 174 | 175 | tag, err := repoT.Save(ctx, "GetAllByMemoID Test Tag") 176 | if err != nil { 177 | panic(err) 178 | } 179 | 180 | err = repoT.SaveTagAndMemo(ctx, tag.ID, memo.ID) 181 | if err != nil { 182 | panic(err) 183 | } 184 | 185 | ctx, err = repoTx.Commit(ctx) 186 | if err != nil { 187 | panic(err) 188 | } 189 | 190 | flag := false 191 | list, err := repoT.GetAllByMemoID(ctx, memo.ID) 192 | if err !=nil { 193 | panic(err) 194 | } 195 | for _, tg := range list { 196 | if tg.ID == tag.ID { 197 | flag = true 198 | } 199 | } 200 | 201 | if !flag { 202 | panic(fmt.Errorf("GetAllByMemoID Error")) 203 | } 204 | } 205 | 206 | func TestTagAndMemoSearchMemoIDsByTitleSuccess(t *testing.T) { 207 | 208 | repoT := getTagRepositoryForTest() 209 | repoM := getMemoRepositoryForTest() 210 | repoTx := getTransactionRepositoryForTest() 211 | 212 | ctx := context.Background() 213 | 214 | connectTestDB() 215 | defer func() { 216 | if err := recover(); err != nil { 217 | _, _ = repoTx.Rollback(ctx) 218 | t.Error(err) 219 | } 220 | closeTestDB() 221 | }() 222 | 223 | ctx, err := repoTx.Begin(ctx) 224 | if err != nil { 225 | panic(err) 226 | } 227 | 228 | memo, err := repoM.Save(ctx, "SearchMemoIDsByTitle Test Memo") 229 | if err != nil { 230 | panic(err) 231 | } 232 | 233 | tag, err := repoT.Save(ctx, "SearchMemoIDsByTitle Test Tag") 234 | if err != nil { 235 | panic(err) 236 | } 237 | 238 | err = repoT.SaveTagAndMemo(ctx, tag.ID, memo.ID) 239 | if err != nil { 240 | panic(err) 241 | } 242 | 243 | tag2, err := repoT.Get(ctx, tag.ID) 244 | if err != nil { 245 | panic(err) 246 | } 247 | t.Log(tag2) 248 | 249 | ctx, err = repoTx.Commit(ctx) 250 | if err != nil { 251 | panic(err) 252 | } 253 | 254 | flag := false 255 | list, err := repoT.SearchMemoIDsByTitle(ctx, tag.Title) 256 | if err != nil { 257 | panic(err) 258 | } 259 | for _, id := range list { 260 | if id == memo.ID { 261 | flag = true 262 | } 263 | } 264 | 265 | if !flag { 266 | panic(fmt.Errorf("SearchMemoIDsByTitle Error")) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /adapter/db/transaction_repository.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "memo_sample/domain/repository" 6 | ) 7 | 8 | // NewTransactionRepository get repository 9 | func NewTransactionRepository() repository.TransactionRepository { 10 | return transactionRepository{} 11 | } 12 | 13 | // transactionRepository transaction Repository Sub 14 | type transactionRepository struct{} 15 | 16 | // Begin begin transaction 17 | func (t transactionRepository) Begin(ctx context.Context) (context.Context, error) { 18 | return begin(ctx) 19 | } 20 | 21 | // Rollback rollback transaction 22 | func (t transactionRepository) Rollback(ctx context.Context) (context.Context, error) { 23 | return rollback(ctx) 24 | } 25 | 26 | // Commit commit transaction 27 | func (t transactionRepository) Commit(ctx context.Context) (context.Context, error) { 28 | return commit(ctx) 29 | } 30 | -------------------------------------------------------------------------------- /adapter/error/error.go: -------------------------------------------------------------------------------- 1 | package apperrorsub 2 | 3 | import ( 4 | "fmt" 5 | "memo_sample/infra/error" 6 | 7 | "github.com/srvc/fail" 8 | ) 9 | 10 | // NewErrorManager new error Manager 11 | func NewErrorManager() apperror.ErrorManager { 12 | return errorManager{} 13 | } 14 | 15 | type errorManager struct{} 16 | 17 | func (em errorManager) Wrap(err error, code int) error { 18 | return fail.Wrap( 19 | err, 20 | fail.WithCode(code), 21 | fail.WithIgnorable(), 22 | ) 23 | } 24 | 25 | func (em errorManager) LogMessage(err error) string { 26 | return fmt.Sprintf("%T\nCode:%d\nStackTrace:%+v\n", 27 | err, 28 | fail.Unwrap(err).Code, 29 | fail.Unwrap(err).StackTrace, 30 | ) 31 | } 32 | 33 | func (em errorManager) Code(err error) int { 34 | return fail.Unwrap(err).Code.(int) 35 | } 36 | -------------------------------------------------------------------------------- /adapter/logger/logger.go: -------------------------------------------------------------------------------- 1 | package loggersub 2 | 3 | import ( 4 | "log" 5 | "memo_sample/infra/logger" 6 | ) 7 | 8 | // NewLogger new log manager 9 | func NewLogger() logger.Logger { 10 | return lggr{} 11 | } 12 | 13 | const ( 14 | // LogPrefixError error prefix 15 | LogPrefixError = "[Error]" 16 | 17 | // LogPrefixWarn warn prefix 18 | LogPrefixWarn = "[Warnning]" 19 | 20 | // LogPrefixInfo info prefix 21 | LogPrefixInfo = "[Info]" 22 | 23 | // LogPrefixDebug debug prefix 24 | LogPrefixDebug = "[Debug]" 25 | ) 26 | 27 | type lggr struct{} 28 | 29 | func (l lggr) Errorf(format string, args ...interface{}) { 30 | log.SetPrefix(LogPrefixError) 31 | log.Printf(format, args...) 32 | } 33 | 34 | func (l lggr) Warnf(format string, args ...interface{}) { 35 | log.SetPrefix(LogPrefixWarn) 36 | log.Printf(format, args...) 37 | } 38 | 39 | func (l lggr) Infof(format string, args ...interface{}) { 40 | log.SetPrefix(LogPrefixInfo) 41 | log.Printf(format, args...) 42 | } 43 | 44 | func (l lggr) Debugf(format string, args ...interface{}) { 45 | log.SetPrefix(LogPrefixDebug) 46 | log.Printf(format, args...) 47 | } 48 | -------------------------------------------------------------------------------- /adapter/memory/memo_repository.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "memo_sample/domain/model" 7 | "memo_sample/domain/repository" 8 | "strings" 9 | ) 10 | 11 | // NewMemoRepository get repository 12 | func NewMemoRepository() repository.MemoRepository { 13 | return &memoRepository{[]*model.Memo{}} 14 | } 15 | 16 | // MemoRepository Memo's Repository Sub 17 | type memoRepository struct { 18 | memoList []*model.Memo 19 | } 20 | 21 | // generateID generate Key 22 | func (m *memoRepository) generateID(ctx context.Context) (int, error) { 23 | const initID int = 1 24 | 25 | if len(m.memoList) == 0 { 26 | return initID, nil 27 | } 28 | 29 | var lm = m.memoList[len(m.memoList)-1] 30 | if lm == nil { 31 | return initID, nil 32 | } 33 | var id = lm.ID + 1 34 | return id, nil 35 | } 36 | 37 | // Save save Memo Data 38 | func (m *memoRepository) Save(ctx context.Context, text string) (*model.Memo, error) { 39 | id, err := m.generateID(ctx) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | memo := &model.Memo{ 45 | ID: id, 46 | Text: text, 47 | } 48 | 49 | m.memoList = append(m.memoList, memo) 50 | return memo, nil 51 | } 52 | 53 | // Get get Memo Data by ID 54 | func (m memoRepository) Get(ctx context.Context, id int) (*model.Memo, error) { 55 | for _, ml := range m.memoList { 56 | if ml.ID == id { 57 | return ml, nil 58 | } 59 | } 60 | return nil, fmt.Errorf("Error: %s", "no memo data") 61 | } 62 | 63 | // GetAll get all Memo Data 64 | func (m *memoRepository) GetAll(ctx context.Context) ([]*model.Memo, error) { 65 | return m.memoList, nil 66 | } 67 | 68 | // Search search memo by text 69 | func (m *memoRepository) Search(ctx context.Context, text string) ([]*model.Memo, error) { 70 | list := make([]*model.Memo, 0, len(m.memoList)) 71 | for _, memo := range m.memoList { 72 | if strings.Contains(memo.Text, text) { 73 | list = append(list, memo) 74 | } 75 | } 76 | return list, nil 77 | } 78 | 79 | // GetAllByIDs get all Memo Data by ID 80 | func (m *memoRepository) GetAllByIDs(ctx context.Context, ids []int) ([]*model.Memo, error) { 81 | list := make([]*model.Memo, 0, len(m.memoList)) 82 | for _, memo := range m.memoList { 83 | for _, id := range ids { 84 | if memo.ID == id { 85 | list = append(list, memo) 86 | } 87 | } 88 | } 89 | return list, nil 90 | } 91 | -------------------------------------------------------------------------------- /adapter/memory/memo_repository_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "testing" // テストで使える関数・構造体が用意されているパッケージをimport 6 | ) 7 | 8 | func TestMemoSaveInMemorySuccess(t *testing.T) { 9 | ctx := context.Background() 10 | 11 | repo := NewMemoRepository() 12 | 13 | // 1件名 14 | memo, err := repo.Save(ctx, "First") 15 | if err != nil { 16 | t.Error("failed TestMemoSaveInMemorySuccess Save", err) 17 | } 18 | 19 | memoGet, err := repo.Get(ctx, memo.ID) 20 | if err != nil || memoGet.ID != memo.ID { 21 | t.Error("failed TestMemoSaveInMemorySuccess Get", err, memoGet.ID) 22 | } 23 | t.Logf("TestMemoSaveInMemorySuccess Get MemoRepository id:%d, text:%s", memoGet.ID, memoGet.Text) 24 | 25 | // 2件名 26 | memo, err = repo.Save(ctx, "Second") 27 | if err != nil { 28 | t.Error("failed TestMemoSaveInMemorySuccess Save", err) 29 | } 30 | 31 | memoGet, err = repo.Get(ctx, memo.ID) 32 | if err != nil || memoGet.ID != memo.ID { 33 | t.Error("failed TestMemoSaveInMemorySuccess Get", err, memoGet.ID) 34 | } 35 | t.Logf("TestMemoSaveInMemorySuccess Get MemoRepository id:%d, text:%s", memoGet.ID, memoGet.Text) 36 | 37 | // 全件取得 38 | list, err := repo.GetAll(ctx) 39 | if err != nil || len(list) != 2 { 40 | t.Error("failed TestMemoSaveInMemorySuccess Get", err, len(list)) 41 | } 42 | 43 | for _, v := range list { 44 | t.Logf("TestMemoSaveInMemorySuccess GetAll MemoRepository id:%d, text:%s", v.ID, v.Text) 45 | } 46 | } 47 | 48 | func TestMemoSearchSuccess(t *testing.T) { 49 | repo := NewMemoRepository() 50 | 51 | ctx := context.Background() 52 | 53 | word := "Memo Search Test" 54 | _, err := repo.Save(ctx, word) 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | 59 | word = "Memo" 60 | list, err := repo.Search(ctx, word) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | 65 | for _, m := range list { 66 | t.Log(m) 67 | } 68 | } 69 | 70 | func TestMemoGetAllByIDsSuccess(t *testing.T) { 71 | 72 | repo := NewMemoRepository() 73 | 74 | ctx := context.Background() 75 | 76 | word := "Dummy First" 77 | memo1, err := repo.Save(ctx, word) 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | 82 | word = "Dummy Second" 83 | memo2, err := repo.Save(ctx, word) 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | 88 | ids := []int{memo1.ID, memo2.ID} 89 | list, err := repo.GetAllByIDs(ctx, ids) 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | 94 | for _, m := range list { 95 | t.Log(m) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /adapter/memory/tag_repository.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "memo_sample/domain/model" 7 | "memo_sample/domain/repository" 8 | "strings" 9 | ) 10 | 11 | // NewTagRepository get repository 12 | func NewTagRepository() repository.TagRepository { 13 | return &tagRepository{tagList: []*model.Tag{}, tagMemoMap: map[int]int{}} 14 | } 15 | 16 | // tagRepository Tag's Repository Sub 17 | type tagRepository struct { 18 | tagList []*model.Tag 19 | tagMemoMap map[int]int 20 | } 21 | 22 | // generateID generate Key 23 | func (m *tagRepository) generateID(ctx context.Context) (int, error) { 24 | const initID int = 1 25 | 26 | if len(m.tagList) == 0 { 27 | return initID, nil 28 | } 29 | 30 | var lm = m.tagList[len(m.tagList)-1] 31 | if lm == nil { 32 | return initID, nil 33 | } 34 | var id = lm.ID + 1 35 | return id, nil 36 | } 37 | 38 | // Save save Tag Data 39 | func (m *tagRepository) Save(ctx context.Context, title string) (*model.Tag, error) { 40 | id, err := m.generateID(ctx) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | tag := &model.Tag{ 46 | ID: id, 47 | Title: title, 48 | } 49 | 50 | m.tagList = append(m.tagList, tag) 51 | return tag, nil 52 | } 53 | 54 | // Get get Tag Data by ID 55 | func (m tagRepository) Get(ctx context.Context, id int) (*model.Tag, error) { 56 | for _, ml := range m.tagList { 57 | if ml.ID == id { 58 | return ml, nil 59 | } 60 | } 61 | return nil, fmt.Errorf("Error: %s", "no tag data") 62 | } 63 | 64 | // GetAll get all Tag Data 65 | func (m *tagRepository) GetAll(ctx context.Context) ([]*model.Tag, error) { 66 | return m.tagList, nil 67 | } 68 | 69 | // Search search tag by text 70 | func (m *tagRepository) Search(ctx context.Context, title string) ([]*model.Tag, error) { 71 | list := make([]*model.Tag, 0, len(m.tagList)) 72 | for _, tag := range m.tagList { 73 | if strings.Contains(tag.Title, title) { 74 | list = append(list, tag) 75 | } 76 | } 77 | return list, nil 78 | } 79 | 80 | // SaveTagAndMemo save tag and memo link 81 | func (m *tagRepository) SaveTagAndMemo(ctx context.Context, tagID int, memoID int) error { 82 | m.tagMemoMap[tagID] = memoID 83 | 84 | return nil 85 | } 86 | 87 | // GetAllByMemoID get all Tag Data By MemoID 88 | func (m *tagRepository) GetAllByMemoID(ctx context.Context, id int) ([]*model.Tag, error) { 89 | list := make([]*model.Tag, 0, len(m.tagMemoMap)) 90 | 91 | for i, v := range m.tagMemoMap { 92 | if v != id { 93 | continue 94 | } 95 | 96 | for _, tag := range m.tagList { 97 | if i != tag.ID { 98 | continue 99 | } 100 | list = append(list, tag) 101 | } 102 | } 103 | 104 | return list, nil 105 | } 106 | 107 | // SearchMemoIDsByTitle search memo ids by tag's title 108 | func (m *tagRepository) SearchMemoIDsByTitle(ctx context.Context, title string) ([]int, error) { 109 | memoIDs := make([]int, 0, len(m.tagMemoMap)) 110 | 111 | list, err := m.Search(ctx, title) 112 | if err != nil { 113 | return memoIDs, err 114 | } 115 | 116 | for _, tag := range list { 117 | for tagID, memoID := range m.tagMemoMap { 118 | if tag.ID != tagID { 119 | continue 120 | } 121 | memoIDs = append(memoIDs, memoID) 122 | } 123 | } 124 | 125 | return memoIDs, nil 126 | } 127 | -------------------------------------------------------------------------------- /adapter/memory/tag_repository_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" // テストで使える関数・構造体が用意されているパッケージをimport 7 | ) 8 | 9 | func TestTagSaveInDBSuccess(t *testing.T) { 10 | 11 | repo := NewTagRepository() 12 | 13 | ctx := context.Background() 14 | 15 | me, err := repo.Save(ctx, "Tag First") 16 | if err != nil { 17 | t.Error("failed TestTagSaveInDBSuccess Save", err) 18 | } 19 | 20 | memo, err := repo.Get(ctx, me.ID) 21 | if err != nil { 22 | t.Error("failed TestTagSaveInDBSuccess Get", err) 23 | } 24 | 25 | t.Log(memo) 26 | } 27 | 28 | func TestTagAndMemoGetAllByMemoIDSuccess(t *testing.T) { 29 | 30 | repoTx := NewTransactionRepository() 31 | repoT := NewTagRepository() 32 | repoM := NewMemoRepository() 33 | 34 | ctx := context.Background() 35 | 36 | defer func() { 37 | if err := recover(); err != nil { 38 | _, _ = repoTx.Rollback(ctx) 39 | t.Error(err) 40 | } 41 | }() 42 | 43 | ctx, err := repoTx.Begin(ctx) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | memo, err := repoM.Save(ctx, "GetAllByMemoID Test Memo") 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | tag, err := repoT.Save(ctx, "GetAllByMemoID Test Tag") 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | err = repoT.SaveTagAndMemo(ctx, tag.ID, memo.ID) 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | ctx, err = repoTx.Commit(ctx) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | t.Logf("TestTagAndMemoGetAllByMemoIDSuccess targetMemoID:%d", memo.ID) 69 | 70 | flag := false 71 | list, err := repoT.GetAllByMemoID(ctx, memo.ID) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | for _, tg := range list { 77 | if tg.ID == tag.ID { 78 | flag = true 79 | t.Log(tg) 80 | } 81 | } 82 | 83 | if !flag { 84 | panic(fmt.Errorf("GetAllByMemoID Error")) 85 | } 86 | } 87 | 88 | func TestTagAndMemoSearchMemoIDsByTitleSuccess(t *testing.T) { 89 | 90 | repoTx := NewTransactionRepository() 91 | repoT := NewTagRepository() 92 | repoM := NewMemoRepository() 93 | 94 | ctx := context.Background() 95 | 96 | defer func() { 97 | if err := recover(); err != nil { 98 | _, _ = repoTx.Rollback(ctx) 99 | t.Error(err) 100 | } 101 | }() 102 | 103 | ctx, err := repoTx.Begin(ctx) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | memo, err := repoM.Save(ctx, "SearchMemoIDsByTitle Test Memo") 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | tag, err := repoT.Save(ctx, "SearchMemoIDsByTitle Test Tag") 114 | if err != nil { 115 | panic(err) 116 | } 117 | 118 | err = repoT.SaveTagAndMemo(ctx, tag.ID, memo.ID) 119 | if err != nil { 120 | panic(err) 121 | } 122 | 123 | ctx, err = repoTx.Commit(ctx) 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | flag := false 129 | list, err := repoT.SearchMemoIDsByTitle(ctx, tag.Title) 130 | if err != nil { 131 | panic(err) 132 | } 133 | 134 | for _, id := range list { 135 | if id == memo.ID { 136 | flag = true 137 | } 138 | } 139 | 140 | if !flag { 141 | t.Error(fmt.Errorf("SearchMemoIDsByTitle Error")) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /adapter/memory/transaction_repository.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "context" 5 | "memo_sample/domain/repository" 6 | ) 7 | 8 | // NewTransactionRepository get repository 9 | func NewTransactionRepository() repository.TransactionRepository { 10 | return transactionRepository{} 11 | } 12 | 13 | // transactionRepository transaction Repository Sub 14 | type transactionRepository struct{} 15 | 16 | func (t transactionRepository) Begin(ctx context.Context) (context.Context, error) { 17 | return ctx, nil 18 | } 19 | 20 | func (t transactionRepository) Rollback(ctx context.Context) (context.Context, error) { 21 | return ctx, nil 22 | } 23 | 24 | func (t transactionRepository) Commit(ctx context.Context) (context.Context, error) { 25 | return ctx, nil 26 | } 27 | -------------------------------------------------------------------------------- /adapter/view/render/json.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "memo_sample/domain/model" 6 | "memo_sample/view/model/json" 7 | "memo_sample/view/render" 8 | ) 9 | 10 | // NewJSONRender new json render 11 | func NewJSONRender() render.JSONRender { 12 | return jsonRender{} 13 | } 14 | 15 | type jsonRender struct { 16 | } 17 | 18 | func (m jsonRender) ConvertMemo(md *model.Memo) *json.Memo { 19 | mj := &json.Memo{ 20 | ID: md.ID, 21 | Text: md.Text, 22 | } 23 | return mj 24 | } 25 | 26 | func (m jsonRender) ConvertMemos(list []*model.Memo) []*json.Memo { 27 | listJSON := []*json.Memo{} 28 | for _, v := range list { 29 | listJSON = append(listJSON, m.ConvertMemo(v)) 30 | } 31 | return listJSON 32 | } 33 | 34 | func (m jsonRender) ConvertTag(md *model.Tag) *json.Tag { 35 | mj := &json.Tag{ 36 | ID: md.ID, 37 | Title: md.Title, 38 | } 39 | return mj 40 | } 41 | 42 | func (m jsonRender) ConvertTags(list []*model.Tag) []*json.Tag { 43 | listJSON := []*json.Tag{} 44 | for _, v := range list { 45 | listJSON = append(listJSON, m.ConvertTag(v)) 46 | } 47 | return listJSON 48 | } 49 | 50 | func (m jsonRender) ConvertPostMemoAndTagsResult(memo *model.Memo, tags []*model.Tag) *json.PostMemoAndTagsResult { 51 | 52 | return &json.PostMemoAndTagsResult{ 53 | Memo: m.ConvertMemo(memo), 54 | Tags: m.ConvertTags(tags), 55 | } 56 | } 57 | 58 | func (m jsonRender) ConvertSearchTagsAndMemosResult(memos []*model.Memo, tags []*model.Tag) *json.SearchTagsAndMemosResult { 59 | 60 | return &json.SearchTagsAndMemosResult{ 61 | Tags: m.ConvertTags(tags), 62 | Memos: m.ConvertMemos(memos), 63 | } 64 | } 65 | 66 | func (m jsonRender) ConvertError(err error, code int) *json.Error { 67 | mess := fmt.Sprintf("API: %T(%v)\n", err, err) 68 | 69 | return &json.Error{Code: code, Msg: mess} 70 | } 71 | -------------------------------------------------------------------------------- /di/injector.go: -------------------------------------------------------------------------------- 1 | //+build wireinject 2 | 3 | package di 4 | 5 | import ( 6 | "memo_sample/adapter/db" 7 | "memo_sample/adapter/error" 8 | "memo_sample/adapter/logger" 9 | "memo_sample/adapter/memory" 10 | view "memo_sample/adapter/view/render" 11 | "memo_sample/interface/api" 12 | "memo_sample/usecase" 13 | 14 | "github.com/google/wire" 15 | ) 16 | 17 | // ProvideAPI inject api using wire 18 | var ProvideAPI = wire.NewSet( 19 | ProvideUsecaseIterator, 20 | api.NewAPI, 21 | ) 22 | 23 | // ProvidePresenter inject presenter using wire 24 | var ProvidePresenter = wire.NewSet( 25 | ProvideRender, 26 | ProvideLog, 27 | api.NewPresenter, 28 | ProvideErrorManager, 29 | ) 30 | 31 | // ProvideMemoUsecase inject memo usecase using wire 32 | var ProvideMemoUsecase = wire.NewSet( 33 | ProvideDBRepository, // or ProvideInMemoryRepository 34 | usecase.NewMemo, 35 | ) 36 | 37 | // ProvideUsecaseIterator inject usecase itetator using wire 38 | var ProvideUsecaseIterator = wire.NewSet( 39 | ProvidePresenter, 40 | ProvideMemoUsecase, 41 | usecase.NewInteractor, 42 | ) 43 | 44 | // ProvideInMemoryRepository inject repository using wire 45 | var ProvideInMemoryRepository = wire.NewSet( 46 | memory.NewTransactionRepository, 47 | memory.NewMemoRepository, 48 | memory.NewTagRepository, 49 | ) 50 | 51 | // ProvideDBRepository inject repository using wire 52 | var ProvideDBRepository = wire.NewSet( 53 | db.NewTransactionRepository, 54 | db.NewMemoRepository, 55 | db.NewTagRepository, 56 | ) 57 | 58 | // ProvideLog inject log using wire 59 | var ProvideLog = wire.NewSet(loggersub.NewLogger) 60 | 61 | // ProvideRender inject render using wire 62 | var ProvideRender = wire.NewSet(view.NewJSONRender) 63 | 64 | // ProvideErrorManager inject error manager using wire 65 | var ProvideErrorManager = wire.NewSet(apperrorsub.NewErrorManager) 66 | 67 | // InjectAPIServer build inject api using wire 68 | func InjectAPIServer() api.API { 69 | wire.Build( 70 | ProvideAPI, 71 | ) 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /di/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate wire 4 | //+build !wireinject 5 | 6 | package di 7 | 8 | import ( 9 | "memo_sample/adapter/db" 10 | "memo_sample/adapter/error" 11 | "memo_sample/adapter/logger" 12 | "memo_sample/adapter/memory" 13 | "memo_sample/adapter/view/render" 14 | "memo_sample/interface/api" 15 | "memo_sample/usecase" 16 | 17 | "github.com/google/wire" 18 | ) 19 | 20 | // Injectors from injector.go: 21 | 22 | func InjectAPIServer() api.API { 23 | jsonRender := view.NewJSONRender() 24 | logger := loggersub.NewLogger() 25 | errorManager := apperrorsub.NewErrorManager() 26 | presenter := api.NewPresenter(jsonRender, logger, errorManager) 27 | transactionRepository := db.NewTransactionRepository() 28 | memoRepository := db.NewMemoRepository() 29 | tagRepository := db.NewTagRepository() 30 | memo := usecase.NewMemo(transactionRepository, memoRepository, tagRepository, errorManager) 31 | interactor := usecase.NewInteractor(presenter, memo) 32 | apiAPI := api.NewAPI(interactor, logger) 33 | return apiAPI 34 | } 35 | 36 | // injector.go: 37 | 38 | // ProvideAPI inject api using wire 39 | var ProvideAPI = wire.NewSet( 40 | ProvideUsecaseIterator, api.NewAPI, 41 | ) 42 | 43 | // ProvidePresenter inject presenter using wire 44 | var ProvidePresenter = wire.NewSet( 45 | ProvideRender, 46 | ProvideLog, api.NewPresenter, ProvideErrorManager, 47 | ) 48 | 49 | // ProvideMemoUsecase inject memo usecase using wire 50 | var ProvideMemoUsecase = wire.NewSet( 51 | ProvideDBRepository, usecase.NewMemo, 52 | ) 53 | 54 | // ProvideUsecaseIterator inject usecase itetator using wire 55 | var ProvideUsecaseIterator = wire.NewSet( 56 | ProvidePresenter, 57 | ProvideMemoUsecase, usecase.NewInteractor, 58 | ) 59 | 60 | // ProvideInMemoryRepository inject repository using wire 61 | var ProvideInMemoryRepository = wire.NewSet(memory.NewTransactionRepository, memory.NewMemoRepository, memory.NewTagRepository) 62 | 63 | // ProvideDBRepository inject repository using wire 64 | var ProvideDBRepository = wire.NewSet(db.NewTransactionRepository, db.NewMemoRepository, db.NewTagRepository) 65 | 66 | // ProvideLog inject log using wire 67 | var ProvideLog = wire.NewSet(loggersub.NewLogger) 68 | 69 | // ProvideRender inject render using wire 70 | var ProvideRender = wire.NewSet(view.NewJSONRender) 71 | 72 | // ProvideErrorManager inject error manager using wire 73 | var ProvideErrorManager = wire.NewSet(apperrorsub.NewErrorManager) 74 | -------------------------------------------------------------------------------- /domain/model/memo.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Memo Memo's Entity 4 | type Memo struct { 5 | ID int 6 | Text string 7 | } 8 | -------------------------------------------------------------------------------- /domain/model/tag.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // Tag Tag's Entity 4 | type Tag struct { 5 | ID int 6 | Title string 7 | } 8 | -------------------------------------------------------------------------------- /domain/repository/memo_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "memo_sample/domain/model" 6 | ) 7 | 8 | // MemoRepository Memo's Repository 9 | type MemoRepository interface { 10 | Save(ctx context.Context, text string) (*model.Memo, error) 11 | Get(ctx context.Context, id int) (*model.Memo, error) 12 | GetAll(ctx context.Context) ([]*model.Memo, error) 13 | Search(ctx context.Context, text string) ([]*model.Memo, error) 14 | GetAllByIDs(ctx context.Context, ids []int) ([]*model.Memo, error) 15 | } 16 | -------------------------------------------------------------------------------- /domain/repository/tag_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "memo_sample/domain/model" 6 | ) 7 | 8 | // TagRepository Tag's Repository 9 | type TagRepository interface { 10 | Save(ctx context.Context, title string) (*model.Tag, error) 11 | Get(ctx context.Context, id int) (*model.Tag, error) 12 | GetAll(ctx context.Context) ([]*model.Tag, error) 13 | Search(ctx context.Context, title string) ([]*model.Tag, error) 14 | SaveTagAndMemo(ctx context.Context, tagID int, memoID int) error 15 | GetAllByMemoID(ctx context.Context, id int) ([]*model.Tag, error) 16 | SearchMemoIDsByTitle(ctx context.Context, title string) ([]int, error) 17 | } 18 | -------------------------------------------------------------------------------- /domain/repository/transaction_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // TransactionRepository Transaction's Repository 8 | type TransactionRepository interface { 9 | Begin(ctx context.Context) (context.Context, error) 10 | Rollback(ctx context.Context) (context.Context, error) 11 | Commit(ctx context.Context) (context.Context, error) 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module memo_sample 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/go-sql-driver/mysql v1.4.1 8 | github.com/google/wire v0.3.0 9 | github.com/pkg/errors v0.8.1 // indirect 10 | github.com/srvc/fail v4.0.0+incompatible 11 | github.com/stretchr/testify v1.2.2 // indirect 12 | google.golang.org/appengine v1.6.1 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 4 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 5 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 6 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 7 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 8 | github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 9 | github.com/google/wire v0.3.0 h1:imGQZGEVEHpje5056+K+cgdO72p0LQv2xIIFXNGUf60= 10 | github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= 11 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 12 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 13 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 14 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/srvc/fail v4.0.0+incompatible h1:NyG9nEm+AG/5RcwtmhsjCAwFgycb/MRGe3X78BR+w2U= 18 | github.com/srvc/fail v4.0.0+incompatible/go.mod h1:O6qL3O5sUrJlIh2oGAZ5J9RKGGae3D1U6jMGyzM6RK4= 19 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 20 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 21 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 22 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 23 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 25 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 26 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 29 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 33 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 34 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 35 | golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 36 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 37 | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= 38 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 39 | -------------------------------------------------------------------------------- /infra/database/common.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "math/rand" 8 | "time" 9 | ) 10 | 11 | var dm *DBM 12 | var dbma DBM 13 | 14 | // init initialize 15 | func init() { 16 | dbma = &dbm{txMap: map[string]*sql.Tx{}} 17 | dm = &dbma 18 | } 19 | 20 | // GetDBM get database manager 21 | func GetDBM() *DBM { 22 | return dm 23 | } 24 | 25 | // ContextKey key for transaction context 26 | type ContextKey string 27 | 28 | const ( 29 | txIsKey = "db.transaction" 30 | txKey = "db.transaction.key" 31 | ) 32 | 33 | // DBM database manager 34 | type DBM interface { 35 | ConnectDB() error 36 | ConnectTestDB() error 37 | PingDB() error 38 | CloseDB() error 39 | Begin(ctx context.Context) (context.Context, error) 40 | Rollback(ctx context.Context) (context.Context, error) 41 | Commit(ctx context.Context) (context.Context, error) 42 | Prepare(ctx context.Context, query string) (*sql.Stmt, error) 43 | } 44 | 45 | // dbm database manager 46 | type dbm struct { 47 | db *sql.DB 48 | txMap map[string]*sql.Tx 49 | stmt *sql.Stmt 50 | } 51 | 52 | // openDB open database 53 | func (m *dbm) openDB(driverName, dataSourceName string) error { 54 | var err error 55 | m.db, err = sql.Open(driverName, dataSourceName) 56 | if err != nil { 57 | return err 58 | } 59 | return nil 60 | } 61 | 62 | func (m *dbm) pingDB() error { 63 | return m.db.Ping() 64 | } 65 | 66 | // closeDB close database 67 | func (m *dbm) closeDB() error { 68 | m.stmt.Close() 69 | return m.db.Close() 70 | } 71 | 72 | // Begin begin transaction 73 | func (m *dbm) Begin(ctx context.Context) (context.Context, error) { 74 | t, err := m.db.BeginTx(ctx, nil) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | // Transaction関連の設定 80 | ctx = m.addTx(ctx, t) 81 | 82 | return ctx, nil 83 | } 84 | 85 | // Rollback rollback transaction 86 | func (m *dbm) Rollback(ctx context.Context) (context.Context, error) { 87 | if err := m.getTx(ctx).Rollback(); err != nil { 88 | return ctx, err 89 | } 90 | 91 | // Transaction関連削除 92 | ctx = m.deleteTx(ctx) 93 | 94 | return ctx, nil 95 | } 96 | 97 | // Commit commit transaction 98 | func (m *dbm) Commit(ctx context.Context) (context.Context, error) { 99 | err := m.getTx(ctx).Commit() 100 | 101 | // Transaction関連削除 102 | ctx = m.deleteTx(ctx) 103 | 104 | return ctx, err 105 | } 106 | 107 | // Prepare prepare statement 108 | func (m *dbm) Prepare(ctx context.Context, query string) (*sql.Stmt, error) { 109 | var err error 110 | if m.isTx(ctx) { 111 | m.stmt, err = m.getTx(ctx).PrepareContext(ctx, query) 112 | } else { 113 | m.stmt, err = m.db.PrepareContext(ctx, query) 114 | } 115 | if err != nil { 116 | return m.stmt, err 117 | } 118 | 119 | return m.stmt, nil 120 | } 121 | 122 | // isTx is in transaction or not 123 | func (m *dbm) isTx(ctx context.Context) bool { 124 | if txn, ok := ctx.Value(ContextKey(txIsKey)).(bool); ok { 125 | return txn 126 | } 127 | return false 128 | } 129 | 130 | // getTx 131 | func (m *dbm) getTx(ctx context.Context) *sql.Tx { 132 | key := m.getTxKey(ctx) 133 | return m.txMap[key] 134 | } 135 | 136 | // addTx 137 | func (m *dbm) addTx(ctx context.Context, t *sql.Tx) context.Context { 138 | key := m.generateNewKey() 139 | m.txMap[key] = t 140 | ctx = m.setTxKey(ctx, key) 141 | 142 | // Transaction開始フラグ 143 | ctx = context.WithValue(ctx, ContextKey(txIsKey), true) 144 | 145 | return ctx 146 | } 147 | 148 | // deleteTx 149 | func (m *dbm) deleteTx(ctx context.Context) context.Context { 150 | key := m.getTxKey(ctx) 151 | delete(m.txMap, key) 152 | 153 | // Transaction開始フラグ 154 | return context.WithValue(ctx, ContextKey(txIsKey), false) 155 | } 156 | 157 | // setTxKey 158 | func (m *dbm) setTxKey(ctx context.Context, value string) context.Context { 159 | return context.WithValue(ctx, ContextKey(txKey), value) 160 | } 161 | 162 | // getTxKey 163 | func (m *dbm) getTxKey(ctx context.Context) string { 164 | return m.getKey(ctx, ContextKey(txKey)) 165 | } 166 | 167 | // getKey get key 168 | func (m *dbm) getKey(ctx context.Context, ctxKey ContextKey) string { 169 | key, _ := ctx.Value(ctxKey).(string) 170 | return key 171 | } 172 | 173 | // generateNewKey generate key 174 | func (m *dbm) generateNewKey() string { 175 | rand.Seed(time.Now().UnixNano()) 176 | return fmt.Sprintf("%d", rand.Int()) 177 | } 178 | -------------------------------------------------------------------------------- /infra/database/mysql.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | // MySQL driver 5 | _ "github.com/go-sql-driver/mysql" 6 | ) 7 | 8 | // ConnectDB DB接続 9 | func (m *dbm) ConnectDB() error { 10 | return m.openDB("mysql", "root:@/memo_sample") 11 | } 12 | 13 | // ConnectTestDB Test用 DB接続 14 | func (m *dbm) ConnectTestDB() error { 15 | return m.openDB("mysql", "root:@/memo_sample_test") 16 | } 17 | 18 | // PingDB DB接続確認 19 | func (m *dbm) PingDB() error { 20 | return m.pingDB() 21 | } 22 | 23 | // CloseDB DB切断 24 | func (m *dbm) CloseDB() error { 25 | return m.closeDB() 26 | } 27 | -------------------------------------------------------------------------------- /infra/error/error.go: -------------------------------------------------------------------------------- 1 | package apperror 2 | 3 | // ErrorManager error manager interface 4 | type ErrorManager interface { 5 | Wrap(err error, code int) error 6 | LogMessage(err error) string 7 | Code(err error) int 8 | } 9 | -------------------------------------------------------------------------------- /infra/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | // Logger log executer interface 4 | type Logger interface { 5 | Errorf(format string, args ...interface{}) 6 | Warnf(format string, args ...interface{}) 7 | Infof(format string, args ...interface{}) 8 | Debugf(format string, args ...interface{}) 9 | } 10 | -------------------------------------------------------------------------------- /interface/api/common.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | // ContextKey key for transaction context 12 | type ContextKey string 13 | 14 | const ( 15 | resKey = "http.response.key" 16 | ) 17 | 18 | var responseMap map[string]http.ResponseWriter 19 | 20 | func init() { 21 | responseMap = map[string]http.ResponseWriter{} 22 | } 23 | 24 | // addResponseWriter 25 | func addResponseWriter(ctx context.Context, w http.ResponseWriter) context.Context { 26 | key := generateNewKey() 27 | 28 | ctx = setResKey(ctx, key) 29 | 30 | responseMap[key] = w 31 | 32 | return ctx 33 | } 34 | 35 | // getResponseWriter 36 | func getResponseWriter(ctx context.Context) http.ResponseWriter { 37 | key := getResKey(ctx) 38 | var res http.ResponseWriter 39 | var ok bool 40 | if res, ok = responseMap[key]; !ok { 41 | panic(fmt.Errorf("http.ResponseWriter is none. key:%s", key)) 42 | } 43 | return res 44 | } 45 | 46 | // setResKey 47 | func setResKey(ctx context.Context, value string) context.Context { 48 | return context.WithValue(ctx, ContextKey(resKey), value) 49 | } 50 | 51 | // deleteResponseWriter 52 | func deleteResponseWriter(ctx context.Context) { 53 | key := getResKey(ctx) 54 | delete(responseMap, key) 55 | } 56 | 57 | // getResKey 58 | func getResKey(ctx context.Context) string { 59 | return getKey(ctx, ContextKey(resKey)) 60 | } 61 | 62 | // getKey get key 63 | func getKey(ctx context.Context, ctxKey ContextKey) string { 64 | key, _ := ctx.Value(ctxKey).(string) 65 | return key 66 | } 67 | 68 | // generateNewKey generate key 69 | func generateNewKey() string { 70 | rand.Seed(time.Now().UnixNano()) 71 | return fmt.Sprintf("%d", rand.Int()) 72 | } 73 | -------------------------------------------------------------------------------- /interface/api/controller.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "memo_sample/infra/logger" 5 | "memo_sample/usecase" 6 | "memo_sample/usecase/input" 7 | "net/http" 8 | ) 9 | 10 | // NewAPI Get API instance 11 | func NewAPI(it usecase.Interactor, log logger.Logger) API { 12 | return controller{it, log} 13 | } 14 | 15 | // API api instance 16 | type API interface { 17 | PostMemo(w http.ResponseWriter, r *http.Request) 18 | GetMemos(w http.ResponseWriter, r *http.Request) 19 | PostMemoAndTags(w http.ResponseWriter, r *http.Request) 20 | SearchTagsAndMemos(w http.ResponseWriter, r *http.Request) 21 | } 22 | 23 | type controller struct { 24 | it usecase.Interactor 25 | log logger.Logger 26 | } 27 | 28 | // PostMemo post new memo 29 | func (c controller) PostMemo(w http.ResponseWriter, r *http.Request) { 30 | ctx := r.Context() 31 | 32 | ctx = addResponseWriter(ctx, w) 33 | 34 | ipt := &input.PostMemo{Text: r.URL.Query().Get("text")} 35 | c.it.PostMemo(ctx, *ipt) 36 | } 37 | 38 | // GetMemos get all memo 39 | func (c controller) GetMemos(w http.ResponseWriter, r *http.Request) { 40 | ctx := r.Context() 41 | 42 | ctx = addResponseWriter(ctx, w) 43 | 44 | c.it.GetMemos(ctx) 45 | } 46 | 47 | // PostMemoAndTags save memo and tags 48 | func (c controller) PostMemoAndTags(w http.ResponseWriter, r *http.Request) { 49 | ctx := r.Context() 50 | 51 | _ = r.ParseForm() 52 | text := r.FormValue("memo_text") 53 | titles := r.Form["tag_titles[]"] 54 | 55 | ctx = addResponseWriter(ctx, w) 56 | 57 | ipt := &input.PostMemoAndTags{ 58 | MemoText: text, 59 | TagTitles: titles, 60 | } 61 | 62 | c.it.PostMemoAndTags(ctx, *ipt) 63 | } 64 | 65 | // SearchTagsAndMemos save memo and tags 66 | func (c controller) SearchTagsAndMemos(w http.ResponseWriter, r *http.Request) { 67 | ctx := r.Context() 68 | 69 | ctx = addResponseWriter(ctx, w) 70 | 71 | title := r.URL.Query().Get("tag_title") 72 | 73 | ipt := &input.SearchTagsAndMemos{TagTitle: title} 74 | c.it.SearchTagsAndMemos(ctx, *ipt) 75 | } 76 | -------------------------------------------------------------------------------- /interface/api/presenter.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "memo_sample/domain/model" 7 | "memo_sample/infra/error" 8 | "memo_sample/infra/logger" 9 | "memo_sample/usecase" 10 | "memo_sample/view/render" 11 | "net/http" 12 | ) 13 | 14 | // NewPresenter new presenter 15 | func NewPresenter(render render.JSONRender, log logger.Logger, errm apperror.ErrorManager) usecase.Presenter { 16 | return presenter{render, log, errm} 17 | } 18 | 19 | type presenter struct { 20 | render render.JSONRender 21 | log logger.Logger 22 | errm apperror.ErrorManager 23 | } 24 | 25 | func (m presenter) ViewMemo(ctx context.Context, md *model.Memo) { 26 | defer deleteResponseWriter(ctx) 27 | w := getResponseWriter(ctx) 28 | 29 | m.JSON(ctx, w, m.render.ConvertMemo(md)) 30 | } 31 | 32 | func (m presenter) ViewMemoList(ctx context.Context, list []*model.Memo) { 33 | defer deleteResponseWriter(ctx) 34 | w := getResponseWriter(ctx) 35 | 36 | m.JSON(ctx, w, m.render.ConvertMemos(list)) 37 | } 38 | 39 | func (m presenter) ViewTag(ctx context.Context, md *model.Tag) { 40 | defer deleteResponseWriter(ctx) 41 | w := getResponseWriter(ctx) 42 | 43 | m.JSON(ctx, w, m.render.ConvertTag(md)) 44 | } 45 | 46 | func (m presenter) ViewTagList(ctx context.Context, list []*model.Tag) { 47 | defer deleteResponseWriter(ctx) 48 | w := getResponseWriter(ctx) 49 | 50 | m.JSON(ctx, w, m.render.ConvertTags(list)) 51 | } 52 | 53 | func (m presenter) ViewPostMemoAndTagsResult(ctx context.Context, memo *model.Memo, tags []*model.Tag) { 54 | defer deleteResponseWriter(ctx) 55 | w := getResponseWriter(ctx) 56 | 57 | m.JSON(ctx, w, m.render.ConvertPostMemoAndTagsResult(memo, tags)) 58 | } 59 | 60 | func (m presenter) ViewSearchTagsAndMemosResult(ctx context.Context, memos []*model.Memo, tags []*model.Tag) { 61 | defer deleteResponseWriter(ctx) 62 | w := getResponseWriter(ctx) 63 | 64 | m.JSON(ctx, w, m.render.ConvertSearchTagsAndMemosResult(memos, tags)) 65 | } 66 | 67 | func (m presenter) ViewError(ctx context.Context, err error) { 68 | defer deleteResponseWriter(ctx) 69 | w := getResponseWriter(ctx) 70 | 71 | m.log.Errorf("API: %s\n", m.errm.LogMessage(err)) 72 | 73 | m.JSON(ctx, w, m.render.ConvertError(err, m.errm.Code(err))) 74 | } 75 | 76 | // JSON render json format 77 | func (m presenter) JSON(ctx context.Context, w http.ResponseWriter, value interface{}) { 78 | b, err := json.Marshal(value) 79 | if err != nil { 80 | http.Error(w, err.Error(), http.StatusInternalServerError) 81 | } 82 | 83 | w.Header().Set("content-type", "application/json") 84 | w.WriteHeader(http.StatusOK) 85 | _, _ = w.Write(b) 86 | } 87 | -------------------------------------------------------------------------------- /testutil/testutil.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "memo_sample/adapter/db" 5 | "memo_sample/adapter/error" 6 | "memo_sample/adapter/memory" 7 | "memo_sample/domain/repository" 8 | "memo_sample/infra/database" 9 | "memo_sample/infra/error" 10 | ) 11 | 12 | // NewTestManager test util 13 | func NewTestManager() TestManager { 14 | return testManager{} 15 | } 16 | 17 | // TestManager test manager 18 | type TestManager interface { 19 | GgetInMemoryRepository() (repository.TransactionRepository, repository.MemoRepository, repository.TagRepository, apperror.ErrorManager) 20 | GetDBRepository() (repository.TransactionRepository, repository.MemoRepository, repository.TagRepository, apperror.ErrorManager) 21 | ConnectTestDB() error 22 | CloseTestDB() error 23 | } 24 | 25 | // testManager test manager 26 | type testManager struct { 27 | } 28 | 29 | func (t testManager) GgetInMemoryRepository() (repository.TransactionRepository, repository.MemoRepository, repository.TagRepository, apperror.ErrorManager) { 30 | return memory.NewTransactionRepository(), memory.NewMemoRepository(), memory.NewTagRepository(), apperrorsub.NewErrorManager() 31 | } 32 | 33 | func (t testManager) GetDBRepository() (repository.TransactionRepository, repository.MemoRepository, repository.TagRepository, apperror.ErrorManager) { 34 | return db.NewTransactionRepository(), db.NewMemoRepository(), db.NewTagRepository(), apperrorsub.NewErrorManager() 35 | } 36 | 37 | // connectTestDB DB接続 38 | func (t testManager) ConnectTestDB() error { 39 | return (*database.GetDBM()).ConnectTestDB() 40 | } 41 | 42 | // closeTestDB DB切断 43 | func (t testManager) CloseTestDB() error { 44 | return (*database.GetDBM()).CloseDB() 45 | } 46 | -------------------------------------------------------------------------------- /usecase/common_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "memo_sample/domain/repository" 5 | "memo_sample/infra/error" 6 | "memo_sample/testutil" 7 | ) 8 | 9 | var testManager testutil.TestManager 10 | 11 | func init() { 12 | testManager = testutil.NewTestManager() 13 | } 14 | 15 | // getInMemoryRepository get memory repository 16 | func getInMemoryRepository() (repository.TransactionRepository, repository.MemoRepository, repository.TagRepository, apperror.ErrorManager) { 17 | return testManager.GgetInMemoryRepository() 18 | } 19 | 20 | // getDBRepository get db repository 21 | func getDBRepository() (repository.TransactionRepository, repository.MemoRepository, repository.TagRepository, apperror.ErrorManager) { 22 | return testManager.GetDBRepository() 23 | } 24 | 25 | // connectTestDB DB接続 26 | func connectTestDB() { 27 | if err := testManager.ConnectTestDB(); err != nil { 28 | panic(err) 29 | } 30 | } 31 | 32 | // closeTestDB DB切断 33 | func closeTestDB() { 34 | _ = testManager.CloseTestDB() 35 | } 36 | -------------------------------------------------------------------------------- /usecase/input/memo.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | // PostMemo Input Entity For Posting Memo 4 | type PostMemo struct { 5 | Text string 6 | } 7 | 8 | // PostMemoAndTags Input Entity For Posting Memo And Title 9 | type PostMemoAndTags struct { 10 | MemoText string 11 | TagTitles []string 12 | } 13 | 14 | // GetMemo Input Entity For Get Memo 15 | type GetMemo struct { 16 | ID int 17 | } 18 | 19 | // GetTagsByMemo Input Entity For GetTagsByMemo 20 | type GetTagsByMemo struct { 21 | ID int 22 | } 23 | 24 | // SearchTagsAndMemos Input Entity For SearchTagsAndMemos 25 | type SearchTagsAndMemos struct { 26 | TagTitle string 27 | } 28 | -------------------------------------------------------------------------------- /usecase/interactor.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "memo_sample/usecase/input" 6 | ) 7 | 8 | // NewInteractor new Interactor 9 | func NewInteractor( 10 | pre Presenter, 11 | me Memo, 12 | ) Interactor { 13 | return Interactor{pre, me} 14 | } 15 | 16 | // Interactor usecase interactor 17 | type Interactor struct { 18 | pre Presenter 19 | memo Memo 20 | } 21 | 22 | // PostMemo post memo 23 | func (i Interactor) PostMemo(ctx context.Context, ipt input.PostMemo) { 24 | err := i.memo.ValidatePost(ipt) 25 | if err != nil { 26 | i.pre.ViewError(ctx, err) 27 | return 28 | } 29 | 30 | id, err := i.memo.Post(ctx, ipt) 31 | if err != nil { 32 | i.pre.ViewError(ctx, err) 33 | return 34 | } 35 | 36 | iptf := &input.GetMemo{ID: id} 37 | memo, err := i.memo.GetMemo(ctx, *iptf) 38 | if err != nil { 39 | i.pre.ViewError(ctx, err) 40 | return 41 | } 42 | 43 | i.pre.ViewMemo(ctx, memo) 44 | } 45 | 46 | // GetMemos get all memos 47 | func (i Interactor) GetMemos(ctx context.Context) { 48 | 49 | memos, err := i.memo.GetAllMemoList(ctx) 50 | if err != nil { 51 | i.pre.ViewError(ctx, err) 52 | return 53 | } 54 | 55 | i.pre.ViewMemoList(ctx, memos) 56 | } 57 | 58 | // PostMemoAndTags save memo and tags 59 | func (i Interactor) PostMemoAndTags(ctx context.Context, ipt input.PostMemoAndTags) { 60 | err := i.memo.ValidatePostMemoAndTags(ipt) 61 | if err != nil { 62 | i.pre.ViewError(ctx, err) 63 | return 64 | } 65 | 66 | memo, tags, err := i.memo.PostMemoAndTags(ctx, ipt) 67 | if err != nil { 68 | i.pre.ViewError(ctx, err) 69 | return 70 | } 71 | 72 | i.pre.ViewPostMemoAndTagsResult(ctx, memo, tags) 73 | } 74 | 75 | // SearchTagsAndMemos save memo and tags 76 | func (i Interactor) SearchTagsAndMemos(ctx context.Context, ipt input.SearchTagsAndMemos) { 77 | 78 | memos, tags, err := i.memo.SearchTagsAndMemos(ctx, ipt) 79 | if err != nil { 80 | i.pre.ViewError(ctx, err) 81 | return 82 | } 83 | 84 | i.pre.ViewSearchTagsAndMemosResult(ctx, memos, tags) 85 | } 86 | -------------------------------------------------------------------------------- /usecase/memo.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "memo_sample/domain/model" 7 | "memo_sample/domain/repository" 8 | "memo_sample/infra/error" 9 | "memo_sample/usecase/input" 10 | "net/http" 11 | ) 12 | 13 | // Memo memo related interface 14 | type Memo interface { 15 | ValidatePost(ipt input.PostMemo) error 16 | Post(ctx context.Context, ipt input.PostMemo) (int, error) 17 | ValidateGet(ipt input.GetMemo) error 18 | GetMemo(ctx context.Context, ipt input.GetMemo) (*model.Memo, error) 19 | GetAllMemoList(ctx context.Context) ([]*model.Memo, error) 20 | ValidatePostMemoAndTags(ipt input.PostMemoAndTags) error 21 | PostMemoAndTags(ctx context.Context, ipt input.PostMemoAndTags) (*model.Memo, []*model.Tag, error) 22 | GetTagsByMemo(ctx context.Context, ipt input.GetTagsByMemo) ([]*model.Tag, error) 23 | SearchTagsAndMemos(ctx context.Context, ipt input.SearchTagsAndMemos) ([]*model.Memo, []*model.Tag, error) 24 | } 25 | 26 | // NewMemo generate memo instance 27 | func NewMemo( 28 | transactionRepository repository.TransactionRepository, 29 | memoRepository repository.MemoRepository, 30 | tagRepository repository.TagRepository, 31 | errm apperror.ErrorManager, 32 | ) Memo { 33 | return memo{ 34 | transactionRepository, 35 | memoRepository, 36 | tagRepository, 37 | errm, 38 | } 39 | } 40 | 41 | type memo struct { 42 | transactionRepository repository.TransactionRepository 43 | memoRepository repository.MemoRepository 44 | tagRepository repository.TagRepository 45 | errm apperror.ErrorManager 46 | } 47 | 48 | func (m memo) ValidatePost(ipt input.PostMemo) error { 49 | if ipt.Text == "" { 50 | err := fmt.Errorf("text parameter is invalid. %s", ipt.Text) 51 | return m.errm.Wrap( 52 | err, 53 | http.StatusBadRequest, 54 | ) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (m memo) Post(ctx context.Context, ipt input.PostMemo) (int, error) { 61 | mo, err := m.memoRepository.Save(ctx, ipt.Text) 62 | if err != nil { 63 | return 0, err 64 | } 65 | return mo.ID, err 66 | } 67 | 68 | func (m memo) ValidateGet(ipt input.GetMemo) error { 69 | if ipt.ID <= 0 { 70 | err := fmt.Errorf("ID parameter is invalid. %d", ipt.ID) 71 | return m.errm.Wrap( 72 | err, 73 | http.StatusBadRequest, 74 | ) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (m memo) GetMemo(ctx context.Context, ipt input.GetMemo) (*model.Memo, error) { 81 | me, err := m.memoRepository.Get(ctx, ipt.ID) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return me, nil 87 | } 88 | 89 | func (m memo) GetAllMemoList(ctx context.Context) ([]*model.Memo, error) { 90 | list, err := m.memoRepository.GetAll(ctx) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return list, nil 96 | } 97 | 98 | func (m memo) ValidatePostMemoAndTags(ipt input.PostMemoAndTags) error { 99 | if ipt.MemoText == "" { 100 | err := fmt.Errorf("text parameter(MemoText) is invalid. %s", ipt.MemoText) 101 | return m.errm.Wrap( 102 | err, 103 | http.StatusBadRequest, 104 | ) 105 | } 106 | 107 | for _, title := range ipt.TagTitles { 108 | if title == "" { 109 | err := fmt.Errorf("text parameter(TagTitles) is invalid. %s", title) 110 | return m.errm.Wrap( 111 | err, 112 | http.StatusBadRequest, 113 | ) 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func (m memo) PostMemoAndTags(ctx context.Context, ipt input.PostMemoAndTags) (*model.Memo, []*model.Tag, error) { 121 | tags := make([]*model.Tag, 0) 122 | 123 | defer func() { 124 | if err := recover(); err != nil { 125 | _, _ = m.transactionRepository.Rollback(ctx) 126 | } 127 | } () 128 | 129 | ctx, err := m.transactionRepository.Begin(ctx) 130 | if err != nil { 131 | return nil, nil, err 132 | } 133 | 134 | // Memo 135 | mo, err := m.memoRepository.Save(ctx, ipt.MemoText) 136 | if err != nil { 137 | return nil, nil, err 138 | } 139 | 140 | for _, title := range ipt.TagTitles { 141 | // Tag 142 | tg, err := m.tagRepository.Save(ctx, title) 143 | if err != nil { 144 | return nil, nil, err 145 | } 146 | tags = append(tags, tg) 147 | 148 | // MemoTag 149 | err = m.tagRepository.SaveTagAndMemo(ctx, tg.ID, mo.ID) 150 | if err != nil { 151 | return nil, nil, err 152 | } 153 | } 154 | 155 | _, err = m.transactionRepository.Commit(ctx) 156 | 157 | return mo, tags, err 158 | } 159 | 160 | func (m memo) GetTagsByMemo(ctx context.Context, ipt input.GetTagsByMemo) ([]*model.Tag, error) { 161 | return m.tagRepository.GetAllByMemoID(ctx, ipt.ID) 162 | } 163 | 164 | func (m memo) SearchTagsAndMemos(ctx context.Context, ipt input.SearchTagsAndMemos) ([]*model.Memo, []*model.Tag, error) { 165 | mIDs, err := m.tagRepository.SearchMemoIDsByTitle(ctx, ipt.TagTitle) 166 | if err != nil { 167 | return nil, nil, err 168 | } 169 | 170 | memos, err := m.memoRepository.GetAllByIDs(ctx, mIDs) 171 | if err != nil { 172 | return nil, nil, err 173 | } 174 | 175 | tags := []*model.Tag{} 176 | for _, mID := range mIDs { 177 | tgs, err := m.tagRepository.GetAllByMemoID(ctx, mID) 178 | if err != nil { 179 | return nil, nil, err 180 | } 181 | for _, tg := range tgs { 182 | tags = append(tgs, tg) 183 | } 184 | } 185 | 186 | return memos, tags, nil 187 | } 188 | -------------------------------------------------------------------------------- /usecase/memo_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "memo_sample/usecase/input" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestMemoPostAndGetMemoInMemorySuccess(t *testing.T) { 12 | ctx := context.Background() 13 | 14 | // InMemoryでテストした場合 15 | memo := NewMemo(getInMemoryRepository()) 16 | 17 | text := "Next Memo" 18 | 19 | ipt := &input.PostMemo{Text: text} 20 | 21 | id, err := memo.Post(ctx, *ipt) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | iptf := &input.GetMemo{ID: id} 27 | m, err := memo.GetMemo(ctx, *iptf) 28 | if err != nil { 29 | t.Error(err) 30 | } 31 | b, err := json.Marshal(m) 32 | if err != nil { 33 | t.Error(err) 34 | return 35 | } 36 | t.Logf("TestMemoPostAndGetSuccess Get MemoRepository json: %s", b) 37 | 38 | l, err := memo.GetAllMemoList(ctx) 39 | if err != nil { 40 | t.Error(err) 41 | } 42 | lb, err := json.Marshal(l) 43 | if err != nil { 44 | t.Error(err) 45 | return 46 | } 47 | 48 | t.Logf("TestMemoPostAndGetSuccess GetAllJSON MemoRepository json: %s", lb) 49 | } 50 | 51 | func TestMemoPostAndGetMemoInDBSuccess(t *testing.T) { 52 | ctx := context.Background() 53 | 54 | // DBでテストした場合 55 | memo := NewMemo(getDBRepository()) 56 | 57 | connectTestDB() 58 | defer closeTestDB() 59 | 60 | text := "Next Memo" 61 | 62 | ipt := &input.PostMemo{Text: text} 63 | 64 | id, err := memo.Post(ctx, *ipt) 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | 69 | iptf := &input.GetMemo{ID: id} 70 | m, err := memo.GetMemo(ctx, *iptf) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | b, err := json.Marshal(m) 75 | if err != nil { 76 | t.Error(err) 77 | return 78 | } 79 | t.Logf("TestMemoPostAndGetSuccess Get MemoRepository json: %s", b) 80 | 81 | l, err := memo.GetAllMemoList(ctx) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | lb, err := json.Marshal(l) 86 | if err != nil { 87 | t.Error(err) 88 | return 89 | } 90 | 91 | t.Logf("TestMemoPostAndGetSuccess GetAllJSON MemoRepository json: %s", lb) 92 | } 93 | 94 | func TestMemoPostMemoAndTagSuccess(t *testing.T) { 95 | ctx := context.Background() 96 | 97 | memo := NewMemo(getInMemoryRepository()) 98 | 99 | memoText := "Post Memo And Tag Test" 100 | tagTitles := []string{"MemoTest", "UnitTest"} 101 | ipt := &input.PostMemoAndTags{MemoText: memoText, TagTitles: tagTitles} 102 | 103 | mo, tgs, err := memo.PostMemoAndTags(ctx, *ipt) 104 | if err != nil { 105 | t.Error(err) 106 | } 107 | 108 | if mo.Text != memoText { 109 | t.Errorf("Memo Save Error: %s", mo.Text) 110 | } 111 | 112 | ok := make([]int, 0, len(tgs)) 113 | for _, tg := range tgs { 114 | for _, title := range tagTitles { 115 | if tg.Title == title { 116 | ok = append(ok, 1) 117 | } 118 | } 119 | } 120 | 121 | if len(ok) != 2 { 122 | t.Errorf("Tag Save Error: %s", mo.Text) 123 | } 124 | 125 | // 検索して取得して確認 126 | ipt2 := &input.GetTagsByMemo{ID: mo.ID} 127 | tgs2, err := memo.GetTagsByMemo(ctx, *ipt2) 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | 132 | ok = []int{} 133 | for _, tg := range tgs2 { 134 | for _, title := range tagTitles { 135 | if tg.Title == title { 136 | ok = append(ok, 1) 137 | } 138 | } 139 | } 140 | 141 | if len(ok) < 2 { 142 | t.Errorf("Tag Save Error: %s", mo.Text) 143 | } 144 | } 145 | 146 | func TestMemoSearchTagsAndMemosSuccess(t *testing.T) { 147 | ctx := context.Background() 148 | 149 | memo := NewMemo(getDBRepository()) 150 | connectTestDB() 151 | defer closeTestDB() 152 | 153 | // test deta post 154 | memoTexts := []string{"SearchTagsAndMemos 1", "SearchTagsAndMemos 2"} 155 | tagTitle := "SearchTagsAndMemos" 156 | tagTitles := []string{tagTitle} 157 | for _, memoText := range memoTexts { 158 | ipt1 := &input.PostMemoAndTags{MemoText: memoText, TagTitles: tagTitles} 159 | _, _, err := memo.PostMemoAndTags(ctx, *ipt1) 160 | if err != nil { 161 | t.Error(err) 162 | } 163 | } 164 | 165 | ipt := &input.SearchTagsAndMemos{TagTitle: tagTitle} 166 | 167 | mos, tgs, err := memo.SearchTagsAndMemos(ctx, *ipt) 168 | if err != nil { 169 | t.Error(err) 170 | } 171 | 172 | // check Tag 173 | for _, tag := range tgs { 174 | if !strings.Contains(tag.Title, tagTitle) { 175 | t.Errorf("Tag And Memo Save Error. tag.Title:%s", tag.Title) 176 | } 177 | } 178 | 179 | // check Memo 180 | ok := make([]int, 0, len(mos)) 181 | for _, mm := range mos { 182 | for _, memoText := range memoTexts { 183 | if mm.Text == memoText { 184 | ok = append(ok, 1) 185 | } 186 | } 187 | } 188 | 189 | if len(ok) < 2 { 190 | t.Error("Tag And Memo Save Error") 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /usecase/presenter.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "memo_sample/domain/model" 6 | ) 7 | 8 | // Presenter presenter interface 9 | type Presenter interface { 10 | ViewError(ctx context.Context, err error) 11 | ViewMemo(ctx context.Context, md *model.Memo) 12 | ViewMemoList(ctx context.Context, list []*model.Memo) 13 | ViewTag(ctx context.Context, md *model.Tag) 14 | ViewTagList(ctx context.Context, list []*model.Tag) 15 | ViewPostMemoAndTagsResult(ctx context.Context, memo *model.Memo, tags []*model.Tag) 16 | ViewSearchTagsAndMemosResult(ctx context.Context, memos []*model.Memo, tags []*model.Tag) 17 | } 18 | -------------------------------------------------------------------------------- /view/model/json/error.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | // Error Out Entity For Error Result 4 | type Error struct { 5 | Code int `json:"error_code"` 6 | Msg string `json:"error_msg"` 7 | } 8 | -------------------------------------------------------------------------------- /view/model/json/memo.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | // Memo Memo's Out Entity 4 | type Memo struct { 5 | ID int `json:"id"` 6 | Text string `json:"text"` 7 | } 8 | 9 | // Tag Tag's Out Entity 10 | type Tag struct { 11 | ID int `json:"id"` 12 | Title string `json:"title"` 13 | } 14 | 15 | // PostMemoAndTagsResult Out Entity For PostMemoAndTags Result 16 | type PostMemoAndTagsResult struct { 17 | Memo *Memo `json:"memo"` 18 | Tags []*Tag `json:"tags"` 19 | } 20 | 21 | // SearchTagsAndMemosResult Out Entity For SearchTagsAndMemos Result 22 | type SearchTagsAndMemosResult struct { 23 | Tags []*Tag `json:"tags"` 24 | Memos []*Memo `json:"memos"` 25 | } 26 | -------------------------------------------------------------------------------- /view/render/json.go: -------------------------------------------------------------------------------- 1 | package render 2 | 3 | import ( 4 | "memo_sample/domain/model" 5 | "memo_sample/view/model/json" 6 | ) 7 | 8 | // JSONRender api render interface 9 | type JSONRender interface { 10 | ConvertError(err error, code int) *json.Error 11 | ConvertMemo(md *model.Memo) *json.Memo 12 | ConvertMemos(list []*model.Memo) []*json.Memo 13 | ConvertTag(md *model.Tag) *json.Tag 14 | ConvertTags(list []*model.Tag) []*json.Tag 15 | ConvertPostMemoAndTagsResult(memo *model.Memo, tags []*model.Tag) *json.PostMemoAndTagsResult 16 | ConvertSearchTagsAndMemosResult(memos []*model.Memo, tags []*model.Tag) *json.SearchTagsAndMemosResult 17 | } 18 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | loggersub "memo_sample/adapter/logger" 6 | "memo_sample/di" 7 | "memo_sample/infra/database" 8 | "net/http" 9 | ) 10 | 11 | func main() { 12 | ping := flag.Bool("ping", false, "check ping") 13 | flag.Parse() 14 | 15 | defer func() { 16 | _ = (*database.GetDBM()).CloseDB() 17 | }() 18 | 19 | err := (*database.GetDBM()).ConnectDB() 20 | if err != nil { 21 | loggersub.NewLogger().Errorf("db open error: %#+v\n", err) 22 | return 23 | } 24 | 25 | interceptor := func(h func(w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) { 26 | return func(w http.ResponseWriter, r *http.Request) { 27 | var err error 28 | if *ping { 29 | err = (*database.GetDBM()).PingDB() 30 | } 31 | if err != nil { 32 | loggersub.NewLogger().Errorf("db open error: %#+v\n", err) 33 | panic(err) 34 | } 35 | h(w, r) 36 | } 37 | } 38 | 39 | loggersub.NewLogger().Debugf("main called. ping check:%v\n", *ping) 40 | 41 | api := di.InjectAPIServer() 42 | http.HandleFunc("/", interceptor(api.GetMemos)) 43 | http.HandleFunc("/post", interceptor(api.PostMemo)) 44 | http.HandleFunc("/post/memo_tags", interceptor(api.PostMemoAndTags)) 45 | http.HandleFunc("/search/tags_memos", interceptor(api.SearchTagsAndMemos)) 46 | err = http.ListenAndServe(":8080", nil) 47 | if err != nil { 48 | loggersub.NewLogger().Errorf("ListenAndServe error: %#+v\n", err) 49 | } 50 | } 51 | --------------------------------------------------------------------------------