├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── mysql ├── my.cnf └── setup.sql └── server ├── Gopkg.lock ├── Gopkg.toml ├── Makefile ├── adapter └── api │ ├── api_helper.go │ ├── api_helper_test.go │ ├── const.go │ ├── err.go │ ├── program_lang_api.go │ └── program_lang_api_test.go ├── domain ├── model │ ├── const.go │ ├── error.go │ ├── programming_lang.go │ └── test_helper.go ├── repository │ └── programming_lang_repository.go └── service │ ├── program_lang_service.go │ └── program_lang_service_test.go ├── infra ├── dao │ ├── mock │ │ └── programming_lang_repository_mock.go │ └── rdb │ │ ├── const.go │ │ ├── program_lang_dao.go │ │ ├── program_lang_dao_test.go │ │ ├── sql_manager.go │ │ └── sql_manager_interface.go └── router │ └── router.go ├── main.go ├── usecase ├── input │ └── programming_lang_input.go ├── mock │ └── programming_lang_input_mock.go ├── program_lang_usecase.go └── program_lang_usecase_test.go └── util ├── checker.go ├── checker_test.go ├── trimer.go └── trimer_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | # General 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### Go template 30 | # Binaries for programs and plugins 31 | *.exe 32 | *.exe~ 33 | *.dll 34 | *.so 35 | *.dylib 36 | 37 | # Test binary, build with `go test -c` 38 | *.test 39 | 40 | # Output of the go coverage tool, specifically when used with LiteIDE 41 | *.out 42 | 43 | .idea/ 44 | server/vendor/ 45 | server/gobinary 46 | mysql/data/ 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.9.9 : Release Product without auto DB config.
2 | 0.9.9.1 : modify README.md
3 | 1.0.0 : modify bug
4 | 1.0.1 : modify README.md
5 | 1.0.2 : modify repository test 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 sekky0905 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clean-architecture-with-go 2 | 3 | The sample codes of Clean Architecture with Go. 4 | 5 | ## How to start 6 | 1 clone this repository. 7 | 8 | ``` 9 | cd ${Your_Working_Directory(Working Directory should be on your $GOPATH/src)} 10 | git clone git@github.com:SekiguchiKai/clean-architecture-with-go.git 11 | ``` 12 | 13 | 2 initialize the project by Makefile. 14 | 15 | ``` 16 | cd clean-architecture-with-go/server 17 | make init 18 | ``` 19 | 20 | 3 start application. 21 | 22 | ``` 23 | docker-compose up -d 24 | ``` 25 | 26 | ### Access Point 27 | 28 | #### LIST 29 | ``` 30 | http://localhost:8080/v1/langs?limit=${num} 31 | ``` 32 | 33 | #### GET 34 | ``` 35 | http://localhost:8080/v1/langs/${id} 36 | ``` 37 | 38 | #### POST 39 | ``` 40 | http://localhost:8080/v1/langs 41 | ``` 42 | 43 | #### PUT 44 | ``` 45 | http://localhost:8080/v1/langs/${id} 46 | ``` 47 | 48 | #### DELETE 49 | ``` 50 | http://localhost:8080/v1/langs/${id} 51 | ``` 52 | 53 | #### Json Data Format sample 54 | 55 | You can Post or Put Data like the sample below. 56 | 57 | ``` 58 | { 59 | "name":"JavaScript", 60 | "feature":"Dynamic type. Works on Web browser." 61 | } 62 | ``` 63 | 64 | ## Reference 65 | 66 | エリック・エヴァンス(著)、 今関 剛 (監修)、 和智 右桂 (翻訳) (2011/4/9)『エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)』 翔泳社 67 | 68 | Robert C.Martin (著)、 角 征典 (翻訳)、 高木 正弘 (翻訳) (2018/7/27)『Clean Architecture 達人に学ぶソフトウェアの構造と設計』 KADOKAWA 69 | 70 | アラン・シャロウェイ (著)、 ジェームズ・R・トロット (著)、 村上 雅章 (翻訳) (2014/3/11)『オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)』 丸善出版 71 | 72 | 結城 浩 (2004/6/19)『増補改訂版Java言語で学ぶデザインパターン入門』 ソフトバンククリエイティブ 73 | 74 | InfoQ.com、徳武 聡(翻訳) (2009年6月7日) 『Domain Driven Design(ドメイン駆動設計) Quickly 日本語版』 InfoQ.com Domain Driven Design(ドメイン駆動設計) Quickly 日本語版 75 | 76 | https://blog.tai2.net/the_clean_architecture.html 77 | 78 | https://nrslib.com/clean-architecture/ 79 | 80 | https://www.slideshare.net/pospome/go-80591000 81 | 82 | https://qiita.com/hirotakan/items/698c1f5773a3cca6193e 83 | 84 | https://postd.cc/golang-clean-archithecture/ 85 | 86 | https://qiita.com/kondei/items/41c28674c1bfd4156186 87 | 88 | https://golang.org/pkg/database/sql 89 | 90 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | environment: 7 | MYSQL_DATABASE: "sample" 8 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 9 | volumes: 10 | - "./mysql:/etc/mysql/conf.d" 11 | - "./mysql/data:/var/lib/mysql" 12 | - "./mysql:/docker-entrypoint-initdb.d" 13 | ports: 14 | - "3306:3306" 15 | app: 16 | image: golang:1.9 17 | command: "go run main.go" 18 | volumes: 19 | - ./server:/go/src/github.com/SekiguchiKai/clean-architecture-with-go/server 20 | working_dir: /go/src/github.com/SekiguchiKai/clean-architecture-with-go/server 21 | ports: 22 | - "8080:8080" 23 | depends_on: 24 | - db 25 | -------------------------------------------------------------------------------- /mysql/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | character-set-server=utf8mb4 3 | collation-server=utf8mb4_general_ci 4 | 5 | 6 | [client] 7 | default-character-set=utf8mb4 8 | -------------------------------------------------------------------------------- /mysql/setup.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE programming_langs ( 2 | id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 3 | name VARCHAR(20) NOT NULL, 4 | feature TEXT, 5 | created_at datetime DEFAULT NULL, 6 | updated_at datetime DEFAULT NULL, 7 | PRIMARY KEY (id) 8 | ); 9 | 10 | ALTER DATABASE sample CHARACTER SET utf8mb4; 11 | ALTER TABLE programming_langs CONVERT TO CHARACTER SET utf8mb4; 12 | 13 | -------------------------------------------------------------------------------- /server/Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/gin-contrib/sse" 7 | packages = ["."] 8 | revision = "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae" 9 | 10 | [[projects]] 11 | name = "github.com/gin-gonic/gin" 12 | packages = [ 13 | ".", 14 | "binding", 15 | "json", 16 | "render" 17 | ] 18 | revision = "b869fe1415e4b9eb52f247441830d502aece2d4d" 19 | version = "v1.3.0" 20 | 21 | [[projects]] 22 | name = "github.com/go-sql-driver/mysql" 23 | packages = ["."] 24 | revision = "d523deb1b23d913de5bdada721a6071e71283618" 25 | version = "v1.4.0" 26 | 27 | [[projects]] 28 | name = "github.com/golang/mock" 29 | packages = ["gomock"] 30 | revision = "c34cdb4725f4c3844d095133c6e40e448b86589b" 31 | version = "v1.1.1" 32 | 33 | [[projects]] 34 | name = "github.com/golang/protobuf" 35 | packages = ["proto"] 36 | revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5" 37 | version = "v1.2.0" 38 | 39 | [[projects]] 40 | name = "github.com/json-iterator/go" 41 | packages = ["."] 42 | revision = "1624edc4454b8682399def8740d46db5e4362ba4" 43 | version = "v1.1.5" 44 | 45 | [[projects]] 46 | name = "github.com/mattn/go-isatty" 47 | packages = ["."] 48 | revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" 49 | version = "v0.0.4" 50 | 51 | [[projects]] 52 | name = "github.com/modern-go/concurrent" 53 | packages = ["."] 54 | revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94" 55 | version = "1.0.3" 56 | 57 | [[projects]] 58 | name = "github.com/modern-go/reflect2" 59 | packages = ["."] 60 | revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" 61 | version = "1.0.1" 62 | 63 | [[projects]] 64 | name = "github.com/pkg/errors" 65 | packages = ["."] 66 | revision = "645ef00459ed84a119197bfb8d8205042c6df63d" 67 | version = "v0.8.0" 68 | 69 | [[projects]] 70 | name = "github.com/ugorji/go" 71 | packages = ["codec"] 72 | revision = "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab" 73 | version = "v1.1.1" 74 | 75 | [[projects]] 76 | branch = "master" 77 | name = "golang.org/x/net" 78 | packages = ["context"] 79 | revision = "26e67e76b6c3f6ce91f7c52def5af501b4e0f3a2" 80 | 81 | [[projects]] 82 | branch = "master" 83 | name = "golang.org/x/sys" 84 | packages = ["unix"] 85 | revision = "1561086e645b2809fb9f8a1e2a38160bf8d53bf4" 86 | 87 | [[projects]] 88 | name = "google.golang.org/appengine" 89 | packages = ["cloudsql"] 90 | revision = "ae0ab99deb4dc413a2b4bd6c8bdd0eb67f1e4d06" 91 | version = "v1.2.0" 92 | 93 | [[projects]] 94 | name = "gopkg.in/DATA-DOG/go-sqlmock.v1" 95 | packages = ["."] 96 | revision = "d76b18b42f285b792bf985118980ce9eacea9d10" 97 | version = "v1.3.0" 98 | 99 | [[projects]] 100 | name = "gopkg.in/go-playground/validator.v8" 101 | packages = ["."] 102 | revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" 103 | version = "v8.18.2" 104 | 105 | [[projects]] 106 | name = "gopkg.in/yaml.v2" 107 | packages = ["."] 108 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 109 | version = "v2.2.1" 110 | 111 | [solve-meta] 112 | analyzer-name = "dep" 113 | analyzer-version = 1 114 | inputs-digest = "3711685058a6f382d69f79c97a89dcd2a05d57872eb178ac4683ab12d9e52165" 115 | solver-name = "gps-cdcl" 116 | solver-version = 1 117 | -------------------------------------------------------------------------------- /server/Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [prune] 29 | go-tests = true 30 | unused-packages = true -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | # パラメータ 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GOCLEAN=$(GOCMD) clean 5 | GOTEST=$(GOCMD) test 6 | GOGET=$(GOCMD) get 7 | BINARY_NAME=gobinary 8 | 9 | .PHONY: init 10 | init: clean deps test precommit build 11 | 12 | .PHONY: all 13 | all: test precommit build run 14 | 15 | .PHONY: build 16 | build: 17 | $(GOBUILD) -o $(BINARY_NAME) -v 18 | 19 | .PHONY: test 20 | test: 21 | $(GOCMD) test ./... 22 | 23 | .PHONY: clean 24 | clean: 25 | $(GOCLEAN) 26 | rm -f $(BINARY_NAME) 27 | 28 | .PHONY: run 29 | run: 30 | ./$(BINARY_NAME) 31 | 32 | .PHONY: deps 33 | deps: 34 | $(GOGET) github.com/golang/dep/cmd/dep 35 | $(GOGET) github.com/gin-gonic/gin 36 | $(GOGET) golang.org/x/lint/golint 37 | $(GOGET) golang.org/x/tools/cmd/goimports 38 | $(GOGET) github.com/kisielk/errcheck 39 | $(GOGET) gopkg.in/DATA-DOG/go-sqlmock.v1 40 | $(GOGET) github.com/go-sql-driver/mysql 41 | dep ensure 42 | 43 | .PHONY: precommit 44 | precommit : 45 | # 静的解析 46 | go vet ./... 47 | # go list で import path を表示する 48 | # pipe を使用した後、 xargs でコマンドラインを作成し、xargs で作成したコマンドラインは、 go list の表示結果 49 | go list ./... | xargs golint -set_exit_status 50 | # エラーハンドリングの確認 51 | # test と Close の部分を無視している 52 | errcheck -ignoretests -ignore 'Close' ./... 53 | -------------------------------------------------------------------------------- /server/adapter/api/api_helper.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 7 | "github.com/gin-gonic/gin" 8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/util" 9 | ) 10 | 11 | // getID は、URLからIDの値を取得する。 12 | func getID(c *gin.Context) (int, error) { 13 | id, err := strconv.Atoi(c.Param(ID)) 14 | if err != nil { 15 | return -1, &model.InvalidParameterError{ 16 | Parameter: ID, 17 | Message: IDShouldBeIntErr, 18 | } 19 | } 20 | 21 | return id, nil 22 | } 23 | 24 | // getLimit は、Query StringからLimitの値を取得する。 25 | func getLimit(c *gin.Context) (int, error) { 26 | var err error 27 | limit := 20 28 | 29 | limitStr := c.Query(Limit) 30 | if !util.IsEmpty(limitStr){ 31 | limit, err = strconv.Atoi(limitStr) 32 | if err != nil { 33 | return -1, &model.InvalidParameterError{ 34 | Parameter: Limit, 35 | Message: LimitShouldBeIntErr, 36 | } 37 | } 38 | } 39 | 40 | return limit, nil 41 | } 42 | 43 | // ManageLimit は、Limitを制御する。 44 | func ManageLimit(targetLimit, maxLimit, minLimit, defaultLimit int) int { 45 | if maxLimit < targetLimit || targetLimit < minLimit { 46 | return defaultLimit 47 | } 48 | return targetLimit 49 | } 50 | -------------------------------------------------------------------------------- /server/adapter/api/api_helper_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | "github.com/SekiguchiKai/clean-architecture-with-go/server/adapter/api" 6 | ) 7 | 8 | func TestManageLimit(t *testing.T) { 9 | type args struct { 10 | targetLimit int 11 | maxLimit int 12 | minLimit int 13 | defaultLimit int 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | want int 19 | }{ 20 | { 21 | name: "ターゲットが10、Maxが30、Minが5、Defaultが20のとき、10を返す", 22 | args: args{ 23 | targetLimit: 10, 24 | maxLimit: 30, 25 | minLimit:5, 26 | defaultLimit: 20, 27 | }, 28 | want: 10, 29 | }, 30 | { 31 | name: "ターゲットが40、Maxが30、Defaultが20のとき、20を返す", 32 | args: args{ 33 | targetLimit: 20, 34 | maxLimit: 30, 35 | defaultLimit: 20, 36 | }, 37 | want: 20, 38 | }, 39 | { 40 | name: "ターゲットが31、Maxが30、Defaultが20のとき、20を返す", 41 | args: args{ 42 | targetLimit: 31, 43 | maxLimit: 30, 44 | defaultLimit: 20, 45 | }, 46 | want: 20, 47 | }, 48 | { 49 | name: "ターゲットが29、Maxが30、Defaultが20のとき、29を返す", 50 | args: args{ 51 | targetLimit: 29, 52 | maxLimit: 30, 53 | defaultLimit: 29, 54 | }, 55 | want: 29, 56 | }, 57 | { 58 | name: "ターゲットが30、Maxが30、Defaultが20のとき、30を返す", 59 | args: args{ 60 | targetLimit: 30, 61 | maxLimit: 30, 62 | defaultLimit: 30, 63 | }, 64 | want: 30, 65 | }, 66 | { 67 | name: "ターゲットが4、Maxが30、Minが5、Defaultが20のとき、20を返す", 68 | args: args{ 69 | targetLimit: 4, 70 | maxLimit: 30, 71 | minLimit:5, 72 | defaultLimit: 20, 73 | }, 74 | want: 20, 75 | }, 76 | { 77 | name: "ターゲットが6、Maxが30、Minが5、Defaultが20のとき、6を返す", 78 | args: args{ 79 | targetLimit: 6, 80 | maxLimit: 30, 81 | minLimit:5, 82 | defaultLimit: 20, 83 | }, 84 | want: 6, 85 | }, 86 | { 87 | name: "ターゲットが5、Maxが30、Minが5、Defaultが20のとき、5を返す", 88 | args: args{ 89 | targetLimit: 5, 90 | maxLimit: 30, 91 | minLimit:5, 92 | defaultLimit: 20, 93 | }, 94 | want: 5, 95 | }, 96 | } 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | if got := api.ManageLimit(tt.args.targetLimit, tt.args.maxLimit, tt.args.minLimit, tt.args.defaultLimit); got != tt.want { 100 | t.Errorf("ManageLimit() = %v, want %v", got, tt.want) 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /server/adapter/api/const.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // パスの定義。 4 | const ( 5 | ProgrammingLangAPIPath = "/langs" 6 | ) 7 | 8 | // クエリストリングの属性。 9 | const ( 10 | Limit = "limit" 11 | ) 12 | 13 | // Limitの定義。 14 | const ( 15 | MaxLimit = 100 16 | MinLimit = 5 17 | DefaultLimit = 20 18 | ) 19 | 20 | // パラメータの属性 21 | const ( 22 | ID = "id" 23 | ) 24 | 25 | // HTTPのメソッド。 26 | const ( 27 | Get = "GET" 28 | Post = "POST" 29 | Put = "PUT" 30 | Delete = "DELETE" 31 | ) 32 | -------------------------------------------------------------------------------- /server/adapter/api/err.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // エラーの定数。 11 | const ( 12 | OtherErr = "some error has occurred" 13 | IDShouldBeIntErr = "ID Should be int" 14 | LimitShouldBeIntErr = "Limit Should be int" 15 | ) 16 | 17 | // handledError はハンドリング後のエラー。 18 | type handledError struct { 19 | code int 20 | message string 21 | } 22 | 23 | // handleError は、エラーをハンドリングする。 24 | func handleError(err error) *handledError { 25 | switch errors.Cause(err).(type) { 26 | case *model.NoSuchDataError: 27 | return &handledError{ 28 | code: http.StatusNotFound, 29 | message: errors.Cause(err).Error(), 30 | } 31 | case *model.RequiredError: 32 | return &handledError{ 33 | code: http.StatusBadRequest, 34 | message: errors.Cause(err).Error(), 35 | } 36 | case *model.InvalidPropertyError: 37 | return &handledError{ 38 | code: http.StatusBadRequest, 39 | message: errors.Cause(err).Error(), 40 | } 41 | case *model.InvalidParameterError: 42 | return &handledError{ 43 | code: http.StatusBadRequest, 44 | message: errors.Cause(err).Error(), 45 | } 46 | case *model.AlreadyExistError: 47 | return &handledError{ 48 | code: http.StatusConflict, 49 | message: errors.Cause(err).Error(), 50 | } 51 | case *model.DBError: 52 | return &handledError{ 53 | code: http.StatusInternalServerError, 54 | message: errors.Cause(err).Error(), 55 | } 56 | default: 57 | return &handledError{ 58 | code: http.StatusInternalServerError, 59 | message: OtherErr, 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/adapter/api/program_lang_api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/input" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // ProgrammingLangAPI は、ProgrammingLangのAPI。 13 | type ProgrammingLangAPI struct { 14 | UseCase input.ProgrammingLangInputPort 15 | } 16 | 17 | // NewProgrammingLangAPI は、ProgrammingLangAPIを生成し、返す。 18 | func NewProgrammingLangAPI(useCase input.ProgrammingLangInputPort) *ProgrammingLangAPI { 19 | return &ProgrammingLangAPI{ 20 | UseCase: useCase, 21 | } 22 | } 23 | 24 | // InitAPI は、APIを初期設定する。 25 | func (api *ProgrammingLangAPI) InitAPI(g *gin.RouterGroup) { 26 | g.GET(ProgrammingLangAPIPath, api.List) 27 | g.GET(fmt.Sprintf("%s/:%s", ProgrammingLangAPIPath, ID), api.Get) 28 | g.POST(ProgrammingLangAPIPath, api.Create) 29 | g.PUT(fmt.Sprintf("%s/:%s", ProgrammingLangAPIPath, ID), api.Update) 30 | g.DELETE(fmt.Sprintf("%s/:%s", ProgrammingLangAPIPath, ID), api.Delete) 31 | } 32 | 33 | // List は、ProgrammingLangの一覧を返す。 34 | func (api *ProgrammingLangAPI) List(c *gin.Context) { 35 | limit, err := getLimit(c) 36 | if err != nil { 37 | he := handleError(err) 38 | c.JSON(he.code, he.message) 39 | return 40 | } 41 | 42 | limit = ManageLimit(limit, MaxLimit, MinLimit, DefaultLimit) 43 | 44 | ctx := c.Request.Context() 45 | langSlice, err := api.UseCase.List(ctx, limit) 46 | if err != nil { 47 | he := handleError(err) 48 | c.JSON(he.code, he.message) 49 | return 50 | } 51 | 52 | c.JSON(http.StatusOK, langSlice) 53 | } 54 | 55 | // Get は、ProgrammingLangを取得する。 56 | func (api *ProgrammingLangAPI) Get(c *gin.Context) { 57 | id, err := getID(c) 58 | if err != nil { 59 | he := handleError(err) 60 | c.JSON(he.code, he.message) 61 | return 62 | } 63 | 64 | ctx := c.Request.Context() 65 | lang, err := api.UseCase.Get(ctx, id) 66 | if err != nil { 67 | he := handleError(err) 68 | c.JSON(he.code, he.message) 69 | return 70 | } 71 | 72 | c.JSON(http.StatusOK, lang) 73 | } 74 | 75 | // Create は、ProgrammingLangを生成する。 76 | func (api *ProgrammingLangAPI) Create(c *gin.Context) { 77 | var params *model.ProgrammingLang 78 | if err := c.BindJSON(¶ms); err != nil { 79 | c.JSON(http.StatusBadRequest, err.Error()) 80 | return 81 | } 82 | 83 | ctx := c.Request.Context() 84 | lang, err := api.UseCase.Create(ctx, params) 85 | if err != nil { 86 | he := handleError(err) 87 | c.JSON(he.code, he.message) 88 | return 89 | } 90 | 91 | c.JSON(http.StatusOK, lang) 92 | } 93 | 94 | // Update は、ProgrammingLangを更新する。 95 | func (api *ProgrammingLangAPI) Update(c *gin.Context) { 96 | var params *model.ProgrammingLang 97 | if err := c.BindJSON(¶ms); err != nil { 98 | c.JSON(http.StatusBadRequest, err.Error()) 99 | return 100 | } 101 | 102 | ctx := c.Request.Context() 103 | id, err := getID(c) 104 | if err != nil { 105 | he := handleError(err) 106 | c.JSON(he.code, he.message) 107 | return 108 | } 109 | 110 | lang, err := api.UseCase.Update(ctx, id, params) 111 | if err != nil { 112 | he := handleError(err) 113 | c.JSON(he.code, he.message) 114 | return 115 | } 116 | 117 | c.JSON(http.StatusOK, lang) 118 | } 119 | 120 | // Delete は、ProgrammingLangを削除する。 121 | func (api *ProgrammingLangAPI) Delete(c *gin.Context) { 122 | id, err := getID(c) 123 | if err != nil { 124 | he := handleError(err) 125 | c.JSON(he.code, he.message) 126 | return 127 | } 128 | 129 | ctx := c.Request.Context() 130 | if err := api.UseCase.Delete(ctx, id); err != nil { 131 | he := handleError(err) 132 | c.JSON(he.code, he.message) 133 | return 134 | } 135 | 136 | c.JSON(http.StatusOK, nil) 137 | } 138 | -------------------------------------------------------------------------------- /server/adapter/api/program_lang_api_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "reflect" 10 | "testing" 11 | "context" 12 | 13 | "github.com/SekiguchiKai/clean-architecture-with-go/server/adapter/api" 14 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 15 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/input" 16 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/mock" 17 | "github.com/SekiguchiKai/clean-architecture-with-go/server/util" 18 | "github.com/gin-gonic/gin" 19 | "github.com/golang/mock/gomock" 20 | ) 21 | 22 | func TestNewProgrammingLangAPI(t *testing.T) { 23 | ctrl := gomock.NewController(t) 24 | defer ctrl.Finish() 25 | 26 | mock := mock_input.NewMockProgrammingLangInputPort(ctrl) 27 | 28 | type args struct { 29 | useCase input.ProgrammingLangInputPort 30 | } 31 | tests := []struct { 32 | name string 33 | args args 34 | }{ 35 | { 36 | name: "適切な引数を与えるとProgrammingLangAPIを返すこと", 37 | args: args{ 38 | useCase: mock, 39 | }, 40 | }, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if got := api.NewProgrammingLangAPI(tt.args.useCase); got == nil { 45 | t.Errorf("NewProgrammingLangAPI() = %v, want nil", got) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestProgrammingLangAPI_List(t *testing.T) { 52 | ctrl := gomock.NewController(t) 53 | defer ctrl.Finish() 54 | 55 | u := mock_input.NewMockProgrammingLangInputPort(ctrl) 56 | 57 | langAPI := &api.ProgrammingLangAPI{ 58 | UseCase: u, 59 | } 60 | handler := langAPI.List 61 | 62 | paramErr := &model.InvalidParameterError{ 63 | Parameter: api.Limit, 64 | Message: api.LimitShouldBeIntErr, 65 | } 66 | 67 | dbErr := &model.DBError{ 68 | ModelName: model.ModelNameProgrammingLang, 69 | DBMethod: model.DBMethodList, 70 | Detail: "Test", 71 | } 72 | 73 | type params struct { 74 | limit string 75 | } 76 | 77 | type mock struct { 78 | ctx context.Context 79 | limit int 80 | result []*model.ProgrammingLang 81 | err error 82 | } 83 | 84 | type want struct { 85 | code int 86 | result []*model.ProgrammingLang 87 | errMessage string 88 | } 89 | 90 | tests := []struct { 91 | name string 92 | params params 93 | mock mock 94 | want want 95 | }{ 96 | { 97 | name: "リクエストのクエリパラメータが20で、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと", 98 | params:params{ 99 | limit:"20", 100 | }, 101 | mock: mock{ 102 | ctx: context.Background(), 103 | limit: 20, 104 | result: model.CreateProgrammingLangs(20), 105 | err: nil, 106 | }, 107 | want: want{ 108 | code: http.StatusOK, 109 | result: model.CreateProgrammingLangs(20), 110 | }, 111 | }, 112 | { 113 | name: "リクエストのクエリパラメータが6で、データが6件以上存在する場合、ステータスコード200と6件のデータを返すこと", 114 | params:params{ 115 | limit:"6", 116 | }, 117 | mock: mock{ 118 | ctx: context.Background(), 119 | limit: 6, 120 | result: model.CreateProgrammingLangs(6), 121 | err: nil, 122 | }, 123 | want: want{ 124 | code: http.StatusOK, 125 | result: model.CreateProgrammingLangs(6), 126 | }, 127 | }, 128 | { 129 | name: "リクエストのクエリパラメータが4で、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと", 130 | params:params{ 131 | limit:"4", 132 | }, 133 | mock: mock{ 134 | ctx: context.Background(), 135 | limit: 20, 136 | result: model.CreateProgrammingLangs(20), 137 | err: nil, 138 | }, 139 | want: want{ 140 | code: http.StatusOK, 141 | result: model.CreateProgrammingLangs(20), 142 | }, 143 | }, 144 | { 145 | name: "リクエストのクエリパラメータが99で、データが99件以上存在する場合、ステータスコード200と99件のデータを返すこと", 146 | params:params{ 147 | limit:"99", 148 | }, 149 | mock: mock{ 150 | ctx: context.Background(), 151 | limit: 99, 152 | result: model.CreateProgrammingLangs(99), 153 | err: nil, 154 | }, 155 | want: want{ 156 | code: http.StatusOK, 157 | result: model.CreateProgrammingLangs(99), 158 | }, 159 | }, 160 | { 161 | name: "リクエストのクエリパラメータが101で、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと", 162 | params:params{ 163 | limit:"101", 164 | }, 165 | mock: mock{ 166 | ctx: context.Background(), 167 | limit: 20, 168 | result: model.CreateProgrammingLangs(20), 169 | err: nil, 170 | }, 171 | want: want{ 172 | code: http.StatusOK, 173 | result: model.CreateProgrammingLangs(20), 174 | }, 175 | }, 176 | { 177 | name: "リクエストのクエリパラメータの指定がなく、データが20件以上存在する場合、ステータスコード200と20件のデータを返すこと", 178 | params:params{ 179 | limit:"", 180 | }, 181 | mock: mock{ 182 | ctx: context.Background(), 183 | limit: 20, 184 | result: model.CreateProgrammingLangs(20), 185 | err: nil, 186 | }, 187 | want: want{ 188 | code: http.StatusOK, 189 | result: model.CreateProgrammingLangs(20), 190 | }, 191 | }, 192 | { 193 | name: "リクエストのクエリパラメータが文字列の場合、ステータスコード400とエラーメッセージを返すこと", 194 | params:params{ 195 | limit:"test", 196 | }, 197 | mock: mock{ 198 | ctx: context.Background(), 199 | result: nil, 200 | err: paramErr, 201 | }, 202 | want: want{ 203 | code: http.StatusBadRequest, 204 | result: nil, 205 | errMessage: paramErr.Error(), 206 | }, 207 | }, 208 | { 209 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと", 210 | params:params{ 211 | limit:"20", 212 | }, 213 | mock: mock{ 214 | ctx: context.Background(), 215 | limit: 20, 216 | result: nil, 217 | err: dbErr, 218 | }, 219 | want: want{ 220 | code: http.StatusInternalServerError, 221 | result: nil, 222 | errMessage: dbErr.Error(), 223 | }, 224 | }, 225 | } 226 | for _, tt := range tests { 227 | t.Run(tt.name, func(t *testing.T) { 228 | r := gin.New() 229 | r.GET(api.ProgrammingLangAPIPath, handler) 230 | 231 | url := fmt.Sprintf("%s?%s=%s", api.ProgrammingLangAPIPath, api.Limit, tt.params.limit) 232 | if !reflect.DeepEqual(tt.mock.err, paramErr) { 233 | u.EXPECT().List(tt.mock.ctx, tt.mock.limit).Return(tt.mock.result, tt.mock.err) 234 | } 235 | 236 | rec := httptest.NewRecorder() 237 | req, err := http.NewRequest(api.Get, url, nil) 238 | if err != nil { 239 | t.Fatal(err) 240 | } 241 | r.ServeHTTP(rec, req) 242 | 243 | if tt.want.code == http.StatusOK { 244 | var got []*model.ProgrammingLang 245 | err = json.Unmarshal(rec.Body.Bytes(), &got) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | 250 | for i, v := range tt.want.result { 251 | if !reflect.DeepEqual(got[i], v) { 252 | t.Errorf("Response Body = %v, want %v", got[i], v) 253 | } 254 | } 255 | 256 | } else { 257 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage { 258 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage) 259 | } 260 | } 261 | if !reflect.DeepEqual(rec.Code, tt.want.code) { 262 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code) 263 | } 264 | }) 265 | } 266 | } 267 | 268 | func TestProgrammingLangAPI_Get(t *testing.T) { 269 | ctrl := gomock.NewController(t) 270 | defer ctrl.Finish() 271 | 272 | u := mock_input.NewMockProgrammingLangInputPort(ctrl) 273 | 274 | langAPI := &api.ProgrammingLangAPI{ 275 | UseCase: u, 276 | } 277 | handler := langAPI.Get 278 | 279 | noDataErr := &model.NoSuchDataError{ 280 | ID: 1, 281 | Name: model.TestName, 282 | ModelName: model.ModelNameProgrammingLang, 283 | } 284 | 285 | dbErr := &model.DBError{ 286 | ModelName: model.ModelNameProgrammingLang, 287 | DBMethod: model.DBMethodRead, 288 | Detail: "Test", 289 | } 290 | 291 | type mock struct { 292 | ctx context.Context 293 | id int 294 | result *model.ProgrammingLang 295 | err error 296 | } 297 | 298 | type want struct { 299 | code int 300 | result *model.ProgrammingLang 301 | errMessage string 302 | } 303 | 304 | tests := []struct { 305 | name string 306 | mock mock 307 | want want 308 | }{ 309 | { 310 | name: "リクエストのURLのIDのパラメータが適切な場合、ステータスコード200と1件のデータを返すこと", 311 | mock: mock{ 312 | ctx: context.Background(), 313 | id: 1, 314 | result: model.CreateProgrammingLangs(1)[0], 315 | err: nil, 316 | }, 317 | want: want{ 318 | code: http.StatusOK, 319 | result: model.CreateProgrammingLangs(1)[0], 320 | }, 321 | }, 322 | { 323 | name: "リクエストのURLのIDのパラメータと同一のIDを持つデータが存在しない場合、ステータスコード404とエラーメッセージを返すこと", 324 | mock: mock{ 325 | ctx: context.Background(), 326 | id: 100, 327 | result: nil, 328 | err: noDataErr, 329 | }, 330 | want: want{ 331 | code: http.StatusNotFound, 332 | result: nil, 333 | errMessage: noDataErr.Error(), 334 | }, 335 | }, 336 | { 337 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと", 338 | mock: mock{ 339 | ctx: context.Background(), 340 | id: 20, 341 | result: nil, 342 | err: dbErr, 343 | }, 344 | want: want{ 345 | code: http.StatusInternalServerError, 346 | result: nil, 347 | errMessage: dbErr.Error(), 348 | }, 349 | }, 350 | } 351 | for _, tt := range tests { 352 | t.Run(tt.name, func(t *testing.T) { 353 | r := gin.New() 354 | r.GET(fmt.Sprintf("%s/:%s", api.ProgrammingLangAPIPath, api.ID), handler) 355 | u.EXPECT().Get(tt.mock.ctx, tt.mock.id).Return(tt.mock.result, tt.mock.err) 356 | 357 | rec := httptest.NewRecorder() 358 | url := fmt.Sprintf("%s/%d", api.ProgrammingLangAPIPath, tt.mock.id) 359 | req, err := http.NewRequest(api.Get, url, nil) 360 | if err != nil { 361 | t.Fatal(err) 362 | } 363 | r.ServeHTTP(rec, req) 364 | 365 | if tt.want.code == http.StatusOK { 366 | var got *model.ProgrammingLang 367 | err = json.Unmarshal(rec.Body.Bytes(), &got) 368 | if err != nil { 369 | t.Fatal(err) 370 | } 371 | if !reflect.DeepEqual(got, tt.want.result) { 372 | t.Errorf("Response Body = %v, want %v", got, tt.want.result) 373 | } 374 | 375 | } else { 376 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage { 377 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage) 378 | } 379 | } 380 | if !reflect.DeepEqual(rec.Code, tt.want.code) { 381 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code) 382 | } 383 | }) 384 | } 385 | } 386 | 387 | func TestProgrammingLangAPI_Create(t *testing.T) { 388 | ctrl := gomock.NewController(t) 389 | defer ctrl.Finish() 390 | 391 | u := mock_input.NewMockProgrammingLangInputPort(ctrl) 392 | 393 | langAPI := &api.ProgrammingLangAPI{ 394 | UseCase: u, 395 | } 396 | handler := langAPI.Create 397 | 398 | conflictErr := &model.AlreadyExistError{ 399 | ID: 1, 400 | Name: model.TestName, 401 | ModelName: model.ModelNameProgrammingLang, 402 | } 403 | 404 | dbErr := &model.DBError{ 405 | ModelName: model.ModelNameProgrammingLang, 406 | DBMethod: model.DBMethodRead, 407 | Detail: "Test", 408 | } 409 | 410 | type mock struct { 411 | ctx context.Context 412 | param *model.ProgrammingLang 413 | result *model.ProgrammingLang 414 | err error 415 | } 416 | 417 | type want struct { 418 | code int 419 | result *model.ProgrammingLang 420 | errMessage string 421 | } 422 | 423 | tests := []struct { 424 | name string 425 | mock mock 426 | want want 427 | }{ 428 | { 429 | name: "リクエストボディのProgrammingLangが適切な場合、ステータスコード200と1件のデータを返すこと", 430 | mock: mock{ 431 | ctx: context.Background(), 432 | param: model.CreateProgrammingLangs(1)[0], 433 | result: model.CreateProgrammingLangs(1)[0], 434 | err: nil, 435 | }, 436 | want: want{ 437 | code: http.StatusOK, 438 | result: model.CreateProgrammingLangs(1)[0], 439 | }, 440 | }, 441 | { 442 | name: "リクエストボディのProgrammingLangが既に存在している場合、ステータスコード409とエラーメッセージを返すこと", 443 | mock: mock{ 444 | ctx: context.Background(), 445 | param: model.CreateProgrammingLangs(1)[0], 446 | result: nil, 447 | err: conflictErr, 448 | }, 449 | want: want{ 450 | code: http.StatusConflict, 451 | result: nil, 452 | errMessage: conflictErr.Error(), 453 | }, 454 | }, 455 | { 456 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと", 457 | mock: mock{ 458 | ctx: context.Background(), 459 | param: model.CreateProgrammingLangs(1)[0], 460 | result: nil, 461 | err: dbErr, 462 | }, 463 | want: want{ 464 | code: http.StatusInternalServerError, 465 | result: nil, 466 | errMessage: dbErr.Error(), 467 | }, 468 | }, 469 | } 470 | for _, tt := range tests { 471 | t.Run(tt.name, func(t *testing.T) { 472 | r := gin.New() 473 | r.POST(api.ProgrammingLangAPIPath, handler) 474 | 475 | u.EXPECT().Create(tt.mock.ctx, tt.mock.param).Return(tt.mock.result, tt.mock.err) 476 | 477 | rec := httptest.NewRecorder() 478 | b, err := json.Marshal(tt.mock.param) 479 | if err != nil { 480 | t.Fatal(err) 481 | } 482 | body := bytes.NewReader(b) 483 | 484 | req, err := http.NewRequest(api.Post, api.ProgrammingLangAPIPath, body) 485 | if err != nil { 486 | t.Fatal(err) 487 | } 488 | r.ServeHTTP(rec, req) 489 | 490 | if tt.want.code == http.StatusOK { 491 | var got *model.ProgrammingLang 492 | err = json.Unmarshal(rec.Body.Bytes(), &got) 493 | if err != nil { 494 | t.Fatal(err) 495 | } 496 | if !reflect.DeepEqual(got, tt.want.result) { 497 | t.Errorf("Response Body = %v, want %v", got, tt.want.result) 498 | } 499 | 500 | } else { 501 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage { 502 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage) 503 | } 504 | } 505 | if !reflect.DeepEqual(rec.Code, tt.want.code) { 506 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code) 507 | } 508 | }) 509 | } 510 | } 511 | 512 | func TestProgrammingLangAPI_Update(t *testing.T) { 513 | ctrl := gomock.NewController(t) 514 | defer ctrl.Finish() 515 | 516 | u := mock_input.NewMockProgrammingLangInputPort(ctrl) 517 | 518 | langAPI := &api.ProgrammingLangAPI{ 519 | UseCase: u, 520 | } 521 | handler := langAPI.Update 522 | 523 | noDataErr := &model.NoSuchDataError{ 524 | ID: 1, 525 | Name: model.TestName, 526 | ModelName: model.ModelNameProgrammingLang, 527 | } 528 | 529 | dbErr := &model.DBError{ 530 | ModelName: model.ModelNameProgrammingLang, 531 | DBMethod: model.DBMethodRead, 532 | Detail: "Test", 533 | } 534 | 535 | type mock struct { 536 | ctx context.Context 537 | id int 538 | param *model.ProgrammingLang 539 | result *model.ProgrammingLang 540 | err error 541 | } 542 | 543 | type want struct { 544 | code int 545 | result *model.ProgrammingLang 546 | errMessage string 547 | } 548 | 549 | type fields struct { 550 | UseCase input.ProgrammingLangInputPort 551 | } 552 | 553 | type param struct { 554 | id int 555 | } 556 | 557 | tests := []struct { 558 | name string 559 | fields fields 560 | mock mock 561 | want want 562 | param param 563 | }{ 564 | { 565 | name: "リクエストボディのProgrammingLangが適切な場合、ステータスコード200と1件のデータを返すこと", 566 | mock: mock{ 567 | ctx: context.Background(), 568 | id: 1, 569 | param: model.CreateProgrammingLangs(1)[0], 570 | result: model.CreateProgrammingLangs(1)[0], 571 | err: nil, 572 | }, 573 | want: want{ 574 | code: http.StatusOK, 575 | result: model.CreateProgrammingLangs(1)[0], 576 | }, 577 | param: param{ 578 | id: 1, 579 | }, 580 | }, 581 | { 582 | name: "リクエストのURLのIDのパラメータと同一のIDを持つデータが存在しない場合、ステータスコード404とエラーメッセージを返すこと", 583 | mock: mock{ 584 | ctx: context.Background(), 585 | id: 100, 586 | param: model.CreateProgrammingLangs(1)[0], 587 | result: nil, 588 | err: noDataErr, 589 | }, 590 | want: want{ 591 | code: http.StatusNotFound, 592 | result: nil, 593 | errMessage: noDataErr.Error(), 594 | }, 595 | param: param{ 596 | id: 100, 597 | }, 598 | }, 599 | { 600 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと", 601 | mock: mock{ 602 | ctx: context.Background(), 603 | id: 1, 604 | param: model.CreateProgrammingLangs(1)[0], 605 | result: nil, 606 | err: dbErr, 607 | }, 608 | want: want{ 609 | code: http.StatusInternalServerError, 610 | result: nil, 611 | errMessage: dbErr.Error(), 612 | }, 613 | param: param{ 614 | id: 1, 615 | }, 616 | }, 617 | } 618 | for _, tt := range tests { 619 | t.Run(tt.name, func(t *testing.T) { 620 | r := gin.New() 621 | r.PUT(fmt.Sprintf("%s/:%s", api.ProgrammingLangAPIPath, api.ID), handler) 622 | 623 | u.EXPECT().Update(tt.mock.ctx, tt.mock.id, tt.mock.param).Return(tt.mock.result, tt.mock.err) 624 | 625 | rec := httptest.NewRecorder() 626 | b, err := json.Marshal(tt.mock.param) 627 | if err != nil { 628 | t.Fatal(err) 629 | } 630 | body := bytes.NewReader(b) 631 | 632 | url := fmt.Sprintf("%s/%d", api.ProgrammingLangAPIPath, tt.param.id) 633 | req, err := http.NewRequest(api.Put, url, body) 634 | if err != nil { 635 | t.Fatal(err) 636 | } 637 | r.ServeHTTP(rec, req) 638 | 639 | if tt.want.code == http.StatusOK { 640 | var got *model.ProgrammingLang 641 | err = json.Unmarshal(rec.Body.Bytes(), &got) 642 | if err != nil { 643 | t.Fatal(err) 644 | } 645 | if !reflect.DeepEqual(got, tt.want.result) { 646 | t.Errorf("Response Body = %v, want %v", got, tt.want.result) 647 | } 648 | 649 | } else { 650 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage { 651 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage) 652 | } 653 | } 654 | if !reflect.DeepEqual(rec.Code, tt.want.code) { 655 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code) 656 | } 657 | }) 658 | } 659 | } 660 | 661 | func TestProgrammingLangAPI_Delete(t *testing.T) { 662 | ctrl := gomock.NewController(t) 663 | defer ctrl.Finish() 664 | 665 | u := mock_input.NewMockProgrammingLangInputPort(ctrl) 666 | 667 | langAPI := &api.ProgrammingLangAPI{ 668 | UseCase: u, 669 | } 670 | handler := langAPI.Delete 671 | 672 | noDataErr := &model.NoSuchDataError{ 673 | ID: 1, 674 | Name: model.TestName, 675 | ModelName: model.ModelNameProgrammingLang, 676 | } 677 | 678 | dbErr := &model.DBError{ 679 | ModelName: model.ModelNameProgrammingLang, 680 | DBMethod: model.DBMethodRead, 681 | Detail: "Test", 682 | } 683 | 684 | type mock struct { 685 | ctx context.Context 686 | id int 687 | err error 688 | } 689 | 690 | type want struct { 691 | code int 692 | result *model.ProgrammingLang 693 | errMessage string 694 | } 695 | 696 | type param struct { 697 | id int 698 | } 699 | 700 | tests := []struct { 701 | name string 702 | mock mock 703 | want want 704 | param param 705 | }{ 706 | { 707 | name: "リクエストボディのProgrammingLangが適切な場合、ステータスコード200を返すこと", 708 | mock: mock{ 709 | ctx: context.Background(), 710 | id: 1, 711 | err: nil, 712 | }, 713 | want: want{ 714 | code: http.StatusOK, 715 | result: nil, 716 | }, 717 | param: param{ 718 | id: 1, 719 | }, 720 | }, 721 | { 722 | name: "リクエストのURLのIDのパラメータと同一のIDを持つデータが存在しない場合、ステータスコード404とエラーメッセージを返すこと", 723 | mock: mock{ 724 | ctx: context.Background(), 725 | id: 100, 726 | err: noDataErr, 727 | }, 728 | want: want{ 729 | code: http.StatusNotFound, 730 | result: nil, 731 | errMessage: noDataErr.Error(), 732 | }, 733 | param: param{ 734 | id: 100, 735 | }, 736 | }, 737 | { 738 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと", 739 | mock: mock{ 740 | ctx: context.Background(), 741 | id: 1, 742 | err: dbErr, 743 | }, 744 | want: want{ 745 | code: http.StatusInternalServerError, 746 | result: nil, 747 | errMessage: dbErr.Error(), 748 | }, 749 | param: param{ 750 | id: 1, 751 | }, 752 | }, 753 | } 754 | for _, tt := range tests { 755 | t.Run(tt.name, func(t *testing.T) { 756 | r := gin.New() 757 | r.DELETE(fmt.Sprintf("%s/:%s", api.ProgrammingLangAPIPath, api.ID), handler) 758 | 759 | u.EXPECT().Delete(tt.mock.ctx, tt.mock.id).Return(tt.mock.err) 760 | 761 | url := fmt.Sprintf("%s/%d", api.ProgrammingLangAPIPath, tt.param.id) 762 | 763 | req, err := http.NewRequest(api.Delete, url, nil) 764 | if err != nil { 765 | t.Fatal(err) 766 | } 767 | 768 | rec := httptest.NewRecorder() 769 | r.ServeHTTP(rec, req) 770 | 771 | if tt.want.code == http.StatusOK { 772 | var got *model.ProgrammingLang 773 | err = json.Unmarshal(rec.Body.Bytes(), &got) 774 | if err != nil { 775 | t.Fatal(err) 776 | } 777 | if !reflect.DeepEqual(got, tt.want.result) { 778 | t.Errorf("Response Body = %v, want %v", got, tt.want.result) 779 | } 780 | 781 | } else { 782 | if util.TrimDoubleQuotes(rec.Body.String()) != tt.want.errMessage { 783 | t.Errorf("Error Message = %v, want %v", util.TrimDoubleQuotes(rec.Body.String()), tt.want.errMessage) 784 | } 785 | } 786 | if !reflect.DeepEqual(rec.Code, tt.want.code) { 787 | t.Errorf("Status Code = %v, want %v", rec.Code, tt.want.code) 788 | } 789 | }) 790 | } 791 | } 792 | 793 | -------------------------------------------------------------------------------- /server/domain/model/const.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // プロパティの名称。 4 | const ( 5 | PropertyName = "Name" 6 | ) 7 | 8 | // エラー系。 9 | const ( 10 | NameShouldBeMoreThanOneUnderTheTwenty = "Length of Name should be 0 < name < 21" 11 | ) 12 | 13 | // エラー用の名称。 14 | const ( 15 | ErrorProperty = "Property" 16 | ErrorMessage = "Message" 17 | ) 18 | 19 | // モデル名。 20 | const ( 21 | ModelNameProgrammingLang = "ProgrammingLang" 22 | ) 23 | 24 | // テスト用の定数。 25 | const ( 26 | TestName = "testName" 27 | TestFeature = "testFeature, testFeature, testFeature, testFeature, testFeature, testFeature, testFeature" 28 | TestDBSomeErr = "DB some error" 29 | ) 30 | 31 | // DBの操作。 32 | const ( 33 | DBMethodCreate = "Create" 34 | DBMethodList = "List" 35 | DBMethodRead = "Read" 36 | DBMethodUpdate = "Update" 37 | DBMethodDelete = "Delete" 38 | ) 39 | -------------------------------------------------------------------------------- /server/domain/model/error.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "fmt" 4 | 5 | // RequiredError は、必要なものが存在しない場合のエラー。 6 | type RequiredError struct { 7 | Property string 8 | } 9 | 10 | // Error は、エラー文を返す。 11 | func (e *RequiredError) Error() string { 12 | return fmt.Sprintf("%s is required", e.Property) 13 | } 14 | 15 | // InvalidPropertyError は、Propertyが不適切な場合のエラー。 16 | type InvalidPropertyError struct { 17 | Property string 18 | Message string 19 | } 20 | 21 | // Error は、エラー文を返す。 22 | func (e *InvalidPropertyError) Error() string { 23 | return fmt.Sprintf("%s is invalid. %s", e.Property, e.Message) 24 | } 25 | 26 | // InvalidParameterError は、Parameterが不適切な場合のエラー。 27 | type InvalidParameterError struct { 28 | Parameter string 29 | Message string 30 | } 31 | 32 | // Error は、エラー文を返す。 33 | func (e *InvalidParameterError) Error() string { 34 | return fmt.Sprintf("%s is invalid. %s", e.Parameter, e.Message) 35 | } 36 | 37 | // DBError は、DBのエラーを表す。 38 | type DBError struct { 39 | ModelName string 40 | DBMethod string 41 | Detail string 42 | } 43 | 44 | // Error は、エラー文を返す。 45 | func (e *DBError) Error() string { 46 | return fmt.Sprintf("failed DB operation. Method : %s, Model : %s, Detail : %s", e.DBMethod, e.ModelName, e.Detail) 47 | } 48 | 49 | // AlreadyExistError は、既に存在することを表すエラー。 50 | type AlreadyExistError struct { 51 | ID int 52 | Name string 53 | ModelName string 54 | } 55 | 56 | // Error は、エラーメッセージを返却する。 57 | func (e *AlreadyExistError) Error() string { 58 | return fmt.Sprintf("already exists. model: %s, id: %d, name: %s", e.ModelName, e.ID, e.Name) 59 | } 60 | 61 | // NoSuchDataError は、データが存在しないことを表すエラー。 62 | type NoSuchDataError struct { 63 | ID int 64 | Name string 65 | ModelName string 66 | } 67 | 68 | // Error は、エラーメッセージを返す。 69 | func (e *NoSuchDataError) Error() string { 70 | return fmt.Sprintf("no such model.model: %s, id: %d, name: %s", e.ModelName, e.ID, e.Name) 71 | } 72 | -------------------------------------------------------------------------------- /server/domain/model/programming_lang.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // ProgrammingLang は、プログラミング言語を表す。 6 | type ProgrammingLang struct { 7 | ID int `json:"id"` 8 | Name string `json:"name"` 9 | Feature string `json:"feature"` 10 | CreatedAt time.Time `json:"createdAt"` 11 | UpdatedAt time.Time `json:"updatedAt"` 12 | } 13 | -------------------------------------------------------------------------------- /server/domain/model/test_helper.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // GetTestTime は、テスト用の時刻を生成し、返す。 9 | func GetTestTime(month time.Month, day int) time.Time { 10 | return time.Date(2018, month, day, 12, 0, 0, 0, time.UTC) 11 | } 12 | 13 | // CreateProgrammingLangs は、引数で与えられた数だけProgrammingLangを生成し、返す。 14 | func CreateProgrammingLangs(num int) []*ProgrammingLang { 15 | langSlice := make([]*ProgrammingLang, num, num) 16 | for i := range langSlice { 17 | langSlice[i] = &ProgrammingLang{ 18 | ID: i + 1, 19 | Name: fmt.Sprintf("%s%d", TestName, i), 20 | Feature: fmt.Sprintf("%s%d", TestFeature, i), 21 | CreatedAt: GetTestTime(time.October, i+1), 22 | UpdatedAt: GetTestTime(time.October, i+1), 23 | } 24 | } 25 | return langSlice 26 | } 27 | -------------------------------------------------------------------------------- /server/domain/repository/programming_lang_repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 7 | ) 8 | 9 | // ProgrammingLangRepository は、ProgrammingLangのRepository。 10 | type ProgrammingLangRepository interface { 11 | List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) 12 | Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) 13 | Read(ctx context.Context, id int) (*model.ProgrammingLang, error) 14 | ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error) 15 | Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) 16 | Delete(ctx context.Context, id int) error 17 | } 18 | -------------------------------------------------------------------------------- /server/domain/service/program_lang_service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 5 | "github.com/pkg/errors" 6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/util" 7 | ) 8 | 9 | // NewProgrammingLang は、ProgrammingLangを生成し、返す。 10 | func NewProgrammingLang(name string) (*model.ProgrammingLang, error) { 11 | if err := ValidateProgrammingLang(name); err != nil { 12 | return nil, errors.WithStack(err) 13 | } 14 | 15 | return &model.ProgrammingLang{ 16 | Name: name, 17 | }, nil 18 | } 19 | 20 | // ValidateProgrammingLang は、ProgrammingLangの生成に必要な属性に与えられる引数をチェックする。 21 | func ValidateProgrammingLang(name string) error { 22 | if util.IsEmpty(name) || len(name) > 20 { 23 | return &model.InvalidPropertyError{ 24 | Property: model.PropertyName, 25 | Message: model.NameShouldBeMoreThanOneUnderTheTwenty, 26 | } 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /server/domain/service/program_lang_service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 7 | ) 8 | 9 | func TestNewProgrammingLang(t *testing.T) { 10 | type args struct { 11 | name string 12 | } 13 | 14 | type wantErr struct { 15 | isErr bool 16 | err error 17 | } 18 | 19 | invalidPropertyErr := &model.InvalidPropertyError{ 20 | Property: model.PropertyName, 21 | Message: model.NameShouldBeMoreThanOneUnderTheTwenty, 22 | } 23 | 24 | tests := []struct { 25 | name string 26 | args args 27 | want *model.ProgrammingLang 28 | wantErr wantErr 29 | }{ 30 | { 31 | name: "Nameが20文字の場合、ProgrammingLangを返す", 32 | args: args{ 33 | name: "abcdefghijklmnopqrst", 34 | }, 35 | wantErr: wantErr{ 36 | isErr: false, 37 | }, 38 | }, 39 | { 40 | name: "Nameが21文字の場合、エラーを返す", 41 | args: args{ 42 | name: "abcdefghijklmnopqrstu", 43 | }, 44 | wantErr: wantErr{ 45 | isErr: true, 46 | err: invalidPropertyErr, 47 | }, 48 | }, 49 | { 50 | name: "Nameが空文字の場合、エラーを返す", 51 | args: args{ 52 | name: "", 53 | }, 54 | wantErr: wantErr{ 55 | isErr: true, 56 | err: invalidPropertyErr, 57 | }, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | got, err := NewProgrammingLang(tt.args.name) 63 | if (err != nil) != tt.wantErr.isErr { 64 | t.Errorf("NewProgrammingLang() error = %v, wantErr %v", err, tt.wantErr.isErr) 65 | return 66 | } 67 | 68 | if tt.wantErr.isErr { 69 | if err.Error() != tt.wantErr.err.Error() { 70 | t.Errorf("ValidateProgrammingLang() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error()) 71 | } 72 | } else { 73 | if got == nil { 74 | t.Errorf("NewProgrammingLang() = %v, want not nil", got) 75 | } 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestValidateProgrammingLang(t *testing.T) { 82 | type args struct { 83 | name string 84 | } 85 | 86 | type wantErr struct { 87 | isErr bool 88 | err error 89 | } 90 | 91 | invalidPropertyErr := &model.InvalidPropertyError{ 92 | Property: model.PropertyName, 93 | Message: model.NameShouldBeMoreThanOneUnderTheTwenty, 94 | } 95 | 96 | tests := []struct { 97 | name string 98 | args args 99 | wantErr wantErr 100 | }{ 101 | { 102 | name: "Nameが20文字の場合、エラーを返さない", 103 | args: args{ 104 | name: "abcdefghijklmnopqrst", 105 | }, 106 | wantErr: wantErr{ 107 | isErr: false, 108 | }, 109 | }, 110 | { 111 | name: "Nameが21文字の場合、エラーを返す", 112 | args: args{ 113 | name: "abcdefghijklmnopqrstu", 114 | }, 115 | wantErr: wantErr{ 116 | isErr: true, 117 | err: invalidPropertyErr, 118 | }, 119 | }, 120 | { 121 | name: "Nameが空文字の場合、エラーを返す", 122 | args: args{ 123 | name: "", 124 | }, 125 | wantErr: wantErr{ 126 | isErr: true, 127 | err: invalidPropertyErr, 128 | }, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | err := ValidateProgrammingLang(tt.args.name) 134 | if (err != nil) != tt.wantErr.isErr { 135 | t.Errorf("ValidateProgrammingLang() error = %v, wantErr %v", err, tt.wantErr.isErr) 136 | } 137 | 138 | if tt.wantErr.isErr { 139 | if err.Error() != tt.wantErr.err.Error() { 140 | t.Errorf("ValidateProgrammingLang() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error()) 141 | } 142 | } 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /server/infra/dao/mock/programming_lang_repository_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: domain/repository/programming_lang_repository.go 3 | 4 | // Package mock_repository is a generated GoMock package. 5 | package mock_repository 6 | 7 | import ( 8 | context "context" 9 | model "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 10 | gomock "github.com/golang/mock/gomock" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockProgrammingLangRepository is a mock of ProgrammingLangRepository interface 15 | type MockProgrammingLangRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockProgrammingLangRepositoryMockRecorder 18 | } 19 | 20 | // MockProgrammingLangRepositoryMockRecorder is the mock recorder for MockProgrammingLangRepository 21 | type MockProgrammingLangRepositoryMockRecorder struct { 22 | mock *MockProgrammingLangRepository 23 | } 24 | 25 | // NewMockProgrammingLangRepository creates a new mock instance 26 | func NewMockProgrammingLangRepository(ctrl *gomock.Controller) *MockProgrammingLangRepository { 27 | mock := &MockProgrammingLangRepository{ctrl: ctrl} 28 | mock.recorder = &MockProgrammingLangRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockProgrammingLangRepository) EXPECT() *MockProgrammingLangRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // List mocks base method 38 | func (m *MockProgrammingLangRepository) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) { 39 | ret := m.ctrl.Call(m, "List", ctx, limit) 40 | ret0, _ := ret[0].([]*model.ProgrammingLang) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // List indicates an expected call of List 46 | func (mr *MockProgrammingLangRepositoryMockRecorder) List(ctx, limit interface{}) *gomock.Call { 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProgrammingLangRepository)(nil).List), ctx, limit) 48 | } 49 | 50 | // Create mocks base method 51 | func (m *MockProgrammingLangRepository) Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) { 52 | ret := m.ctrl.Call(m, "Create", ctx, lang) 53 | ret0, _ := ret[0].(*model.ProgrammingLang) 54 | ret1, _ := ret[1].(error) 55 | return ret0, ret1 56 | } 57 | 58 | // Create indicates an expected call of Create 59 | func (mr *MockProgrammingLangRepositoryMockRecorder) Create(ctx, lang interface{}) *gomock.Call { 60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Create), ctx, lang) 61 | } 62 | 63 | // Read mocks base method 64 | func (m *MockProgrammingLangRepository) Read(ctx context.Context, id int) (*model.ProgrammingLang, error) { 65 | ret := m.ctrl.Call(m, "Read", ctx, id) 66 | ret0, _ := ret[0].(*model.ProgrammingLang) 67 | ret1, _ := ret[1].(error) 68 | return ret0, ret1 69 | } 70 | 71 | // Read indicates an expected call of Read 72 | func (mr *MockProgrammingLangRepositoryMockRecorder) Read(ctx, id interface{}) *gomock.Call { 73 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Read), ctx, id) 74 | } 75 | 76 | // ReadByName mocks base method 77 | func (m *MockProgrammingLangRepository) ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error) { 78 | ret := m.ctrl.Call(m, "ReadByName", ctx, name) 79 | ret0, _ := ret[0].(*model.ProgrammingLang) 80 | ret1, _ := ret[1].(error) 81 | return ret0, ret1 82 | } 83 | 84 | // ReadByName indicates an expected call of ReadByName 85 | func (mr *MockProgrammingLangRepositoryMockRecorder) ReadByName(ctx, name interface{}) *gomock.Call { 86 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadByName", reflect.TypeOf((*MockProgrammingLangRepository)(nil).ReadByName), ctx, name) 87 | } 88 | 89 | // Update mocks base method 90 | func (m *MockProgrammingLangRepository) Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) { 91 | ret := m.ctrl.Call(m, "Update", ctx, lang) 92 | ret0, _ := ret[0].(*model.ProgrammingLang) 93 | ret1, _ := ret[1].(error) 94 | return ret0, ret1 95 | } 96 | 97 | // Update indicates an expected call of Update 98 | func (mr *MockProgrammingLangRepositoryMockRecorder) Update(ctx, lang interface{}) *gomock.Call { 99 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Update), ctx, lang) 100 | } 101 | 102 | // Delete mocks base method 103 | func (m *MockProgrammingLangRepository) Delete(ctx context.Context, id int) error { 104 | ret := m.ctrl.Call(m, "Delete", ctx, id) 105 | ret0, _ := ret[0].(error) 106 | return ret0 107 | } 108 | 109 | // Delete indicates an expected call of Delete 110 | func (mr *MockProgrammingLangRepositoryMockRecorder) Delete(ctx, id interface{}) *gomock.Call { 111 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProgrammingLangRepository)(nil).Delete), ctx, id) 112 | } 113 | -------------------------------------------------------------------------------- /server/infra/dao/rdb/const.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | // rdb packageの定数。 4 | const ( 5 | TotalAffected = "Total Affected" 6 | ) 7 | -------------------------------------------------------------------------------- /server/infra/dao/rdb/program_lang_dao.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // ProgrammingLangDAO は、ProgrammingLangのDAO。 13 | type ProgrammingLangDAO struct { 14 | SQLManager SQLManagerInterface 15 | } 16 | 17 | // NewProgrammingLangDAO は、ProgrammingLangDAO生成して返す。 18 | func NewProgrammingLangDAO(manager SQLManagerInterface) repository.ProgrammingLangRepository { 19 | fmt.Printf("NewProgrammingLangDAO") 20 | 21 | return &ProgrammingLangDAO{ 22 | SQLManager: manager, 23 | } 24 | } 25 | 26 | // ErrorMsg は、エラー文を生成し、返す。 27 | func (dao *ProgrammingLangDAO) ErrorMsg(method string, err error) error { 28 | return &model.DBError{ 29 | ModelName: model.ModelNameProgrammingLang, 30 | DBMethod: method, 31 | Detail: err.Error(), 32 | } 33 | } 34 | 35 | // Create は、レコードを1件生成する。 36 | func (dao *ProgrammingLangDAO) Create(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) { 37 | query := "INSERT INTO programming_langs (name, feature, created_at, updated_at) VALUES (?, ?, ?, ?)" 38 | stmt, err := dao.SQLManager.PrepareContext(ctx, query) 39 | if err != nil { 40 | return nil, dao.ErrorMsg(model.DBMethodCreate, err) 41 | } 42 | defer stmt.Close() 43 | 44 | result, err := stmt.ExecContext(ctx, lang.Name, lang.Feature, lang.CreatedAt, lang.UpdatedAt) 45 | if err != nil { 46 | return nil, dao.ErrorMsg(model.DBMethodCreate, err) 47 | } 48 | 49 | affect, err := result.RowsAffected() 50 | if affect != 1 { 51 | err = fmt.Errorf("%s: %d ", TotalAffected, affect) 52 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err) 53 | } 54 | 55 | id, err := result.LastInsertId() 56 | if err != nil { 57 | return nil, dao.ErrorMsg(model.DBMethodCreate, err) 58 | } 59 | 60 | lang.ID = int(id) 61 | 62 | return lang, nil 63 | } 64 | 65 | // List は、レコードの一覧を取得して返す。 66 | func (dao *ProgrammingLangDAO) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) { 67 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs ORDER BY name LIMIT ?" 68 | langSlice, err := dao.list(ctx, query, limit) 69 | 70 | if len(langSlice) == 0 { 71 | return nil, &model.NoSuchDataError{ 72 | ModelName: model.ModelNameProgrammingLang, 73 | } 74 | } 75 | 76 | if err != nil { 77 | return nil, errors.WithStack(err) 78 | } 79 | 80 | return langSlice, nil 81 | } 82 | 83 | // Read は、レコードを1件取得して返す。 84 | func (dao *ProgrammingLangDAO) Read(ctx context.Context, id int) (*model.ProgrammingLang, error) { 85 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE ID=?" 86 | 87 | langSlice, err := dao.list(ctx, query, id) 88 | 89 | if len(langSlice) == 0 { 90 | return nil, &model.NoSuchDataError{ 91 | ID: id, 92 | ModelName: model.ModelNameProgrammingLang, 93 | } 94 | } 95 | 96 | if err != nil { 97 | return nil, errors.WithStack(err) 98 | } 99 | 100 | return langSlice[0], nil 101 | } 102 | 103 | // ReadByName は、指定したNameを保持するレコードを1返す。 104 | func (dao *ProgrammingLangDAO) ReadByName(ctx context.Context, name string) (*model.ProgrammingLang, error) { 105 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE name=? ORDER BY name LIMIT ?" 106 | langSlice, err := dao.list(ctx, query, name, 1) 107 | 108 | if len(langSlice) == 0 { 109 | return nil, &model.NoSuchDataError{ 110 | Name: name, 111 | ModelName: model.ModelNameProgrammingLang, 112 | } 113 | } 114 | 115 | if err != nil { 116 | return nil, errors.WithStack(err) 117 | } 118 | 119 | return langSlice[0], nil 120 | } 121 | 122 | // list は、レコードの一覧を取得して返す。 123 | func (dao *ProgrammingLangDAO) list(ctx context.Context, query string, args ...interface{}) ([]*model.ProgrammingLang, error) { 124 | stmt, err := dao.SQLManager.PrepareContext(ctx, query) 125 | if err != nil { 126 | return nil, dao.ErrorMsg(model.DBMethodList, err) 127 | } 128 | defer stmt.Close() 129 | 130 | rows, err := stmt.QueryContext(ctx, args...) 131 | if err != nil { 132 | return nil, dao.ErrorMsg(model.DBMethodList, err) 133 | } 134 | defer rows.Close() 135 | 136 | langSlice := make([]*model.ProgrammingLang, 0) 137 | for rows.Next() { 138 | lang := &model.ProgrammingLang{} 139 | 140 | err = rows.Scan( 141 | &lang.ID, 142 | &lang.Name, 143 | &lang.Feature, 144 | &lang.CreatedAt, 145 | &lang.UpdatedAt, 146 | ) 147 | 148 | if err != nil { 149 | return nil, dao.ErrorMsg(model.DBMethodList, err) 150 | } 151 | 152 | langSlice = append(langSlice, lang) 153 | } 154 | 155 | return langSlice, nil 156 | } 157 | 158 | // Update は、レコードを1件更新する。 159 | func (dao *ProgrammingLangDAO) Update(ctx context.Context, lang *model.ProgrammingLang) (*model.ProgrammingLang, error) { 160 | query := "UPDATE programming_langs SET name=?, feature=?, created_at=?, updated_at=? WHERE id=?" 161 | 162 | stmt, err := dao.SQLManager.PrepareContext(ctx, query) 163 | defer stmt.Close() 164 | 165 | if err != nil { 166 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err) 167 | } 168 | 169 | result, err := stmt.ExecContext(ctx, lang.Name, lang.Feature, lang.CreatedAt, lang.UpdatedAt, lang.ID) 170 | if err != nil { 171 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err) 172 | } 173 | 174 | affect, err := result.RowsAffected() 175 | if affect != 1 { 176 | err = fmt.Errorf("%s: %d ", TotalAffected, affect) 177 | return nil, dao.ErrorMsg(model.DBMethodUpdate, err) 178 | } 179 | 180 | return lang, nil 181 | } 182 | 183 | // Delete は、レコードを1件削除する。 184 | func (dao *ProgrammingLangDAO) Delete(ctx context.Context, id int) error { 185 | query := "DELETE FROM programming_langs WHERE id=?" 186 | 187 | stmt, err := dao.SQLManager.PrepareContext(ctx, query) 188 | if err != nil { 189 | return dao.ErrorMsg(model.DBMethodDelete, err) 190 | } 191 | defer stmt.Close() 192 | 193 | result, err := stmt.ExecContext(ctx, id) 194 | if err != nil { 195 | return dao.ErrorMsg(model.DBMethodDelete, err) 196 | } 197 | 198 | affect, err := result.RowsAffected() 199 | if err != nil { 200 | return dao.ErrorMsg(model.DBMethodDelete, err) 201 | } 202 | if affect != 1 { 203 | err = fmt.Errorf("%s: %d ", TotalAffected, affect) 204 | return dao.ErrorMsg(model.DBMethodDelete, err) 205 | } 206 | 207 | return nil 208 | } 209 | -------------------------------------------------------------------------------- /server/infra/dao/rdb/program_lang_dao_test.go: -------------------------------------------------------------------------------- 1 | package rdb_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "reflect" 8 | "testing" 9 | "time" 10 | 11 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 12 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository" 13 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/dao/rdb" 14 | "gopkg.in/DATA-DOG/go-sqlmock.v1" 15 | ) 16 | 17 | func TestNewProgrammingLangDAO(t *testing.T) { 18 | type args struct { 19 | ctx context.Context 20 | manager rdb.SQLManagerInterface 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | want repository.ProgrammingLangRepository 26 | }{ 27 | { 28 | name: "適切な引数を与えると、ProgrammingLangDAOが返されること", 29 | args: args{ 30 | ctx: context.Background(), 31 | manager: &rdb.SQLManager{ 32 | Conn: &sql.DB{}, 33 | }, 34 | }, 35 | }, 36 | } 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | if got := rdb.NewProgrammingLangDAO(tt.args.manager); got == nil { 40 | t.Errorf("NewProgrammingLangDAO() = nil") 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestProgrammingLangDAO_Create(t *testing.T) { 47 | // sqlmockの設定を行う 48 | db, mock, err := sqlmock.New() 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | defer db.Close() 54 | 55 | type fields struct { 56 | SQLManager rdb.SQLManagerInterface 57 | } 58 | type args struct { 59 | ctx context.Context 60 | lang *model.ProgrammingLang 61 | } 62 | tests := []struct { 63 | name string 64 | fields fields 65 | args args 66 | want *model.ProgrammingLang 67 | rowAffected int64 68 | wantErr bool 69 | }{ 70 | { 71 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと", 72 | fields: fields{ 73 | SQLManager: &rdb.SQLManager{Conn: db}, 74 | }, 75 | args: args{ 76 | ctx: context.Background(), 77 | lang: &model.ProgrammingLang{ 78 | Name: model.TestName, 79 | Feature: model.TestFeature, 80 | CreatedAt: model.GetTestTime(time.September, 1), 81 | UpdatedAt: model.GetTestTime(time.September, 2), 82 | }, 83 | }, 84 | want: &model.ProgrammingLang{ 85 | ID: 1, 86 | Name: model.TestName, 87 | Feature: model.TestFeature, 88 | CreatedAt: model.GetTestTime(time.September, 1), 89 | UpdatedAt: model.GetTestTime(time.September, 2), 90 | }, 91 | rowAffected: 1, 92 | wantErr: false, 93 | }, 94 | { 95 | name: "Nameのみを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと", 96 | fields: fields{ 97 | SQLManager: &rdb.SQLManager{Conn: db}, 98 | }, 99 | args: args{ 100 | ctx: context.Background(), 101 | lang: &model.ProgrammingLang{ 102 | Name: model.TestName, 103 | CreatedAt: model.GetTestTime(time.September, 1), 104 | UpdatedAt: model.GetTestTime(time.September, 2), 105 | }, 106 | }, 107 | want: &model.ProgrammingLang{ 108 | ID: 1, 109 | Name: model.TestName, 110 | CreatedAt: model.GetTestTime(time.September, 1), 111 | UpdatedAt: model.GetTestTime(time.September, 2), 112 | }, 113 | rowAffected: 1, 114 | wantErr: false, 115 | }, 116 | { 117 | name: "RowAffectedが1以外の場合、エラーを返すこと", 118 | fields: fields{ 119 | SQLManager: &rdb.SQLManager{Conn: db}, 120 | }, 121 | args: args{ 122 | ctx: context.Background(), 123 | lang: &model.ProgrammingLang{ 124 | Name: model.TestName, 125 | CreatedAt: model.GetTestTime(time.September, 1), 126 | UpdatedAt: model.GetTestTime(time.September, 2), 127 | }, 128 | }, 129 | want: nil, 130 | rowAffected: 2, 131 | wantErr: true, 132 | }, 133 | { 134 | name: "空のProgrammingLangを与えられた場合、エラーを返すこと", 135 | fields: fields{ 136 | SQLManager: &rdb.SQLManager{Conn: db}, 137 | }, 138 | args: args{ 139 | ctx: context.Background(), 140 | lang: &model.ProgrammingLang{}, 141 | }, 142 | want: nil, 143 | rowAffected: 0, 144 | wantErr: true, 145 | }, 146 | } 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | query := "INSERT INTO programming_langs" 150 | prep := mock.ExpectPrepare(query) 151 | 152 | if tt.rowAffected == 0 { 153 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt).WillReturnError(fmt.Errorf(model.TestDBSomeErr)) 154 | } else { 155 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt).WillReturnResult(sqlmock.NewResult(1, tt.rowAffected)) 156 | } 157 | 158 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager) 159 | 160 | got, err := dao.Create(tt.args.ctx, tt.args.lang) 161 | if (err != nil) != tt.wantErr { 162 | t.Errorf("ProgrammingLangDAO.Create() error = %v, wantErr %v", err, tt.wantErr) 163 | return 164 | } 165 | if !reflect.DeepEqual(got, tt.want) { 166 | t.Errorf("ProgrammingLangDAO.Create() = %v, want %v", got, tt.want) 167 | } 168 | }) 169 | } 170 | } 171 | 172 | func TestProgrammingLangDAO_List(t *testing.T) { 173 | testName1 := fmt.Sprintf("%s1", model.TestName) 174 | testName2 := fmt.Sprintf("%s2", model.TestName) 175 | testName3 := fmt.Sprintf("%s3", model.TestName) 176 | 177 | testFeature1 := fmt.Sprintf("%s1", model.TestFeature) 178 | testFeature2 := fmt.Sprintf("%s2", model.TestFeature) 179 | testFeature3 := fmt.Sprintf("%s3", model.TestFeature) 180 | 181 | // sqlmockの設定を行う 182 | db, mock, err := sqlmock.New() 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | 187 | defer db.Close() 188 | 189 | type fields struct { 190 | SQLManager rdb.SQLManagerInterface 191 | } 192 | type args struct { 193 | ctx context.Context 194 | limit int 195 | } 196 | 197 | tests := []struct { 198 | name string 199 | fields fields 200 | args args 201 | want []*model.ProgrammingLang 202 | wantErr bool 203 | }{ 204 | { 205 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと", 206 | fields: fields{ 207 | SQLManager: &rdb.SQLManager{Conn: db}, 208 | }, 209 | args: args{ 210 | ctx: context.Background(), 211 | limit: 100, 212 | }, 213 | want: []*model.ProgrammingLang{ 214 | { 215 | ID: 1, 216 | Name: testName1, 217 | Feature: testFeature1, 218 | CreatedAt: model.GetTestTime(time.September, 1), 219 | UpdatedAt: model.GetTestTime(time.September, 2), 220 | }, 221 | { 222 | ID: 2, 223 | Name: testName2, 224 | Feature: testFeature2, 225 | CreatedAt: model.GetTestTime(time.September, 3), 226 | UpdatedAt: model.GetTestTime(time.September, 4), 227 | }, 228 | { 229 | ID: 3, 230 | Name: testName3, 231 | Feature: testFeature3, 232 | CreatedAt: model.GetTestTime(time.September, 5), 233 | UpdatedAt: model.GetTestTime(time.September, 6), 234 | }, 235 | }, 236 | wantErr: false, 237 | }, 238 | { 239 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと", 240 | fields: fields{ 241 | SQLManager: &rdb.SQLManager{Conn: db}, 242 | }, 243 | args: args{ 244 | ctx: context.Background(), 245 | limit: 100, 246 | }, 247 | want: nil, 248 | wantErr: true, 249 | }, 250 | } 251 | for _, tt := range tests { 252 | t.Run(tt.name, func(t *testing.T) { 253 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs ORDER BY name LIMIT \\?" 254 | prep := mock.ExpectPrepare(query) 255 | 256 | if tt.wantErr { 257 | prep.ExpectQuery().WillReturnError(fmt.Errorf(model.TestDBSomeErr)) 258 | } else { 259 | rows := sqlmock.NewRows([]string{"id", "name", "feature", "created_at", "updated_at"}). 260 | AddRow(tt.want[0].ID, tt.want[0].Name, tt.want[0].Feature, tt.want[0].CreatedAt, tt.want[0].UpdatedAt). 261 | AddRow(tt.want[1].ID, tt.want[1].Name, tt.want[1].Feature, tt.want[1].CreatedAt, tt.want[1].UpdatedAt). 262 | AddRow(tt.want[2].ID, tt.want[2].Name, tt.want[2].Feature, tt.want[2].CreatedAt, tt.want[2].UpdatedAt) 263 | prep.ExpectQuery().WillReturnRows(rows) 264 | } 265 | 266 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager) 267 | 268 | got, err := dao.List(tt.args.ctx, tt.args.limit) 269 | if (err != nil) != tt.wantErr { 270 | t.Errorf("ProgrammingLangDAO.List() error = %v, wantErr %v", err, tt.wantErr) 271 | return 272 | } 273 | 274 | for i := range got { 275 | if !reflect.DeepEqual(got[i], tt.want[i]) { 276 | t.Errorf("ProgrammingLangDAO.List() = %v, want %v", got[i], tt.want[i]) 277 | } 278 | } 279 | }) 280 | } 281 | } 282 | 283 | func TestProgrammingLangDAO_Read(t *testing.T) { 284 | // sqlmockの設定を行う 285 | db, mock, err := sqlmock.New() 286 | if err != nil { 287 | t.Fatal(err) 288 | } 289 | 290 | defer db.Close() 291 | 292 | type fields struct { 293 | SQLManager rdb.SQLManagerInterface 294 | } 295 | type args struct { 296 | ctx context.Context 297 | id int 298 | } 299 | tests := []struct { 300 | name string 301 | fields fields 302 | args args 303 | want *model.ProgrammingLang 304 | wantErr bool 305 | }{ 306 | { 307 | name: "IDで指定したProgrammingLangが存在する場合、ProgrammingLangを1件返すこと", 308 | fields: fields{ 309 | SQLManager: &rdb.SQLManager{Conn: db}, 310 | }, 311 | args: args{ 312 | ctx: context.Background(), 313 | id: 1, 314 | }, 315 | want: &model.ProgrammingLang{ 316 | ID: 1, 317 | Name: model.TestName, 318 | Feature: model.TestFeature, 319 | CreatedAt: model.GetTestTime(time.September, 1), 320 | UpdatedAt: model.GetTestTime(time.September, 2), 321 | }, 322 | wantErr: false, 323 | }, 324 | { 325 | name: "IDで指定したProgrammingLangが存在しない場合、エラー返すこと", 326 | fields: fields{ 327 | SQLManager: &rdb.SQLManager{Conn: db}, 328 | }, 329 | args: args{ 330 | ctx: context.Background(), 331 | id: 2, 332 | }, 333 | want: nil, 334 | wantErr: true, 335 | }, 336 | } 337 | for _, tt := range tests { 338 | t.Run(tt.name, func(t *testing.T) { 339 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE ID=\\?" 340 | prep := mock.ExpectPrepare(query) 341 | 342 | if tt.wantErr { 343 | prep.ExpectQuery().WillReturnError(fmt.Errorf(model.TestDBSomeErr)) 344 | } else { 345 | rows := sqlmock.NewRows([]string{"id", "name", "feature", "created_at", "updated_at"}). 346 | AddRow(tt.want.ID, tt.want.Name, tt.want.Feature, tt.want.CreatedAt, tt.want.UpdatedAt) 347 | prep.ExpectQuery().WillReturnRows(rows) 348 | } 349 | 350 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager) 351 | 352 | got, err := dao.Read(tt.args.ctx, tt.args.id) 353 | if (err != nil) != tt.wantErr { 354 | t.Errorf("ProgrammingLangDAO.Read() error = %v, wantErr %v", err, tt.wantErr) 355 | return 356 | } 357 | if !reflect.DeepEqual(got, tt.want) { 358 | t.Errorf("ProgrammingLangDAO.Read() = %v, want %v", got, tt.want) 359 | } 360 | }) 361 | } 362 | } 363 | 364 | func TestProgrammingLangDAO_ReadByName(t *testing.T) { 365 | // sqlmockの設定を行う 366 | db, mock, err := sqlmock.New() 367 | if err != nil { 368 | t.Fatal(err) 369 | } 370 | 371 | defer db.Close() 372 | 373 | type fields struct { 374 | SQLManager rdb.SQLManagerInterface 375 | } 376 | type args struct { 377 | ctx context.Context 378 | name string 379 | } 380 | tests := []struct { 381 | name string 382 | fields fields 383 | args args 384 | want *model.ProgrammingLang 385 | wantErr bool 386 | }{ 387 | { 388 | name: "IDで指定したProgrammingLangが1件存在する場合、ProgrammingLangを1件返すこと", 389 | fields: fields{ 390 | SQLManager: &rdb.SQLManager{Conn: db}, 391 | }, 392 | args: args{ 393 | ctx: context.Background(), 394 | name: model.TestName, 395 | }, 396 | want: &model.ProgrammingLang{ 397 | ID: 1, 398 | Name: model.TestName, 399 | Feature: model.TestFeature, 400 | CreatedAt: model.GetTestTime(time.September, 1), 401 | UpdatedAt: model.GetTestTime(time.September, 2), 402 | }, 403 | wantErr: false, 404 | }, 405 | { 406 | name: "Nameで指定したProgrammingLangが存在しない場合、エラー返すこと", 407 | fields: fields{ 408 | SQLManager: &rdb.SQLManager{Conn: db}, 409 | }, 410 | args: args{ 411 | ctx: context.Background(), 412 | name: "test", 413 | }, 414 | want: nil, 415 | wantErr: true, 416 | }, 417 | } 418 | for _, tt := range tests { 419 | t.Run(tt.name, func(t *testing.T) { 420 | query := "SELECT id, name, feature, created_at, updated_at FROM programming_langs WHERE name=\\? ORDER BY name LIMIT \\?" 421 | prep := mock.ExpectPrepare(query) 422 | 423 | if tt.wantErr { 424 | prep.ExpectQuery().WillReturnError(fmt.Errorf(model.TestDBSomeErr)) 425 | } else { 426 | rows := sqlmock.NewRows([]string{"id", "name", "feature", "created_at", "updated_at"}). 427 | AddRow(tt.want.ID, tt.want.Name, tt.want.Feature, tt.want.CreatedAt, tt.want.UpdatedAt) 428 | prep.ExpectQuery().WillReturnRows(rows) 429 | } 430 | 431 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager) 432 | 433 | got, err := dao.ReadByName(tt.args.ctx, tt.args.name) 434 | if (err != nil) != tt.wantErr { 435 | t.Errorf("ProgrammingLangDAO.ReadByName() error = %v, wantErr %v", err, tt.wantErr) 436 | return 437 | } 438 | if err == nil { 439 | if !reflect.DeepEqual(got, tt.want) { 440 | t.Errorf("ProgrammingLangDAO.ReadByName() = %v, want %v", got, tt.want) 441 | } 442 | } 443 | }) 444 | } 445 | } 446 | 447 | func TestProgrammingLangDAO_Update(t *testing.T) { 448 | // sqlmockの設定を行う 449 | db, mock, err := sqlmock.New() 450 | if err != nil { 451 | t.Fatal(err) 452 | } 453 | 454 | defer db.Close() 455 | 456 | type fields struct { 457 | SQLManager rdb.SQLManagerInterface 458 | } 459 | type args struct { 460 | ctx context.Context 461 | lang *model.ProgrammingLang 462 | } 463 | tests := []struct { 464 | name string 465 | fields fields 466 | args args 467 | want *model.ProgrammingLang 468 | rowAffected int64 469 | wantErr bool 470 | }{ 471 | { 472 | name: "NameとFeatureを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと", 473 | fields: fields{ 474 | SQLManager: &rdb.SQLManager{Conn: db}, 475 | }, 476 | args: args{ 477 | ctx: context.Background(), 478 | lang: &model.ProgrammingLang{ 479 | ID: 1, 480 | Name: model.TestName, 481 | Feature: model.TestFeature, 482 | CreatedAt: model.GetTestTime(time.September, 1), 483 | UpdatedAt: model.GetTestTime(time.September, 2), 484 | }, 485 | }, 486 | want: &model.ProgrammingLang{ 487 | ID: 1, 488 | Name: model.TestName, 489 | Feature: model.TestFeature, 490 | CreatedAt: model.GetTestTime(time.September, 1), 491 | UpdatedAt: model.GetTestTime(time.September, 2), 492 | }, 493 | rowAffected: 1, 494 | wantErr: false, 495 | }, 496 | { 497 | name: "Nameのみを保持するProgrammingLangを与えられた場合、IDを付与したProgrammingLangを返すこと", 498 | fields: fields{ 499 | SQLManager: &rdb.SQLManager{Conn: db}, 500 | }, 501 | args: args{ 502 | ctx: context.Background(), 503 | lang: &model.ProgrammingLang{ 504 | ID: 1, 505 | Name: model.TestName, 506 | CreatedAt: model.GetTestTime(time.September, 1), 507 | UpdatedAt: model.GetTestTime(time.September, 2), 508 | }, 509 | }, 510 | want: &model.ProgrammingLang{ 511 | ID: 1, 512 | Name: model.TestName, 513 | CreatedAt: model.GetTestTime(time.September, 1), 514 | UpdatedAt: model.GetTestTime(time.September, 2), 515 | }, 516 | rowAffected: 1, 517 | wantErr: false, 518 | }, 519 | { 520 | name: "RowAffectedが1以外の場合、エラーを返すこと", 521 | fields: fields{ 522 | SQLManager: &rdb.SQLManager{Conn: db}, 523 | }, 524 | args: args{ 525 | ctx: context.Background(), 526 | lang: &model.ProgrammingLang{ 527 | Name: model.TestName, 528 | CreatedAt: model.GetTestTime(time.September, 1), 529 | UpdatedAt: model.GetTestTime(time.September, 2), 530 | }, 531 | }, 532 | want: nil, 533 | rowAffected: 2, 534 | wantErr: true, 535 | }, 536 | { 537 | name: "空のProgrammingLangを与えられた場合、エラーを返すこと", 538 | fields: fields{ 539 | SQLManager: &rdb.SQLManager{Conn: db}, 540 | }, 541 | args: args{ 542 | ctx: context.Background(), 543 | lang: &model.ProgrammingLang{}, 544 | }, 545 | want: nil, 546 | rowAffected: 0, 547 | wantErr: true, 548 | }, 549 | } 550 | for _, tt := range tests { 551 | t.Run(tt.name, func(t *testing.T) { 552 | query := "UPDATE programming_langs SET name=\\?, feature=\\?, created_at=\\?, updated_at=\\? WHERE id=\\?" 553 | prep := mock.ExpectPrepare(query) 554 | 555 | if tt.wantErr { 556 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt, tt.args.lang.ID).WillReturnError(fmt.Errorf(model.TestDBSomeErr)) 557 | } else { 558 | prep.ExpectExec().WithArgs(tt.args.lang.Name, tt.args.lang.Feature, tt.args.lang.CreatedAt, tt.args.lang.UpdatedAt, tt.args.lang.ID).WillReturnResult(sqlmock.NewResult(1, tt.rowAffected)) 559 | } 560 | 561 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager) 562 | 563 | got, err := dao.Update(tt.args.ctx, tt.args.lang) 564 | if (err != nil) != tt.wantErr { 565 | t.Errorf("ProgrammingLangDAO.Update() error = %v, wantErr %v", err, tt.wantErr) 566 | return 567 | } 568 | if !reflect.DeepEqual(got, tt.want) { 569 | t.Errorf("ProgrammingLangDAO.Update() = %v, want %v", got, tt.want) 570 | } 571 | }) 572 | } 573 | } 574 | 575 | func TestProgrammingLangDAO_Delete(t *testing.T) { 576 | // sqlmockの設定を行う 577 | db, mock, err := sqlmock.New() 578 | if err != nil { 579 | t.Fatal(err) 580 | } 581 | 582 | defer db.Close() 583 | 584 | type fields struct { 585 | SQLManager rdb.SQLManagerInterface 586 | } 587 | type args struct { 588 | ctx context.Context 589 | id int 590 | } 591 | tests := []struct { 592 | name string 593 | fields fields 594 | args args 595 | rowAffected int64 596 | wantErr bool 597 | }{ 598 | { 599 | name: "適切なIDを与えると、エラーを返さないこと", 600 | fields: fields{ 601 | SQLManager: &rdb.SQLManager{Conn: db}, 602 | }, 603 | args: args{ 604 | ctx: context.Background(), 605 | id: 1, 606 | }, 607 | rowAffected: 1, 608 | wantErr: false, 609 | }, 610 | { 611 | name: "rowAffectedが1以外の場合、エラーを返すこと", 612 | fields: fields{ 613 | SQLManager: &rdb.SQLManager{Conn: db}, 614 | }, 615 | args: args{ 616 | ctx: context.Background(), 617 | id: 1, 618 | }, 619 | rowAffected: 2, 620 | wantErr: true, 621 | }, 622 | } 623 | for _, tt := range tests { 624 | t.Run(tt.name, func(t *testing.T) { 625 | query := "DELETE FROM programming_langs WHERE id=\\?" 626 | prep := mock.ExpectPrepare(query) 627 | 628 | if tt.wantErr { 629 | prep.ExpectExec().WithArgs(tt.args.id).WillReturnError(fmt.Errorf(model.TestDBSomeErr)) 630 | } else { 631 | prep.ExpectExec().WithArgs(tt.args.id).WillReturnResult(sqlmock.NewResult(1, tt.rowAffected)) 632 | } 633 | 634 | dao := rdb.NewProgrammingLangDAO(tt.fields.SQLManager) 635 | 636 | if err := dao.Delete(tt.args.ctx, tt.args.id); (err != nil) != tt.wantErr { 637 | t.Errorf("ProgrammingLangDAO.Delete() error = %v, wantErr %v", err, tt.wantErr) 638 | return 639 | } 640 | }) 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /server/infra/dao/rdb/sql_manager.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | // SQL Driver。 7 | _ "github.com/go-sql-driver/mysql" 8 | ) 9 | 10 | // SQLManager は、SQLを管理する。 11 | type SQLManager struct { 12 | Conn *sql.DB 13 | } 14 | 15 | // NewSQLManager は、SQLManagerを生成し、返す。 16 | func NewSQLManager() SQLManagerInterface { 17 | conn, err := sql.Open("mysql", "root:@tcp(db:3306)/sample?charset=utf8mb4&parseTime=True") 18 | if err != nil { 19 | panic(err.Error()) 20 | } 21 | 22 | return &SQLManager{ 23 | Conn: conn, 24 | } 25 | } 26 | 27 | // Exec は、SQL実行する。 28 | func (s *SQLManager) Exec(query string, args ...interface{}) (Result, error) { 29 | return s.Conn.Exec(query, args...) 30 | } 31 | 32 | // ExecContext は、SQL実行する。 33 | func (s *SQLManager) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) { 34 | return s.Conn.ExecContext(ctx, query, args...) 35 | } 36 | 37 | // Query は、rowを返すようなQueryを実行する。 38 | func (s *SQLManager) Query(query string, args ...interface{}) (Rows, error) { 39 | rows, err := s.Conn.Query(query, args...) 40 | if err != nil { 41 | return nil, err 42 | } 43 | row := &SQLRowManager{ 44 | Rows: rows, 45 | } 46 | return row, nil 47 | } 48 | 49 | // QueryContext は、rowを返すようなQueryを実行する。 50 | func (s *SQLManager) QueryContext(ctx context.Context, query string, args ...interface{}) (Rows, error) { 51 | rows, err := s.Conn.Query(query, args...) 52 | if err != nil { 53 | return nil, err 54 | } 55 | row := &SQLRowManager{ 56 | Rows: rows, 57 | } 58 | return row, nil 59 | } 60 | 61 | // Prepare は、後でQueryやExecを行うために、準備された状態にする。 62 | func (s *SQLManager) Prepare(query string) (*sql.Stmt, error) { 63 | return s.Conn.Prepare(query) 64 | } 65 | 66 | // PrepareContext は、後でQueryやExecを行うために、準備された状態にする。 67 | func (s *SQLManager) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { 68 | return s.Conn.PrepareContext(ctx, query) 69 | } 70 | 71 | // SQLRowManager は、Rowを管理する。 72 | type SQLRowManager struct { 73 | Rows *sql.Rows 74 | } 75 | 76 | // Scan は、destに現在読み込んでいるrowのcolumnsをコピーする。 77 | func (r SQLRowManager) Scan(dest ...interface{}) error { 78 | return r.Rows.Scan(dest...) 79 | } 80 | 81 | // Next は、Scanによって読み込まれる次のrowの結果を準備する。 82 | func (r SQLRowManager) Next() bool { 83 | return r.Rows.Next() 84 | } 85 | 86 | // Close は、Rowsを終了する。 87 | func (r SQLRowManager) Close() error { 88 | return r.Rows.Close() 89 | } 90 | -------------------------------------------------------------------------------- /server/infra/dao/rdb/sql_manager_interface.go: -------------------------------------------------------------------------------- 1 | package rdb 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | ) 7 | 8 | // SQLManagerInterface は、SQLManagerのinterface。 9 | type SQLManagerInterface interface { 10 | Executor 11 | Preparer 12 | Queryer 13 | } 14 | 15 | // DBに関するInterfaceの定義。 16 | type ( 17 | // Executor は、Rowが返ってこないQueryを実行するためのメソッドを集めたinterface。 18 | Executor interface { 19 | // Exec は、Rowが返ってこないQueryを実行する 20 | Exec(query string, args ...interface{}) (Result, error) 21 | // ExecContext は、Rowが返ってこないQueryを実行する 22 | ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) 23 | } 24 | 25 | // Preparer は、後でQueryやExecを行うために、準備された状態にするメソッドを集めたinterface。 26 | Preparer interface { 27 | // Prepare は、後でQueryやExecを行うために、準備された状態にする。 28 | // callerは、準備された状態が必要ない場合には、Closeを呼び出す必要がある。 29 | Prepare(query string) (*sql.Stmt, error) 30 | // PrepareContext は、後でQueryやExecを行うために、準備された状態にする。 31 | // callerは、準備された状態が必要ない場合には、Closeを呼び出す必要がある。 32 | // 引数で渡されたcontextは、状態の準備に使用するのであって、状態の実行に使用するのではない。 33 | PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) 34 | } 35 | 36 | // Queryer は、rowを返すようなQueryを実行するメソッドを集めたinterface。 37 | Queryer interface { 38 | // Query は、rowを返すようなQueryを実行する。SELECTが典型的な例。 39 | Query(query string, args ...interface{}) (Rows, error) 40 | // QueryContext は、rowを返すようなQueryを実行する。SELECTが典型的な例。 41 | QueryContext(ctx context.Context, query string, args ...interface{}) (Rows, error) 42 | } 43 | 44 | // Row は、1行を選択するQueryRowの結果を表す。 45 | Row interface { 46 | // Scan は、destに現在読み込んでいるrowのcolumnsをコピーする。 47 | Scan(...interface{}) error 48 | } 49 | 50 | // Rows は、Queryの結果を表す。 51 | Rows interface { 52 | Row 53 | // Next は、Scanによって読み込まれる次のrowの結果を準備する。 54 | // Nextが成功した場合には、trueを返して、失敗した場合にはfalseを返す。 55 | Next() bool 56 | // Close は、Rowsを終了する。 57 | // Nextが呼ばれて、falseが返ってきて結果がそれ以上ない場合は、Rowsは自動でCloseする。 58 | // Errの結果を確認するにはそれで十分である。 59 | // Closeを使うと冪等になるし、Errの結果に影響を受けなくなる。 60 | Close() error 61 | } 62 | 63 | // Result は、DBからのレスポンスを扱うメソッドを集めたinterface。 64 | Result interface { 65 | // LastInsertId は、DBのレスポンスによって生成された数字を返す。 66 | LastInsertId() (int64, error) 67 | // RowsAffected は、update、insert、deleteで影響を受けたrowの数を返す。 68 | RowsAffected() (int64, error) 69 | } 70 | ) 71 | -------------------------------------------------------------------------------- /server/infra/router/router.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/SekiguchiKai/clean-architecture-with-go/server/adapter/api" 5 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/dao/rdb" 6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // G は、ginのフレームワークのインスタンス。 11 | var G *gin.Engine 12 | 13 | // init は、アプリケーションの初期設定を行う。 14 | func init() { 15 | g := gin.New() 16 | apiV1 := g.Group("/v1") 17 | 18 | sqlM := rdb.NewSQLManager() 19 | langAPI := initProgrammingLang(sqlM) 20 | langAPI.InitAPI(apiV1) 21 | 22 | G = g 23 | } 24 | 25 | // initProgrammingLang は、ProgrammingLangに関する初期設定を行う。 26 | func initProgrammingLang(sqlM rdb.SQLManagerInterface) *api.ProgrammingLangAPI { 27 | rep := rdb.NewProgrammingLangDAO(sqlM) 28 | u := usecase.NewProgrammingLangUseCase(rep) 29 | api := api.NewProgrammingLangAPI(u) 30 | return api 31 | } 32 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/router" 5 | ) 6 | 7 | func main() { 8 | if err := router.G.Run(":8080"); err != nil { 9 | panic(err.Error()) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/usecase/input/programming_lang_input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 7 | ) 8 | 9 | // ProgrammingLangInputPort は、ProgrammingLangのInputPort。 10 | type ProgrammingLangInputPort interface { 11 | List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) 12 | Get(ctx context.Context, id int) (*model.ProgrammingLang, error) 13 | Create(ctx context.Context, param *model.ProgrammingLang) (*model.ProgrammingLang, error) 14 | Update(ctx context.Context, id int, param *model.ProgrammingLang) (*model.ProgrammingLang, error) 15 | Delete(ctx context.Context, id int) error 16 | } 17 | -------------------------------------------------------------------------------- /server/usecase/mock/programming_lang_input_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: usecase/input/programming_lang_input.go 3 | 4 | // Package mock_input is a generated GoMock package. 5 | package mock_input 6 | 7 | import ( 8 | context "context" 9 | model "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 10 | gomock "github.com/golang/mock/gomock" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockProgrammingLangInputPort is a mock of ProgrammingLangInputPort interface 15 | type MockProgrammingLangInputPort struct { 16 | ctrl *gomock.Controller 17 | recorder *MockProgrammingLangInputPortMockRecorder 18 | } 19 | 20 | // MockProgrammingLangInputPortMockRecorder is the mock recorder for MockProgrammingLangInputPort 21 | type MockProgrammingLangInputPortMockRecorder struct { 22 | mock *MockProgrammingLangInputPort 23 | } 24 | 25 | // NewMockProgrammingLangInputPort creates a new mock instance 26 | func NewMockProgrammingLangInputPort(ctrl *gomock.Controller) *MockProgrammingLangInputPort { 27 | mock := &MockProgrammingLangInputPort{ctrl: ctrl} 28 | mock.recorder = &MockProgrammingLangInputPortMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockProgrammingLangInputPort) EXPECT() *MockProgrammingLangInputPortMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // List mocks base method 38 | func (m *MockProgrammingLangInputPort) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) { 39 | ret := m.ctrl.Call(m, "List", ctx, limit) 40 | ret0, _ := ret[0].([]*model.ProgrammingLang) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // List indicates an expected call of List 46 | func (mr *MockProgrammingLangInputPortMockRecorder) List(ctx, limit interface{}) *gomock.Call { 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).List), ctx, limit) 48 | } 49 | 50 | // Get mocks base method 51 | func (m *MockProgrammingLangInputPort) Get(ctx context.Context, id int) (*model.ProgrammingLang, error) { 52 | ret := m.ctrl.Call(m, "Get", ctx, id) 53 | ret0, _ := ret[0].(*model.ProgrammingLang) 54 | ret1, _ := ret[1].(error) 55 | return ret0, ret1 56 | } 57 | 58 | // Get indicates an expected call of Get 59 | func (mr *MockProgrammingLangInputPortMockRecorder) Get(ctx, id interface{}) *gomock.Call { 60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Get), ctx, id) 61 | } 62 | 63 | // Create mocks base method 64 | func (m *MockProgrammingLangInputPort) Create(ctx context.Context, param *model.ProgrammingLang) (*model.ProgrammingLang, error) { 65 | ret := m.ctrl.Call(m, "Create", ctx, param) 66 | ret0, _ := ret[0].(*model.ProgrammingLang) 67 | ret1, _ := ret[1].(error) 68 | return ret0, ret1 69 | } 70 | 71 | // Create indicates an expected call of Create 72 | func (mr *MockProgrammingLangInputPortMockRecorder) Create(ctx, param interface{}) *gomock.Call { 73 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Create), ctx, param) 74 | } 75 | 76 | // Update mocks base method 77 | func (m *MockProgrammingLangInputPort) Update(ctx context.Context, id int, param *model.ProgrammingLang) (*model.ProgrammingLang, error) { 78 | ret := m.ctrl.Call(m, "Update", ctx, id, param) 79 | ret0, _ := ret[0].(*model.ProgrammingLang) 80 | ret1, _ := ret[1].(error) 81 | return ret0, ret1 82 | } 83 | 84 | // Update indicates an expected call of Update 85 | func (mr *MockProgrammingLangInputPortMockRecorder) Update(ctx, id, param interface{}) *gomock.Call { 86 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Update), ctx, id, param) 87 | } 88 | 89 | // Delete mocks base method 90 | func (m *MockProgrammingLangInputPort) Delete(ctx context.Context, id int) error { 91 | ret := m.ctrl.Call(m, "Delete", ctx, id) 92 | ret0, _ := ret[0].(error) 93 | return ret0 94 | } 95 | 96 | // Delete indicates an expected call of Delete 97 | func (mr *MockProgrammingLangInputPortMockRecorder) Delete(ctx, id interface{}) *gomock.Call { 98 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProgrammingLangInputPort)(nil).Delete), ctx, id) 99 | } 100 | -------------------------------------------------------------------------------- /server/usecase/program_lang_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository" 9 | "github.com/SekiguchiKai/clean-architecture-with-go/server/usecase/input" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // ProgrammingLangUseCase は、ProgrammingLangのUseCase。 14 | type ProgrammingLangUseCase struct { 15 | Repo repository.ProgrammingLangRepository 16 | } 17 | 18 | // NewProgrammingLangUseCase は、ProgrammingLangUseCaseを生成し、返す。 19 | func NewProgrammingLangUseCase(repo repository.ProgrammingLangRepository) input.ProgrammingLangInputPort { 20 | return &ProgrammingLangUseCase{ 21 | Repo: repo, 22 | } 23 | } 24 | 25 | // List は、ProgrammingLangの一覧を返す。 26 | func (u *ProgrammingLangUseCase) List(ctx context.Context, limit int) ([]*model.ProgrammingLang, error) { 27 | return u.Repo.List(ctx, limit) 28 | } 29 | 30 | // Get は、ProgrammingLang1件返す。 31 | func (u *ProgrammingLangUseCase) Get(ctx context.Context, id int) (*model.ProgrammingLang, error) { 32 | return u.Repo.Read(ctx, id) 33 | } 34 | 35 | // Create は、ProgrammingLangを生成する。 36 | func (u *ProgrammingLangUseCase) Create(ctx context.Context, param *model.ProgrammingLang) (*model.ProgrammingLang, error) { 37 | lang, err := u.Repo.ReadByName(ctx, param.Name) 38 | if lang != nil { 39 | return nil, &model.AlreadyExistError{ 40 | ID: lang.ID, 41 | Name: lang.Name, 42 | ModelName: model.ModelNameProgrammingLang, 43 | } 44 | } 45 | 46 | if _, ok := errors.Cause(err).(*model.NoSuchDataError); !ok { 47 | return nil, errors.WithStack(err) 48 | } 49 | 50 | param.CreatedAt = time.Now().UTC() 51 | param.UpdatedAt = time.Now().UTC() 52 | 53 | lang, err = u.Repo.Create(ctx, param) 54 | if err != nil { 55 | return nil, errors.WithStack(err) 56 | } 57 | 58 | return lang, nil 59 | } 60 | 61 | // Update は、ProgrammingLangを更新する。 62 | func (u *ProgrammingLangUseCase) Update(ctx context.Context, id int, param *model.ProgrammingLang) (*model.ProgrammingLang, error) { 63 | lang, err := u.Repo.Read(ctx, id) 64 | if lang == nil { 65 | return nil, &model.NoSuchDataError{ 66 | ID: id, 67 | Name: param.Name, 68 | ModelName: model.ModelNameProgrammingLang, 69 | } 70 | } else if err != nil { 71 | return nil, errors.WithStack(err) 72 | } 73 | 74 | lang.ID = id 75 | lang.Name = param.Name 76 | lang.Feature = param.Feature 77 | lang.UpdatedAt = time.Now().UTC() 78 | 79 | return u.Repo.Update(ctx, lang) 80 | } 81 | 82 | // Delete は、ProgrammingLangを削除する。 83 | func (u *ProgrammingLangUseCase) Delete(ctx context.Context, id int) error { 84 | lang, err := u.Repo.Read(ctx, id) 85 | if lang == nil { 86 | return &model.NoSuchDataError{ 87 | ID: id, 88 | ModelName: model.ModelNameProgrammingLang, 89 | } 90 | } else if err != nil { 91 | return errors.WithStack(err) 92 | } 93 | 94 | return u.Repo.Delete(ctx, id) 95 | } 96 | -------------------------------------------------------------------------------- /server/usecase/program_lang_usecase_test.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 9 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/repository" 10 | "github.com/SekiguchiKai/clean-architecture-with-go/server/infra/dao/mock" 11 | "github.com/golang/mock/gomock" 12 | ) 13 | 14 | func TestNewProgrammingLangUseCase(t *testing.T) { 15 | ctrl := gomock.NewController(t) 16 | defer ctrl.Finish() 17 | 18 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl) 19 | 20 | type args struct { 21 | repo repository.ProgrammingLangRepository 22 | } 23 | tests := []struct { 24 | name string 25 | args args 26 | }{ 27 | { 28 | name: "適切な引数を与えると、ProgrammingLangUseCaseが返されること", 29 | args: args{ 30 | repo: mock, 31 | }, 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := NewProgrammingLangUseCase(tt.args.repo); got == nil { 37 | t.Errorf("NewProgrammingLangUseCase() = %v, want not nil", got) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestProgrammingLangUseCase_List(t *testing.T) { 44 | ctrl := gomock.NewController(t) 45 | defer ctrl.Finish() 46 | 47 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl) 48 | 49 | type fields struct { 50 | Repo repository.ProgrammingLangRepository 51 | } 52 | type args struct { 53 | ctx context.Context 54 | limit int 55 | } 56 | 57 | type wantErr struct { 58 | isErr bool 59 | err error 60 | } 61 | 62 | tests := []struct { 63 | name string 64 | fields fields 65 | args args 66 | want []*model.ProgrammingLang 67 | wantErr wantErr 68 | }{ 69 | { 70 | name: "limitを20件にした時に、20件のProgrammingLangを含んだProgrammingLangを返すこと", 71 | fields: fields{ 72 | Repo: mock, 73 | }, 74 | args: args{ 75 | ctx: context.Background(), 76 | limit: 20, 77 | }, 78 | want: model.CreateProgrammingLangs(20), 79 | wantErr: wantErr{ 80 | isErr:false, 81 | }, 82 | }, 83 | { 84 | name: "サーバー側のエラーが発生した場合、ステータスコード500とエラーメッセージを返すこと", 85 | fields: fields{ 86 | Repo: mock, 87 | }, 88 | args: args{ 89 | ctx: context.Background(), 90 | limit: 20, 91 | }, 92 | want: nil, 93 | wantErr: wantErr{ 94 | isErr:true, 95 | err:&model.DBError{ 96 | ModelName: model.ModelNameProgrammingLang, 97 | DBMethod: model.DBMethodList, 98 | Detail: model.TestDBSomeErr, 99 | }, 100 | }, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | u := &ProgrammingLangUseCase{ 106 | Repo: tt.fields.Repo, 107 | } 108 | 109 | mock.EXPECT().List(tt.args.ctx, tt.args.limit).Return(tt.want, tt.wantErr.err) 110 | 111 | got, err := u.List(tt.args.ctx, tt.args.limit) 112 | if (err != nil) != tt.wantErr.isErr { 113 | t.Errorf("ProgrammingLangUseCase.List() error = %v, wantErr %v", err, tt.wantErr) 114 | return 115 | } 116 | if !reflect.DeepEqual(got, tt.want) { 117 | t.Errorf("ProgrammingLangUseCase.List() = %v, want %v", got, tt.want) 118 | } 119 | 120 | if !reflect.DeepEqual(err, tt.wantErr.err) { 121 | t.Errorf("ProgrammingLangUseCase.List() = %v, want %v", err, tt.wantErr.err) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | func TestProgrammingLangUseCase_Get(t *testing.T) { 128 | ctrl := gomock.NewController(t) 129 | defer ctrl.Finish() 130 | 131 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl) 132 | 133 | wantErrValue := &model.NoSuchDataError{ 134 | ID: 1, 135 | ModelName: model.ModelNameProgrammingLang, 136 | } 137 | 138 | type fields struct { 139 | Repo repository.ProgrammingLangRepository 140 | } 141 | type args struct { 142 | ctx context.Context 143 | id int 144 | } 145 | 146 | type wantErr struct { 147 | isErr bool 148 | err error 149 | } 150 | 151 | tests := []struct { 152 | name string 153 | fields fields 154 | args args 155 | want *model.ProgrammingLang 156 | wantErr wantErr 157 | }{ 158 | { 159 | name: "IDで指定したProgrammingLang存在する場合、ProgrammingLangを1件返すこと", 160 | fields: fields{ 161 | Repo: mock, 162 | }, 163 | args: args{ 164 | ctx: context.Background(), 165 | id: 1, 166 | }, 167 | want: model.CreateProgrammingLangs(1)[0], 168 | wantErr: wantErr{ 169 | isErr: false, 170 | err: nil, 171 | }, 172 | }, 173 | { 174 | name: "IDで指定したProgrammingLang存在しない場合、nilとエラーを返すこと", 175 | fields: fields{ 176 | Repo: mock, 177 | }, 178 | args: args{ 179 | ctx: context.Background(), 180 | id: 1, 181 | }, 182 | want: model.CreateProgrammingLangs(1)[0], 183 | wantErr: wantErr{ 184 | isErr: true, 185 | err: wantErrValue, 186 | }, 187 | }, 188 | } 189 | for _, tt := range tests { 190 | t.Run(tt.name, func(t *testing.T) { 191 | u := &ProgrammingLangUseCase{ 192 | Repo: tt.fields.Repo, 193 | } 194 | 195 | mock.EXPECT().Read(tt.args.ctx, tt.args.id).Return(tt.want, tt.wantErr.err) 196 | 197 | got, err := u.Get(tt.args.ctx, tt.args.id) 198 | if (err != nil) != tt.wantErr.isErr { 199 | t.Errorf("ProgrammingLangUseCase.Get() error = %v, wantErr %v", err, tt.wantErr) 200 | return 201 | } 202 | 203 | if tt.wantErr.isErr { 204 | if err.Error() != tt.wantErr.err.Error() { 205 | t.Errorf("ProgrammingLangUseCase.Get() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error()) 206 | } 207 | } 208 | 209 | if !reflect.DeepEqual(got, tt.want) { 210 | t.Errorf("ProgrammingLangUseCase.Get() = %v, want %v", got, tt.want) 211 | } 212 | }) 213 | } 214 | } 215 | 216 | func TestProgrammingLangUseCase_Create(t *testing.T) { 217 | ctrl := gomock.NewController(t) 218 | defer ctrl.Finish() 219 | 220 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl) 221 | 222 | lang := model.CreateProgrammingLangs(1)[0] 223 | 224 | wantErrValue := &model.AlreadyExistError{ 225 | ID: 1, 226 | ModelName: model.ModelNameProgrammingLang, 227 | Name: model.CreateProgrammingLangs(1)[0].Name, 228 | } 229 | 230 | type fields struct { 231 | Repo repository.ProgrammingLangRepository 232 | } 233 | type args struct { 234 | ctx context.Context 235 | param *model.ProgrammingLang 236 | } 237 | 238 | type readWant struct { 239 | result *model.ProgrammingLang 240 | err error 241 | } 242 | 243 | type wantErr struct { 244 | isErr bool 245 | err error 246 | } 247 | 248 | tests := []struct { 249 | name string 250 | fields fields 251 | args args 252 | want *model.ProgrammingLang 253 | readWant readWant 254 | wantErr wantErr 255 | }{ 256 | { 257 | name: "同一のProgrammingLang存在しない場合、ProgrammingLangを登録すること", 258 | fields: fields{ 259 | Repo: mock, 260 | }, 261 | args: args{ 262 | ctx: context.Background(), 263 | param: lang, 264 | }, 265 | want: lang, 266 | readWant: readWant{ 267 | result: nil, 268 | err: &model.NoSuchDataError{ 269 | Name: lang.Name, 270 | ModelName: model.ModelNameProgrammingLang, 271 | }, 272 | }, 273 | wantErr: wantErr{ 274 | isErr: false, 275 | err: nil, 276 | }, 277 | }, 278 | { 279 | name: "同一のProgrammingLang存在する場合、nilとエラーを返すこと", 280 | fields: fields{ 281 | Repo: mock, 282 | }, 283 | args: args{ 284 | ctx: context.Background(), 285 | param: lang, 286 | }, 287 | want: nil, 288 | readWant: readWant{ 289 | result: lang, 290 | err: wantErrValue, 291 | }, 292 | wantErr: wantErr{ 293 | isErr: true, 294 | err: wantErrValue, 295 | }, 296 | }, 297 | } 298 | for _, tt := range tests { 299 | t.Run(tt.name, func(t *testing.T) { 300 | u := &ProgrammingLangUseCase{ 301 | Repo: tt.fields.Repo, 302 | } 303 | 304 | mock.EXPECT().ReadByName(tt.args.ctx, tt.args.param.Name).Return(tt.readWant.result, tt.readWant.err) 305 | 306 | if !tt.wantErr.isErr { 307 | mock.EXPECT().Create(tt.args.ctx, tt.args.param).Return(tt.want, tt.wantErr.err) 308 | } 309 | 310 | got, err := u.Create(tt.args.ctx, tt.args.param) 311 | 312 | if (err != nil) != tt.wantErr.isErr { 313 | t.Errorf("ProgrammingLangUseCase.Create() error = %v, wantErr %v", err, tt.wantErr) 314 | return 315 | } 316 | 317 | if tt.wantErr.isErr { 318 | if err.Error() != tt.wantErr.err.Error() { 319 | t.Errorf("ProgrammingLangUseCase.Create() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error()) 320 | } 321 | } 322 | 323 | if !reflect.DeepEqual(got, tt.want) { 324 | t.Errorf("ProgrammingLangUseCase.Create() = %v, want %v", got, tt.want) 325 | } 326 | }) 327 | } 328 | } 329 | 330 | func TestProgrammingLangUseCase_Update(t *testing.T) { 331 | ctrl := gomock.NewController(t) 332 | defer ctrl.Finish() 333 | 334 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl) 335 | 336 | lang := model.CreateProgrammingLangs(1)[0] 337 | 338 | type fields struct { 339 | Repo repository.ProgrammingLangRepository 340 | } 341 | type args struct { 342 | ctx context.Context 343 | id int 344 | param *model.ProgrammingLang 345 | } 346 | 347 | type readWant struct { 348 | result *model.ProgrammingLang 349 | err error 350 | } 351 | 352 | type wantErr struct { 353 | isErr bool 354 | err error 355 | } 356 | 357 | tests := []struct { 358 | name string 359 | fields fields 360 | args args 361 | want *model.ProgrammingLang 362 | readWant readWant 363 | wantErr wantErr 364 | }{ 365 | { 366 | name: "同一のProgrammingLang存在する場合、ProgrammingLangを更新すること", 367 | fields: fields{ 368 | Repo: mock, 369 | }, 370 | args: args{ 371 | ctx: context.Background(), 372 | id: 1, 373 | param: lang, 374 | }, 375 | want: lang, 376 | readWant: readWant{ 377 | result: lang, 378 | err: nil, 379 | }, 380 | wantErr: wantErr{ 381 | isErr: false, 382 | err: nil, 383 | }, 384 | }, 385 | { 386 | name: "指定したProgrammingLang存在しない場合、nilとエラーを返すこと", 387 | fields: fields{ 388 | Repo: mock, 389 | }, 390 | args: args{ 391 | ctx: context.Background(), 392 | id: 100, 393 | param: lang, 394 | }, 395 | want: nil, 396 | readWant: readWant{ 397 | result: nil, 398 | err: &model.DBError{ 399 | ModelName: model.ModelNameProgrammingLang, 400 | DBMethod: model.DBMethodRead, 401 | Detail: model.TestDBSomeErr, 402 | }, 403 | }, 404 | wantErr: wantErr{ 405 | isErr: true, 406 | err: &model.NoSuchDataError{ 407 | ID: 100, 408 | ModelName: model.ModelNameProgrammingLang, 409 | Name: lang.Name, 410 | }, 411 | }, 412 | }, 413 | } 414 | for _, tt := range tests { 415 | t.Run(tt.name, func(t *testing.T) { 416 | u := &ProgrammingLangUseCase{ 417 | Repo: tt.fields.Repo, 418 | } 419 | 420 | mock.EXPECT().Read(tt.args.ctx, tt.args.id).Return(tt.readWant.result, tt.readWant.err) 421 | 422 | if !tt.wantErr.isErr { 423 | mock.EXPECT().Update(tt.args.ctx, tt.args.param).Return(tt.want, tt.wantErr.err) 424 | } 425 | 426 | got, err := u.Update(tt.args.ctx, tt.args.id, tt.args.param) 427 | if (err != nil) != tt.wantErr.isErr { 428 | t.Errorf("ProgrammingLangUseCase.Update() error = %v, wantErr %v", err, tt.wantErr.isErr) 429 | return 430 | } 431 | 432 | if tt.wantErr.isErr { 433 | if err.Error() != tt.wantErr.err.Error() { 434 | t.Errorf("ProgrammingLangUseCase.Update() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error()) 435 | } 436 | } 437 | 438 | if !reflect.DeepEqual(got, tt.want) { 439 | t.Errorf("ProgrammingLangUseCase.Update() = %v, want %v", got, tt.want) 440 | } 441 | }) 442 | } 443 | } 444 | 445 | func TestProgrammingLangUseCase_Delete(t *testing.T) { 446 | ctrl := gomock.NewController(t) 447 | defer ctrl.Finish() 448 | 449 | mock := mock_repository.NewMockProgrammingLangRepository(ctrl) 450 | 451 | err := &model.NoSuchDataError{ 452 | ID: 2, 453 | ModelName: model.ModelNameProgrammingLang, 454 | } 455 | 456 | type fields struct { 457 | Ctx context.Context 458 | Repo repository.ProgrammingLangRepository 459 | } 460 | type args struct { 461 | ctx context.Context 462 | id int 463 | } 464 | 465 | type wantErr struct { 466 | isErr bool 467 | err error 468 | } 469 | 470 | type readWant struct { 471 | result *model.ProgrammingLang 472 | err error 473 | } 474 | 475 | tests := []struct { 476 | name string 477 | fields fields 478 | args args 479 | wantErr wantErr 480 | readWant readWant 481 | }{ 482 | { 483 | name: "同一のProgrammingLang存在する場合、ProgrammingLangを更新すること", 484 | fields: fields{ 485 | Repo: mock, 486 | }, 487 | args: args{ 488 | ctx: context.Background(), 489 | id: 1, 490 | }, 491 | wantErr: wantErr{ 492 | isErr: false, 493 | err: nil, 494 | }, 495 | readWant: readWant{ 496 | result: model.CreateProgrammingLangs(1)[0], 497 | err: nil, 498 | }, 499 | }, 500 | { 501 | name: "IDで指定したProgrammingLang存在しない場合、nilとエラーを返すこと", 502 | fields: fields{ 503 | Repo: mock, 504 | }, 505 | args: args{ 506 | ctx: context.Background(), 507 | id: 2, 508 | }, 509 | wantErr: wantErr{ 510 | isErr: true, 511 | err: err, 512 | }, 513 | readWant: readWant{ 514 | result: nil, 515 | err: err, 516 | }, 517 | }, 518 | } 519 | for _, tt := range tests { 520 | t.Run(tt.name, func(t *testing.T) { 521 | u := &ProgrammingLangUseCase{ 522 | Repo: tt.fields.Repo, 523 | } 524 | 525 | mock.EXPECT().Read(tt.args.ctx, tt.args.id).Return(tt.readWant.result, tt.readWant.err) 526 | 527 | if tt.readWant.result != nil { 528 | mock.EXPECT().Delete(tt.args.ctx, tt.args.id).Return(tt.wantErr.err) 529 | } 530 | 531 | err := u.Delete(tt.args.ctx, tt.args.id); 532 | if (err != nil) != tt.wantErr.isErr { 533 | t.Errorf("ProgrammingLangUseCase.Delete() error = %v, wantErr %v", err, tt.wantErr.isErr) 534 | } 535 | 536 | if tt.wantErr.isErr { 537 | if err.Error() != tt.wantErr.err.Error() { 538 | t.Errorf("ProgrammingLangUseCase.Delete() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error()) 539 | } 540 | } 541 | 542 | 543 | if tt.wantErr.isErr { 544 | if err.Error() != tt.wantErr.err.Error() { 545 | t.Errorf("ProgrammingLangUseCase.Delete() error = %v, wantErr %v", err.Error(), tt.wantErr.err.Error()) 546 | } 547 | } 548 | }) 549 | } 550 | } 551 | -------------------------------------------------------------------------------- /server/util/checker.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // IsEmpty は与えられた文字列が空文字かどうかを確認する 4 | func IsEmpty(target string) bool { 5 | return target == "" 6 | } -------------------------------------------------------------------------------- /server/util/checker_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | "github.com/SekiguchiKai/clean-architecture-with-go/server/domain/model" 6 | ) 7 | 8 | func TestIsEmpty(t *testing.T) { 9 | type args struct { 10 | target string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want bool 16 | }{ 17 | { 18 | name :"空の場合は、trueを返す", 19 | args :args{target:""}, 20 | want:true, 21 | }, 22 | { 23 | name :"空でない場合は、falseを返す", 24 | args :args{target:model.TestName}, 25 | want:false, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | if got := IsEmpty(tt.args.target); got != tt.want { 31 | t.Errorf("IsEmpty() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/util/trimer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | // TrimDoubleQuotes は、ダブルクォーテーションを削除する。 6 | func TrimDoubleQuotes(target string) string { 7 | return strings.Trim(target, "\"") 8 | } 9 | -------------------------------------------------------------------------------- /server/util/trimer_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestTrimDoubleQuotes(t *testing.T) { 6 | type args struct { 7 | target string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want string 13 | }{ 14 | { 15 | name: "ダブルクォーテーションが1つ存在する場合、ダブルクォーテーションを削除した文字列を返す", 16 | args: args{ 17 | target: "\"test", 18 | }, 19 | want: "test", 20 | }, 21 | { 22 | name: "ダブルクォーテーションが3つ存在する場合、ダブルクォーテーションを削除した文字列を返す", 23 | args: args{ 24 | target: "\"\"test\"", 25 | }, 26 | want: "test", 27 | }, 28 | { 29 | name: "ダブルクォーテーションが存在しない場合、そのままの文字列を返す", 30 | args: args{ 31 | target: "test", 32 | }, 33 | want: "test", 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := TrimDoubleQuotes(tt.args.target); got != tt.want { 39 | t.Errorf("TrimDoubleQuotes() = %v, want %v", got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | --------------------------------------------------------------------------------