├── LICENSE ├── Makefile ├── README.md ├── _sql ├── sample_data.sql └── schema.sql ├── application └── user.go ├── config └── database.go ├── domain ├── repository │ ├── mock_user.go │ └── user.go └── user.go ├── go.mod ├── go.sum ├── infrastructure └── persistence │ ├── main_test.go │ ├── testdata │ ├── schema.sql │ └── users.yml │ ├── user_repository.go │ └── user_repository_test.go └── interfaces ├── handler.go ├── handler_test.go ├── main_test.go └── testdata ├── schema.sql └── users.yml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Takashi Abe 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SUBPACKAGES := $(shell go list ./...) 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | .PHONY: generate 6 | generate: ## Invoke go generate 7 | go generate ./... 8 | 9 | .PHONY: test 10 | test: ## Run go test 11 | go test -v$(SUBPACKAGES) 12 | 13 | .PHONY: help 14 | help: ## Help 15 | @grep -E '^[0-9a-zA-Z_/()$$-]+:.*?## .*$$' $(lastword $(MAKEFILE_LIST)) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-ddd-sample 2 | 3 | This is sample application for like the DDD architecture. 4 | 5 | ## Design 6 | 7 | * application 8 | * Write business logic 9 | * domain 10 | * Define interface 11 | * repository interface for infrastructure 12 | * Define struct 13 | * Entity struct that represent mapping to data model 14 | * infrastructure 15 | * Implements repository interface 16 | * Solves backend technical topics 17 | * e.x. message queue, persistence with RDB 18 | * interfaces 19 | * Write HTTP handler and middleware 20 | 21 | #### References: 22 | 23 | * https://speakerdeck.com/mercari/ja-golang-package-composition-for-web-application-the-case-of-mercari-kauru 24 | * http://pospome.hatenablog.com/entry/2017/10/11/023848 25 | * https://medium.com/@timakin/go%E3%81%AE%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E6%A7%8B%E6%88%90%E3%81%AE%E5%A4%B1%E6%95%97%E9%81%8D%E6%AD%B4%E3%81%A8%E7%8F%BE%E7%8A%B6%E7%A2%BA%E8%AA%8D-fc6a4369337 26 | -------------------------------------------------------------------------------- /_sql/sample_data.sql: -------------------------------------------------------------------------------- 1 | use ddd_sample; 2 | 3 | DELETE FROM users; 4 | INSERT INTO users (name) VALUES ('satoshi'), ('kasumi'), ('takeshi'), ('kojiro'), ('musashi'); 5 | -------------------------------------------------------------------------------- /_sql/schema.sql: -------------------------------------------------------------------------------- 1 | use ddd_sample; 2 | 3 | CREATE TABLE IF NOT EXISTS users ( 4 | `id` int NOT NULL AUTO_INCREMENT, 5 | `name` varchar(256) NOT NULL, 6 | `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 8 | PRIMARY KEY (id) 9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 10 | -------------------------------------------------------------------------------- /application/user.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/takashabe/go-ddd-sample/domain" 7 | "github.com/takashabe/go-ddd-sample/domain/repository" 8 | ) 9 | 10 | // UserInteractor provides use-case 11 | type UserInteractor struct { 12 | Repository repository.UserRepository 13 | } 14 | 15 | // GetUser returns user 16 | func (i UserInteractor) GetUser(ctx context.Context, id int) (*domain.User, error) { 17 | return i.Repository.Get(ctx, id) 18 | } 19 | 20 | // GetUsers returns user list 21 | func (i UserInteractor) GetUsers(ctx context.Context) ([]*domain.User, error) { 22 | return i.Repository.GetAll(ctx) 23 | } 24 | 25 | // AddUser saves new user 26 | func (i UserInteractor) AddUser(ctx context.Context, name string) error { 27 | u, err := domain.NewUser(name) 28 | if err != nil { 29 | return err 30 | } 31 | return i.Repository.Save(ctx, u) 32 | } 33 | -------------------------------------------------------------------------------- /config/database.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // NewDBConnection returns initialized sql.DB 10 | func NewDBConnection() (*sql.DB, error) { 11 | user := getEnvWithDefault("DB_USER", "root") 12 | password := getEnvWithDefault("DB_PASSWORD", "") 13 | host := getEnvWithDefault("DB_HOST", "localhost") 14 | port := getEnvWithDefault("DB_PORT", "3306") 15 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/ddd_sample?parseTime=true", user, password, host, port) 16 | db, err := sql.Open("mysql", dsn) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return db, nil 21 | } 22 | 23 | func getEnvWithDefault(name, def string) string { 24 | env := os.Getenv(name) 25 | if len(env) != 0 { 26 | return env 27 | } 28 | return def 29 | } 30 | -------------------------------------------------------------------------------- /domain/repository/mock_user.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: user.go 3 | 4 | // Package repository is a generated GoMock package. 5 | package repository 6 | 7 | import ( 8 | context "context" 9 | gomock "github.com/golang/mock/gomock" 10 | domain "github.com/takashabe/go-ddd-sample/domain" 11 | reflect "reflect" 12 | ) 13 | 14 | // MockUserRepository is a mock of UserRepository interface 15 | type MockUserRepository struct { 16 | ctrl *gomock.Controller 17 | recorder *MockUserRepositoryMockRecorder 18 | } 19 | 20 | // MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository 21 | type MockUserRepositoryMockRecorder struct { 22 | mock *MockUserRepository 23 | } 24 | 25 | // NewMockUserRepository creates a new mock instance 26 | func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository { 27 | mock := &MockUserRepository{ctrl: ctrl} 28 | mock.recorder = &MockUserRepositoryMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use 33 | func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Get mocks base method 38 | func (m *MockUserRepository) Get(ctx context.Context, id int) (*domain.User, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Get", ctx, id) 41 | ret0, _ := ret[0].(*domain.User) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Get indicates an expected call of Get 47 | func (mr *MockUserRepositoryMockRecorder) Get(ctx, id interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockUserRepository)(nil).Get), ctx, id) 50 | } 51 | 52 | // GetAll mocks base method 53 | func (m *MockUserRepository) GetAll(ctx context.Context) ([]*domain.User, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "GetAll", ctx) 56 | ret0, _ := ret[0].([]*domain.User) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // GetAll indicates an expected call of GetAll 62 | func (mr *MockUserRepositoryMockRecorder) GetAll(ctx interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockUserRepository)(nil).GetAll), ctx) 65 | } 66 | 67 | // Save mocks base method 68 | func (m *MockUserRepository) Save(ctx context.Context, user *domain.User) error { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "Save", ctx, user) 71 | ret0, _ := ret[0].(error) 72 | return ret0 73 | } 74 | 75 | // Save indicates an expected call of Save 76 | func (mr *MockUserRepositoryMockRecorder) Save(ctx, user interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockUserRepository)(nil).Save), ctx, user) 79 | } 80 | -------------------------------------------------------------------------------- /domain/repository/user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/takashabe/go-ddd-sample/domain" 7 | ) 8 | 9 | //go:generate mockgen -package $GOPACKAGE -source $GOFILE -destination mock_$GOFILE 10 | 11 | // UserRepository represent repository of the user 12 | // Expect implementation by the infrastructure layer 13 | type UserRepository interface { 14 | Get(ctx context.Context, id int) (*domain.User, error) 15 | GetAll(ctx context.Context) ([]*domain.User, error) 16 | Save(ctx context.Context, user *domain.User) error 17 | } 18 | -------------------------------------------------------------------------------- /domain/user.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "fmt" 4 | 5 | // User represent entity of the user 6 | type User struct { 7 | ID int `json:"id"` 8 | Name string `json:"name"` 9 | } 10 | 11 | // NewUser initialize user 12 | func NewUser(name string) (*User, error) { 13 | if name == "" { 14 | return nil, fmt.Errorf("invalid name") 15 | } 16 | 17 | return &User{ 18 | Name: name, 19 | }, nil 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/takashabe/go-ddd-sample 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.4.1 7 | github.com/golang/mock v1.3.1 8 | github.com/google/go-cmp v0.3.1 9 | github.com/pkg/errors v0.8.1 // indirect 10 | github.com/takashabe/go-fixture v0.0.0-20180310134710-3590f2a91aef 11 | github.com/takashabe/go-router v0.0.0-20180130155705-1eefa4c80f55 12 | google.golang.org/appengine v1.6.1 // indirect 13 | gopkg.in/yaml.v2 v2.2.2 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 2 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 3 | github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= 4 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 5 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 6 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 7 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 8 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 9 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 10 | github.com/takashabe/go-fixture v0.0.0-20180310134710-3590f2a91aef h1:IgPfhuBILTc89sXvWUk/E2cyGE9f6bXB9aGwrbz/5PY= 11 | github.com/takashabe/go-fixture v0.0.0-20180310134710-3590f2a91aef/go.mod h1:ErUAPT9DRpfURcA9fGFXzTcIbzFGlspSzFxFDiINGKs= 12 | github.com/takashabe/go-router v0.0.0-20180130155705-1eefa4c80f55 h1:zNWtF9s1wawUQe2kHu40rN2QhtRcOPKaLRk4doMOMMI= 13 | github.com/takashabe/go-router v0.0.0-20180130155705-1eefa4c80f55/go.mod h1:Jm3rY1VLnCldWsk1MujqGY2OhC6GRvVumTtmXF9RccE= 14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 15 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 16 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 17 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 18 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 19 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 24 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 25 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 26 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 27 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 28 | google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= 29 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 33 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 34 | -------------------------------------------------------------------------------- /infrastructure/persistence/main_test.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/takashabe/go-ddd-sample/config" 8 | fixture "github.com/takashabe/go-fixture" 9 | _ "github.com/takashabe/go-fixture/mysql" 10 | ) 11 | 12 | func MainTest(m *testing.M) { 13 | setup() 14 | os.Exit(m.Run()) 15 | } 16 | 17 | func setup() { 18 | db, err := config.NewDBConnection() 19 | if err != nil { 20 | panic(err.Error()) 21 | } 22 | defer db.Close() 23 | 24 | fixture, err := fixture.NewFixture(db, "mysql") 25 | if err != nil { 26 | panic(err.Error()) 27 | } 28 | err = fixture.Load("testdata/schema.sql") 29 | if err != nil { 30 | panic(err.Error()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /infrastructure/persistence/testdata/schema.sql: -------------------------------------------------------------------------------- 1 | ../../../_sql/schema.sql -------------------------------------------------------------------------------- /infrastructure/persistence/testdata/users.yml: -------------------------------------------------------------------------------- 1 | table: users 2 | record: 3 | - id: 1 4 | name: satoshi 5 | - id: 2 6 | name: kasumi 7 | - id: 3 8 | name: takeshi 9 | - id: 4 10 | name: kojiro 11 | - id: 5 12 | name: musashi 13 | -------------------------------------------------------------------------------- /infrastructure/persistence/user_repository.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | _ "github.com/go-sql-driver/mysql" // driver 8 | "github.com/takashabe/go-ddd-sample/domain" 9 | "github.com/takashabe/go-ddd-sample/domain/repository" 10 | ) 11 | 12 | // userRepository Implements repository.UserRepository 13 | type userRepository struct { 14 | conn *sql.DB 15 | } 16 | 17 | // NewUserRepository returns initialized UserRepositoryImpl 18 | func NewUserRepository(conn *sql.DB) repository.UserRepository { 19 | return &userRepository{conn: conn} 20 | } 21 | 22 | // Get returns domain.User 23 | func (r *userRepository) Get(ctx context.Context, id int) (*domain.User, error) { 24 | row, err := r.queryRow(ctx, "select id, name from users where id=?", id) 25 | if err != nil { 26 | return nil, err 27 | } 28 | u := &domain.User{} 29 | err = row.Scan(&u.ID, &u.Name) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return u, nil 34 | } 35 | 36 | // GetAll returns list of domain.User 37 | func (r *userRepository) GetAll(ctx context.Context) ([]*domain.User, error) { 38 | rows, err := r.query(ctx, "select id, name from users") 39 | if err != nil { 40 | return nil, err 41 | } 42 | defer rows.Close() 43 | us := make([]*domain.User, 0) 44 | for rows.Next() { 45 | u := &domain.User{} 46 | err = rows.Scan(&u.ID, &u.Name) 47 | if err != nil { 48 | return nil, err 49 | } 50 | us = append(us, u) 51 | } 52 | return us, nil 53 | } 54 | 55 | // Save saves domain.User to storage 56 | func (r *userRepository) Save(ctx context.Context, u *domain.User) error { 57 | stmt, err := r.conn.Prepare("insert into users (name) values (?)") 58 | if err != nil { 59 | return err 60 | } 61 | defer stmt.Close() 62 | 63 | _, err = stmt.ExecContext(ctx, u.Name) 64 | return err 65 | } 66 | 67 | func (r *userRepository) query(ctx context.Context, q string, args ...interface{}) (*sql.Rows, error) { 68 | stmt, err := r.conn.Prepare(q) 69 | if err != nil { 70 | return nil, err 71 | } 72 | defer stmt.Close() 73 | 74 | return stmt.QueryContext(ctx, args...) 75 | } 76 | 77 | func (r *userRepository) queryRow(ctx context.Context, q string, args ...interface{}) (*sql.Row, error) { 78 | stmt, err := r.conn.Prepare(q) 79 | if err != nil { 80 | return nil, err 81 | } 82 | defer stmt.Close() 83 | 84 | return stmt.QueryRowContext(ctx, args...), nil 85 | } 86 | -------------------------------------------------------------------------------- /infrastructure/persistence/user_repository_test.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/takashabe/go-ddd-sample/config" 10 | "github.com/takashabe/go-ddd-sample/domain" 11 | fixture "github.com/takashabe/go-fixture" 12 | _ "github.com/takashabe/go-fixture/mysql" 13 | ) 14 | 15 | func loadFixture(t *testing.T, conn *sql.DB, file string) { 16 | fixture, err := fixture.NewFixture(conn, "mysql") 17 | if err != nil { 18 | t.Fatalf("want non error, got %#v", err) 19 | } 20 | err = fixture.Load(file) 21 | if err != nil { 22 | t.Fatalf("want non error, got %#v", err) 23 | } 24 | } 25 | 26 | func TestGetUser(t *testing.T) { 27 | conn, err := config.NewDBConnection() 28 | if err != nil { 29 | t.Fatalf("want non error, got %#v", err) 30 | } 31 | loadFixture(t, conn, "testdata/users.yml") 32 | 33 | cases := []struct { 34 | input int 35 | expectUser *domain.User 36 | expectErr error 37 | }{ 38 | { 39 | 1, 40 | &domain.User{ 41 | ID: 1, 42 | Name: "satoshi", 43 | }, 44 | nil, 45 | }, 46 | { 47 | 0, 48 | nil, 49 | sql.ErrNoRows, 50 | }, 51 | } 52 | for i, c := range cases { 53 | repo := NewUserRepository(conn) 54 | user, err := repo.Get(context.Background(), c.input) 55 | if err != c.expectErr { 56 | t.Fatalf("#%d: want error %#v, got %#v", i, c.expectErr, err) 57 | } 58 | if err != nil { 59 | continue 60 | } 61 | if !reflect.DeepEqual(user, c.expectUser) { 62 | t.Errorf("#%d: want %#v, got %#v", i, c.expectUser, user) 63 | } 64 | } 65 | } 66 | 67 | func TestGetUsers(t *testing.T) { 68 | conn, err := config.NewDBConnection() 69 | if err != nil { 70 | t.Fatalf("want non error, got %#v", err) 71 | } 72 | loadFixture(t, conn, "testdata/users.yml") 73 | 74 | cases := []struct { 75 | expectIDs []int 76 | }{ 77 | {[]int{1, 2, 3, 4, 5}}, 78 | } 79 | for i, c := range cases { 80 | repo := NewUserRepository(conn) 81 | users, err := repo.GetAll(context.Background()) 82 | if err != nil { 83 | t.Fatalf("#%d: want non error, got %#v", i, err) 84 | } 85 | ids := make([]int, 0) 86 | for _, u := range users { 87 | ids = append(ids, u.ID) 88 | } 89 | if !reflect.DeepEqual(ids, c.expectIDs) { 90 | t.Errorf("#%d: want %#v, got %#v", i, c.expectIDs, ids) 91 | } 92 | } 93 | } 94 | 95 | func TestSaveUser(t *testing.T) { 96 | conn, err := config.NewDBConnection() 97 | if err != nil { 98 | t.Fatalf("want non error, got %#v", err) 99 | } 100 | loadFixture(t, conn, "testdata/users.yml") 101 | 102 | cases := []struct { 103 | input string 104 | }{ 105 | {"foo"}, 106 | {"foo"}, // duplicate 107 | } 108 | for i, c := range cases { 109 | ctx := context.Background() 110 | repo := NewUserRepository(conn) 111 | err := repo.Save(ctx, &domain.User{ 112 | Name: c.input, 113 | }) 114 | if err != nil { 115 | t.Fatalf("#%d: want non error, got %#v", i, err) 116 | } 117 | 118 | users, err := repo.GetAll(ctx) 119 | if err != nil { 120 | t.Fatalf("#%d: want non error, got %#v", i, err) 121 | } 122 | find := false 123 | for _, u := range users { 124 | if u.Name == c.input { 125 | find = true 126 | break 127 | } 128 | } 129 | if !find { 130 | t.Errorf("#%d: want contain name %s, but not found it", i, c.input) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /interfaces/handler.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/takashabe/go-ddd-sample/application" 11 | "github.com/takashabe/go-ddd-sample/domain" 12 | "github.com/takashabe/go-ddd-sample/domain/repository" 13 | router "github.com/takashabe/go-router" 14 | ) 15 | 16 | // printDebugf behaves like log.Printf only in the debug env 17 | func printDebugf(format string, args ...interface{}) { 18 | if env := os.Getenv("GO_SERVER_DEBUG"); len(env) != 0 { 19 | log.Printf("[DEBUG] "+format+"\n", args...) 20 | } 21 | } 22 | 23 | // ErrorResponse is Error response template 24 | type ErrorResponse struct { 25 | Message string `json:"reason"` 26 | Error error `json:"-"` 27 | } 28 | 29 | func (e *ErrorResponse) String() string { 30 | return fmt.Sprintf("reason: %s, error: %s", e.Message, e.Error.Error()) 31 | } 32 | 33 | // Respond is response write to ResponseWriter 34 | func Respond(w http.ResponseWriter, code int, src interface{}) { 35 | var body []byte 36 | var err error 37 | 38 | switch s := src.(type) { 39 | case []byte: 40 | if !json.Valid(s) { 41 | Error(w, http.StatusInternalServerError, err, "invalid json") 42 | return 43 | } 44 | body = s 45 | case string: 46 | body = []byte(s) 47 | case *ErrorResponse, ErrorResponse: 48 | // avoid infinite loop 49 | if body, err = json.Marshal(src); err != nil { 50 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 51 | w.WriteHeader(http.StatusInternalServerError) 52 | w.Write([]byte("{\"reason\":\"failed to parse json\"}")) 53 | return 54 | } 55 | default: 56 | if body, err = json.Marshal(src); err != nil { 57 | Error(w, http.StatusInternalServerError, err, "failed to parse json") 58 | return 59 | } 60 | } 61 | w.WriteHeader(code) 62 | w.Write(body) 63 | } 64 | 65 | // Error is wrapped Respond when error response 66 | func Error(w http.ResponseWriter, code int, err error, msg string) { 67 | e := &ErrorResponse{ 68 | Message: msg, 69 | Error: err, 70 | } 71 | printDebugf("%s", e.String()) 72 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 73 | Respond(w, code, e) 74 | } 75 | 76 | // JSON is wrapped Respond when success response 77 | func JSON(w http.ResponseWriter, code int, src interface{}) { 78 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 79 | Respond(w, code, src) 80 | } 81 | 82 | // Handler user handler 83 | type Handler struct { 84 | Repository repository.UserRepository 85 | } 86 | 87 | // Routes returns the initialized router 88 | func (h Handler) Routes() *router.Router { 89 | r := router.NewRouter() 90 | r.Get("/user/:id", h.getUser) 91 | r.Get("/users", h.getUsers) 92 | r.Post("/user", h.createUser) 93 | return r 94 | } 95 | 96 | // Run start server 97 | func (h Handler) Run(port int) error { 98 | log.Printf("Server running at http://localhost:%d/", port) 99 | return http.ListenAndServe(fmt.Sprintf(":%d", port), h.Routes()) 100 | } 101 | 102 | func (h Handler) getUser(w http.ResponseWriter, r *http.Request, id int) { 103 | ctx := r.Context() 104 | 105 | interactor := application.UserInteractor{ 106 | Repository: h.Repository, 107 | } 108 | user, err := interactor.GetUser(ctx, id) 109 | if err != nil { 110 | Error(w, http.StatusNotFound, err, "failed to get user") 111 | return 112 | } 113 | JSON(w, http.StatusOK, user) 114 | } 115 | 116 | func (h Handler) getUsers(w http.ResponseWriter, r *http.Request) { 117 | ctx := r.Context() 118 | 119 | interactor := application.UserInteractor{ 120 | Repository: h.Repository, 121 | } 122 | users, err := interactor.GetUsers(ctx) 123 | if err != nil { 124 | Error(w, http.StatusNotFound, err, "failed to get user list") 125 | return 126 | } 127 | type payload struct { 128 | Users []*domain.User `json:"users"` 129 | } 130 | JSON(w, http.StatusOK, payload{Users: users}) 131 | } 132 | 133 | func (h Handler) createUser(w http.ResponseWriter, r *http.Request) { 134 | ctx := r.Context() 135 | 136 | type payload struct { 137 | Name string `json:"name"` 138 | } 139 | var p payload 140 | if err := json.NewDecoder(r.Body).Decode(&p); err != nil { 141 | Error(w, http.StatusBadRequest, err, "failed to parse request") 142 | return 143 | } 144 | 145 | interactor := application.UserInteractor{ 146 | Repository: h.Repository, 147 | } 148 | 149 | if err := interactor.AddUser(ctx, p.Name); err != nil { 150 | Error(w, http.StatusInternalServerError, err, "failed to create user") 151 | return 152 | } 153 | JSON(w, http.StatusCreated, nil) 154 | } 155 | -------------------------------------------------------------------------------- /interfaces/handler_test.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/golang/mock/gomock" 13 | "github.com/google/go-cmp/cmp" 14 | "github.com/takashabe/go-ddd-sample/domain" 15 | "github.com/takashabe/go-ddd-sample/domain/repository" 16 | ) 17 | 18 | func prepareServer(t *testing.T, h *Handler) *httptest.Server { 19 | return httptest.NewServer(h.Routes()) 20 | } 21 | 22 | func sendRequest(t *testing.T, method, url string, body io.Reader) *http.Response { 23 | req, err := http.NewRequest(method, url, body) 24 | if err != nil { 25 | t.Fatalf("want non error, got %#v", err) 26 | } 27 | res, err := http.DefaultClient.Do(req) 28 | if err != nil { 29 | t.Fatalf("want non error, got %#v", err) 30 | } 31 | return res 32 | } 33 | 34 | func TestGetUser(t *testing.T) { 35 | tests := []struct { 36 | input int 37 | wantBody []byte 38 | wantCode int 39 | mock func(*repository.MockUserRepository) 40 | }{ 41 | { 42 | input: 1, 43 | wantBody: []byte(`{"id":1,"name":"foo"}`), 44 | wantCode: http.StatusOK, 45 | mock: func(r *repository.MockUserRepository) { 46 | r.EXPECT().Get(gomock.Any(), 1).Return(&domain.User{ID: 1, Name: "foo"}, nil) 47 | }, 48 | }, 49 | { 50 | input: 0, 51 | wantBody: nil, 52 | wantCode: http.StatusNotFound, 53 | mock: func(r *repository.MockUserRepository) { 54 | r.EXPECT().Get(gomock.Any(), 0).Return(nil, sql.ErrNoRows) 55 | }, 56 | }, 57 | } 58 | for i, tt := range tests { 59 | t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { 60 | ctrl := gomock.NewController(t) 61 | defer ctrl.Finish() 62 | 63 | userRepo := repository.NewMockUserRepository(ctrl) 64 | tt.mock(userRepo) 65 | 66 | h := &Handler{ 67 | Repository: userRepo, 68 | } 69 | ts := prepareServer(t, h) 70 | defer ts.Close() 71 | 72 | res := sendRequest(t, "GET", fmt.Sprintf("%s/user/%d", ts.URL, tt.input), nil) 73 | defer res.Body.Close() 74 | 75 | if tt.wantCode != res.StatusCode { 76 | t.Errorf("want %d, got %d", tt.wantCode, res.StatusCode) 77 | } 78 | if res.StatusCode != http.StatusOK { 79 | return 80 | } 81 | 82 | payload, err := ioutil.ReadAll(res.Body) 83 | if err != nil { 84 | t.Fatalf("want non error, got %#v", err) 85 | } 86 | if diff := cmp.Diff(tt.wantBody, payload); diff != "" { 87 | t.Errorf("body mismatch %s", string(diff)) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /interfaces/main_test.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestMain(m *testing.M) { 9 | os.Exit(m.Run()) 10 | } 11 | -------------------------------------------------------------------------------- /interfaces/testdata/schema.sql: -------------------------------------------------------------------------------- 1 | ../../_sql/schema.sql -------------------------------------------------------------------------------- /interfaces/testdata/users.yml: -------------------------------------------------------------------------------- 1 | table: users 2 | record: 3 | - id: 1 4 | name: satoshi 5 | - id: 2 6 | name: kasumi 7 | --------------------------------------------------------------------------------