├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── employee ├── adapter │ ├── in │ │ └── web │ │ │ ├── getEmployeeDTO.go │ │ │ ├── getEmployeeDataHandler.go │ │ │ └── getEmployeeDomainDTOMapper.go │ └── out │ │ ├── db │ │ ├── inMemDB │ │ │ ├── inMemDB.go │ │ │ └── inMemRepository.go │ │ └── postgres │ │ │ └── postgresRepository.go │ │ └── persistence │ │ ├── employeeDataMapper.go │ │ ├── employeeEntity.go │ │ ├── employeePersistenceAdapter.go │ │ └── employeeRepository.go ├── application │ ├── port │ │ ├── in │ │ │ ├── employeeQueryID.go │ │ │ └── getEmployeeDetailsQuery.go │ │ └── out │ │ │ └── loadEmployeeDataPort.go │ └── service │ │ ├── getEmployeeDetailsService.go │ │ └── getEmployeeDetailsService_test.go └── domain │ ├── employee.go │ ├── employee_test.go │ ├── employeeid.go │ ├── employeename.go │ ├── money.go │ └── money_test.go ├── go.mod ├── go.sum ├── integrationtests └── adapter │ ├── employeePersistenceAdapter_test.go │ └── getEmployeeDataHandler_test.go ├── main.go └── scripts └── init.sql /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | __debug_bin 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # vscode 19 | .vscode 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine as build 2 | 3 | WORKDIR /src/ 4 | COPY . go.* /src/ 5 | RUN CGO_ENABLED=0 go build -o /bin/demo 6 | 7 | FROM scratch 8 | COPY --from=build /bin/demo /bin/demo 9 | ENTRYPOINT ["/bin/demo"] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 RubinThomas 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 | 2 | # docker build 3 | build: 4 | docker compose build 5 | 6 | # test 7 | test: build run 8 | sleep 5 && go test -coverprofile=cover.out ./... 9 | 10 | # integration 11 | test-integration: build run 12 | sleep 5 && go test -coverprofile=cover.out -coverpkg ./.../persistence,./.../web ./integrationtests/adapter/... 13 | 14 | # build and run 15 | run : build 16 | docker compose up -d --remove-orphans -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-employee 2 | Clean architecture in Go. 3 | 4 | Based on the learnings from the book [Get Your Hands Dirty on Clean Architecture](https://www.packtpub.com/product/get-your-hands-dirty-on-clean-architecture/9781839211966) by [Tom Hombergs](https://twitter.com/TomHombergs) 5 | 6 | # testing 7 | ## requires docker and make 8 | ``` 9 | unit tests with code coverage: 10 | TEST_PG_HOST=localhost TEST_PG_PORT=5432 TEST_PG_USER=postgres TEST_PG_PASSWORD=testpwd TEST_PG_DBNAME=postgres make test 11 | 12 | integration tests with code coverage: 13 | TEST_PG_HOST=localhost TEST_PG_PORT=5432 TEST_PG_USER=postgres TEST_PG_PASSWORD=testpwd TEST_PG_DBNAME=postgres make test-integration 14 | ``` 15 | 16 | 17 | # run 18 | ## requires docker and postgres 19 | 20 | ``` 21 | docker build . -t rubinthomasdev/go-employee:latest 22 | 23 | for postgres running on windows host 24 | docker run -p 8080:8080 -e PG_HOST=host.docker.internal -e PG_PORT=5432 -e PG_USER=postgres -e PG_PASSWORD=yourpgpassword -e PG_DBNAME=postgres --name employeeapi rubinthomasdev/go-employee:latest 25 | 26 | for postgres running on mac host 27 | docker run -p 8080:8080 -e PG_HOST=docker.for.mac.localhost -e PG_PORT=5432 -e PG_USER=postgres -e PG_PASSWORD=yourpgpassword -e PG_DBNAME=postgres --name employeeapi rubinthomasdev/go-employee:latest 28 | 29 | ``` 30 | 31 | # endpoints: 32 | ### to get all employees data: 33 | ``` 34 | curl --location --request GET 'localhost:8080/api/v1/employees' \ 35 | --header 'Content-Type: application/json' 36 | ``` 37 | 38 | ### to get a single employees data: 39 | ``` 40 | curl --location --request GET 'localhost:8080/api/v1/employees/1' \ 41 | --header 'Content-Type: application/json' 42 | ``` 43 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | # employeeapi: 4 | # restart: always 5 | # build: . 6 | # ports: 7 | # - "8080:8080" 8 | # depends_on: 9 | # - postgres 10 | # environment: 11 | # PG_HOST: postgres 12 | # PG_PORT: 5432 13 | # PG_USER: postgres 14 | # PG_PASSWORD: testpwd 15 | # PG_DBNAME: postgres 16 | postgres: 17 | image: postgres:13.3-alpine 18 | restart: always 19 | ports: 20 | - "5432:5432" 21 | volumes: 22 | - ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql 23 | environment: 24 | POSTGRES_PASSWORD: testpwd 25 | -------------------------------------------------------------------------------- /employee/adapter/in/web/getEmployeeDTO.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | type GetEmployeeDTO struct { 4 | EmployeeID int `json:"employeeID"` 5 | FirstName string `json:"firstName"` 6 | LastName string `json:"lastName"` 7 | BaseSalary float64 `json:"baseSalary"` 8 | Bonus float64 `json:"bonus"` 9 | TotalSalary float64 `json:"totalSalary"` 10 | } 11 | -------------------------------------------------------------------------------- /employee/adapter/in/web/getEmployeeDataHandler.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/rubinthomasdev/go-employee/employee/application/port/in" 10 | ) 11 | 12 | type GetEmployeeDataHandler struct { 13 | GetEmployeeDataUseCase in.GetEmployeeDetailsQuery 14 | Mapper GetEmployeeDomainDTOMapper 15 | } 16 | 17 | func (g GetEmployeeDataHandler) GetEmployeeDetails(w http.ResponseWriter, r *http.Request) { 18 | 19 | // return if method is not GET 20 | if r.Method != http.MethodGet { 21 | w.WriteHeader(http.StatusMethodNotAllowed) 22 | return 23 | } 24 | 25 | // create an encoder and set the response type as json 26 | enc := json.NewEncoder(w) 27 | w.Header().Set("Content-Type", "application/json") 28 | 29 | // if no id passed return all employees 30 | if r.URL.Path == "/api/v1/employees/" || r.URL.Path == "/api/v1/employees" { 31 | log.Println("getting all employees") 32 | empDTOSlice := []GetEmployeeDTO{} 33 | for _, empDomain := range g.GetEmployeeDataUseCase.GetAllEmployees() { 34 | empDTOSlice = append(empDTOSlice, g.Mapper.MapDomainToDTO(empDomain)) 35 | } 36 | err := enc.Encode(empDTOSlice) 37 | if err != nil { 38 | log.Println(err) 39 | w.WriteHeader(http.StatusInternalServerError) 40 | return 41 | } 42 | return 43 | } 44 | 45 | // return employee for the given id 46 | empID, err := strconv.Atoi(r.URL.Path[len("/api/v1/employees/"):]) 47 | if err != nil { 48 | log.Println(err) 49 | w.WriteHeader(http.StatusBadRequest) 50 | return 51 | } 52 | 53 | log.Printf("getting employee data for employee id : %d \n", empID) 54 | empModel := g.GetEmployeeDataUseCase.GetEmployeeDetails(in.EmployeeQueryID{ID: empID}) 55 | 56 | err = enc.Encode(g.Mapper.MapDomainToDTO(empModel)) 57 | if err != nil { 58 | log.Println(err) 59 | w.WriteHeader(http.StatusInternalServerError) 60 | return 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /employee/adapter/in/web/getEmployeeDomainDTOMapper.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import "github.com/rubinthomasdev/go-employee/employee/domain" 4 | 5 | type GetEmployeeDomainDTOMapper struct { 6 | } 7 | 8 | func (g GetEmployeeDomainDTOMapper) MapDomainToDTO(empDomain domain.Employee) GetEmployeeDTO { 9 | return GetEmployeeDTO{ 10 | EmployeeID: empDomain.EmployeeID.ID, 11 | FirstName: empDomain.Name.FirstName, 12 | LastName: empDomain.Name.LastName, 13 | BaseSalary: empDomain.BaseSalary.Amount, 14 | Bonus: empDomain.Bonus.Amount, 15 | TotalSalary: empDomain.CalculateTotalSalary().Amount, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /employee/adapter/out/db/inMemDB/inMemDB.go: -------------------------------------------------------------------------------- 1 | package inmemdb 2 | 3 | import "github.com/rubinthomasdev/go-employee/employee/adapter/out/persistence" 4 | 5 | type InMemDB struct { 6 | Employees map[int]persistence.EmployeeEntity 7 | } 8 | -------------------------------------------------------------------------------- /employee/adapter/out/db/inMemDB/inMemRepository.go: -------------------------------------------------------------------------------- 1 | package inmemdb 2 | 3 | import "github.com/rubinthomasdev/go-employee/employee/adapter/out/persistence" 4 | 5 | type InMemRepository struct { 6 | Db *InMemDB 7 | } 8 | 9 | func (i InMemRepository) FindByID(id int) persistence.EmployeeEntity { 10 | return i.Db.Employees[id] 11 | } 12 | 13 | func (i InMemRepository) FindAll() []persistence.EmployeeEntity { 14 | employees := []persistence.EmployeeEntity{} 15 | for _, emp := range i.Db.Employees { 16 | employees = append(employees, emp) 17 | } 18 | return employees 19 | } 20 | 21 | func (i InMemRepository) Initialize() { 22 | e1 := persistence.EmployeeEntity{ 23 | EmployeeID: 1, 24 | FirstName: "john", 25 | LastName: "doe", 26 | BaseSalary: 100.0, 27 | Bonus: 12.5, 28 | } 29 | 30 | e2 := persistence.EmployeeEntity{ 31 | EmployeeID: 2, 32 | FirstName: "jane", 33 | LastName: "doe", 34 | BaseSalary: 100.0, 35 | Bonus: 12.57, 36 | } 37 | 38 | i.Db.Employees = make(map[int]persistence.EmployeeEntity) 39 | i.Db.Employees[1] = e1 40 | i.Db.Employees[2] = e2 41 | 42 | } 43 | -------------------------------------------------------------------------------- /employee/adapter/out/db/postgres/postgresRepository.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/rubinthomasdev/go-employee/employee/adapter/out/persistence" 9 | ) 10 | 11 | type PostgresRepository struct { 12 | DB *sql.DB 13 | } 14 | 15 | func (p PostgresRepository) FindByID(id int) persistence.EmployeeEntity { 16 | employeeEntity := persistence.EmployeeEntity{} 17 | statement := `select id,firstName,lastName,baseSalary,bonus from public.employee where ID=$1;` 18 | row := p.DB.QueryRow(statement, id) 19 | switch err := row.Scan(&employeeEntity.EmployeeID, &employeeEntity.FirstName, &employeeEntity.LastName, &employeeEntity.BaseSalary, &employeeEntity.Bonus); err { 20 | case sql.ErrNoRows: 21 | fmt.Println("No rows were returned!") 22 | case nil: 23 | fmt.Println(employeeEntity) 24 | default: 25 | fmt.Println("err :", err) 26 | } 27 | return employeeEntity 28 | } 29 | 30 | func (p PostgresRepository) FindAll() []persistence.EmployeeEntity { 31 | employees := []persistence.EmployeeEntity{} 32 | statement := `select id,firstName,lastName,baseSalary,bonus from public.employee limit 20` 33 | rows, err := p.DB.Query(statement) 34 | if err != nil { 35 | log.Println("Error fetching employees data. ", err) 36 | return employees 37 | } 38 | defer rows.Close() 39 | 40 | for rows.Next() { 41 | employeeEntity := persistence.EmployeeEntity{} 42 | switch err = rows.Scan(&employeeEntity.EmployeeID, &employeeEntity.FirstName, &employeeEntity.LastName, &employeeEntity.BaseSalary, &employeeEntity.Bonus); err { 43 | case sql.ErrNoRows: 44 | fmt.Println("No rows were returned!") 45 | case nil: 46 | fmt.Println(employeeEntity) 47 | employees = append(employees, employeeEntity) 48 | default: 49 | fmt.Println("err :", err) 50 | } 51 | } 52 | return employees 53 | } 54 | -------------------------------------------------------------------------------- /employee/adapter/out/persistence/employeeDataMapper.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "github.com/rubinthomasdev/go-employee/employee/domain" 5 | ) 6 | 7 | type EmployeeDataMapper struct{} 8 | 9 | func (e EmployeeDataMapper) MapToDomain(empEntity EmployeeEntity) domain.Employee { 10 | return domain.Employee{ 11 | EmployeeID: domain.EmployeeID{ID: empEntity.EmployeeID}, 12 | Name: domain.EmployeeName{FirstName: empEntity.FirstName, LastName: empEntity.LastName}, 13 | BaseSalary: domain.Money{Amount: empEntity.BaseSalary}, 14 | Bonus: domain.Money{Amount: float64(empEntity.Bonus)}, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /employee/adapter/out/persistence/employeeEntity.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | type EmployeeEntity struct { 4 | EmployeeID int `json:"employeeID"` 5 | FirstName string `json:"firstName"` 6 | LastName string `json:"lastName"` 7 | BaseSalary float64 `json:"baseSalary"` 8 | Bonus float64 `json:"bonus"` 9 | } -------------------------------------------------------------------------------- /employee/adapter/out/persistence/employeePersistenceAdapter.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import "github.com/rubinthomasdev/go-employee/employee/domain" 4 | 5 | type EmployeePersistenceAdapter struct { 6 | EmployeeRepo EmployeeRepository 7 | Mapper EmployeeDataMapper 8 | } 9 | 10 | func (e EmployeePersistenceAdapter) GetEmployeeDataFromPersistence(id domain.EmployeeID) domain.Employee { 11 | empEntity := e.EmployeeRepo.FindByID(id.ID) 12 | return e.Mapper.MapToDomain(empEntity) 13 | } 14 | 15 | func (e EmployeePersistenceAdapter) GetAllEmployees() []domain.Employee { 16 | empDomainSlice := []domain.Employee{} 17 | for _, empEntity := range e.EmployeeRepo.FindAll() { 18 | empDomainSlice = append(empDomainSlice, e.Mapper.MapToDomain(empEntity)) 19 | } 20 | return empDomainSlice 21 | } 22 | -------------------------------------------------------------------------------- /employee/adapter/out/persistence/employeeRepository.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | type EmployeeRepository interface { 4 | FindByID(int) EmployeeEntity 5 | FindAll() []EmployeeEntity 6 | } 7 | -------------------------------------------------------------------------------- /employee/application/port/in/employeeQueryID.go: -------------------------------------------------------------------------------- 1 | package in 2 | 3 | type EmployeeQueryID struct { 4 | ID int 5 | } 6 | -------------------------------------------------------------------------------- /employee/application/port/in/getEmployeeDetailsQuery.go: -------------------------------------------------------------------------------- 1 | package in 2 | 3 | import "github.com/rubinthomasdev/go-employee/employee/domain" 4 | 5 | type GetEmployeeDetailsQuery interface { 6 | GetEmployeeDetails(id EmployeeQueryID) domain.Employee 7 | GetAllEmployees() []domain.Employee 8 | } 9 | -------------------------------------------------------------------------------- /employee/application/port/out/loadEmployeeDataPort.go: -------------------------------------------------------------------------------- 1 | package out 2 | 3 | import "github.com/rubinthomasdev/go-employee/employee/domain" 4 | 5 | type LoadEmployeeDataPort interface { 6 | GetEmployeeDataFromPersistence(id domain.EmployeeID) domain.Employee 7 | GetAllEmployees() []domain.Employee 8 | } 9 | -------------------------------------------------------------------------------- /employee/application/service/getEmployeeDetailsService.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/rubinthomasdev/go-employee/employee/application/port/in" 5 | "github.com/rubinthomasdev/go-employee/employee/application/port/out" 6 | "github.com/rubinthomasdev/go-employee/employee/domain" 7 | ) 8 | 9 | type GetEmployeeDetailsService struct { 10 | LoadEmployeeDataPort out.LoadEmployeeDataPort 11 | } 12 | 13 | func (g GetEmployeeDetailsService) GetEmployeeDetails(inputID in.EmployeeQueryID) domain.Employee { 14 | return g.LoadEmployeeDataPort.GetEmployeeDataFromPersistence(domain.EmployeeID{ID: inputID.ID}) 15 | } 16 | 17 | func (g GetEmployeeDetailsService) GetAllEmployees() []domain.Employee { 18 | return g.LoadEmployeeDataPort.GetAllEmployees() 19 | } 20 | -------------------------------------------------------------------------------- /employee/application/service/getEmployeeDetailsService_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/rubinthomasdev/go-employee/employee/application/port/in" 8 | "github.com/rubinthomasdev/go-employee/employee/domain" 9 | ) 10 | 11 | type MockLoadEmployeeDataPort struct{} 12 | 13 | func (m MockLoadEmployeeDataPort) GetEmployeeDataFromPersistence(id domain.EmployeeID) domain.Employee { 14 | return domain.Employee{ 15 | EmployeeID: domain.EmployeeID{ID: 1}, 16 | Name: domain.EmployeeName{ 17 | FirstName: "jane", 18 | LastName: "doe", 19 | }, 20 | BaseSalary: domain.Money{Amount: 10.5}, 21 | Bonus: domain.Money{Amount: 3.8}, 22 | } 23 | } 24 | 25 | func (m MockLoadEmployeeDataPort) GetAllEmployees() []domain.Employee { 26 | return []domain.Employee{ 27 | { 28 | EmployeeID: domain.EmployeeID{ID: 1}, 29 | Name: domain.EmployeeName{ 30 | FirstName: "john", 31 | LastName: "doe", 32 | }, 33 | BaseSalary: domain.Money{Amount: 10.5}, 34 | Bonus: domain.Money{Amount: 3.8}, 35 | }, 36 | { 37 | EmployeeID: domain.EmployeeID{ID: 2}, 38 | Name: domain.EmployeeName{ 39 | FirstName: "jane", 40 | LastName: "doe", 41 | }, 42 | BaseSalary: domain.Money{Amount: 10.5}, 43 | Bonus: domain.Money{Amount: 3.8}, 44 | }, 45 | } 46 | } 47 | 48 | func TestGetEmployeeDetails(t *testing.T) { 49 | inputID := in.EmployeeQueryID{ID: 1} 50 | service := GetEmployeeDetailsService{LoadEmployeeDataPort: MockLoadEmployeeDataPort{}} 51 | 52 | want := MockLoadEmployeeDataPort{}.GetEmployeeDataFromPersistence(domain.EmployeeID(inputID)) 53 | 54 | got := service.GetEmployeeDetails(inputID) 55 | 56 | if !reflect.DeepEqual(want, got) { 57 | t.Errorf("Get Employee Details failed. Wanted : %v, Got %v", want, got) 58 | } 59 | 60 | } 61 | 62 | func TestGetAllEmployeeDetails(t *testing.T) { 63 | service := GetEmployeeDetailsService{LoadEmployeeDataPort: MockLoadEmployeeDataPort{}} 64 | 65 | want := MockLoadEmployeeDataPort{}.GetAllEmployees() 66 | 67 | got := service.GetAllEmployees() 68 | 69 | if !reflect.DeepEqual(want, got) { 70 | t.Errorf("Get Employee Details failed. Wanted : %v, Got %v", want, got) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /employee/domain/employee.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Employee struct { 4 | EmployeeID EmployeeID `json:"employeeID"` 5 | Name EmployeeName `json:"employeeName"` 6 | BaseSalary Money `json:"baseSalary"` 7 | Bonus Money `json:"bonus"` 8 | } 9 | 10 | func (e Employee) CalculateTotalSalary() Money { 11 | return e.BaseSalary.Add(e.Bonus) 12 | } 13 | -------------------------------------------------------------------------------- /employee/domain/employee_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "testing" 4 | 5 | func TestGetTotalSalary(t *testing.T) { 6 | testCases := []struct { 7 | employee Employee 8 | expected Money 9 | }{ 10 | { 11 | Employee{EmployeeID: EmployeeID{1}, Name: EmployeeName{"john", "doe"}, BaseSalary: Money{10.0}, Bonus: Money{1.2}}, 12 | Money{11.2}, 13 | }, 14 | } 15 | 16 | for _, tc := range testCases { 17 | want := tc.expected 18 | got := tc.employee.CalculateTotalSalary() 19 | 20 | if want != got { 21 | t.Errorf("Employee get total Salary failed. Wanted %.1f, Got %.1f", want.Amount, got.Amount) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /employee/domain/employeeid.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type EmployeeID struct { 4 | ID int `json:"id"` 5 | } 6 | -------------------------------------------------------------------------------- /employee/domain/employeename.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type EmployeeName struct { 4 | FirstName string `json:"firstName"` 5 | LastName string `json:"lastName"` 6 | } 7 | -------------------------------------------------------------------------------- /employee/domain/money.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "math" 4 | 5 | type Money struct { 6 | Amount float64 `json:"amount"` 7 | } 8 | 9 | func (m Money) Add(n Money) Money { 10 | return Money{math.Round((m.Amount+n.Amount)*10) / 10} 11 | } 12 | -------------------------------------------------------------------------------- /employee/domain/money_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "testing" 4 | 5 | func TestMoneyAdd(t *testing.T) { 6 | testCases := []struct { 7 | a Money 8 | b Money 9 | expected Money 10 | }{ 11 | {Money{10.5}, Money{11.5}, Money{22.0}}, 12 | {Money{10.56}, Money{11.517}, Money{22.1}}, 13 | {Money{10.12}, Money{11.34}, Money{21.5}}, 14 | {Money{10.12}, Money{-11.34}, Money{-1.2}}, 15 | {Money{10.12}, Money{-11.77}, Money{-1.7}}, 16 | {Money{10.12}, Money{-11.76}, Money{-1.6}}, 17 | } 18 | 19 | for _, tc := range testCases { 20 | want := tc.expected 21 | got := tc.a.Add(tc.b) 22 | 23 | if want != got { 24 | t.Errorf("Money add failed. Wanted %.1f, Got %.1f", want.Amount, got.Amount) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rubinthomasdev/go-employee 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gorilla/mux v1.8.0 // indirect 7 | github.com/lib/pq v1.10.2 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 2 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 3 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 4 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 5 | -------------------------------------------------------------------------------- /integrationtests/adapter/employeePersistenceAdapter_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "testing" 10 | 11 | _ "github.com/lib/pq" 12 | "github.com/rubinthomasdev/go-employee/employee/adapter/out/db/postgres" 13 | "github.com/rubinthomasdev/go-employee/employee/adapter/out/persistence" 14 | "github.com/rubinthomasdev/go-employee/employee/domain" 15 | ) 16 | 17 | func TestGetEmployeeDataFromPersistence(t *testing.T) { 18 | // create DB connection 19 | host := os.Getenv("TEST_PG_HOST") 20 | port, _ := strconv.Atoi(os.Getenv("TEST_PG_PORT")) 21 | user := os.Getenv("TEST_PG_USER") 22 | password := os.Getenv("TEST_PG_PASSWORD") 23 | dbname := os.Getenv("TEST_PG_DBNAME") 24 | psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+ 25 | "password=%s dbname=%s sslmode=disable", 26 | host, port, user, password, dbname) 27 | 28 | db, err := sql.Open("postgres", psqlInfo) 29 | if err != nil { 30 | panic(err) 31 | } 32 | defer db.Close() 33 | 34 | err = db.Ping() 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | // create repo 40 | getEmpRepo := postgres.PostgresRepository{DB: db} 41 | 42 | // ** postgres end ** 43 | 44 | // create adapter 45 | getEmpAdapter := persistence.EmployeePersistenceAdapter{EmployeeRepo: getEmpRepo, Mapper: persistence.EmployeeDataMapper{}} 46 | 47 | wantEmp := domain.Employee{ 48 | EmployeeID: domain.EmployeeID{ID: 1}, 49 | Name: domain.EmployeeName{FirstName: "test1", LastName: "user"}, 50 | BaseSalary: domain.Money{Amount: 12.12}, 51 | Bonus: domain.Money{Amount: 13.13}, 52 | } 53 | gotEmp := getEmpAdapter.GetEmployeeDataFromPersistence(domain.EmployeeID{ID: 1}) 54 | 55 | if !reflect.DeepEqual(wantEmp, gotEmp) { 56 | t.Errorf("Data mismatched from DB. Wanted : %v, Got : %v\n", wantEmp, gotEmp) 57 | } 58 | } 59 | 60 | func TestGetAllEmployees(t *testing.T) { 61 | // create DB connection 62 | host := os.Getenv("TEST_PG_HOST") 63 | port, _ := strconv.Atoi(os.Getenv("TEST_PG_PORT")) 64 | user := os.Getenv("TEST_PG_USER") 65 | password := os.Getenv("TEST_PG_PASSWORD") 66 | dbname := os.Getenv("TEST_PG_DBNAME") 67 | psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+ 68 | "password=%s dbname=%s sslmode=disable", 69 | host, port, user, password, dbname) 70 | 71 | db, err := sql.Open("postgres", psqlInfo) 72 | if err != nil { 73 | panic(err) 74 | } 75 | defer db.Close() 76 | 77 | err = db.Ping() 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | // create repo 83 | getEmpRepo := postgres.PostgresRepository{DB: db} 84 | 85 | // ** postgres end ** 86 | 87 | // create adapter 88 | getEmpAdapter := persistence.EmployeePersistenceAdapter{EmployeeRepo: getEmpRepo, Mapper: persistence.EmployeeDataMapper{}} 89 | 90 | wantEmp := []domain.Employee{ 91 | { 92 | EmployeeID: domain.EmployeeID{ID: 1}, 93 | Name: domain.EmployeeName{FirstName: "test1", LastName: "user"}, 94 | BaseSalary: domain.Money{Amount: 12.12}, 95 | Bonus: domain.Money{Amount: 13.13}, 96 | }, 97 | { 98 | EmployeeID: domain.EmployeeID{ID: 2}, 99 | Name: domain.EmployeeName{FirstName: "test2", LastName: "user"}, 100 | BaseSalary: domain.Money{Amount: 12.56}, 101 | Bonus: domain.Money{Amount: 13.18}, 102 | }, 103 | } 104 | gotEmp := getEmpAdapter.GetAllEmployees() 105 | 106 | if !reflect.DeepEqual(wantEmp, gotEmp) { 107 | t.Errorf("Data mismatched from DB. Wanted : %v, Got : %v\n", wantEmp, gotEmp) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /integrationtests/adapter/getEmployeeDataHandler_test.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/rubinthomasdev/go-employee/employee/adapter/in/web" 12 | "github.com/rubinthomasdev/go-employee/employee/application/port/in" 13 | "github.com/rubinthomasdev/go-employee/employee/domain" 14 | ) 15 | 16 | type MockGetEmployeeDataUseCase struct{} 17 | 18 | func (m MockGetEmployeeDataUseCase) GetEmployeeDetails(id in.EmployeeQueryID) domain.Employee { 19 | return domain.Employee{ 20 | EmployeeID: domain.EmployeeID{ID: 100}, 21 | Name: domain.EmployeeName{FirstName: "test", LastName: "user"}, 22 | BaseSalary: domain.Money{Amount: 12.12}, 23 | Bonus: domain.Money{Amount: 14.14}, 24 | } 25 | } 26 | 27 | func (m MockGetEmployeeDataUseCase) GetAllEmployees() []domain.Employee { 28 | return []domain.Employee{ 29 | { 30 | EmployeeID: domain.EmployeeID{ID: 123}, 31 | Name: domain.EmployeeName{FirstName: "test", LastName: "user"}, 32 | BaseSalary: domain.Money{Amount: 12.12}, 33 | Bonus: domain.Money{Amount: 14.14}, 34 | }, 35 | } 36 | } 37 | 38 | func TestGetEmployeeDetailsForID(t *testing.T) { 39 | getHandler := web.GetEmployeeDataHandler{ 40 | GetEmployeeDataUseCase: MockGetEmployeeDataUseCase{}, 41 | Mapper: web.GetEmployeeDomainDTOMapper{}, 42 | } 43 | 44 | wantStatus := 200 45 | wantEmployeeID := 100 46 | 47 | w := httptest.NewRecorder() 48 | r := httptest.NewRequest(http.MethodGet, "http://localhost:8080/api/v1/employees/1", nil) 49 | getHandler.GetEmployeeDetails(w, r) 50 | 51 | resp := w.Result() 52 | body, _ := ioutil.ReadAll(resp.Body) 53 | 54 | var gotEmp web.GetEmployeeDTO 55 | fmt.Println(string(body)) 56 | err := json.Unmarshal(body, &gotEmp) 57 | if err != nil { 58 | t.Errorf("error json parsing in web adapter call. %v", err) 59 | } 60 | 61 | if wantEmployeeID != gotEmp.EmployeeID { 62 | t.Errorf("web adaper call failed. Wanted %d, Got %d", wantEmployeeID, gotEmp.EmployeeID) 63 | } 64 | 65 | if wantStatus != w.Code { 66 | t.Errorf("web adaper call status failed. Wanted %d, Got %d", wantStatus, w.Code) 67 | } 68 | } 69 | 70 | func TestGetAllEmployeeDetails(t *testing.T) { 71 | getHandler := web.GetEmployeeDataHandler{ 72 | GetEmployeeDataUseCase: MockGetEmployeeDataUseCase{}, 73 | Mapper: web.GetEmployeeDomainDTOMapper{}, 74 | } 75 | 76 | wantStatus := 200 77 | wantEmployeeID := 123 78 | 79 | w := httptest.NewRecorder() 80 | r := httptest.NewRequest(http.MethodGet, "http://localhost:8080/api/v1/employees", nil) 81 | getHandler.GetEmployeeDetails(w, r) 82 | 83 | resp := w.Result() 84 | body, _ := ioutil.ReadAll(resp.Body) 85 | 86 | var gotEmp []web.GetEmployeeDTO 87 | fmt.Println(string(body)) 88 | err := json.Unmarshal(body, &gotEmp) 89 | if err != nil { 90 | t.Errorf("error json parsing in web adapter call. %v", err) 91 | } 92 | 93 | if wantEmployeeID != gotEmp[0].EmployeeID { 94 | t.Errorf("web adaper call failed. Wanted %d, Got %d", wantEmployeeID, gotEmp[0].EmployeeID) 95 | } 96 | 97 | if wantStatus != w.Code { 98 | t.Errorf("web adaper call status failed. Wanted %d, Got %d", wantStatus, w.Code) 99 | } 100 | } 101 | 102 | func TestGetAllReturnsIfMethodIsNotGet(t *testing.T) { 103 | getHandler := web.GetEmployeeDataHandler{ 104 | GetEmployeeDataUseCase: MockGetEmployeeDataUseCase{}, 105 | Mapper: web.GetEmployeeDomainDTOMapper{}, 106 | } 107 | 108 | wantStatus := http.StatusMethodNotAllowed 109 | 110 | w := httptest.NewRecorder() 111 | r := httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/employees", nil) 112 | getHandler.GetEmployeeDetails(w, r) 113 | 114 | if wantStatus != w.Code { 115 | t.Errorf("web adaper call status failed. Wanted %d, Got %d", wantStatus, w.Code) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/gorilla/mux" 12 | _ "github.com/lib/pq" 13 | "github.com/rubinthomasdev/go-employee/employee/adapter/in/web" 14 | "github.com/rubinthomasdev/go-employee/employee/adapter/out/db/postgres" 15 | "github.com/rubinthomasdev/go-employee/employee/adapter/out/persistence" 16 | "github.com/rubinthomasdev/go-employee/employee/application/service" 17 | ) 18 | 19 | func main() { 20 | 21 | // ** inmem start ** 22 | // create DB connection 23 | // inMemDB := inmemdb.InMemDB{} 24 | // create repo 25 | // getEmpRepo := inmemdb.InMemRepository{Db: &inMemDB} 26 | // getEmpRepo.Initialize() 27 | // ** inmem end ** 28 | 29 | // ** postgres start ** 30 | // create DB connection 31 | host := os.Getenv("PG_HOST") 32 | port, _ := strconv.Atoi(os.Getenv("PG_PORT")) 33 | user := os.Getenv("PG_USER") 34 | password := os.Getenv("PG_PASSWORD") 35 | dbname := os.Getenv("PG_DBNAME") 36 | psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+ 37 | "password=%s dbname=%s sslmode=disable", 38 | host, port, user, password, dbname) 39 | 40 | db, err := sql.Open("postgres", psqlInfo) 41 | if err != nil { 42 | panic(err) 43 | } 44 | defer db.Close() 45 | 46 | err = db.Ping() 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | // create repo 52 | getEmpRepo := postgres.PostgresRepository{DB: db} 53 | // ** postgres end ** 54 | 55 | // create adapter 56 | getEmpAdapter := persistence.EmployeePersistenceAdapter{EmployeeRepo: getEmpRepo, Mapper: persistence.EmployeeDataMapper{}} 57 | 58 | // create service 59 | getEmpSvc := service.GetEmployeeDetailsService{LoadEmployeeDataPort: getEmpAdapter} 60 | 61 | // create handler 62 | getEmpHandler := web.GetEmployeeDataHandler{GetEmployeeDataUseCase: getEmpSvc} 63 | 64 | //http handler registration 65 | // http.HandleFunc("/api/v1/employees/", getEmpHandler.GetEmployeeDetails) 66 | 67 | // gorilla mux start 68 | r := mux.NewRouter() 69 | r.HandleFunc("/api/v1/employees/{empid}", getEmpHandler.GetEmployeeDetails) 70 | r.HandleFunc("/api/v1/employees", getEmpHandler.GetEmployeeDetails) 71 | http.Handle("/", r) 72 | // gorilla mux end 73 | 74 | //start the server 75 | log.Fatal(http.ListenAndServe(":8080", nil)) 76 | } 77 | -------------------------------------------------------------------------------- /scripts/init.sql: -------------------------------------------------------------------------------- 1 | -- Table: public.employee 2 | 3 | -- DROP TABLE public.employee; 4 | 5 | CREATE TABLE public.employee 6 | ( 7 | lastname character varying COLLATE pg_catalog."default" NOT NULL, 8 | id integer NOT NULL, 9 | firstname character varying COLLATE pg_catalog."default" NOT NULL, 10 | basesalary double precision NOT NULL, 11 | bonus double precision NOT NULL, 12 | CONSTRAINT employee_pkey PRIMARY KEY (id) 13 | ) 14 | 15 | TABLESPACE pg_default; 16 | 17 | ALTER TABLE public.employee 18 | OWNER to postgres; 19 | 20 | 21 | INSERT INTO public.employee( 22 | lastname, id, firstname, basesalary, bonus) 23 | VALUES ('user', 1, 'test1', 12.12, 13.13); 24 | INSERT INTO public.employee( 25 | lastname, id, firstname, basesalary, bonus) 26 | VALUES ('user', 2, 'test2', 12.56, 13.18); --------------------------------------------------------------------------------