From dfd8b01dc0ada7a44352132fe2d6db1c5c552cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Fri, 2 May 2025 10:24:25 +0200 Subject: [PATCH 01/11] feat(messaging): add basic send message feature --- go.mod | 12 ++ go.sum | 14 ++ .../messaging/application/send_message.go | 53 +++++++ .../application/send_message_test.go | 141 ++++++++++++++++++ internal/messaging/domain/author.go | 13 ++ internal/messaging/domain/message.go | 9 ++ 6 files changed, 242 insertions(+) create mode 100644 go.sum create mode 100644 internal/messaging/application/send_message.go create mode 100644 internal/messaging/application/send_message_test.go create mode 100644 internal/messaging/domain/author.go create mode 100644 internal/messaging/domain/message.go diff --git a/go.mod b/go.mod index a58446a..4023ed2 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,15 @@ module alexandre-gerault.fr/gochat-server go 1.24.2 + +require ( + github.com/fufuok/random v0.0.1 + github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..357ba89 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fufuok/random v0.0.1 h1:glLBs5Y8PNlsnWGaUhkiVyuNgOMoatHRpnn3DRjyFh8= +github.com/fufuok/random v0.0.1/go.mod h1:E5tRpJw7fsdE+b8GaJFxAvRMMTraJnMmvJiCWCmRAug= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/messaging/application/send_message.go b/internal/messaging/application/send_message.go new file mode 100644 index 0000000..fbbac87 --- /dev/null +++ b/internal/messaging/application/send_message.go @@ -0,0 +1,53 @@ +package application + +import ( + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + "github.com/google/uuid" +) + +type SendMessageDto struct { + author_id string + room_id string + content string +} + +type SendMessagePresenter interface { + Presents() + MessageEmpty() + TooLongMessage() + AuthorNotFound() +} + +type MessageRepository interface { + Save(message domain.Message) +} + +type UserRepository interface { + GetById(id uuid.UUID) (domain.Author, bool) +} + +func SendMessageHandler(userRepository UserRepository, messageRepository MessageRepository) func(dto SendMessageDto, presenter SendMessagePresenter) { + return func(dto SendMessageDto, presenter SendMessagePresenter) { + if len(dto.content) == 0 { + presenter.MessageEmpty() + return + } + + if len(dto.content) > 2000 { + presenter.TooLongMessage() + return + } + + author_id, _ := uuid.Parse(dto.author_id) + + if _, found := userRepository.GetById(author_id); found == false { + presenter.AuthorNotFound() + return + } + + message := domain.NewMessage(dto.content) + messageRepository.Save(message) + + presenter.Presents() + } +} diff --git a/internal/messaging/application/send_message_test.go b/internal/messaging/application/send_message_test.go new file mode 100644 index 0000000..f74a7eb --- /dev/null +++ b/internal/messaging/application/send_message_test.go @@ -0,0 +1,141 @@ +package application + +import ( + "testing" + + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + "github.com/fufuok/random" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +type SendMessageTestPresenter struct { + response string +} + +type InMemoryMessageRepository struct { + messages []domain.Message +} + +type InMemoryAuthorRepository struct { + users []domain.Author +} + +func (inMemoryUserRepository *InMemoryAuthorRepository) GetById(id uuid.UUID) (domain.Author, bool) { + for _, author := range inMemoryUserRepository.users { + if author.Uuid.String() == id.String() { + return author, true + } + } + + return domain.Author{}, false +} + +func (inMemoryMessageRepository *InMemoryMessageRepository) Save(message domain.Message) { + inMemoryMessageRepository.messages = append(inMemoryMessageRepository.messages, message) +} + +func (presenter *SendMessageTestPresenter) Presents() { + presenter.response = "success" +} + +func (presenter *SendMessageTestPresenter) MessageEmpty() { + presenter.response = "empty" +} + +func (presenter *SendMessageTestPresenter) TooLongMessage() { + presenter.response = "too_long" +} + +func (presenter *SendMessageTestPresenter) AuthorNotFound() { + presenter.response = "author_not_found" +} + +func TestItCanSendMessage(t *testing.T) { + author_id, err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + + dto := SendMessageDto{author_id.String(), "room_id", "Some message"} + + if err != nil { + t.Error("Cannot generate author_id") + } + + message_repository := InMemoryMessageRepository{} + authors_repository := InMemoryAuthorRepository{ + []domain.Author{domain.NewAuthor(author_id)}, + } + + presenter := SendMessageTestPresenter{} + + handler := SendMessageHandler(&authors_repository, &message_repository) + + handler(dto, &presenter) + + assert.Equal(t, "success", presenter.response) + assert.Equal(t, 1, len(message_repository.messages)) +} + +func TestItCannotSendAnEmptyMessage(t *testing.T) { + author_id, err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + + dto := SendMessageDto{author_id.String(), "room_id", ""} + + if err != nil { + t.Error("Cannot generate author_id") + } + + message_repository := InMemoryMessageRepository{} + authors_repository := InMemoryAuthorRepository{ + []domain.Author{domain.NewAuthor(author_id)}, + } + + presenter := SendMessageTestPresenter{} + + handler := SendMessageHandler(&authors_repository, &message_repository) + + handler(dto, &presenter) + + assert.Equal(t, "empty", presenter.response) + assert.Equal(t, 0, len(message_repository.messages)) +} + +func TestItCannotSendAnOversizedMessage(t *testing.T) { + author_id, err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + + dto := SendMessageDto{author_id.String(), "room_id", random.RandString(2001)} + + if err != nil { + t.Error("Cannot generate author_id") + } + + message_repository := InMemoryMessageRepository{} + authors_repository := InMemoryAuthorRepository{ + []domain.Author{domain.NewAuthor(author_id)}, + } + + presenter := SendMessageTestPresenter{} + + handler := SendMessageHandler(&authors_repository, &message_repository) + + handler(dto, &presenter) + + assert.Equal(t, "too_long", presenter.response) + assert.Equal(t, 0, len(message_repository.messages)) +} + +func TestItCannotSendMessageIfAuthorDoesNotExist(t *testing.T) { + dto := SendMessageDto{"user_id", "room_id", "Some legal content"} + + message_repository := InMemoryMessageRepository{} + authors_repository := InMemoryAuthorRepository{} + + presenter := SendMessageTestPresenter{} + + handler := SendMessageHandler(&authors_repository, &message_repository) + + handler(dto, &presenter) + + assert.Equal(t, "author_not_found", presenter.response) + assert.Equal(t, 0, len(message_repository.messages)) +} + diff --git a/internal/messaging/domain/author.go b/internal/messaging/domain/author.go new file mode 100644 index 0000000..18baa20 --- /dev/null +++ b/internal/messaging/domain/author.go @@ -0,0 +1,13 @@ +package domain + +import "github.com/google/uuid" + +type Author struct { + Uuid uuid.UUID +} + +func NewAuthor(uuid uuid.UUID) Author { + return Author{ + uuid, + } +} diff --git a/internal/messaging/domain/message.go b/internal/messaging/domain/message.go new file mode 100644 index 0000000..d9e48df --- /dev/null +++ b/internal/messaging/domain/message.go @@ -0,0 +1,9 @@ +package domain + +type Message struct { + content string +} + +func NewMessage(content string) Message { + return Message{content} +} From 881cc0094802a1b2390a30b6a1c12b967dba84b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Sat, 3 May 2025 11:34:05 +0200 Subject: [PATCH 02/11] feat(messaging): add missing ids --- .../messaging/application/send_message.go | 26 +++++++-- .../application/send_message_test.go | 56 ++++++++++++------- internal/messaging/domain/message.go | 9 ++- internal/testing/uuid.go | 15 +++++ 4 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 internal/testing/uuid.go diff --git a/internal/messaging/application/send_message.go b/internal/messaging/application/send_message.go index fbbac87..51aeb29 100644 --- a/internal/messaging/application/send_message.go +++ b/internal/messaging/application/send_message.go @@ -1,6 +1,8 @@ package application import ( + "log" + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" "github.com/google/uuid" ) @@ -22,11 +24,19 @@ type MessageRepository interface { Save(message domain.Message) } -type UserRepository interface { +type AuthorRepository interface { GetById(id uuid.UUID) (domain.Author, bool) } -func SendMessageHandler(userRepository UserRepository, messageRepository MessageRepository) func(dto SendMessageDto, presenter SendMessagePresenter) { +type UuidProvider interface{ + Generate() uuid.UUID +} + +func SendMessageHandler( + userRepository AuthorRepository, + messageRepository MessageRepository, + uuidProvider UuidProvider, +) func(dto SendMessageDto, presenter SendMessagePresenter) { return func(dto SendMessageDto, presenter SendMessagePresenter) { if len(dto.content) == 0 { presenter.MessageEmpty() @@ -38,14 +48,20 @@ func SendMessageHandler(userRepository UserRepository, messageRepository Message return } - author_id, _ := uuid.Parse(dto.author_id) + author_id, author_err := uuid.Parse(dto.author_id) + room_id, room_err := uuid.Parse(dto.room_id) + + if author_err != nil || room_err != nil { + log.Fatal("Invalid UUID format") + } - if _, found := userRepository.GetById(author_id); found == false { + if _, found := userRepository.GetById(author_id); !found { presenter.AuthorNotFound() return } - message := domain.NewMessage(dto.content) + message_id := uuidProvider.Generate() + message := domain.NewMessage(message_id, room_id, author_id, dto.content) messageRepository.Save(message) presenter.Presents() diff --git a/internal/messaging/application/send_message_test.go b/internal/messaging/application/send_message_test.go index f74a7eb..db321e8 100644 --- a/internal/messaging/application/send_message_test.go +++ b/internal/messaging/application/send_message_test.go @@ -4,6 +4,7 @@ import ( "testing" "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + testUtils "alexandre-gerault.fr/gochat-server/internal/testing" "github.com/fufuok/random" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -18,11 +19,11 @@ type InMemoryMessageRepository struct { } type InMemoryAuthorRepository struct { - users []domain.Author + authors []domain.Author } func (inMemoryUserRepository *InMemoryAuthorRepository) GetById(id uuid.UUID) (domain.Author, bool) { - for _, author := range inMemoryUserRepository.users { + for _, author := range inMemoryUserRepository.authors { if author.Uuid.String() == id.String() { return author, true } @@ -52,14 +53,17 @@ func (presenter *SendMessageTestPresenter) AuthorNotFound() { } func TestItCanSendMessage(t *testing.T) { - author_id, err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + fakeUuidProvider := testUtils.FakeUuidProvider{} - dto := SendMessageDto{author_id.String(), "room_id", "Some message"} + author_id, author_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") - if err != nil { - t.Error("Cannot generate author_id") + if author_err != nil || room_err != nil { + t.Error("Error while parsing a uuid") } + dto := SendMessageDto{author_id.String(), room_id.String(), "Some message"} + message_repository := InMemoryMessageRepository{} authors_repository := InMemoryAuthorRepository{ []domain.Author{domain.NewAuthor(author_id)}, @@ -67,7 +71,7 @@ func TestItCanSendMessage(t *testing.T) { presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository) + handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) handler(dto, &presenter) @@ -76,14 +80,17 @@ func TestItCanSendMessage(t *testing.T) { } func TestItCannotSendAnEmptyMessage(t *testing.T) { - author_id, err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + fakeUuidProvider := testUtils.FakeUuidProvider{} - dto := SendMessageDto{author_id.String(), "room_id", ""} + author_id, author_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") - if err != nil { - t.Error("Cannot generate author_id") + if author_err != nil || room_err != nil { + t.Error("Error while parsing a uuid") } + dto := SendMessageDto{author_id.String(), room_id.String(), ""} + message_repository := InMemoryMessageRepository{} authors_repository := InMemoryAuthorRepository{ []domain.Author{domain.NewAuthor(author_id)}, @@ -91,7 +98,7 @@ func TestItCannotSendAnEmptyMessage(t *testing.T) { presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository) + handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) handler(dto, &presenter) @@ -100,14 +107,17 @@ func TestItCannotSendAnEmptyMessage(t *testing.T) { } func TestItCannotSendAnOversizedMessage(t *testing.T) { - author_id, err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + fakeUuidProvider := testUtils.FakeUuidProvider{} - dto := SendMessageDto{author_id.String(), "room_id", random.RandString(2001)} + author_id, auth_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") - if err != nil { + if auth_err != nil || room_err != nil { t.Error("Cannot generate author_id") } + dto := SendMessageDto{author_id.String(), room_id.String(), random.RandString(2001)} + message_repository := InMemoryMessageRepository{} authors_repository := InMemoryAuthorRepository{ []domain.Author{domain.NewAuthor(author_id)}, @@ -115,7 +125,7 @@ func TestItCannotSendAnOversizedMessage(t *testing.T) { presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository) + handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) handler(dto, &presenter) @@ -124,18 +134,26 @@ func TestItCannotSendAnOversizedMessage(t *testing.T) { } func TestItCannotSendMessageIfAuthorDoesNotExist(t *testing.T) { - dto := SendMessageDto{"user_id", "room_id", "Some legal content"} + fakeUuidProvider := testUtils.FakeUuidProvider{} + + author_id, auth_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") + room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") + + if auth_err != nil || room_err != nil { + t.Error("Cannot generate author_id") + } + + dto := SendMessageDto{author_id.String(), room_id.String(), "Some legal content"} message_repository := InMemoryMessageRepository{} authors_repository := InMemoryAuthorRepository{} presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository) + handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) handler(dto, &presenter) assert.Equal(t, "author_not_found", presenter.response) assert.Equal(t, 0, len(message_repository.messages)) } - diff --git a/internal/messaging/domain/message.go b/internal/messaging/domain/message.go index d9e48df..d5b3187 100644 --- a/internal/messaging/domain/message.go +++ b/internal/messaging/domain/message.go @@ -1,9 +1,14 @@ package domain +import "github.com/google/uuid" + type Message struct { + id uuid.UUID + room_id uuid.UUID + author_id uuid.UUID content string } -func NewMessage(content string) Message { - return Message{content} +func NewMessage(message_id uuid.UUID, room_id uuid.UUID, author_id uuid.UUID, content string) Message { + return Message{message_id, room_id, author_id, content} } diff --git a/internal/testing/uuid.go b/internal/testing/uuid.go new file mode 100644 index 0000000..360f0ee --- /dev/null +++ b/internal/testing/uuid.go @@ -0,0 +1,15 @@ +package testing + +import "github.com/google/uuid" + +type FakeUuidProvider struct { + uuidToReturn uuid.UUID +} + +func (provider *FakeUuidProvider) Generate() uuid.UUID { + return provider.uuidToReturn +} + +func (provider *FakeUuidProvider) ChangeNextUuid(nextUuid uuid.UUID) { + provider.uuidToReturn = nextUuid +} From c67e170c9584aa1e6c2a1fafca2733d593207e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Mon, 5 May 2025 20:17:44 +0200 Subject: [PATCH 03/11] feat(infrastructure): add database migrations --- db/migrations/000001_create_users_table.down.sql | 1 + db/migrations/000001_create_users_table.up.sql | 9 +++++++++ db/migrations/000002_create_rooms_table.down.sql | 1 + db/migrations/000002_create_rooms_table.up.sql | 7 +++++++ db/migrations/000003_create_messages_table.down.sql | 1 + db/migrations/000003_create_messages_table.up.sql | 9 +++++++++ 6 files changed, 28 insertions(+) create mode 100644 db/migrations/000001_create_users_table.down.sql create mode 100644 db/migrations/000001_create_users_table.up.sql create mode 100644 db/migrations/000002_create_rooms_table.down.sql create mode 100644 db/migrations/000002_create_rooms_table.up.sql create mode 100644 db/migrations/000003_create_messages_table.down.sql create mode 100644 db/migrations/000003_create_messages_table.up.sql diff --git a/db/migrations/000001_create_users_table.down.sql b/db/migrations/000001_create_users_table.down.sql new file mode 100644 index 0000000..365a210 --- /dev/null +++ b/db/migrations/000001_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/db/migrations/000001_create_users_table.up.sql b/db/migrations/000001_create_users_table.up.sql new file mode 100644 index 0000000..1f857e8 --- /dev/null +++ b/db/migrations/000001_create_users_table.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE + IF NOT EXISTS users ( + id UUID PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); \ No newline at end of file diff --git a/db/migrations/000002_create_rooms_table.down.sql b/db/migrations/000002_create_rooms_table.down.sql new file mode 100644 index 0000000..5f8b282 --- /dev/null +++ b/db/migrations/000002_create_rooms_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS rooms; \ No newline at end of file diff --git a/db/migrations/000002_create_rooms_table.up.sql b/db/migrations/000002_create_rooms_table.up.sql new file mode 100644 index 0000000..c10c979 --- /dev/null +++ b/db/migrations/000002_create_rooms_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE + IF NOT EXISTS rooms ( + id UUID PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); diff --git a/db/migrations/000003_create_messages_table.down.sql b/db/migrations/000003_create_messages_table.down.sql new file mode 100644 index 0000000..36f514b --- /dev/null +++ b/db/migrations/000003_create_messages_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS messages; \ No newline at end of file diff --git a/db/migrations/000003_create_messages_table.up.sql b/db/migrations/000003_create_messages_table.up.sql new file mode 100644 index 0000000..d73ecfd --- /dev/null +++ b/db/migrations/000003_create_messages_table.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE + IF NOT EXISTS messages ( + id UUID PRIMARY KEY, + room_id UUID NOT NULL REFERENCES rooms (id), + author_id UUID NOT NULL REFERENCES users (id), + content VARCHAR(2000) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); From 3f441c19ba071c5e7a0f703b7ccdcb5facb799d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Mon, 5 May 2025 20:18:09 +0200 Subject: [PATCH 04/11] feat: prepare application with container and router --- go.mod | 7 +++ go.sum | 62 +++++++++++++++++++ .../messaging/application/send_message.go | 54 ++++++++-------- .../application/send_message_test.go | 8 +++ internal/messaging/domain/author.go | 6 ++ internal/messaging/domain/message.go | 4 ++ .../infrastructure/sql_author_repository.go | 13 ++++ .../infrastructure/sql_message_repository.go | 8 +++ .../ui/http/send_message_endpoint.go | 55 ++++++++++++++++ internal/shared/infrastructure/application.go | 32 ++++++++++ internal/shared/infrastructure/database.go | 36 +++++++++++ internal/shared/infrastructure/uuid.go | 13 ++++ internal/testing/uuid.go | 4 +- main.go | 34 +++++++++- 14 files changed, 304 insertions(+), 32 deletions(-) create mode 100644 internal/messaging/infrastructure/sql_author_repository.go create mode 100644 internal/messaging/infrastructure/sql_message_repository.go create mode 100644 internal/messaging/ui/http/send_message_endpoint.go create mode 100644 internal/shared/infrastructure/application.go create mode 100644 internal/shared/infrastructure/database.go create mode 100644 internal/shared/infrastructure/uuid.go diff --git a/go.mod b/go.mod index 4023ed2..a469c07 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,19 @@ go 1.24.2 require ( github.com/fufuok/random v0.0.1 + github.com/golang-migrate/migrate v3.5.4+incompatible + github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/golobby/container/v3 v3.3.2 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.10.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 357ba89..d50f170 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,75 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= +github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fufuok/random v0.0.1 h1:glLBs5Y8PNlsnWGaUhkiVyuNgOMoatHRpnn3DRjyFh8= github.com/fufuok/random v0.0.1/go.mod h1:E5tRpJw7fsdE+b8GaJFxAvRMMTraJnMmvJiCWCmRAug= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= +github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= +github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= +github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw= +github.com/golobby/container/v3 v3.3.2/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/messaging/application/send_message.go b/internal/messaging/application/send_message.go index 51aeb29..70c7912 100644 --- a/internal/messaging/application/send_message.go +++ b/internal/messaging/application/send_message.go @@ -1,16 +1,14 @@ package application import ( - "log" - "alexandre-gerault.fr/gochat-server/internal/messaging/domain" "github.com/google/uuid" ) type SendMessageDto struct { - author_id string - room_id string - content string + Author_id string + Room_id string + Content string } type SendMessagePresenter interface { @@ -18,41 +16,36 @@ type SendMessagePresenter interface { MessageEmpty() TooLongMessage() AuthorNotFound() + InvalidPayload() + UnexpectedError(error string) } -type MessageRepository interface { - Save(message domain.Message) -} - -type AuthorRepository interface { - GetById(id uuid.UUID) (domain.Author, bool) -} - -type UuidProvider interface{ - Generate() uuid.UUID +type UuidProvider interface { + Generate() (uuid.UUID, error) } func SendMessageHandler( - userRepository AuthorRepository, - messageRepository MessageRepository, + userRepository domain.AuthorRepository, + messageRepository domain.MessageRepository, uuidProvider UuidProvider, ) func(dto SendMessageDto, presenter SendMessagePresenter) { return func(dto SendMessageDto, presenter SendMessagePresenter) { - if len(dto.content) == 0 { - presenter.MessageEmpty() + author_id, author_err := uuid.Parse(dto.Author_id) + room_id, room_err := uuid.Parse(dto.Room_id) + + if author_err != nil || room_err != nil { + presenter.InvalidPayload() return } - if len(dto.content) > 2000 { - presenter.TooLongMessage() + if len(dto.Content) == 0 { + presenter.MessageEmpty() return } - author_id, author_err := uuid.Parse(dto.author_id) - room_id, room_err := uuid.Parse(dto.room_id) - - if author_err != nil || room_err != nil { - log.Fatal("Invalid UUID format") + if len(dto.Content) > 2000 { + presenter.TooLongMessage() + return } if _, found := userRepository.GetById(author_id); !found { @@ -60,8 +53,13 @@ func SendMessageHandler( return } - message_id := uuidProvider.Generate() - message := domain.NewMessage(message_id, room_id, author_id, dto.content) + message_id, err := uuidProvider.Generate() + + if err != nil { + presenter.UnexpectedError(err.Error()) + } + + message := domain.NewMessage(message_id, room_id, author_id, dto.Content) messageRepository.Save(message) presenter.Presents() diff --git a/internal/messaging/application/send_message_test.go b/internal/messaging/application/send_message_test.go index db321e8..eb7b041 100644 --- a/internal/messaging/application/send_message_test.go +++ b/internal/messaging/application/send_message_test.go @@ -52,6 +52,14 @@ func (presenter *SendMessageTestPresenter) AuthorNotFound() { presenter.response = "author_not_found" } +func (presenter *SendMessageTestPresenter) InvalidPayload() { + presenter.response = "invalid_payload" +} + +func (presenter *SendMessageTestPresenter) UnexpectedError(error string) { + presenter.response = error +} + func TestItCanSendMessage(t *testing.T) { fakeUuidProvider := testUtils.FakeUuidProvider{} diff --git a/internal/messaging/domain/author.go b/internal/messaging/domain/author.go index 18baa20..fc80cc6 100644 --- a/internal/messaging/domain/author.go +++ b/internal/messaging/domain/author.go @@ -11,3 +11,9 @@ func NewAuthor(uuid uuid.UUID) Author { uuid, } } + +type AuthorRepository interface { + GetById(id uuid.UUID) (Author, bool) +} + + diff --git a/internal/messaging/domain/message.go b/internal/messaging/domain/message.go index d5b3187..0d8ee8b 100644 --- a/internal/messaging/domain/message.go +++ b/internal/messaging/domain/message.go @@ -12,3 +12,7 @@ type Message struct { func NewMessage(message_id uuid.UUID, room_id uuid.UUID, author_id uuid.UUID, content string) Message { return Message{message_id, room_id, author_id, content} } + +type MessageRepository interface { + Save(message Message) +} diff --git a/internal/messaging/infrastructure/sql_author_repository.go b/internal/messaging/infrastructure/sql_author_repository.go new file mode 100644 index 0000000..c1bcd3c --- /dev/null +++ b/internal/messaging/infrastructure/sql_author_repository.go @@ -0,0 +1,13 @@ +package infrastructure + +import ( + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + "github.com/google/uuid" +) + +type SqlAuthorRepository struct {} + +func (sql_author_repository *SqlAuthorRepository) GetById(id uuid.UUID) (domain.Author, bool) { + return domain.Author{}, true +} + diff --git a/internal/messaging/infrastructure/sql_message_repository.go b/internal/messaging/infrastructure/sql_message_repository.go new file mode 100644 index 0000000..43e0098 --- /dev/null +++ b/internal/messaging/infrastructure/sql_message_repository.go @@ -0,0 +1,8 @@ +package infrastructure + +import "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + +type SqlMessageRepository struct{} + +func (sql_message_repository *SqlMessageRepository) Save(message domain.Message) { +} diff --git a/internal/messaging/ui/http/send_message_endpoint.go b/internal/messaging/ui/http/send_message_endpoint.go new file mode 100644 index 0000000..1d43bf2 --- /dev/null +++ b/internal/messaging/ui/http/send_message_endpoint.go @@ -0,0 +1,55 @@ +package http + +import ( + "net/http" + + "alexandre-gerault.fr/gochat-server/internal/messaging/application" + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + shared_infrastructure "alexandre-gerault.fr/gochat-server/internal/shared/infrastructure" +) + +type SendMessagePresenter struct { + writer http.ResponseWriter +} + +func (p *SendMessagePresenter) AuthorNotFound() { + p.writer.WriteHeader(http.StatusNotFound) +} + +func (p *SendMessagePresenter) MessageEmpty() { + p.writer.WriteHeader(http.StatusBadRequest) +} + +func (p *SendMessagePresenter) TooLongMessage() { + p.writer.WriteHeader(http.StatusBadRequest) +} + +func (p *SendMessagePresenter) Presents() { + p.writer.WriteHeader(http.StatusCreated) +} + +func (p *SendMessagePresenter) InvalidPayload() { + p.writer.WriteHeader(http.StatusBadRequest) +} + + + +func NewSendMessageEndpoint(app *shared_infrastructure.Application) func(writer http.ResponseWriter, request *http.Request) { + return func(writer http.ResponseWriter, request *http.Request) { + handler := application.SendMessageHandler( + app.Container().Resolve(domain.AuthorRepository), + app.Container().Resolve(domain.MessageRepository), + app.Container().Resolve(application.UuidProvider), + ) + + presenter := &SendMessagePresenter{writer} + + handler( + application.SendMessageDto{ + Room_id: request.FormValue("room_id"), + Content: request.FormValue("content"), + }, + presenter, + ) + } +} diff --git a/internal/shared/infrastructure/application.go b/internal/shared/infrastructure/application.go new file mode 100644 index 0000000..7203582 --- /dev/null +++ b/internal/shared/infrastructure/application.go @@ -0,0 +1,32 @@ +package shared_infrastructure + +import ( + "alexandre-gerault.fr/gochat-server/internal/messaging/application" + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + "alexandre-gerault.fr/gochat-server/internal/messaging/infrastructure" + "github.com/golobby/container/v3" +) + +type Application struct { + _container *container.Container +} + +func (app *Application) Register() *Application { + app._container.Singleton(func() domain.MessageRepository { + return &infrastructure.SqlMessageRepository{} + }) + + app._container.Singleton(func() domain.AuthorRepository { + return &infrastructure.SqlAuthorRepository{} + }) + + app._container.Singleton(func() application.UuidProvider { + return &UuidGenerator{} + }) + + return app +} + +func (app *Application) Container() *container.Container { + return app._container +} diff --git a/internal/shared/infrastructure/database.go b/internal/shared/infrastructure/database.go new file mode 100644 index 0000000..d54c76f --- /dev/null +++ b/internal/shared/infrastructure/database.go @@ -0,0 +1,36 @@ +package shared_infrastructure + +import ( + "fmt" + "log" + "os" + + "github.com/golang-migrate/migrate" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" +) + +func RunMigrations() { + migrator, err := migrate.New( + "file://db/migrations", + os.Getenv("DATABASE_URL"), + ) + + if err != nil { + log.Fatal(fmt.Sprintf("Cannot load migrate: %s", err.Error())) + } + + log.Println("Running migrations...") + + upErr := migrator.Up() + + if upErr != nil { + if upErr == migrate.ErrNoChange { + log.Println("No migrations to apply.") + } else { + log.Fatalf("Migration failed: %v", upErr) + } + } else { + log.Println("Migrations ran successfully") + } +} diff --git a/internal/shared/infrastructure/uuid.go b/internal/shared/infrastructure/uuid.go new file mode 100644 index 0000000..9b3426a --- /dev/null +++ b/internal/shared/infrastructure/uuid.go @@ -0,0 +1,13 @@ +package shared_infrastructure + +import ( + "github.com/google/uuid" +) + +type UuidGenerator struct {} + +func (uuidGenerator *UuidGenerator) Generate() (uuid.UUID, error) { + uuid, err := uuid.NewV7() + + return uuid, err +} diff --git a/internal/testing/uuid.go b/internal/testing/uuid.go index 360f0ee..2e5b606 100644 --- a/internal/testing/uuid.go +++ b/internal/testing/uuid.go @@ -6,8 +6,8 @@ type FakeUuidProvider struct { uuidToReturn uuid.UUID } -func (provider *FakeUuidProvider) Generate() uuid.UUID { - return provider.uuidToReturn +func (provider *FakeUuidProvider) Generate() (uuid.UUID, error) { + return provider.uuidToReturn, nil } func (provider *FakeUuidProvider) ChangeNextUuid(nextUuid uuid.UUID) { diff --git a/main.go b/main.go index 70a7fe5..99e5cc3 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,37 @@ package main -import "fmt" +import ( + "log" + "net/http" + "time" + + messagingHttp "alexandre-gerault.fr/gochat-server/internal/messaging/ui/http" + shared_infrastructure "alexandre-gerault.fr/gochat-server/internal/shared/infrastructure" +) + + func main() { - fmt.Println("Hello world!") + app := shared_infrastructure.Application {} + + app.Boot() + shared_infrastructure.RunMigrations() + + log.Println("Start http server...") + + router := http.NewServeMux() + + router.HandleFunc("POST /messages/", messagingHttp.SendMessageEndpoint) + + httpServer := &http.Server{ + Addr: ":8080", + Handler: router, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + log.Fatal(httpServer.ListenAndServe()) + + log.Println("Http server started successfully") } From 64d39f6a76f32cd9e49fc63d67ff9eb47cf8bc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Tue, 13 May 2025 13:27:13 +0200 Subject: [PATCH 05/11] feat(messaging): add sql repository implementations --- go.mod | 4 +-- go.sum | 4 --- .../application/send_message_test.go | 7 +++-- internal/messaging/domain/author.go | 4 +-- internal/messaging/domain/message.go | 6 ++-- .../infrastructure/sql_author_repository.go | 23 ++++++++++++-- .../infrastructure/sql_message_repository.go | 9 ++++-- .../ui/http/send_message_endpoint.go | 15 ++++++--- internal/shared/infrastructure/application.go | 31 +++++++++---------- internal/shared/infrastructure/database.go | 25 ++++++++++++--- main.go | 14 ++++----- start.sh | 1 + 12 files changed, 87 insertions(+), 56 deletions(-) create mode 100755 start.sh diff --git a/go.mod b/go.mod index a469c07..dcbda7b 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,9 @@ go 1.24.2 require ( github.com/fufuok/random v0.0.1 - github.com/golang-migrate/migrate v3.5.4+incompatible github.com/golang-migrate/migrate/v4 v4.18.3 - github.com/golobby/container/v3 v3.3.2 github.com/google/uuid v1.6.0 + github.com/lib/pq v1.10.9 github.com/stretchr/testify v1.10.0 ) @@ -15,7 +14,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/atomic v1.7.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d50f170..479db27 100644 --- a/go.sum +++ b/go.sum @@ -25,12 +25,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= -github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= -github.com/golobby/container/v3 v3.3.2 h1:7u+RgNnsdVlhGoS8gY4EXAG601vpMMzLZlYqSp77Quw= -github.com/golobby/container/v3 v3.3.2/go.mod h1:RDdKpnKpV1Of11PFBe7Dxc2C1k2KaLE4FD47FflAmj0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/internal/messaging/application/send_message_test.go b/internal/messaging/application/send_message_test.go index eb7b041..b91e1fc 100644 --- a/internal/messaging/application/send_message_test.go +++ b/internal/messaging/application/send_message_test.go @@ -1,6 +1,7 @@ package application import ( + "fmt" "testing" "alexandre-gerault.fr/gochat-server/internal/messaging/domain" @@ -22,14 +23,14 @@ type InMemoryAuthorRepository struct { authors []domain.Author } -func (inMemoryUserRepository *InMemoryAuthorRepository) GetById(id uuid.UUID) (domain.Author, bool) { +func (inMemoryUserRepository *InMemoryAuthorRepository) GetById(id uuid.UUID) (domain.Author, error) { for _, author := range inMemoryUserRepository.authors { if author.Uuid.String() == id.String() { - return author, true + return author, nil } } - return domain.Author{}, false + return domain.Author{}, fmt.Errorf("Cannot find author %s", id.String()) } func (inMemoryMessageRepository *InMemoryMessageRepository) Save(message domain.Message) { diff --git a/internal/messaging/domain/author.go b/internal/messaging/domain/author.go index fc80cc6..1a9724a 100644 --- a/internal/messaging/domain/author.go +++ b/internal/messaging/domain/author.go @@ -13,7 +13,5 @@ func NewAuthor(uuid uuid.UUID) Author { } type AuthorRepository interface { - GetById(id uuid.UUID) (Author, bool) + GetById(id uuid.UUID) (Author, error) } - - diff --git a/internal/messaging/domain/message.go b/internal/messaging/domain/message.go index 0d8ee8b..9d20010 100644 --- a/internal/messaging/domain/message.go +++ b/internal/messaging/domain/message.go @@ -3,10 +3,10 @@ package domain import "github.com/google/uuid" type Message struct { - id uuid.UUID - room_id uuid.UUID + id uuid.UUID + room_id uuid.UUID author_id uuid.UUID - content string + content string } func NewMessage(message_id uuid.UUID, room_id uuid.UUID, author_id uuid.UUID, content string) Message { diff --git a/internal/messaging/infrastructure/sql_author_repository.go b/internal/messaging/infrastructure/sql_author_repository.go index c1bcd3c..792b299 100644 --- a/internal/messaging/infrastructure/sql_author_repository.go +++ b/internal/messaging/infrastructure/sql_author_repository.go @@ -1,13 +1,30 @@ package infrastructure import ( + "database/sql" + "fmt" + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" "github.com/google/uuid" ) -type SqlAuthorRepository struct {} +type SqlAuthorRepository struct { + Database *sql.DB +} + +func (sql_author_repository *SqlAuthorRepository) GetById(id uuid.UUID) (domain.Author, error) { + row := sql_author_repository.Database.QueryRow("SELECT uuid FROM authors WHERE uuid = $1", id.String()) + + var author domain.Author + + if err := row.Scan(&author.Uuid); err != nil { + if err == sql.ErrNoRows { + return author, fmt.Errorf("Cannot find author %s", id.String()) + } + + return author, fmt.Errorf("Error finding author (%s): %s", id, err) + } -func (sql_author_repository *SqlAuthorRepository) GetById(id uuid.UUID) (domain.Author, bool) { - return domain.Author{}, true + return author, nil } diff --git a/internal/messaging/infrastructure/sql_message_repository.go b/internal/messaging/infrastructure/sql_message_repository.go index 43e0098..164617e 100644 --- a/internal/messaging/infrastructure/sql_message_repository.go +++ b/internal/messaging/infrastructure/sql_message_repository.go @@ -1,8 +1,13 @@ package infrastructure -import "alexandre-gerault.fr/gochat-server/internal/messaging/domain" +import ( + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + "database/sql" +) -type SqlMessageRepository struct{} +type SqlMessageRepository struct { + Database *sql.DB +} func (sql_message_repository *SqlMessageRepository) Save(message domain.Message) { } diff --git a/internal/messaging/ui/http/send_message_endpoint.go b/internal/messaging/ui/http/send_message_endpoint.go index 1d43bf2..4e94473 100644 --- a/internal/messaging/ui/http/send_message_endpoint.go +++ b/internal/messaging/ui/http/send_message_endpoint.go @@ -1,10 +1,11 @@ package http import ( + "fmt" + "io" "net/http" "alexandre-gerault.fr/gochat-server/internal/messaging/application" - "alexandre-gerault.fr/gochat-server/internal/messaging/domain" shared_infrastructure "alexandre-gerault.fr/gochat-server/internal/shared/infrastructure" ) @@ -32,20 +33,24 @@ func (p *SendMessagePresenter) InvalidPayload() { p.writer.WriteHeader(http.StatusBadRequest) } - +func (p *SendMessagePresenter) UnexpectedError(error string) { + p.writer.WriteHeader(http.StatusInternalServerError) + io.WriteString(p.writer, fmt.Sprintf("{\"message\": \"%s\"}", error)) +} func NewSendMessageEndpoint(app *shared_infrastructure.Application) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { handler := application.SendMessageHandler( - app.Container().Resolve(domain.AuthorRepository), - app.Container().Resolve(domain.MessageRepository), - app.Container().Resolve(application.UuidProvider), + app.Dependencies.AuthorRepository, + app.Dependencies.MessageRepository, + app.Dependencies.UuidProvider, ) presenter := &SendMessagePresenter{writer} handler( application.SendMessageDto{ + Author_id: request.FormValue("author_id"), Room_id: request.FormValue("room_id"), Content: request.FormValue("content"), }, diff --git a/internal/shared/infrastructure/application.go b/internal/shared/infrastructure/application.go index 7203582..e48489e 100644 --- a/internal/shared/infrastructure/application.go +++ b/internal/shared/infrastructure/application.go @@ -1,32 +1,29 @@ package shared_infrastructure import ( + "database/sql" + "alexandre-gerault.fr/gochat-server/internal/messaging/application" "alexandre-gerault.fr/gochat-server/internal/messaging/domain" "alexandre-gerault.fr/gochat-server/internal/messaging/infrastructure" - "github.com/golobby/container/v3" ) type Application struct { - _container *container.Container + Database *sql.DB + Dependencies Dependencies } -func (app *Application) Register() *Application { - app._container.Singleton(func() domain.MessageRepository { - return &infrastructure.SqlMessageRepository{} - }) - - app._container.Singleton(func() domain.AuthorRepository { - return &infrastructure.SqlAuthorRepository{} - }) +type Dependencies struct { + AuthorRepository domain.AuthorRepository + MessageRepository domain.MessageRepository + UuidProvider application.UuidProvider +} - app._container.Singleton(func() application.UuidProvider { - return &UuidGenerator{} - }) +func (app *Application) Register() *Application { + app.Database = CreateDatabase() + app.Dependencies.MessageRepository = &infrastructure.SqlMessageRepository{Database: app.Database} + app.Dependencies.AuthorRepository = &infrastructure.SqlAuthorRepository{Database: app.Database} + app.Dependencies.UuidProvider = &UuidGenerator{} return app } - -func (app *Application) Container() *container.Container { - return app._container -} diff --git a/internal/shared/infrastructure/database.go b/internal/shared/infrastructure/database.go index d54c76f..7778871 100644 --- a/internal/shared/infrastructure/database.go +++ b/internal/shared/infrastructure/database.go @@ -1,13 +1,16 @@ package shared_infrastructure import ( + "database/sql" "fmt" "log" "os" - "github.com/golang-migrate/migrate" + "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" + + _ "github.com/lib/pq" ) func RunMigrations() { @@ -22,15 +25,27 @@ func RunMigrations() { log.Println("Running migrations...") - upErr := migrator.Up() + up_err := migrator.Up() - if upErr != nil { - if upErr == migrate.ErrNoChange { + if up_err != nil { + if up_err == migrate.ErrNoChange { log.Println("No migrations to apply.") } else { - log.Fatalf("Migration failed: %v", upErr) + log.Fatalf("Migration failed: %v", up_err) } } else { log.Println("Migrations ran successfully") } } + +func CreateDatabase() *sql.DB { + connection_string := os.Getenv("DATABASE_URL") + + database, err := sql.Open("postgres", connection_string) + + if err != nil { + log.Fatal(err) + } + + return database +} diff --git a/main.go b/main.go index 99e5cc3..25defde 100644 --- a/main.go +++ b/main.go @@ -5,23 +5,21 @@ import ( "net/http" "time" - messagingHttp "alexandre-gerault.fr/gochat-server/internal/messaging/ui/http" + messaging_http "alexandre-gerault.fr/gochat-server/internal/messaging/ui/http" shared_infrastructure "alexandre-gerault.fr/gochat-server/internal/shared/infrastructure" ) - - func main() { - app := shared_infrastructure.Application {} + app := shared_infrastructure.Application{} - app.Boot() + app.Register() shared_infrastructure.RunMigrations() - log.Println("Start http server...") + log.Println("Start http server (http://localhost:8080)...") router := http.NewServeMux() - router.HandleFunc("POST /messages/", messagingHttp.SendMessageEndpoint) + router.HandleFunc("POST /messages/", messaging_http.NewSendMessageEndpoint(&app)) httpServer := &http.Server{ Addr: ":8080", @@ -33,5 +31,5 @@ func main() { log.Fatal(httpServer.ListenAndServe()) - log.Println("Http server started successfully") + log.Println("Gracefully shutdown.") } diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..c38f5bd --- /dev/null +++ b/start.sh @@ -0,0 +1 @@ +DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres?sslmode=disable" ./gochat-server From 86057cee677b35537f2cab29c9f271f55eef575b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Wed, 14 May 2025 23:16:25 +0200 Subject: [PATCH 06/11] style: use snake case where it should be --- .../messaging/application/send_message.go | 16 +++++----- .../application/send_message_test.go | 31 +++++++++---------- .../infrastructure/sql_author_repository.go | 14 +++------ .../ui/http/send_message_endpoint.go | 18 +++++------ internal/shared/infrastructure/application.go | 12 +++---- internal/shared/infrastructure/uuid.go | 4 +-- 6 files changed, 44 insertions(+), 51 deletions(-) diff --git a/internal/messaging/application/send_message.go b/internal/messaging/application/send_message.go index 70c7912..1a4bfe6 100644 --- a/internal/messaging/application/send_message.go +++ b/internal/messaging/application/send_message.go @@ -12,7 +12,7 @@ type SendMessageDto struct { } type SendMessagePresenter interface { - Presents() + MessageSentSuccessfully() MessageEmpty() TooLongMessage() AuthorNotFound() @@ -25,9 +25,9 @@ type UuidProvider interface { } func SendMessageHandler( - userRepository domain.AuthorRepository, - messageRepository domain.MessageRepository, - uuidProvider UuidProvider, + author_repository domain.AuthorRepository, + message_repository domain.MessageRepository, + uuid_provider UuidProvider, ) func(dto SendMessageDto, presenter SendMessagePresenter) { return func(dto SendMessageDto, presenter SendMessagePresenter) { author_id, author_err := uuid.Parse(dto.Author_id) @@ -48,20 +48,20 @@ func SendMessageHandler( return } - if _, found := userRepository.GetById(author_id); !found { + if found := author_repository.Exist(author_id); found == false { presenter.AuthorNotFound() return } - message_id, err := uuidProvider.Generate() + message_id, err := uuid_provider.Generate() if err != nil { presenter.UnexpectedError(err.Error()) } message := domain.NewMessage(message_id, room_id, author_id, dto.Content) - messageRepository.Save(message) + message_repository.Save(message) - presenter.Presents() + presenter.MessageSentSuccessfully() } } diff --git a/internal/messaging/application/send_message_test.go b/internal/messaging/application/send_message_test.go index b91e1fc..a699a58 100644 --- a/internal/messaging/application/send_message_test.go +++ b/internal/messaging/application/send_message_test.go @@ -1,7 +1,6 @@ package application import ( - "fmt" "testing" "alexandre-gerault.fr/gochat-server/internal/messaging/domain" @@ -23,21 +22,21 @@ type InMemoryAuthorRepository struct { authors []domain.Author } -func (inMemoryUserRepository *InMemoryAuthorRepository) GetById(id uuid.UUID) (domain.Author, error) { - for _, author := range inMemoryUserRepository.authors { +func (in_memory_author_repository *InMemoryAuthorRepository) Exist(id uuid.UUID) bool { + for _, author := range in_memory_author_repository.authors { if author.Uuid.String() == id.String() { - return author, nil + return true } } - return domain.Author{}, fmt.Errorf("Cannot find author %s", id.String()) + return false } -func (inMemoryMessageRepository *InMemoryMessageRepository) Save(message domain.Message) { - inMemoryMessageRepository.messages = append(inMemoryMessageRepository.messages, message) +func (in_memory_message_repository *InMemoryMessageRepository) Save(message domain.Message) { + in_memory_message_repository.messages = append(in_memory_message_repository.messages, message) } -func (presenter *SendMessageTestPresenter) Presents() { +func (presenter *SendMessageTestPresenter) MessageSentSuccessfully() { presenter.response = "success" } @@ -62,7 +61,7 @@ func (presenter *SendMessageTestPresenter) UnexpectedError(error string) { } func TestItCanSendMessage(t *testing.T) { - fakeUuidProvider := testUtils.FakeUuidProvider{} + fake_uuid_provider := testUtils.FakeUuidProvider{} author_id, author_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") @@ -80,7 +79,7 @@ func TestItCanSendMessage(t *testing.T) { presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) + handler := SendMessageHandler(&authors_repository, &message_repository, &fake_uuid_provider) handler(dto, &presenter) @@ -89,7 +88,7 @@ func TestItCanSendMessage(t *testing.T) { } func TestItCannotSendAnEmptyMessage(t *testing.T) { - fakeUuidProvider := testUtils.FakeUuidProvider{} + fake_uuid_provider := testUtils.FakeUuidProvider{} author_id, author_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") @@ -107,7 +106,7 @@ func TestItCannotSendAnEmptyMessage(t *testing.T) { presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) + handler := SendMessageHandler(&authors_repository, &message_repository, &fake_uuid_provider) handler(dto, &presenter) @@ -116,7 +115,7 @@ func TestItCannotSendAnEmptyMessage(t *testing.T) { } func TestItCannotSendAnOversizedMessage(t *testing.T) { - fakeUuidProvider := testUtils.FakeUuidProvider{} + fake_uuid_provider := testUtils.FakeUuidProvider{} author_id, auth_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") @@ -134,7 +133,7 @@ func TestItCannotSendAnOversizedMessage(t *testing.T) { presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) + handler := SendMessageHandler(&authors_repository, &message_repository, &fake_uuid_provider) handler(dto, &presenter) @@ -143,7 +142,7 @@ func TestItCannotSendAnOversizedMessage(t *testing.T) { } func TestItCannotSendMessageIfAuthorDoesNotExist(t *testing.T) { - fakeUuidProvider := testUtils.FakeUuidProvider{} + fake_uuid_provider := testUtils.FakeUuidProvider{} author_id, auth_err := uuid.Parse("01968e00-1b4d-7a91-bb2a-c55bd56a2dac") room_id, room_err := uuid.Parse("01969529-0a14-7556-a125-e9224be7b3ab") @@ -159,7 +158,7 @@ func TestItCannotSendMessageIfAuthorDoesNotExist(t *testing.T) { presenter := SendMessageTestPresenter{} - handler := SendMessageHandler(&authors_repository, &message_repository, &fakeUuidProvider) + handler := SendMessageHandler(&authors_repository, &message_repository, &fake_uuid_provider) handler(dto, &presenter) diff --git a/internal/messaging/infrastructure/sql_author_repository.go b/internal/messaging/infrastructure/sql_author_repository.go index 792b299..ff7f85d 100644 --- a/internal/messaging/infrastructure/sql_author_repository.go +++ b/internal/messaging/infrastructure/sql_author_repository.go @@ -2,7 +2,6 @@ package infrastructure import ( "database/sql" - "fmt" "alexandre-gerault.fr/gochat-server/internal/messaging/domain" "github.com/google/uuid" @@ -12,19 +11,14 @@ type SqlAuthorRepository struct { Database *sql.DB } -func (sql_author_repository *SqlAuthorRepository) GetById(id uuid.UUID) (domain.Author, error) { +func (sql_author_repository *SqlAuthorRepository) Exist(id uuid.UUID) bool { row := sql_author_repository.Database.QueryRow("SELECT uuid FROM authors WHERE uuid = $1", id.String()) var author domain.Author - - if err := row.Scan(&author.Uuid); err != nil { - if err == sql.ErrNoRows { - return author, fmt.Errorf("Cannot find author %s", id.String()) - } - return author, fmt.Errorf("Error finding author (%s): %s", id, err) + if err := row.Scan(&author.Uuid); err != nil { + return false } - return author, nil + return true } - diff --git a/internal/messaging/ui/http/send_message_endpoint.go b/internal/messaging/ui/http/send_message_endpoint.go index 4e94473..c32b2a8 100644 --- a/internal/messaging/ui/http/send_message_endpoint.go +++ b/internal/messaging/ui/http/send_message_endpoint.go @@ -25,10 +25,6 @@ func (p *SendMessagePresenter) TooLongMessage() { p.writer.WriteHeader(http.StatusBadRequest) } -func (p *SendMessagePresenter) Presents() { - p.writer.WriteHeader(http.StatusCreated) -} - func (p *SendMessagePresenter) InvalidPayload() { p.writer.WriteHeader(http.StatusBadRequest) } @@ -38,12 +34,16 @@ func (p *SendMessagePresenter) UnexpectedError(error string) { io.WriteString(p.writer, fmt.Sprintf("{\"message\": \"%s\"}", error)) } +func (p *SendMessagePresenter) MessageSentSuccessfully() { + p.writer.WriteHeader(http.StatusCreated) +} + func NewSendMessageEndpoint(app *shared_infrastructure.Application) func(writer http.ResponseWriter, request *http.Request) { return func(writer http.ResponseWriter, request *http.Request) { handler := application.SendMessageHandler( - app.Dependencies.AuthorRepository, - app.Dependencies.MessageRepository, - app.Dependencies.UuidProvider, + app.Dependencies.Author_Repository, + app.Dependencies.Message_Repository, + app.Dependencies.Uuid_Provider, ) presenter := &SendMessagePresenter{writer} @@ -51,8 +51,8 @@ func NewSendMessageEndpoint(app *shared_infrastructure.Application) func(writer handler( application.SendMessageDto{ Author_id: request.FormValue("author_id"), - Room_id: request.FormValue("room_id"), - Content: request.FormValue("content"), + Room_id: request.FormValue("room_id"), + Content: request.FormValue("content"), }, presenter, ) diff --git a/internal/shared/infrastructure/application.go b/internal/shared/infrastructure/application.go index e48489e..ade5e90 100644 --- a/internal/shared/infrastructure/application.go +++ b/internal/shared/infrastructure/application.go @@ -14,16 +14,16 @@ type Application struct { } type Dependencies struct { - AuthorRepository domain.AuthorRepository - MessageRepository domain.MessageRepository - UuidProvider application.UuidProvider + Author_Repository domain.AuthorRepository + Message_Repository domain.MessageRepository + Uuid_Provider application.UuidProvider } func (app *Application) Register() *Application { app.Database = CreateDatabase() - app.Dependencies.MessageRepository = &infrastructure.SqlMessageRepository{Database: app.Database} - app.Dependencies.AuthorRepository = &infrastructure.SqlAuthorRepository{Database: app.Database} - app.Dependencies.UuidProvider = &UuidGenerator{} + app.Dependencies.Message_Repository = &infrastructure.SqlMessageRepository{Database: app.Database} + app.Dependencies.Author_Repository = &infrastructure.SqlAuthorRepository{Database: app.Database} + app.Dependencies.Uuid_Provider = &UuidGenerator{} return app } diff --git a/internal/shared/infrastructure/uuid.go b/internal/shared/infrastructure/uuid.go index 9b3426a..8b0d6a8 100644 --- a/internal/shared/infrastructure/uuid.go +++ b/internal/shared/infrastructure/uuid.go @@ -4,9 +4,9 @@ import ( "github.com/google/uuid" ) -type UuidGenerator struct {} +type UuidGenerator struct{} -func (uuidGenerator *UuidGenerator) Generate() (uuid.UUID, error) { +func (uuid_generator *UuidGenerator) Generate() (uuid.UUID, error) { uuid, err := uuid.NewV7() return uuid, err From 707a2bd43ae56652097ed7c59277a2085996c9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Wed, 14 May 2025 23:16:49 +0200 Subject: [PATCH 07/11] refactor(messaging): change GetById to Exists --- internal/messaging/domain/author.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/messaging/domain/author.go b/internal/messaging/domain/author.go index 1a9724a..971ce68 100644 --- a/internal/messaging/domain/author.go +++ b/internal/messaging/domain/author.go @@ -13,5 +13,5 @@ func NewAuthor(uuid uuid.UUID) Author { } type AuthorRepository interface { - GetById(id uuid.UUID) (Author, error) + Exist(id uuid.UUID) bool } From 43a0fe225aa49b4b9af5c7aeed85586c1302236a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Tue, 20 May 2025 17:53:26 +0200 Subject: [PATCH 08/11] feat(messaging): implement save method in message repository --- .../messaging/application/send_message.go | 2 +- .../application/send_message_test.go | 4 +++- internal/messaging/domain/message.go | 10 ++++---- .../infrastructure/sql_message_repository.go | 23 +++++++++++++++++-- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/internal/messaging/application/send_message.go b/internal/messaging/application/send_message.go index 1a4bfe6..7f1f114 100644 --- a/internal/messaging/application/send_message.go +++ b/internal/messaging/application/send_message.go @@ -48,7 +48,7 @@ func SendMessageHandler( return } - if found := author_repository.Exist(author_id); found == false { + if !author_repository.Exist(author_id) { presenter.AuthorNotFound() return } diff --git a/internal/messaging/application/send_message_test.go b/internal/messaging/application/send_message_test.go index a699a58..63c253c 100644 --- a/internal/messaging/application/send_message_test.go +++ b/internal/messaging/application/send_message_test.go @@ -32,8 +32,10 @@ func (in_memory_author_repository *InMemoryAuthorRepository) Exist(id uuid.UUID) return false } -func (in_memory_message_repository *InMemoryMessageRepository) Save(message domain.Message) { +func (in_memory_message_repository *InMemoryMessageRepository) Save(message domain.Message) (uuid.UUID, error) { in_memory_message_repository.messages = append(in_memory_message_repository.messages, message) + + return message.Id, nil } func (presenter *SendMessageTestPresenter) MessageSentSuccessfully() { diff --git a/internal/messaging/domain/message.go b/internal/messaging/domain/message.go index 9d20010..62b1868 100644 --- a/internal/messaging/domain/message.go +++ b/internal/messaging/domain/message.go @@ -3,10 +3,10 @@ package domain import "github.com/google/uuid" type Message struct { - id uuid.UUID - room_id uuid.UUID - author_id uuid.UUID - content string + Id uuid.UUID + Room_Id uuid.UUID + Author_Id uuid.UUID + Content string } func NewMessage(message_id uuid.UUID, room_id uuid.UUID, author_id uuid.UUID, content string) Message { @@ -14,5 +14,5 @@ func NewMessage(message_id uuid.UUID, room_id uuid.UUID, author_id uuid.UUID, co } type MessageRepository interface { - Save(message Message) + Save(message Message) (uuid.UUID, error) } diff --git a/internal/messaging/infrastructure/sql_message_repository.go b/internal/messaging/infrastructure/sql_message_repository.go index 164617e..d091043 100644 --- a/internal/messaging/infrastructure/sql_message_repository.go +++ b/internal/messaging/infrastructure/sql_message_repository.go @@ -1,13 +1,32 @@ package infrastructure import ( - "alexandre-gerault.fr/gochat-server/internal/messaging/domain" "database/sql" + + "alexandre-gerault.fr/gochat-server/internal/messaging/domain" + "github.com/google/uuid" ) type SqlMessageRepository struct { Database *sql.DB } -func (sql_message_repository *SqlMessageRepository) Save(message domain.Message) { +func (sql_message_repository SqlMessageRepository) Save(message domain.Message) (uuid.UUID, error) { + query := ` + INSERT INTO messages (id, room_id, author_id, content) + VALUES ($1, $2, $3, $4) + ` + _, err := sql_message_repository.Database.Exec( + query, + message.Id, + message.Room_Id, + message.Author_Id, + message.Content, + ) + + if err != nil { + return uuid.Nil, err + } + + return message.Id, nil } From 07674c175586dad66288d75eb7c6931092c29a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Tue, 20 May 2025 20:45:37 +0200 Subject: [PATCH 09/11] fix(messaging): use correct column --- internal/messaging/infrastructure/sql_author_repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/messaging/infrastructure/sql_author_repository.go b/internal/messaging/infrastructure/sql_author_repository.go index ff7f85d..c145ec2 100644 --- a/internal/messaging/infrastructure/sql_author_repository.go +++ b/internal/messaging/infrastructure/sql_author_repository.go @@ -12,7 +12,7 @@ type SqlAuthorRepository struct { } func (sql_author_repository *SqlAuthorRepository) Exist(id uuid.UUID) bool { - row := sql_author_repository.Database.QueryRow("SELECT uuid FROM authors WHERE uuid = $1", id.String()) + row := sql_author_repository.Database.QueryRow("SELECT id FROM users WHERE id = $1", id.String()) var author domain.Author From 2207e123a92abdc390f513f2f527ba11dd0ce3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Tue, 20 May 2025 20:50:12 +0200 Subject: [PATCH 10/11] feat(messaging): update readme --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 0998a97..760bf2a 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,20 @@ If you're having trouble with permissions, ensure you have the executable right: sudo chmod +x ./gochat ``` +Also you can start a PostgreSQL database container with this docker command: + +```bash +docker run --name gochat-server-db -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres + ``` + + This way you can run the server like so: + + ```bash + DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres?sslmode=disable" ./gochat-server +``` + +Keep in mind this is for local development of course, not production ready. + ## Test Simply run From c5389d4f3340d0c55b1862f27dd0e7aa92c9080d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20G=C3=A9rault?= Date: Tue, 20 May 2025 21:13:58 +0200 Subject: [PATCH 11/11] fix(messaging): fix some codacy reported issues --- README.md | 6 +++--- start.sh | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 760bf2a..76713d6 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ As a learning feature, I don't plan password forgotten features and similar. Jus Regarding the architecture I plan to stick to something relatively simple. However I did not have definitive answers yet. At the moment here are the points I think are gonna be true: -- It is a server/client architecture, opposed to P2P (peer-to-peer) ; -- It is going to use http(s) transport for synchronous communication ; -- It is going to use some Socket-like transport for real-time. + - It is a server/client architecture, opposed to P2P (peer-to-peer) ; + - It is going to use http(s) transport for synchronous communication ; + - It is going to use some Socket-like transport for real-time. To store data I'll start with a relationnal database. Regarding the code organization I think I'll follow some hexagonal architecture principals. diff --git a/start.sh b/start.sh index c38f5bd..e88386a 100755 --- a/start.sh +++ b/start.sh @@ -1 +1,2 @@ +#!/usr/bin/env DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres?sslmode=disable" ./gochat-server