Skip to content

Commit c6b25e6

Browse files
committed
Add lesson03 / video02
1 parent 329be76 commit c6b25e6

File tree

14 files changed

+504
-0
lines changed

14 files changed

+504
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package bootstrap
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
7+
"github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/internal/platform/server"
8+
"github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/internal/platform/storage/mysql"
9+
_ "github.com/go-sql-driver/mysql"
10+
)
11+
12+
const (
13+
host = "localhost"
14+
port = 8080
15+
16+
dbUser = "codely"
17+
dbPass = "codely"
18+
dbHost = "localhost"
19+
dbPort = "3306"
20+
dbName = "codely"
21+
)
22+
23+
func Run() error {
24+
mysqlURI := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName)
25+
db, err := sql.Open("mysql", mysqlURI)
26+
if err != nil {
27+
return err
28+
}
29+
30+
courseRepository := mysql.NewCourseRepository(db)
31+
32+
srv := server.New(host, port, courseRepository)
33+
return srv.Run()
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
import (
4+
"log"
5+
6+
"github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/cmd/api/bootstrap"
7+
)
8+
9+
func main() {
10+
if err := bootstrap.Run(); err != nil {
11+
log.Fatal(err)
12+
}
13+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package mooc
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/google/uuid"
9+
)
10+
11+
var ErrInvalidCourseID = errors.New("invalid Course ID")
12+
13+
// CourseID represents the course unique identifier.
14+
type CourseID struct {
15+
value string
16+
}
17+
18+
// NewCourseID instantiate the VO for CourseID
19+
func NewCourseID(value string) (CourseID, error) {
20+
v, err := uuid.Parse(value)
21+
if err != nil {
22+
return CourseID{}, fmt.Errorf("%w: %s", ErrInvalidCourseID, value)
23+
}
24+
25+
return CourseID{
26+
value: v.String(),
27+
}, nil
28+
}
29+
30+
// String type converts the CourseID into string.
31+
func (id CourseID) String() string {
32+
return id.value
33+
}
34+
35+
var ErrEmptyCourseName = errors.New("the field Course Name can not be empty")
36+
37+
// CourseName represents the course name.
38+
type CourseName struct {
39+
value string
40+
}
41+
42+
// NewCourseName instantiate VO for CourseName
43+
func NewCourseName(value string) (CourseName, error) {
44+
if value == "" {
45+
return CourseName{}, ErrEmptyCourseName
46+
}
47+
48+
return CourseName{
49+
value: value,
50+
}, nil
51+
}
52+
53+
// String type converts the CourseName into string.
54+
func (name CourseName) String() string {
55+
return name.value
56+
}
57+
58+
var ErrEmptyDuration = errors.New("the field Duration can not be empty")
59+
60+
// CourseDuration represents the course duration.
61+
type CourseDuration struct {
62+
value string
63+
}
64+
65+
func NewCourseDuration(value string) (CourseDuration, error) {
66+
if value == "" {
67+
return CourseDuration{}, ErrEmptyDuration
68+
}
69+
70+
return CourseDuration{
71+
value: value,
72+
}, nil
73+
}
74+
75+
// String type converts the CourseDuration into string.
76+
func (duration CourseDuration) String() string {
77+
return duration.value
78+
}
79+
80+
// CourseRepository defines the expected behaviour from a course storage.
81+
type CourseRepository interface {
82+
Save(ctx context.Context, course Course) error
83+
}
84+
85+
//go:generate mockery --case=snake --outpkg=storagemocks --output=platform/storage/storagemocks --name=CourseRepository
86+
87+
// Course is the data structure that represents a course.
88+
type Course struct {
89+
id CourseID
90+
name CourseName
91+
duration CourseDuration
92+
}
93+
94+
// NewCourse creates a new course.
95+
func NewCourse(id, name, duration string) (Course, error) {
96+
idVO, err := NewCourseID(id)
97+
if err != nil {
98+
return Course{}, err
99+
}
100+
101+
nameVO, err := NewCourseName(name)
102+
if err != nil {
103+
return Course{}, err
104+
}
105+
106+
durationVO, err := NewCourseDuration(duration)
107+
if err != nil {
108+
return Course{}, err
109+
}
110+
111+
return Course{
112+
id: idVO,
113+
name: nameVO,
114+
duration: durationVO,
115+
}, nil
116+
}
117+
118+
// ID returns the course unique identifier.
119+
func (c Course) ID() CourseID {
120+
return c.id
121+
}
122+
123+
// Name returns the course name.
124+
func (c Course) Name() CourseName {
125+
return c.name
126+
}
127+
128+
// Duration returns the course duration.
129+
func (c Course) Duration() CourseDuration {
130+
return c.duration
131+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package courses
2+
3+
import (
4+
"net/http"
5+
6+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/internal"
7+
"github.com/gin-gonic/gin"
8+
)
9+
10+
type createRequest struct {
11+
ID string `json:"id" binding:"required"`
12+
Name string `json:"name" binding:"required"`
13+
Duration string `json:"duration" binding:"required"`
14+
}
15+
16+
// CreateHandler returns an HTTP handler for courses creation.
17+
func CreateHandler(courseRepository mooc.CourseRepository) gin.HandlerFunc {
18+
return func(ctx *gin.Context) {
19+
var req createRequest
20+
if err := ctx.BindJSON(&req); err != nil {
21+
ctx.JSON(http.StatusBadRequest, err.Error())
22+
return
23+
}
24+
25+
course, err := mooc.NewCourse(req.ID, req.Name, req.Duration)
26+
if err != nil {
27+
ctx.JSON(http.StatusBadRequest, err.Error())
28+
return
29+
}
30+
31+
if err := courseRepository.Save(ctx, course); err != nil {
32+
ctx.JSON(http.StatusInternalServerError, err.Error())
33+
return
34+
}
35+
36+
ctx.Status(http.StatusCreated)
37+
}
38+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package courses
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/internal/platform/storage/storagemocks"
11+
"github.com/gin-gonic/gin"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/mock"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestHandler_Create(t *testing.T) {
18+
courseRepository := new(storagemocks.CourseRepository)
19+
courseRepository.On("Save", mock.Anything, mock.AnythingOfType("mooc.Course")).Return(nil)
20+
21+
gin.SetMode(gin.TestMode)
22+
r := gin.New()
23+
r.POST("/courses", CreateHandler(courseRepository))
24+
25+
t.Run("given an invalid request it returns 400", func(t *testing.T) {
26+
createCourseReq := createRequest{
27+
Name: "Demo Course",
28+
Duration: "10 months",
29+
}
30+
31+
b, err := json.Marshal(createCourseReq)
32+
require.NoError(t, err)
33+
34+
req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
35+
require.NoError(t, err)
36+
37+
rec := httptest.NewRecorder()
38+
r.ServeHTTP(rec, req)
39+
40+
res := rec.Result()
41+
defer res.Body.Close()
42+
43+
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
44+
})
45+
46+
t.Run("given a valid request it returns 201", func(t *testing.T) {
47+
createCourseReq := createRequest{
48+
ID: "8a1c5cdc-ba57-445a-994d-aa412d23723f",
49+
Name: "Demo Course",
50+
Duration: "10 months",
51+
}
52+
53+
b, err := json.Marshal(createCourseReq)
54+
require.NoError(t, err)
55+
56+
req, err := http.NewRequest(http.MethodPost, "/courses", bytes.NewBuffer(b))
57+
require.NoError(t, err)
58+
59+
rec := httptest.NewRecorder()
60+
r.ServeHTTP(rec, req)
61+
62+
res := rec.Result()
63+
defer res.Body.Close()
64+
65+
assert.Equal(t, http.StatusCreated, res.StatusCode)
66+
})
67+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package health
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gin-gonic/gin"
7+
)
8+
9+
// CheckHandler returns an HTTP handler to perform health checks.
10+
func CheckHandler() gin.HandlerFunc {
11+
return func(ctx *gin.Context) {
12+
ctx.String(http.StatusOK, "everything is ok!")
13+
}
14+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package health
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/gin-gonic/gin"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestHandler_Check(t *testing.T) {
14+
gin.SetMode(gin.TestMode)
15+
r := gin.New()
16+
r.GET("/health", CheckHandler())
17+
18+
t.Run("it returns 200", func(t *testing.T) {
19+
req, err := http.NewRequest(http.MethodGet, "/health", nil)
20+
require.NoError(t, err)
21+
22+
rec := httptest.NewRecorder()
23+
r.ServeHTTP(rec, req)
24+
25+
res := rec.Result()
26+
defer res.Body.Close()
27+
28+
assert.Equal(t, http.StatusOK, res.StatusCode)
29+
})
30+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
mooc "github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/internal"
8+
"github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/internal/platform/server/handler/courses"
9+
"github.com/CodelyTV/go-hexagonal_http_api-course/03-02-repository-test/internal/platform/server/handler/health"
10+
"github.com/gin-gonic/gin"
11+
)
12+
13+
type Server struct {
14+
httpAddr string
15+
engine *gin.Engine
16+
17+
// deps
18+
courseRepository mooc.CourseRepository
19+
}
20+
21+
func New(host string, port uint, courseRepository mooc.CourseRepository) Server {
22+
srv := Server{
23+
engine: gin.New(),
24+
httpAddr: fmt.Sprintf("%s:%d", host, port),
25+
26+
courseRepository: courseRepository,
27+
}
28+
29+
srv.registerRoutes()
30+
return srv
31+
}
32+
33+
func (s *Server) Run() error {
34+
log.Println("Server running on", s.httpAddr)
35+
return s.engine.Run(s.httpAddr)
36+
}
37+
38+
func (s *Server) registerRoutes() {
39+
s.engine.GET("/health", health.CheckHandler())
40+
s.engine.POST("/courses", courses.CreateHandler(s.courseRepository))
41+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package mysql
2+
3+
const (
4+
sqlCourseTable = "courses"
5+
)
6+
7+
type sqlCourse struct {
8+
ID string `db:"id"`
9+
Name string `db:"name"`
10+
Duration string `db:"duration"`
11+
}

0 commit comments

Comments
 (0)